21 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
175 changed files with 5164 additions and 2779 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.
+25
View File
@@ -43,3 +43,28 @@ msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?" msgstr "Discard unsaved changes?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Characters"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Items"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Settings"
#: 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"
+28
View File
@@ -44,3 +44,31 @@ msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?" msgstr "¿Descartar los cambios no guardados?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Personajes"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Objetos"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Configuración"
#: src/dusk/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"
+28
View File
@@ -44,3 +44,31 @@ msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c #: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes" msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?" msgstr "未保存の変更を破棄しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "キャラクター"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "アイテム"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "設定"
#: src/dusk/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 "リンゴ"
+22
View File
@@ -2179,5 +2179,27 @@
0 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 DOL=1
ISO=2 ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE} DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
# like every other target instead of relying on endian.h's runtime probe.
DUSK_PLATFORM_ENDIAN_BIG
) )
# Custom compiler flags # Custom compiler flags
+1
View File
@@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_DISPLAY_HEIGHT=272 DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD DUSK_THREAD_PTHREAD
DUSK_TIME_DYNAMIC DUSK_TIME_DYNAMIC
DUSK_DISPLAY_OVERSCAN=6
) )
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+36 -45
View File
@@ -59,7 +59,10 @@ assetentry_t * assetGetEntry(
entry++; entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing, Find first available slot. // We did not find one existing. Find first available slot, reaping
// zero-ref entries to make room if none are immediately available.
bool_t reaped = false;
for(;;) {
entry = ASSET.entries; entry = ASSET.entries;
do { do {
if(entry->type != ASSET_LOADER_TYPE_NULL) { if(entry->type != ASSET_LOADER_TYPE_NULL) {
@@ -74,6 +77,11 @@ assetentry_t * assetGetEntry(
entry++; entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(reaped) break;
reaped = true;
errorCatch(assetReapUnused());
}
assertUnreachable("No available asset entry slots."); assertUnreachable("No available asset entry slots.");
return NULL; return NULL;
} }
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
assetEntryUnlock(entry); assetEntryUnlock(entry);
} }
errorret_t assetReapUnused(void) {
assertIsMainThread("assetReapUnused must be called from the main thread.");
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
// This handles dependency chains where an entry (e.g. a model) holds refs
// on child entries (mesh, texture): dispose parents first so child ref
// counts drop to zero, then pick up the children on the next pass. Without
// this, a forward-only scan fails when a shared child entry appears before
// a parent that still holds a ref to it.
bool_t any;
do {
any = false;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
if(entry->refs.count > 0) { entry++; continue; }
errorChain(assetEntryDispose(entry));
any = true;
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorOk();
}
errorret_t assetUpdate(void) { errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread."); assertIsMainThread("assetUpdate must be called from the main thread.");
@@ -324,30 +358,6 @@ errorret_t assetUpdate(void) {
} }
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
// consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk(); errorOk();
} }
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread."); assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread); threadStop(&ASSET.loadThread);
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries errorChain(assetReapUnused());
// until none remain. This handles dependency chains where an entry
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
// parents first so child ref counts drop to zero, then pick up the
// children on the next pass. Without this, a forward-only scan fails
// when a shared child entry appears before a parent that still holds a
// ref to it.
bool_t any;
do {
any = false;
assetentry_t *e = ASSET.entries;
do {
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
if(e->refs.count > 0) { e++; continue; }
errorChain(assetEntryDispose(e));
any = true;
e++;
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
// Cleanup zip file. // Cleanup zip file.
if(ASSET.zip != NULL) { if(ASSET.zip != NULL) {
+12 -2
View File
@@ -23,8 +23,8 @@
#define ASSET_FILE_NAME "dusk.dsk" #define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3 #define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 20 #define ASSET_LOADING_COUNT_MAX 10
#define ASSET_ENTRY_COUNT_MAX 128 #define ASSET_ENTRY_COUNT_MAX 64
typedef struct asset_s { typedef struct asset_s {
zip_t *zip; zip_t *zip;
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
*/ */
void assetUnlockEntry(assetentry_t *entry); void assetUnlockEntry(assetentry_t *entry);
/**
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
* until a full pass frees nothing further, since disposing a parent entry
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
* making it eligible for reaping too.
*
* @return An error code if any entry could not be disposed properly.
*/
errorret_t assetReapUnused(void);
/** /**
* Requires an asset entry to be loaded. This will block until the asset entry * Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded. * is fully loaded.
@@ -14,6 +14,26 @@
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "asset/asset.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) { errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread."); assertNotMainThread("Should be called from an async thread.");
@@ -111,9 +131,15 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
size_t offset = 8; size_t offset = 8;
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t); size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
out->tiles = memoryAllocate(tileSize);
memoryCopy(out->tiles, data + offset, tileSize); memoryCopy(out->tiles, data + offset, tileSize);
offset += tileSize; offset += tileSize;
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
*shape = endianLittleToHost32(*shape);
}
out->meshCount = data[offset]; out->meshCount = data[offset];
offset += sizeof(uint8_t); offset += sizeof(uint8_t);
assertTrue( assertTrue(
@@ -135,6 +161,65 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3)); memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
offset += sizeof(vec3); offset += sizeof(vec3);
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
}
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); memoryFree(data);
@@ -157,6 +242,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread."); assertIsMainThread("Must be called from the main thread.");
assetchunkoutput_t *out = &entry->data.chunk; assetchunkoutput_t *out = &entry->data.chunk;
if(out->tiles != NULL) {
memoryFree(out->tiles);
out->tiles = NULL;
}
for(uint8_t m = 0; m < out->meshCount; m++) { for(uint8_t m = 0; m < out->meshCount; m++) {
if(out->modelEntries[m] == NULL) continue; if(out->modelEntries[m] == NULL) continue;
assetUnlockEntry(out->modelEntries[m]); assetUnlockEntry(out->modelEntries[m]);
+29 -2
View File
@@ -9,7 +9,7 @@
#include "asset/assetfile.h" #include "asset/assetfile.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 4 #define ASSET_CHUNK_FILE_VERSION 5
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -33,12 +33,39 @@ typedef struct {
uint8_t modelIndex; uint8_t modelIndex;
} assetchunkloaderloading_t; } assetchunkloaderloading_t;
typedef enum {
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
CHUNK_ENTITY_SPAWN_KIND_ITEM
} chunkentityspawnkind_t;
typedef struct { 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; uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX]; assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entitySpawnCount;
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
uint8_t areaSpawnCount;
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
} assetchunkoutput_t; } assetchunkoutput_t;
/** /**
+23 -4
View File
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8)); uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
meshvertex_t *vertices = NULL; meshvertex_t *vertices = NULL;
if(vertCount > 0) { if(vertCount > 0) {
vertices = memoryAllocate(vertCount * sizeof(meshvertex_t)); // 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
// require this for the DMA'd vertex data to actually reach the GPU
// coherently. Static compiled-in vertex arrays happen to get this from
// the linker; a plain memoryAllocate here would not.
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t)); memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
for(uint32_t v = 0; v < vertCount; v++) {
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
}
} }
memoryFree(raw); memoryFree(raw);
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
out->vertices = NULL; out->vertices = NULL;
errorChain(ret); errorChain(ret);
} }
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount); ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) { if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR; loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh); meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices); memoryFree(out->vertices);
out->vertices = NULL; out->vertices = NULL;
errorChain(ret); errorChain(ret);
} }
#ifndef DUSK_OPENGL_LEGACY #if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
// VBO owns the data now; CPU copy is no longer needed. // VBO owns the data now; CPU copy is no longer needed. The platform
// mesh object itself still needs meshDispose later - tracked via
// out->meshInitialized, independent of the CPU buffer's lifetime.
memoryFree(out->vertices); memoryFree(out->vertices);
out->vertices = NULL; out->vertices = NULL;
#endif #endif
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread."); assertIsMainThread("Must be called from the main thread.");
assetmeshoutput_t *out = &entry->data.mesh; assetmeshoutput_t *out = &entry->data.mesh;
if(out->vertices != NULL) { if(out->meshInitialized) {
errorChain(meshDispose(&out->mesh)); errorChain(meshDispose(&out->mesh));
out->meshInitialized = false;
}
if(out->vertices != NULL) {
memoryFree(out->vertices); memoryFree(out->vertices);
out->vertices = NULL; out->vertices = NULL;
} }
@@ -30,6 +30,7 @@ typedef struct {
typedef struct { typedef struct {
mesh_t mesh; mesh_t mesh;
bool_t meshInitialized;
meshvertex_t *vertices; meshvertex_t *vertices;
} assetmeshoutput_t; } assetmeshoutput_t;
+1 -1
View File
@@ -17,7 +17,7 @@ console_t CONSOLE;
void consoleInit(void) { void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t)); memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = true; CONSOLE.visible = false;
#ifdef DUSK_CONSOLE_POSIX #ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex); threadMutexInit(&CONSOLE.printMutex);
+3 -2
View File
@@ -37,7 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(systemInit()); errorChain(systemInit());
errorChain(inputInit()); errorChain(inputInit());
errorChain(assetInit()); errorChain(assetInit());
// errorChain(saveInit()); errorChain(saveInit());
errorChain(localeManagerInit()); errorChain(localeManagerInit());
errorChain(displayInit()); errorChain(displayInit());
errorChain(uiInit()); errorChain(uiInit());
@@ -62,6 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorret_t engineUpdate(void) { errorret_t engineUpdate(void) {
// Order here is important. // Order here is important.
errorChain(networkUpdate()); errorChain(networkUpdate());
errorChain(saveUpdate());
timeUpdate(); timeUpdate();
inputUpdate(); inputUpdate();
consoleUpdate(); consoleUpdate();
@@ -88,7 +89,7 @@ errorret_t engineDispose(void) {
errorChain(uiDispose()); errorChain(uiDispose());
consoleDispose(); consoleDispose();
errorChain(displayDispose()); errorChain(displayDispose());
// errorChain(saveDispose()); errorChain(saveDispose());
errorChain(assetDispose()); errorChain(assetDispose());
errorOk(); errorOk();
-1
View File
@@ -17,7 +17,6 @@ input_t INPUT;
errorret_t inputInit(void) { errorret_t inputInit(void) {
memoryZero(&INPUT, sizeof(input_t)); memoryZero(&INPUT, sizeof(input_t));
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) { for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
INPUT.actions[i].action = (inputaction_t)i; INPUT.actions[i].action = (inputaction_t)i;
-4
View File
@@ -12,15 +12,11 @@
#define INPUT_LISTENER_PRESSED_MAX 16 #define INPUT_LISTENER_PRESSED_MAX 16
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX #define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
#define INPUT_DEADZONE_DEFAULT 0.1f
typedef struct { typedef struct {
inputactiondata_t actions[INPUT_ACTION_COUNT]; inputactiondata_t actions[INPUT_ACTION_COUNT];
inputplatform_t platform; inputplatform_t platform;
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
float_t deadzone;
} input_t; } input_t;
extern input_t INPUT; extern input_t INPUT;
+175 -1
View File
@@ -6,6 +6,7 @@
*/ */
#include "battle.h" #include "battle.h"
#include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
battle_t BATTLE; battle_t BATTLE;
@@ -40,10 +41,183 @@ battlefighter_t *battleAddFighter(
return fighter; return fighter;
} }
void battleStart(void) { void battleStart(
const battleencountertype_t encounterType,
const bool_t fleeAvailable
) {
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
BATTLE.encounterType = encounterType;
BATTLE.fleeAvailable = fleeAvailable;
BATTLE.result = BATTLE_RESULT_NONE;
BATTLE.round = 1;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(true);
BATTLE.active = true; BATTLE.active = true;
} }
void battleDispose(void) { void battleDispose(void) {
battleInit(); battleInit();
} }
battlefighter_t *battleGetCurrentFighter(void) {
if(!BATTLE.active) return NULL;
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
}
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].team != team) continue;
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
count++;
}
return count;
}
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
) {
assertNotNull(attacker, "Attacker cannot be NULL");
assertNotNull(defender, "Defender cannot be NULL");
const int32_t rawDamage =
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
defender->health = damage >= defender->health ? 0 : defender->health - damage;
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
}
void battleNextTurn(void) {
BATTLE.turnIndex++;
if(BATTLE.turnIndex < BATTLE.turnCount) return;
BATTLE.round++;
BATTLE.turnIndex = 0;
battleBuildTurnOrder(false);
}
battleresult_t battleCheckResult(void) {
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
BATTLE.result = BATTLE_RESULT_LOSS;
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
BATTLE.result = BATTLE_RESULT_WIN;
}
return BATTLE.result;
}
void battlePlayerAttack(const uint8_t targetIndex) {
battlefighter_t *attacker = battleGetCurrentFighter();
if(attacker == NULL) return;
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
if(!battleFighterIsAlive(defender)) return;
battleResolveAttack(attacker, defender);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
}
void battlePlayerFlee(void) {
battlefighter_t *fighter = battleGetCurrentFighter();
if(fighter == NULL) return;
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
if(!BATTLE.fleeAvailable) return;
BATTLE.result = BATTLE_RESULT_FLED;
}
void battleUpdate(void) {
if(!BATTLE.active) return;
if(BATTLE.result != BATTLE_RESULT_NONE) return;
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) return;
if(!battleFighterIsAlive(current)) {
battleNextTurn();
return;
}
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
battlefighter_t *target = battleAIChooseTarget(current);
if(target != NULL) battleResolveAttack(current, target);
battleCheckResult();
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
}
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
BATTLE.turnCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
BATTLE.turnOrder[BATTLE.turnCount++] = i;
}
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
const uint8_t key = BATTLE.turnOrder[i];
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
int8_t j = (int8_t)i - 1;
while(
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
) {
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
j--;
}
BATTLE.turnOrder[j + 1] = key;
}
if(!applyEncounterBias) return;
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
}
}
void battleMoveTeamFirst(const battlefighterteam_t team) {
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
sorted[count++] = BATTLE.turnOrder[i];
}
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
sorted[count++] = BATTLE.turnOrder[i];
}
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
}
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
const battlefighterteam_t enemyTeam =
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
battlefighter_t *weakest = NULL;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
battlefighter_t *candidate = &BATTLE.fighters[i];
if(candidate->team != enemyTeam) continue;
if(!battleFighterIsAlive(candidate)) continue;
if(weakest == NULL || candidate->health < weakest->health) {
weakest = candidate;
}
}
return weakest;
}
+136 -3
View File
@@ -10,9 +10,36 @@
#define BATTLE_FIGHTER_COUNT_MAX 8 #define BATTLE_FIGHTER_COUNT_MAX 8
typedef enum {
BATTLE_ENCOUNTER_REGULAR,
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
BATTLE_ENCOUNTER_BACK_ATTACK,
BATTLE_ENCOUNTER_COUNT
} battleencountertype_t;
typedef enum {
BATTLE_RESULT_NONE,
BATTLE_RESULT_WIN,
BATTLE_RESULT_LOSS,
BATTLE_RESULT_FLED,
BATTLE_RESULT_COUNT
} battleresult_t;
typedef struct { typedef struct {
bool_t active; bool_t active;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
battleencountertype_t encounterType;
bool_t fleeAvailable;
battleresult_t result;
// Fighter indices (into fighters[]), sorted for the current round.
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
uint8_t turnCount;
uint8_t turnIndex;
uint16_t round;
} battle_t; } battle_t;
extern battle_t BATTLE; extern battle_t BATTLE;
@@ -50,12 +77,118 @@ battlefighter_t *battleAddFighter(
); );
/** /**
* Starts a battle, marking it active. Any fighters already added via * Starts the battle: builds the opening turn order (biased by
* battleAddFighter remain in place. * encounterType for the first round only) and marks the battle active.
* Call once every fighter has been added via battleAddFighter.
*
* @param encounterType Determines the opening round's turn order.
* @param fleeAvailable Whether the party may attempt to flee this battle.
*/ */
void battleStart(void); void battleStart(
const battleencountertype_t encounterType,
const bool_t fleeAvailable
);
/** /**
* Disposes of the battle, clearing all fighters and marking it inactive. * Disposes of the battle, clearing all fighters and marking it inactive.
*/ */
void battleDispose(void); void battleDispose(void);
/**
* Returns the fighter whose turn it currently is.
*
* @return Pointer to the active fighter, or NULL if the battle isn't
* active or has no living fighters left to act.
*/
battlefighter_t *battleGetCurrentFighter(void);
/**
* Returns the number of living fighters on a team.
*
* @param team The team to count.
* @return Count of living fighters on that team.
*/
uint8_t battleGetAliveCount(const battlefighterteam_t team);
/**
* Resolves a physical attack from attacker onto defender: damage is the
* attacker's attack stat minus the defender's defense stat (minimum 1),
* subtracted from the defender's health. The defender is marked dead
* once health reaches 0.
*
* @param attacker The attacking fighter.
* @param defender The defending fighter.
*/
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
);
/**
* Ends the current fighter's turn and advances to the next fighter in
* the turn order, starting a new round (rebuilding turn order purely by
* speed) once every fighter in the current round has acted.
*/
void battleNextTurn(void);
/**
* Checks whether the battle has been won or lost, updating and
* returning BATTLE.result. Does nothing if a result has already been
* set (e.g. by a successful flee).
*
* @return The battle's current result.
*/
battleresult_t battleCheckResult(void);
/**
* Submits the current fighter's attack against a target, if it is
* currently a player-controlled fighter's turn. Resolves the attack,
* checks for a battle result, and advances the turn.
*
* @param targetIndex Index into BATTLE.fighters of the target.
*/
void battlePlayerAttack(const uint8_t targetIndex);
/**
* Submits a flee attempt for the current fighter's turn, if it is
* currently a player-controlled fighter's turn and fleeing is
* available for this battle. Always succeeds, ending the battle with
* BATTLE_RESULT_FLED.
*/
void battlePlayerFlee(void);
/**
* Updates the battle simulation for one frame: resolves the current
* fighter's turn automatically if AI-controlled, otherwise waits for a
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
* battle isn't active or already has a result.
*/
void battleUpdate(void);
/**
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
* fighter, sorted by speed descending.
*
* @param applyEncounterBias If true, reorders the freshly speed-sorted
* queue so BATTLE.encounterType's favoured team goes first (used only
* for the opening round).
*/
void battleBuildTurnOrder(const bool_t applyEncounterBias);
/**
* Stably partitions BATTLE.turnOrder so every fighter on the given team
* comes first, preserving each side's relative (speed-sorted) order.
*
* @param team The team to move to the front of the turn order.
*/
void battleMoveTeamFirst(const battlefighterteam_t team);
/**
* Picks an AI target for fighter: the lowest-health living fighter on
* the opposing team.
*
* @param fighter The AI-controlled fighter choosing a target.
* @return The chosen target, or NULL if the opposing team has no
* living fighters.
*/
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
+34
View File
@@ -34,6 +34,22 @@ typedef struct cutscene_s {
#define CUTSCENE_TEXT(TEXT) \ #define CUTSCENE_TEXT(TEXT) \
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } } { .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
.textMini = { \
.text = TEXT, \
.position = { X, Y, Z }, \
.duration = DURATION \
} \
}
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
.textMiniHide = { .index = INDEX } \
}
#define CUTSCENE_WAIT(WAIT) \ #define CUTSCENE_WAIT(WAIT) \
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT } { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
@@ -128,6 +144,24 @@ typedef struct cutscene_s {
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \ #define CUTSCENE_FADE_FROM_WHITE(DURATION) \
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR) CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
#define CUTSCENE_EMOJI(ENTITY_INDEX, EMOJI_TYPE, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_EMOJI, \
.emoji = { \
.entityIndex = ENTITY_INDEX, \
.emojiType = EMOJI_TYPE, \
.duration = DURATION \
} \
}
// AMOUNT ranges 0 (no shake) to 4 (three tiles): 1 is half a tile, 2 is
// a full tile, 3 is two tiles, and 4 is three tiles.
#define CUTSCENE_SHAKE(AMOUNT, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_SHAKE, \
.shake = { .amount = AMOUNT, .duration = DURATION } \
}
#define CUTSCENE_SET_PAUSE(FLAGS) \ #define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) } { .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
+15
View File
@@ -37,6 +37,7 @@ void cutsceneSystemStartCutsceneWith(
CUTSCENE_SYSTEM.entityLastCreated = NULL; CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
cutsceneSystemNext(); cutsceneSystemNext();
} }
@@ -65,6 +66,7 @@ void cutsceneSystemNext() {
CUTSCENE_SYSTEM.entityLastCreated = NULL; CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
return; return;
} }
@@ -130,6 +132,18 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
return areaId; return areaId;
} }
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
"shown"
);
return CUTSCENE_SYSTEM.textMiniLastCreated;
}
return index;
}
void cutsceneSystemDispose() { void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.scene = NULL;
CUTSCENE_SYSTEM.currentItem = 0xFF; CUTSCENE_SYSTEM.currentItem = 0xFF;
@@ -139,4 +153,5 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.entityLastCreated = NULL; CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL; CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
} }
+11
View File
@@ -15,6 +15,7 @@ typedef struct entity_s entity_t;
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC) #define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB) #define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF) #define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
// Maximum number of bytes a running cutscene may request via // Maximum number of bytes a running cutscene may request via
// cutscene_t.dataSize. // cutscene_t.dataSize.
@@ -29,6 +30,7 @@ typedef struct {
entity_t *entityLastCreated; entity_t *entityLastCreated;
entity_t *entityLastRef; entity_t *entityLastRef;
uint8_t areaLastCreated; uint8_t areaLastCreated;
uint8_t textMiniLastCreated;
// Data (used by the current item). // Data (used by the current item).
cutsceneitemdata_t data; cutsceneitemdata_t data;
@@ -86,6 +88,15 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
*/ */
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId); uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
/**
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
*
* @param index Raw slot index or sentinel value.
* @returns The resolved slot index.
*/
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
/** /**
* Advance to the next item in the cutscene. * Advance to the next item in the cutscene.
*/ */
@@ -14,3 +14,4 @@ add_subdirectory(entity)
add_subdirectory(item) add_subdirectory(item)
add_subdirectory(maparea) add_subdirectory(maparea)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(battle)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenestartbattle.c
)
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/battle/party.h"
#include "scene/scene.h"
void cutsceneStartBattleStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
const cutscenestartbattle_t *config = &item->startBattle;
battleInit();
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
battlefighter_t *fighter = battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
member->stats, member->healthMax, member->mpMax
);
if(fighter == NULL) continue;
fighter->health = member->health;
fighter->mp = member->mp;
fighter->status = member->status;
}
for(uint8_t i = 0; i < config->enemyCount; i++) {
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemy->stats, enemy->healthMax, enemy->mpMax
);
}
battleStart(config->encounterType, config->fleeAvailable);
sceneSet(SCENE_TYPE_BATTLE);
}
bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
// Sync ally HP/MP back to the persistent party roster. Relies on
// ally fighters having been added to BATTLE.fighters in the same
// order partyGetOrderMember() iterates, starting at index 0 (see
// cutsceneStartBattleStart).
uint8_t allySlot = 0;
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
battlefighter_t *member = partyGetOrderMember(i);
if(member == NULL) continue;
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
member->health = fighter->health;
member->mp = fighter->mp;
member->status = fighter->status;
}
sceneSet(SCENE_TYPE_OVERWORLD);
battleDispose();
return true;
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/battle.h"
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
typedef struct {
battlefighterstats_t stats;
uint16_t healthMax;
uint16_t mpMax;
} cutscenestartbattleenemy_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
battleencountertype_t encounterType;
bool_t fleeAvailable;
uint8_t enemyCount;
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
} cutscenestartbattle_t;
/**
* Starts a battle: seeds BATTLE with the party's active order members
* and the item's configured enemies, then switches to the battle
* scene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneStartBattleStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Waits for the battle to produce a result, syncs ally HP/MP back to
* the party roster, then returns to the overworld scene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the battle has ended.
*/
bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+134 -120
View File
@@ -7,132 +7,146 @@
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
[CUTSCENE_ITEM_TYPE_NULL] = { 0 },
[CUTSCENE_ITEM_TYPE_TEXT] = {
.init = cutsceneTextStart,
.update = cutsceneTextUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
.init = cutsceneTextMiniStart,
.update = cutsceneTextMiniUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
.init = cutsceneTextMiniHideStart,
.update = cutsceneTextMiniHideUpdate
},
[CUTSCENE_ITEM_TYPE_CALLBACK] = {
.init = cutsceneCallbackStart,
.update = cutsceneCallbackUpdate
},
[CUTSCENE_ITEM_TYPE_WAIT] = {
.init = cutsceneWaitStart,
.update = cutsceneWaitUpdate
},
[CUTSCENE_ITEM_TYPE_CUTSCENE] = {
.init = cutsceneCutsceneStart,
.update = cutsceneCutsceneUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
.init = cutsceneEntityTeleportStart,
.update = cutsceneEntityTeleportUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
.init = cutsceneEntityWalkToStart,
.update = cutsceneEntityWalkToUpdate
},
[CUTSCENE_ITEM_TYPE_FADE] = {
.init = cutsceneFadeStart,
.update = cutsceneFadeUpdate
},
[CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
.init = cutsceneSetPauseStart,
.update = cutsceneSetPauseUpdate
},
[CUTSCENE_ITEM_TYPE_CONCURRENT] = {
.init = cutsceneConcurrentStart,
.update = cutsceneConcurrentUpdate
},
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
.init = cutsceneItemGiveStart,
.update = cutsceneItemGiveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
.init = cutsceneEntityRemoveStart,
.update = cutsceneEntityRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
.init = cutsceneEntityAddStart,
.update = cutsceneEntityAddUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
.init = cutsceneEntityTurnStart,
.update = cutsceneEntityTurnUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
.init = cutsceneEntityWalkToEntityStart,
.update = cutsceneEntityWalkToEntityUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
.init = cutsceneMapAreaAddStart,
.update = cutsceneMapAreaAddUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
.init = cutsceneMapAreaRemoveStart,
.update = cutsceneMapAreaRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
.init = cutsceneMapAreaWaitStart,
.update = cutsceneMapAreaWaitUpdate
},
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
.init = cutsceneStartBattleStart,
.update = cutsceneStartBattleUpdate
},
[CUTSCENE_ITEM_TYPE_EMOJI] = {
.init = cutsceneEmojiStart,
.update = cutsceneEmojiUpdate
},
[CUTSCENE_ITEM_TYPE_SHAKE] = {
.init = cutsceneShakeStart,
.update = cutsceneShakeUpdate
}
};
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) { cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
case CUTSCENE_ITEM_TYPE_TEXT: if(init != NULL) init(item, data);
cutsceneTextStart(item, data); }
break;
case CUTSCENE_ITEM_TYPE_CALLBACK: bool_t cutsceneItemUpdate(
cutsceneCallbackStart(item, data); const cutsceneitem_t *item,
break; cutsceneitemdata_t *data
) {
cutsceneitemupdatecallback_t *update =
CUTSCENE_ITEM_CALLBACKS[item->type].update;
if(update == NULL) return false;
case CUTSCENE_ITEM_TYPE_WAIT: return update(item, data);
cutsceneWaitStart(item, data); }
break;
case CUTSCENE_ITEM_TYPE_CUTSCENE: void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene); if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
cutsceneEntityTeleportStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
cutsceneEntityWalkToStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_FADE:
cutsceneFadeStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
cutsceneSetPauseStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CONCURRENT:
cutsceneConcurrentStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
cutsceneItemGiveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
cutsceneEntityRemoveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
cutsceneEntityAddStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
cutsceneEntityTurnStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
cutsceneEntityWalkToEntityStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_ADD:
cutsceneMapAreaAddStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
cutsceneMapAreaRemoveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT:
cutsceneMapAreaWaitStart(item, data);
break;
default:
break;
}
} }
bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) { bool_t cutsceneCutsceneUpdate(
switch(item->type) { const cutsceneitem_t *item,
case CUTSCENE_ITEM_TYPE_TEXT: cutsceneitemdata_t *data
return cutsceneTextUpdate(item, data); ) {
case CUTSCENE_ITEM_TYPE_CALLBACK:
return cutsceneCallbackUpdate(item, data);
case CUTSCENE_ITEM_TYPE_WAIT:
return cutsceneWaitUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
return cutsceneEntityTeleportUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
return cutsceneEntityWalkToUpdate(item, data);
case CUTSCENE_ITEM_TYPE_FADE:
return cutsceneFadeUpdate(item, data);
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
return cutsceneSetPauseUpdate(item, data);
case CUTSCENE_ITEM_TYPE_CONCURRENT:
return cutsceneConcurrentUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
return cutsceneItemGiveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
return cutsceneEntityRemoveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
return cutsceneEntityAddUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
return cutsceneEntityTurnUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
return cutsceneEntityWalkToEntityUpdate(item, data);
case CUTSCENE_ITEM_TYPE_MAP_AREA_ADD:
return cutsceneMapAreaAddUpdate(item, data);
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
return cutsceneMapAreaRemoveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT:
return cutsceneMapAreaWaitUpdate(item, data);
default:
return false; return false;
} }
}
+63 -1
View File
@@ -17,17 +17,25 @@
#include "entity/cutsceneentityturn.h" #include "entity/cutsceneentityturn.h"
#include "entity/cutsceneentitywalktoentity.h" #include "entity/cutsceneentitywalktoentity.h"
#include "ui/cutscenetext.h" #include "ui/cutscenetext.h"
#include "ui/cutscenetextmini.h"
#include "ui/cutscenetextminihide.h"
#include "ui/cutscenefade.h" #include "ui/cutscenefade.h"
#include "ui/cutsceneemoji.h"
#include "ui/cutsceneshake.h"
#include "item/cutsceneitemgive.h" #include "item/cutsceneitemgive.h"
#include "maparea/cutscenemapareaadd.h" #include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h" #include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h" #include "maparea/cutscenemapareawait.h"
#include "battle/cutscenestartbattle.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
typedef enum { typedef enum {
CUTSCENE_ITEM_TYPE_NULL, CUTSCENE_ITEM_TYPE_NULL,
CUTSCENE_ITEM_TYPE_TEXT, CUTSCENE_ITEM_TYPE_TEXT,
CUTSCENE_ITEM_TYPE_TEXT_MINI,
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
CUTSCENE_ITEM_TYPE_CALLBACK, CUTSCENE_ITEM_TYPE_CALLBACK,
CUTSCENE_ITEM_TYPE_WAIT, CUTSCENE_ITEM_TYPE_WAIT,
CUTSCENE_ITEM_TYPE_CUTSCENE, CUTSCENE_ITEM_TYPE_CUTSCENE,
@@ -43,7 +51,12 @@ typedef enum {
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
CUTSCENE_ITEM_TYPE_START_BATTLE,
CUTSCENE_ITEM_TYPE_EMOJI,
CUTSCENE_ITEM_TYPE_SHAKE,
CUTSCENE_ITEM_TYPE_COUNT
} cutsceneitemtype_t; } cutsceneitemtype_t;
struct cutsceneitem_s { struct cutsceneitem_s {
@@ -51,6 +64,8 @@ struct cutsceneitem_s {
union { union {
cutscenetext_t text; cutscenetext_t text;
cutscenetextmini_t textMini;
cutscenetextminihide_t textMiniHide;
cutscenecallback_t callback; cutscenecallback_t callback;
cutscenewait_t wait; cutscenewait_t wait;
const cutscene_t *cutscene; const cutscene_t *cutscene;
@@ -67,6 +82,9 @@ struct cutsceneitem_s {
cutscenemapareaadd_t mapAreaAdd; cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove; cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait; cutscenemapareawait_t mapAreaWait;
cutscenestartbattle_t startBattle;
cutsceneemoji_t emoji;
cutsceneshake_t shake;
}; };
}; };
@@ -77,6 +95,24 @@ typedef union cutsceneitemdata_u {
cutscenemapareawaitdata_t mapAreaWait; cutscenemapareawaitdata_t mapAreaWait;
} cutsceneitemdata_t; } cutsceneitemdata_t;
typedef void (cutsceneiteminitcallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef bool_t (cutsceneitemupdatecallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef struct {
cutsceneiteminitcallback_t *init;
cutsceneitemupdatecallback_t *update;
} cutsceneitemcallbacks_t;
extern cutsceneitemcallbacks_t
CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT];
/** /**
* Start the given cutscene item. * Start the given cutscene item.
* *
@@ -99,3 +135,29 @@ bool_t cutsceneItemUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
cutsceneitemdata_t *data cutsceneitemdata_t *data
); );
/**
* Starts a nested-cutscene item, handing control over to the
* referenced cutscene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a nested-cutscene item. By the time this would run, control
* has already moved on to the referenced cutscene, so this always
* reports incomplete.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -7,7 +7,7 @@
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/item/itemgive.h" #include "rpg/item/itemgive.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneItemGiveStart( void cutsceneItemGiveStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -6,5 +6,9 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
cutscenetext.c cutscenetext.c
cutscenetextmini.c
cutscenetextminihide.c
cutscenefade.c cutscenefade.c
cutsceneemoji.c
cutsceneshake.c
) )
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->emoji.entityIndex);
uiEmojiAdd(entity->id, item->emoji.duration, item->emoji.emojiType);
}
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "ui/rpg/uiemoji.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
float_t duration;
uiemojitype_t emojiType;
} cutsceneemoji_t;
/**
* Starts an emoji step (shows an emoji above the entity for the given
* duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an emoji step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/rpgcamera.h"
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
rpgCameraShake(item->shake.amount, item->shake.duration);
}
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t amount;
float_t duration;
} cutsceneshake_t;
/**
* Starts a camera shake item (kicks off the shake on the RPG camera).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a camera shake item. The shake itself runs asynchronously on
* the RPG camera, so this always completes immediately.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/cutsceneitem.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneTextStart( void cutsceneTextStart(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = uiTextboxMiniListGetNext();
uiTextboxMiniShow(
&UI_TEXTBOX_MINI_LIST[index],
item->textMini.text,
item->textMini.position,
item->textMini.duration,
NULL,
NULL
);
CUTSCENE_SYSTEM.textMiniLastCreated = index;
}
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
typedef struct {
char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
vec3 position;
float_t duration;
} cutscenetextmini_t;
/**
* Starts a mini text item (shows a mini textbox at the given world
* position for the given duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
}
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t index;
} cutscenetextminihide_t;
/**
* Starts a mini text hide step (closes the mini textbox immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text hide step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -16,6 +16,10 @@ CUTSCENE(TEST_ONE, 0, DEFAULT,
CUTSCENE(TEST_TWO, 0, DEFAULT, CUTSCENE(TEST_TWO, 0, DEFAULT,
CUTSCENE_TEXT("Test Two."), CUTSCENE_TEXT("Test Two."),
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0), CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
CUTSCENE_EMOJI(
CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f
),
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0), CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
// CUTSCENE_CONCURRENT( // CUTSCENE_CONCURRENT(
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0), // CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
+13 -1
View File
@@ -10,6 +10,7 @@
#include "util/memory.h" #include "util/memory.h"
#include "time/time.h" #include "time/time.h"
#include "util/math.h" #include "util/math.h"
#include "console/console.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h" #include "rpg/overworld/maparea.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
@@ -292,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) { if(chunkIndex != 0xFF) {
chunk_t *next = mapGetChunk(chunkIndex); chunk_t *next = mapGetChunk(chunkIndex);
@@ -300,8 +304,16 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(next->entities[i] != 0xFF) continue; if(next->entities[i] != 0xFF) continue;
next->entities[i] = entity->id; next->entities[i] = entity->id;
entity->chunkIndex = chunkIndex;
break; break;
} }
if(entity->chunkIndex != chunkIndex) {
consolePrint(
"entitySetChunk: chunk %u has no free entity slots, entity %u "
"left untracked",
chunkIndex, entity->id
);
}
} }
} }
} }
+4 -1
View File
@@ -142,7 +142,10 @@ uint8_t entityGetAvailable();
/** /**
* Assigns an entity to a chunk, removing it from its current chunk first. * Assigns an entity to a chunk, removing it from its current chunk first.
* Pass 0xFF as chunkIndex to detach the entity from any chunk. * 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 entity Pointer to the entity.
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none. * @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
@@ -6,4 +6,5 @@
# Sources # Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
globalitemstore.c
) )
@@ -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 "rpg/entity/entity.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void entityInteractWith(entity_t *player, entity_t *target) { void entityInteractWith(entity_t *player, entity_t *target) {
assertNotNull(player, "Player entity pointer cannot be NULL"); assertNotNull(player, "Player entity pointer cannot be NULL");
+1 -1
View File
@@ -9,7 +9,7 @@
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "rpg/item/itemgive.h" #include "rpg/item/itemgive.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
void entityItemInit(entity_t *entity) { void entityItemInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
+5 -4
View File
@@ -6,6 +6,7 @@
# Sources # Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
item.c
inventory.c inventory.c
backpack.c backpack.c
itemgive.c itemgive.c
@@ -13,9 +14,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Item Definitions # Item Definitions
dusk_run_python( dusk_run_python(
dusk_item_csv_defs dusk_item_json_defs
tools.item tools.item
--csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv --json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h
) )
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_csv_defs) add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "item.h"
#include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
errorret_t itemGetName(
const itemid_t item,
char_t *buffer,
const size_t bufferSize
) {
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
ITEMS[item].name,
0,
buffer,
bufferSize
));
errorOk();
}
-4
View File
@@ -1,4 +0,0 @@
id,type,weight
POTION,MEDICINE,1.0
POTATO,FOOD,0.5
APPLE,FOOD,0.3
1 id type weight
2 POTION MEDICINE 1.0
3 POTATO FOOD 0.5
4 APPLE FOOD 0.3
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "rpg/item/itemdef.h"
/**
* Gets the localized display name for an item.
*
* @param item The item ID to look up. Must not be ITEM_ID_NULL.
* @param buffer Buffer to write the localized name into.
* @param bufferSize Size of the buffer.
* @return Any error that occurs.
*/
errorret_t itemGetName(
const itemid_t item,
char_t *buffer,
const size_t bufferSize
);
+5
View File
@@ -0,0 +1,5 @@
[
{ "id": "POTION", "type": "MEDICINE", "weight": 1.0, "name": "potion" },
{ "id": "POTATO", "type": "FOOD", "weight": 0.5, "name": "potato" },
{ "id": "APPLE", "type": "FOOD", "weight": 0.3, "name": "apple" }
]
+9 -2
View File
@@ -7,18 +7,25 @@
#include "itemgive.h" #include "itemgive.h"
#include "rpg/item/backpack.h" #include "rpg/item/backpack.h"
#include "ui/rpg/uitextboxmain.h" #include "rpg/item/item.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "util/string.h" #include "util/string.h"
#include "error/error.h"
#define ITEM_GIVE_NAME_MAX_CHARS 32
void itemGive(const itemid_t item, const uint8_t quantity) { void itemGive(const itemid_t item, const uint8_t quantity) {
backpackAdd(item, quantity); backpackAdd(item, quantity);
char_t name[ITEM_GIVE_NAME_MAX_CHARS];
errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS));
char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS]; char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS];
stringFormat( stringFormat(
msg, msg,
ITEM_GIVE_MESSAGE_MAX_CHARS - 1, ITEM_GIVE_MESSAGE_MAX_CHARS - 1,
"Received %s x%u", "Received %s x%u",
ITEMS[item].name, name,
(uint32_t)quantity (uint32_t)quantity
); );
uiTextboxMainSetText(msg); uiTextboxMainSetText(msg);
+2
View File
@@ -14,3 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
tileshape.c tileshape.c
) )
add_subdirectory(global)
+9
View File
@@ -12,6 +12,8 @@
#define CHUNK_MESH_COUNT_MAX 10 #define CHUNK_MESH_COUNT_MAX 10
#define CHUNK_MESH_NAME_MAX 64 #define CHUNK_MESH_NAME_MAX 64
#define CHUNK_ENTITY_COUNT_MAX 10 #define CHUNK_ENTITY_COUNT_MAX 10
#define CHUNK_ENTITY_SPAWN_COUNT_MAX 8
#define CHUNK_AREA_COUNT_MAX 4
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -28,6 +30,13 @@ typedef struct chunk_s {
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX]; assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
// Map area IDs (into MAP_AREAS) spawned from this chunk's file data.
// Removed via mapAreaRemove when this chunk unloads, and re-added if it
// streams back in - unlike entities (tracked by current position via
// entities[] above), areas have no position-based ownership mechanism of
// their own, so the owning chunk must track and tear them down directly.
uint8_t areas[CHUNK_AREA_COUNT_MAX];
} chunk_t; } chunk_t;
/** /**
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
)
@@ -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 "mapareaglobaldefs.h"
#include "mapareagloballist.h"
#define MAP_AREA_CALLBACK_LIST_COUNT ( \
sizeof(MAP_AREA_CALLBACK_LIST) / \
sizeof(MAP_AREA_CALLBACK_LIST[0]) \
)
//EOF
@@ -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 "rpg/overworld/maparea.h"
#define MAP_AREA_CALLBACK(id) \
static void MAP_AREA_CALLBACK_##id(entity_t *entity, const uint8_t trigger)
#define MAP_AREA_CALLBACK_REF(id) \
MAP_AREA_CALLBACK_##id
//EOF
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "mapareaglobaldefs.h"
#include "console/console.h"
MAP_AREA_CALLBACK(1) {
consolePrint("mapAreaGlobalCallback 1: trigger=%u", trigger);
}
// Index 0 is reserved (not a valid callback ID) - see mapAreaAddGlobal.
static const mapareacallback_t MAP_AREA_CALLBACK_LIST[] = {
NULL,
MAP_AREA_CALLBACK_REF(1),
};
//EOF
+179 -63
View File
@@ -14,28 +14,31 @@
#include "event/event.h" #include "event/event.h"
#include "util/string.h" #include "util/string.h"
#include "rpg/entity/global/entityglobal.h" #include "rpg/entity/global/entityglobal.h"
#include "rpg/entity/item/entityitem.h"
#include "rpg/overworld/maparea.h"
map_t MAP; map_t MAP;
// Clears chunk's mid-load slot, if it currently holds one.
static void mapChunkLoadingSlotClear(chunk_t *chunk) {
for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) {
if(MAP.loadingChunks[i] != chunk) continue;
MAP.loadingChunks[i] = NULL;
return;
}
}
errorret_t mapInit() { errorret_t mapInit() {
memoryZero(&MAP, sizeof(map_t)); memoryZero(&MAP, sizeof(map_t));
MAP.loaded = true; MAP.loaded = true;
MAP.loadPosition = (chunkpos_t){
-(MAP_CHUNK_SKIN),
-(MAP_CHUNK_SKIN),
-(MAP_CHUNK_SKIN)
};
chunkindex_t i = 0; chunkindex_t i = 0;
for(chunkunit_t z = 0; z < MAP_LOADED_CHUNK_DEPTH; z++) { for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
for(chunkunit_t y = 0; y < MAP_LOADED_CHUNK_HEIGHT; y++) { for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
for(chunkunit_t x = 0; x < MAP_LOADED_CHUNK_WIDTH; x++) { for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
chunk_t *chunk = &MAP.chunks[i++]; chunk_t *chunk = &MAP.chunks[i++];
chunk->position = (chunkpos_t){ chunk->position = (chunkpos_t){
MAP.loadPosition.x + (chunkunit_t)x, (chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
MAP.loadPosition.y + (chunkunit_t)y,
MAP.loadPosition.z + (chunkunit_t)z
}; };
errorChain(mapChunkLoad(chunk)); errorChain(mapChunkLoad(chunk));
} }
@@ -54,48 +57,23 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
if(!mapIsLoaded()) errorThrow("No map loaded"); if(!mapIsLoaded()) errorThrow("No map loaded");
if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk(); if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk();
// If the new render window still fits inside the current loaded area,
// just remap chunkOrder — no asset I/O needed.
const chunkpos_t lp = MAP.loadPosition;
if(
newPos.x >= lp.x &&
newPos.y >= lp.y &&
newPos.z >= lp.z &&
newPos.x + MAP_CHUNK_WIDTH <= lp.x + MAP_LOADED_CHUNK_WIDTH &&
newPos.y + MAP_CHUNK_HEIGHT <= lp.y + MAP_LOADED_CHUNK_HEIGHT &&
newPos.z + MAP_CHUNK_DEPTH <= lp.z + MAP_LOADED_CHUNK_DEPTH
) {
MAP.chunkPosition = newPos;
mapRebuildChunkOrder();
errorOk();
}
// Render window fell outside the loaded area — re-centre the load
// window on the new render position (skin buffer on every side).
const chunkpos_t newLoadPos = {
newPos.x - MAP_CHUNK_SKIN,
newPos.y - MAP_CHUNK_SKIN,
newPos.z - MAP_CHUNK_SKIN
};
// Separate loaded chunks into "keep" and "free" buckets. // Separate loaded chunks into "keep" and "free" buckets.
chunkindex_t chunksFreed[MAP_LOADED_CHUNK_COUNT]; chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
uint32_t freedCount = 0; uint32_t freedCount = 0;
// Use a boolean grid so the inner load loop can check O(1). // Use a boolean grid so the inner load loop can check O(1).
bool_t posLoaded[MAP_LOADED_CHUNK_WIDTH][MAP_LOADED_CHUNK_HEIGHT] bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
[MAP_LOADED_CHUNK_DEPTH];
memoryZero(posLoaded, sizeof(posLoaded)); memoryZero(posLoaded, sizeof(posLoaded));
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i]; chunk_t *chunk = &MAP.chunks[i];
chunkunit_t rx = chunk->position.x - newLoadPos.x; chunkunit_t rx = chunk->position.x - newPos.x;
chunkunit_t ry = chunk->position.y - newLoadPos.y; chunkunit_t ry = chunk->position.y - newPos.y;
chunkunit_t rz = chunk->position.z - newLoadPos.z; chunkunit_t rz = chunk->position.z - newPos.z;
if( if(
rx >= 0 && rx < MAP_LOADED_CHUNK_WIDTH && rx >= 0 && rx < MAP_CHUNK_WIDTH &&
ry >= 0 && ry < MAP_LOADED_CHUNK_HEIGHT && ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
rz >= 0 && rz < MAP_LOADED_CHUNK_DEPTH rz >= 0 && rz < MAP_CHUNK_DEPTH
) { ) {
posLoaded[rx][ry][rz] = true; posLoaded[rx][ry][rz] = true;
} else { } else {
@@ -104,23 +82,22 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
} }
} }
for(chunkunit_t z = 0; z < MAP_LOADED_CHUNK_DEPTH; z++) { for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
for(chunkunit_t y = 0; y < MAP_LOADED_CHUNK_HEIGHT; y++) { for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
for(chunkunit_t x = 0; x < MAP_LOADED_CHUNK_WIDTH; x++) { for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
if(posLoaded[x][y][z]) continue; if(posLoaded[x][y][z]) continue;
assertTrue(freedCount > 0, "No free chunk slot available."); assertTrue(freedCount > 0, "No free chunk slot available.");
chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]]; chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]];
chunk->position = (chunkpos_t){ chunk->position = (chunkpos_t){
newLoadPos.x + (chunkunit_t)x, newPos.x + (chunkunit_t)x,
newLoadPos.y + (chunkunit_t)y, newPos.y + (chunkunit_t)y,
newLoadPos.z + (chunkunit_t)z newPos.z + (chunkunit_t)z
}; };
errorChain(mapChunkLoad(chunk)); errorChain(mapChunkLoad(chunk));
} }
} }
} }
MAP.loadPosition = newLoadPos;
MAP.chunkPosition = newPos; MAP.chunkPosition = newPos;
mapRebuildChunkOrder(); mapRebuildChunkOrder();
errorOk(); errorOk();
@@ -131,13 +108,16 @@ errorret_t mapUpdate() {
} }
errorret_t mapDispose() { errorret_t mapDispose() {
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
mapChunkUnload(&MAP.chunks[i]); mapChunkUnload(&MAP.chunks[i]);
} }
errorOk(); errorOk();
} }
void mapChunkUnload(chunk_t *chunk) { void mapChunkUnload(chunk_t *chunk) {
mapChunkLoadQueueRemove(chunk);
mapChunkLoadingSlotClear(chunk);
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(chunk->entities[i] == 0xFF) continue; if(chunk->entities[i] == 0xFF) continue;
entity_t *entity = &ENTITIES[chunk->entities[i]]; entity_t *entity = &ENTITIES[chunk->entities[i]];
@@ -150,6 +130,12 @@ void mapChunkUnload(chunk_t *chunk) {
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
if(chunk->areas[i] == 0xFF) continue;
mapAreaRemove(chunk->areas[i]);
}
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
@@ -157,9 +143,10 @@ void mapChunkUnload(chunk_t *chunk) {
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
} }
// modelEntries are borrowed pointers, not independently locked - the
// chunk asset entry (released above) is what actually holds the ref on
// each model, so nothing to unlock here, just drop our own copies.
for(uint8_t m = 0; m < chunk->meshCount; m++) { for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] == NULL) continue;
assetUnlockEntry(chunk->modelEntries[m]);
chunk->modelEntries[m] = NULL; chunk->modelEntries[m] = NULL;
} }
chunk->meshCount = 0; chunk->meshCount = 0;
@@ -168,6 +155,9 @@ void mapChunkUnload(chunk_t *chunk) {
errorret_t mapChunkLoad(chunk_t *chunk) { errorret_t mapChunkLoad(chunk_t *chunk) {
if(!mapIsLoaded()) errorThrow("No map loaded"); if(!mapIsLoaded()) errorThrow("No map loaded");
mapChunkLoadQueueRemove(chunk);
mapChunkLoadingSlotClear(chunk);
if(chunk->dcfEntry != NULL) { if(chunk->dcfEntry != NULL) {
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
@@ -176,6 +166,16 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
} }
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
// Normally already empty (mapChunkUnload clears these before a chunk is
// handed back for reuse), but cleared defensively here too so a reload
// never leaks a MAP_AREAS slot referenced by a stale owned area ID.
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
if(chunk->areas[i] == 0xFF) continue;
mapAreaRemove(chunk->areas[i]);
}
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
chunk->meshCount = 0; chunk->meshCount = 0;
char_t name[64]; char_t name[64];
@@ -195,12 +195,69 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
errorOk(); errorOk();
} }
assertTrue(
MAP.loadQueueCount < MAP_CHUNK_COUNT,
"Chunk load queue overflow"
);
MAP.loadQueue[MAP.loadQueueCount++] = chunk;
mapChunkLoadNext();
errorOk();
}
void mapChunkLoadNext() {
for(uint32_t slot = 0; slot < MAP_CHUNK_LOAD_CONCURRENCY; slot++) {
if(MAP.loadingChunks[slot] != NULL) continue;
if(MAP.loadQueueCount == 0) return;
chunk_t *chunk = MAP.loadQueue[0];
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
}
MAP.loadQueueCount--;
MAP.loadingChunks[slot] = chunk;
char_t name[64];
stringFormat(
name, sizeof(name),
"chunks/%d_%d_%d.dcf",
(int32_t)chunk->position.x,
(int32_t)chunk->position.y,
(int32_t)chunk->position.z
);
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL); assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
assertNotNull(entry, "Failed to get chunk asset entry"); assertNotNull(entry, "Failed to get chunk asset entry");
chunk->dcfEntry = entry;
// The entry may already be resident from an earlier load that hasn't
// been reaped yet - in that case onLoaded/onError already fired once
// and never will again, so handle the terminal state directly instead
// of waiting on a subscription that would never trigger. Both of these
// recurse back into mapChunkLoadNext once they clear this slot, so the
// outer loop just continues on to try filling the next one.
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
mapChunkLoaded(entry, chunk);
continue;
}
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
mapChunkLoadError(entry, chunk);
continue;
}
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk); eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
eventSubscribe(&entry->onError, mapChunkLoadError, chunk); eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
chunk->dcfEntry = entry; }
errorOk(); }
void mapChunkLoadQueueRemove(chunk_t *chunk) {
for(uint32_t i = 0; i < MAP.loadQueueCount; i++) {
if(MAP.loadQueue[i] != chunk) continue;
for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) {
MAP.loadQueue[j - 1] = MAP.loadQueue[j];
}
MAP.loadQueueCount--;
return;
}
} }
@@ -312,7 +369,7 @@ entity_t * mapSpawnEntity(
void mapRebuildChunkOrder() { void mapRebuildChunkOrder() {
memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder)); memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder));
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i]; chunk_t *chunk = &MAP.chunks[i];
const chunkpos_t rel = { const chunkpos_t rel = {
chunk->position.x - MAP.chunkPosition.x, chunk->position.x - MAP.chunkPosition.x,
@@ -331,17 +388,23 @@ void mapRebuildChunkOrder() {
void mapChunkLoadError(void *params, void *user) { void mapChunkLoadError(void *params, void *user) {
assertNotNull(params, "mapChunkLoadError: params cannot be NULL"); assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
assertNotNull(user, "mapChunkLoadError: user cannot be NULL"); assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
assetentry_t *entry = (assetentry_t *)params;
chunk_t *chunk = (chunk_t *)user; chunk_t *chunk = (chunk_t *)user;
if(chunk->dcfEntry != (assetentry_t *)params) return; if(chunk->dcfEntry != entry) return;
consolePrint( consolePrint(
"Chunk load error: %d %d %d", "Chunk load error: %d %d %d",
(int32_t)chunk->position.x, (int32_t)chunk->position.x,
(int32_t)chunk->position.y, (int32_t)chunk->position.y,
(int32_t)chunk->position.z (int32_t)chunk->position.z
); );
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
eventUnsubscribe(&entry->onError, mapChunkLoadError);
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
mapChunkLoadingSlotClear(chunk);
mapChunkLoadNext();
} }
void mapChunkLoaded(void *params, void *user) { void mapChunkLoaded(void *params, void *user) {
@@ -385,10 +448,63 @@ void mapChunkLoaded(void *params, void *user) {
vec3 pos; vec3 pos;
glm_vec3_add(wpf, scaledOffset, pos); glm_vec3_add(wpf, scaledOffset, pos);
glm_translate_make(chunk->meshModels[m], pos); glm_translate_make(chunk->meshModels[m], pos);
// Borrow the pointer rather than stealing it - the chunk asset entry
// keeps its own lock on each model (taken once while it loaded) and we
// keep the chunk asset entry itself locked (see below), so the models
// stay valid for as long as this chunk_t is using them. The entry may
// now be reused by a later mapChunkLoad for a different chunk_t once we
// eventually unlock it in mapChunkUnload, at which point its
// modelEntries must still be intact for that next reuse to copy from.
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
entry->data.chunk.modelEntries[m] = NULL;
} }
assetUnlockEntry(chunk->dcfEntry); eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
chunk->dcfEntry = NULL; eventUnsubscribe(&entry->onError, mapChunkLoadError);
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
// chunk asset entry (and therefore its model locks) alive for as long as
// this chunk_t is displaying it. Released in mapChunkUnload instead.
chunk->meshCount = meshCount; chunk->meshCount = meshCount;
// Spawn entities declared by this chunk's file. Global entities are
// deduped by mapSpawnEntity itself (a persistent NPC that streams back
// in won't be duplicated); item entities have no persistent identity, so
// each reload spawns a fresh one - picking an item up and then leaving
// and re-entering its chunk will currently respawn it, since nothing
// tracks "already collected" across a chunk unload/reload yet.
for(uint8_t s = 0; s < entry->data.chunk.entitySpawnCount; s++) {
chunkentityspawn_t *spawn = &entry->data.chunk.entitySpawns[s];
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL) {
mapSpawnEntity((entityglobalid_t)spawn->globalId, spawn->position);
continue;
}
uint8_t index = entityGetAvailable();
assertTrue(index != 0xFF, "No available entity slots for chunk spawn");
entity_t *itemEntity = &ENTITIES[index];
entityInit(itemEntity, ENTITY_TYPE_ITEM);
entityItemSet(
itemEntity, (itemid_t)spawn->itemId, spawn->itemQuantity
);
entityPositionSet(itemEntity, spawn->position);
}
// Spawn map areas declared by this chunk's file, tracked as owned by
// this chunk so mapChunkUnload can tear them down again.
for(uint8_t s = 0; s < entry->data.chunk.areaSpawnCount; s++) {
chunkareaspawn_t *area = &entry->data.chunk.areaSpawns[s];
uint8_t areaId = mapAreaAddGlobal(
area->min, area->max, area->callbackId, area->notify, area->trigger
);
uint8_t slot = 0xFF;
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
if(chunk->areas[i] != 0xFF) continue;
slot = i;
break;
}
assertTrue(slot != 0xFF, "Chunk has no free owned-area slots");
chunk->areas[slot] = areaId;
}
mapChunkLoadingSlotClear(chunk);
mapChunkLoadNext();
} }
+24 -2
View File
@@ -12,13 +12,20 @@
#define MAP_FILE_PATH_MAX 128 #define MAP_FILE_PATH_MAX 128
// Number of chunks that may be mid-load (asset locked & awaiting onLoaded/
// onError) at the same time - everything past this waits in loadQueue.
#define MAP_CHUNK_LOAD_CONCURRENCY 2
typedef struct map_s { typedef struct map_s {
bool_t loaded; bool_t loaded;
chunk_t chunks[MAP_LOADED_CHUNK_COUNT]; chunk_t chunks[MAP_CHUNK_COUNT];
chunk_t *chunkOrder[MAP_CHUNK_COUNT]; chunk_t *chunkOrder[MAP_CHUNK_COUNT];
chunkpos_t chunkPosition; chunkpos_t chunkPosition;
chunkpos_t loadPosition;
chunk_t *loadQueue[MAP_CHUNK_COUNT];
uint32_t loadQueueCount;
chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY];
} map_t; } map_t;
extern map_t MAP; extern map_t MAP;
@@ -74,6 +81,21 @@ void mapChunkUnload(chunk_t* chunk);
*/ */
errorret_t mapChunkLoad(chunk_t* chunk); errorret_t mapChunkLoad(chunk_t* chunk);
/**
* Starts loading queued chunks until MAP_CHUNK_LOAD_CONCURRENCY chunks are
* mid-load. Called after mapChunkLoad enqueues a chunk, and again after a
* mid-load chunk finishes (or is unloaded) to advance the queue.
*/
void mapChunkLoadNext();
/**
* Removes a chunk from the load queue if present. Used when a chunk is
* re-queued or unloaded before its turn to load has come up.
*
* @param chunk The chunk to remove from the load queue.
*/
void mapChunkLoadQueueRemove(chunk_t *chunk);
/** /**
* Callback invoked when a chunk DCF asset fails to load. Fills the * Callback invoked when a chunk DCF asset fails to load. Fills the
* chunk tiles with TILE_SHAPE_GROUND as a fallback. * chunk tiles with TILE_SHAPE_GROUND as a fallback.
+19 -1
View File
@@ -10,6 +10,7 @@
#include "util/math.h" #include "util/math.h"
#include "util/memory.h" #include "util/memory.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "rpg/overworld/global/mapareaglobal.h"
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX]; maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
@@ -73,7 +74,7 @@ bool_t mapAreaIsChunkOverlappingOrInside(
bool_t mapAreaCanUnload(const maparea_t *area) { bool_t mapAreaCanUnload(const maparea_t *area) {
assertNotNull(area, "Map area pointer cannot be NULL"); assertNotNull(area, "Map area pointer cannot be NULL");
for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false; if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
} }
@@ -148,3 +149,20 @@ void mapAreaCheckEntity(entity_t *entity) {
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) { void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) {
} }
uint8_t mapAreaAddGlobal(
const worldpos_t min,
const worldpos_t max,
const uint16_t callbackId,
const uint8_t notify,
const uint8_t trigger
) {
assertTrue(callbackId > 0, "Map area callback ID 0 is reserved");
assertTrue(
callbackId < MAP_AREA_CALLBACK_LIST_COUNT,
"Map area callback ID is out of range"
);
return mapAreaAdd(
min, max, MAP_AREA_CALLBACK_LIST[callbackId], notify, trigger
);
}
+25
View File
@@ -159,3 +159,28 @@ void mapAreaCheckEntity(entity_t *entity);
* @param trigger Which MAP_TRIGGER_* condition invoked the callback. * @param trigger Which MAP_TRIGGER_* condition invoked the callback.
*/ */
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger); void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
/**
* Adds a map area using a compiled-in callback referenced by ID (see
* MAP_AREA_CALLBACK_LIST in rpg/overworld/global/mapareagloballist.h),
* rather than a direct function pointer. This is what lets chunk file
* data - which can only reference compiled code by a small integer ID,
* not a function pointer - declare map areas.
*
* @param min The minimum world position of the area.
* @param max The maximum world position of the area.
* @param callbackId Index into MAP_AREA_CALLBACK_LIST. Must be greater
* than 0 (0 is reserved) and within range.
* @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
* should trigger the callback.
* @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
* invoke the callback.
* @returns The ID of the newly added map area.
*/
uint8_t mapAreaAddGlobal(
const worldpos_t min,
const worldpos_t max,
const uint16_t callbackId,
const uint8_t notify,
const uint8_t trigger
);
-10
View File
@@ -30,16 +30,6 @@
#define MAP_CHUNK_DEPTH 4 #define MAP_CHUNK_DEPTH 4
#define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH) #define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH)
// Extra chunks loaded on every side beyond the render window.
// The render window can drift MAP_CHUNK_SKIN chunks in any direction
// before a load/unload cycle is triggered.
#define MAP_CHUNK_SKIN 1
#define MAP_LOADED_CHUNK_WIDTH (MAP_CHUNK_WIDTH + MAP_CHUNK_SKIN)
#define MAP_LOADED_CHUNK_HEIGHT (MAP_CHUNK_HEIGHT + MAP_CHUNK_SKIN)
#define MAP_LOADED_CHUNK_DEPTH (MAP_CHUNK_DEPTH + MAP_CHUNK_SKIN)
#define MAP_LOADED_CHUNK_COUNT \
(MAP_LOADED_CHUNK_WIDTH * MAP_LOADED_CHUNK_HEIGHT * MAP_LOADED_CHUNK_DEPTH)
#define ENTITY_COUNT 32 #define ENTITY_COUNT 32
typedef int16_t worldunit_t; typedef int16_t worldunit_t;
+27 -25
View File
@@ -7,29 +7,36 @@
#include "rpg.h" #include "rpg.h"
#include "entity/entity.h" #include "entity/entity.h"
#include "rpg/entity/npc/npcpath.h"
#include "rpg/entity/item/entityitem.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h" #include "rpg/overworld/maparea.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
#include "rpg/cutscene/scene/testcutscene.h"
#include "rpg/item/backpack.h" #include "rpg/item/backpack.h"
#include "rpg/battle/party.h" #include "rpg/battle/party.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
#include "time/time.h" #include "time/time.h"
#include "rpgcamera.h" #include "rpgcamera.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "console/console.h" #include "save/save.h"
#include "error/error.h"
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) { #include "ui/rpg/uiemoji.h"
consolePrint("rpgTestAreaCallback: trigger=%u", trigger); #include "rpg/story/storyflag.h"
static void rpgTestSaveComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
} }
errorret_t rpgInit(void) { errorret_t rpgInit(void) {
memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(ENTITIES, sizeof(ENTITIES));
memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
// Must run before any code reads a story flag - stamps CSV-defined
// defaults onto the active save slot if it's never actually been
// loaded from disk yet.
storyFlagInitDefaults(saveGet(SAVE_ACTIVE_SLOT));
backpackInit(); backpackInit();
partyInit(); partyInit();
cutsceneSystemInit(); cutsceneSystemInit();
@@ -40,7 +47,9 @@ errorret_t rpgInit(void) {
// Init world // Init world
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 })); errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
// TEST: Create some entities. // The player is the one entity that isn't sourced from map/chunk data -
// every other entity (NPCs, items) and map area comes from the loaded
// chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded).
uint8_t entIndex = entityGetAvailable(); uint8_t entIndex = entityGetAvailable();
assertTrue(entIndex != 0xFF, "No available entity slots!."); assertTrue(entIndex != 0xFF, "No available entity slots!.");
entity_t *ent = &ENTITIES[entIndex]; entity_t *ent = &ENTITIES[entIndex];
@@ -49,25 +58,18 @@ errorret_t rpgInit(void) {
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
RPG_CAMERA.followEntity.followEntityId = ent->id; RPG_CAMERA.followEntity.followEntityId = ent->id;
mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 }); // Starting inventory.
backpackAdd(ITEM_ID_POTION, 5);
backpackAdd(ITEM_ID_POTATO, 3);
backpackAdd(ITEM_ID_APPLE, 8);
// TEST: Place an item entity. // TEST: Verify the save system round-trips real game data, not just the
uint8_t itemEntIndex = entityGetAvailable(); // header/version. Remove once there's an actual name-entry flow. On PSP
assertTrue(itemEntIndex != 0xFF, "No available entity slots!."); // this shows the real native save dialog every boot - expected while
entity_t *itemEnt = &ENTITIES[itemEntIndex]; // testing that path, not something to ship as-is.
entityInit(itemEnt, ENTITY_TYPE_ITEM); savefile_t *saveFile = saveGet(SAVE_ACTIVE_SLOT);
entityItemSet(itemEnt, ITEM_ID_POTION, 1); stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 }); saveWrite(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL);
// TEST: Create a test map area.
uint8_t areaIndex = mapAreaAdd(
(worldpos_t){ 11, 3, 0 },
(worldpos_t){ 16, 9, 10 },
rpgTestAreaCallback,
MAP_AREA_NOTIFY_ALL,
MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT
);
assertTrue(areaIndex != 0xFF, "No available map area slots!.");
// All Good! // All Good!
errorOk(); errorOk();
+57
View File
@@ -7,12 +7,18 @@
#include "rpgcamera.h" #include "rpgcamera.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/random.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "time/time.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
static const float_t RPG_CAMERA_SHAKE_AMOUNTS[] = {
0.0f, 0.5f, 1.0f, 2.0f, 3.0f
};
rpgcamera_t RPG_CAMERA; rpgcamera_t RPG_CAMERA;
void rpgCameraInit(void) { void rpgCameraInit(void) {
@@ -20,6 +26,13 @@ void rpgCameraInit(void) {
RPG_CAMERA.projectionDirty = true; RPG_CAMERA.projectionDirty = true;
} }
void rpgCameraShake(uint8_t amount, float_t duration) {
assertTrue(amount <= 4, "Camera shake amount must be between 0 and 4");
RPG_CAMERA.shakeAmount = RPG_CAMERA_SHAKE_AMOUNTS[amount];
RPG_CAMERA.shakeDuration = duration;
RPG_CAMERA.shakeTime = 0.0f;
}
void rpgCameraGetPosition(vec3 out) { void rpgCameraGetPosition(vec3 out) {
switch(RPG_CAMERA.mode) { switch(RPG_CAMERA.mode) {
case RPG_CAMERA_MODE_FREE: case RPG_CAMERA_MODE_FREE:
@@ -56,7 +69,51 @@ void rpgCameraUpdateProjection(void) {
); );
} }
void rpgCameraUpdateEye(void) {
float_t fov = glm_rad(RPG_CAMERA_FOV);
float_t pixelsPerUnit = TILE_SIZE_PIXELS;
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit;
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
vec3 target;
rpgCameraGetPosition(target);
glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target);
if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
float_t t = 1.0f - (RPG_CAMERA.shakeTime / RPG_CAMERA.shakeDuration);
float_t magnitude = RPG_CAMERA.shakeAmount * t;
target[0] += randomFloat(-magnitude, magnitude);
target[2] += randomFloat(-magnitude, magnitude);
}
glm_lookat(
(vec3){ target[0], target[1] + offset, target[2] + z },
target,
(vec3){ 0, 1, 0 }, // up
RPG_CAMERA.eye
);
}
void rpgCameraToScreen(vec3 worldPos, vec2 out) {
mat4 viewProj;
glm_mat4_mul(RPG_CAMERA.projection, RPG_CAMERA.eye, viewProj);
vec4 viewport = {
0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height
};
vec3 window;
glm_project(worldPos, viewProj, viewport, window);
out[0] = window[0];
out[1] = (float_t)SCREEN.height - window[1];
}
errorret_t rpgCameraUpdate(void) { errorret_t rpgCameraUpdate(void) {
if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) {
RPG_CAMERA.shakeTime += TIME.delta;
}
if(!mapIsLoaded()) errorOk(); if(!mapIsLoaded()) errorOk();
vec3 pos; vec3 pos;
+32
View File
@@ -30,6 +30,10 @@ typedef struct {
mat4 eye; mat4 eye;
mat4 projection; mat4 projection;
bool_t projectionDirty; bool_t projectionDirty;
float_t shakeAmount;
float_t shakeDuration;
float_t shakeTime;
} rpgcamera_t; } rpgcamera_t;
extern rpgcamera_t RPG_CAMERA; extern rpgcamera_t RPG_CAMERA;
@@ -60,3 +64,31 @@ errorret_t rpgCameraUpdate(void);
* is recomputed every call. * is recomputed every call.
*/ */
void rpgCameraUpdateProjection(void); void rpgCameraUpdateProjection(void);
/**
* Recomputes the camera eye/view matrix from the camera's current mode
* and position, and stores it in RPG_CAMERA.eye. Unlike the projection
* matrix this is never cached, since the camera position can change
* every frame.
*/
void rpgCameraUpdateEye(void);
/**
* Shakes the RPG camera, randomly offsetting its position by a
* decreasing amount over the given duration.
*
* @param amount Shake strength from 0 (no shake) to 4 (three tiles).
* 1 is half a tile, 2 is a full tile, 3 is two tiles, and 4 is three
* tiles.
* @param duration How long the shake lasts, in seconds.
*/
void rpgCameraShake(uint8_t amount, float_t duration);
/**
* Converts a world-space position to screen-space pixel coordinates,
* using the camera's current eye and projection matrices.
*
* @param worldPos The world-space position to convert.
* @param out Output vec2 filled with the screen-space pixel position.
*/
void rpgCameraToScreen(vec3 worldPos, vec2 out);
+13 -1
View File
@@ -10,5 +10,17 @@
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) { void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag"); assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
STORY_FLAG_VALUES[flag] = value; saveGet(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value;
}
void storyFlagInitDefaults(savefile_t *file) {
assertNotNull(file, "Save file cannot be NULL");
if(file->exists) return;
assertTrue(
STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX,
"Too many story flags for the save format - bump SAVE_STORY_FLAG_COUNT_MAX"
);
for(storyflag_t i = 0; i < STORY_FLAG_COUNT; i++) {
file->storyFlags[i] = STORY_FLAG_DEFAULTS[i];
}
} }
+18 -3
View File
@@ -7,19 +7,34 @@
#pragma once #pragma once
#include "rpg/story/storyflagvalue.h" #include "rpg/story/storyflagvalue.h"
#include "save/save.h"
/** /**
* Gets the value of a story flag. * Gets the value of a story flag. Reads directly from the active save
* file (see SAVE_ACTIVE_SLOT) - flag values have no separate live copy.
* *
* @param flag The story flag to get. * @param flag The story flag to get.
* @return The value of the story flag. * @return The value of the story flag.
*/ */
#define storyFlagGet(flag) (STORY_FLAG_VALUES[(flag)]) #define storyFlagGet(flag) (saveGet(SAVE_ACTIVE_SLOT)->storyFlags[(flag)])
/** /**
* Sets the value of a story flag. * Sets the value of a story flag, directly in the active save file (see
* SAVE_ACTIVE_SLOT). Does not itself write the save to disk - call
* saveWrite() separately once ready to persist it.
* *
* @param flag The story flag to set. * @param flag The story flag to set.
* @param value The value to set the story flag to. * @param value The value to set the story flag to.
*/ */
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value); void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
/**
* Stamps each story flag's CSV-defined default (STORY_FLAG_DEFAULTS) onto
* the given save file, but only if it hasn't actually been loaded from
* disk yet (file->exists is false) - otherwise leaves already-played
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
* code reads a story flag.
*
* @param file The save file to stamp defaults onto.
*/
void storyFlagInitDefaults(savefile_t *file);
+75 -17
View File
@@ -9,19 +9,38 @@
#include "save/savestream.h" #include "save/savestream.h"
#include "util/memory.h" #include "util/memory.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "error/error.h"
save_t SAVE; save_t SAVE;
errorret_t saveInit(void) { errorret_t saveInit(void) {
memoryZero(&SAVE, sizeof(save_t)); memoryZero(&SAVE, sizeof(save_t));
// Establishes the default for a slot that hasn't actually been loaded
// from disk yet - saveLoad() overwrites this the moment a real file is
// found, so this only matters for a brand new save.
for(uint8_t i = 0; i < SAVE_FILE_COUNT_MAX; i++) {
SAVE.files[i].deadzone = SAVE_DEADZONE_DEFAULT;
}
#ifdef saveInitPlatform #ifdef saveInitPlatform
errorChain(saveInitPlatform()); // A missing/unreachable save medium is expected, recoverable state,
// not a reason to fail booting the whole game - log it and carry on
// with SAVE.available false instead of chaining the error upward.
errorret_t result = saveInitPlatform();
SAVE.available = errorIsOk(result);
if(!SAVE.available) errorCatch(errorPrint(result));
#else
SAVE.available = false;
#endif #endif
errorOk(); errorOk();
} }
bool_t saveIsAvailable(void) {
return SAVE.available;
}
errorret_t saveDispose(void) { errorret_t saveDispose(void) {
#ifdef saveDisposePlatform #ifdef saveDisposePlatform
errorChain(saveDisposePlatform()); errorChain(saveDisposePlatform());
@@ -29,20 +48,46 @@ errorret_t saveDispose(void) {
errorOk(); errorOk();
} }
errorret_t saveLoad(const uint8_t slot) { errorret_t saveUpdate(void) {
#ifdef savePlatformUpdate
errorChain(savePlatformUpdate());
#endif
errorOk();
}
bool_t saveIsBusy(void) {
#ifdef saveIsBusyPlatform
return saveIsBusyPlatform();
#else
return false;
#endif
}
void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
assertNotNull(onComplete, "onComplete cannot be NULL");
savefile_t *file = &SAVE.files[slot]; savefile_t *file = &SAVE.files[slot];
file->exists = false; file->exists = false;
// Some platforms (PSP's native save dialog) can't complete within this
// call - they take over entirely and invoke onComplete later, from
// saveUpdate(), once their own multi-frame flow finishes.
#ifdef saveAsyncLoadPlatform
saveAsyncLoadPlatform(slot, onComplete, user);
return;
#endif
savestream_t stream; savestream_t stream;
memoryZero(&stream, sizeof(savestream_t)); memoryZero(&stream, sizeof(savestream_t));
#ifdef saveStreamOpenReadPlatform #ifdef saveStreamOpenReadPlatform
errorChain(saveStreamOpenReadPlatform(&stream, slot)); errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot);
SAVE.available = errorIsOk(openRet);
if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; }
#endif #endif
if(!stream.found) errorOk(); if(!stream.found) { onComplete(errorOkImpl(), user); return; }
errorret_t ret = saveFileLoad(&stream, file); errorret_t ret = saveFileLoad(&stream, file);
@@ -50,24 +95,37 @@ errorret_t saveLoad(const uint8_t slot) {
saveStreamClosePlatform(&stream); saveStreamClosePlatform(&stream);
#endif #endif
if(errorIsNotOk(ret)) return ret; if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot);
file->exists = errorIsOk(ret);
errorChain(saveStreamVerifyChecksumImpl(&stream, slot)); onComplete(ret, user);
file->exists = true;
errorOk();
} }
errorret_t saveWrite(const uint8_t slot) { void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
assertNotNull(onComplete, "onComplete cannot be NULL");
savefile_t *file = &SAVE.files[slot]; savefile_t *file = &SAVE.files[slot];
// These are metadata about the file itself, not game data - always stamp
// the current magic/version on every write rather than relying on
// whatever happened to already be in memory (zeroed at saveInit, or
// whatever version an old loaded file had), otherwise the written file
// fails its own header check the next time it's loaded.
memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE);
file->version = SAVE_FILE_VERSION;
// See saveLoad() - some platforms take over and complete later.
#ifdef saveAsyncWritePlatform
saveAsyncWritePlatform(slot, onComplete, user);
return;
#endif
savestream_t stream; savestream_t stream;
memoryZero(&stream, sizeof(savestream_t)); memoryZero(&stream, sizeof(savestream_t));
#ifdef saveStreamOpenWritePlatform #ifdef saveStreamOpenWritePlatform
errorChain(saveStreamOpenWritePlatform(&stream, slot)); errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot);
SAVE.available = errorIsOk(openRet);
if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; }
#endif #endif
errorret_t ret = saveFileWrite(&stream, file); errorret_t ret = saveFileWrite(&stream, file);
@@ -80,17 +138,17 @@ errorret_t saveWrite(const uint8_t slot) {
saveStreamClosePlatform(&stream); saveStreamClosePlatform(&stream);
#endif #endif
if(errorIsNotOk(ret)) return ret; file->exists = errorIsOk(ret);
onComplete(ret, user);
file->exists = true;
errorOk();
} }
errorret_t saveDelete(const uint8_t slot) { errorret_t saveDelete(const uint8_t slot) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
#ifdef saveDeletePlatform #ifdef saveDeletePlatform
errorChain(saveDeletePlatform(slot)); errorret_t deleteRet = saveDeletePlatform(slot);
SAVE.available = errorIsOk(deleteRet);
errorChain(deleteRet);
#endif #endif
SAVE.files[slot].exists = false; SAVE.files[slot].exists = false;
+67 -9
View File
@@ -15,17 +15,46 @@ typedef struct {
savefile_t files[SAVE_FILE_COUNT_MAX]; savefile_t files[SAVE_FILE_COUNT_MAX];
/** Platform-specific save system state (paths, card handles, etc.). */ /** Platform-specific save system state (paths, card handles, etc.). */
saveplatform_t platform; saveplatform_t platform;
/**
* True if the save medium (memory card/stick/disk) was reachable the
* last time it was checked - at saveInit(), and refreshed by every
* subsequent saveLoad()/saveWrite() attempt. Starting the game with no
* card/stick inserted, or one being removed mid-session, are both
* expected conditions here, not fatal errors - see saveIsAvailable().
*/
bool_t available;
/**
* Scratch error state used by platforms whose save/load completes
* asynchronously (see saveIsBusy()) to construct a result to hand to a
* savecallback_t from inside saveUpdate(), rather than from a direct
* errorThrow() return - mirrors network_t.errorState for the same reason.
*/
errorstate_t errorState;
} save_t; } save_t;
extern save_t SAVE; extern save_t SAVE;
/** /**
* Initializes the save system. * Initializes the save system. Never fails the way saveWrite/saveLoad can -
* if the platform's save medium isn't reachable (e.g. no memory card/stick
* inserted), that's logged and reflected in saveIsAvailable() rather than
* treated as fatal, since the game should still be playable without save
* support.
* *
* @return An error code if initialization fails. * @return An error code only for unexpected platform failures.
*/ */
errorret_t saveInit(void); errorret_t saveInit(void);
/**
* Checks whether the save medium was reachable as of the last save/load
* attempt (or saveInit(), if none has been attempted yet). Intended for UI
* to decide whether to offer saving/loading at all, or to explain why it
* isn't available right now - e.g. "No memory card inserted".
*
* @return true if the save medium was available last time it was checked.
*/
bool_t saveIsAvailable(void);
/** /**
* Disposes of the save system. * Disposes of the save system.
* *
@@ -34,20 +63,49 @@ errorret_t saveInit(void);
errorret_t saveDispose(void); errorret_t saveDispose(void);
/** /**
* Loads the save file for a given slot from persistent storage. * Updates the save manager, pumping any in-progress async save/load and
* dispatching its callback once complete. No-op on platforms where
* saveWrite()/saveLoad() always complete synchronously (see saveIsBusy()).
* Must be called every engine frame for platforms that need it (PSP's
* native save dialog spans multiple frames).
* *
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). * @return An error code indicating success or failure.
* @return An error code if the load fails.
*/ */
errorret_t saveLoad(const uint8_t slot); errorret_t saveUpdate(void);
/** /**
* Writes the save file for a given slot to persistent storage. * True while an async saveWrite()/saveLoad() is in progress (e.g. PSP's
* native save dialog is open). Calling saveWrite()/saveLoad() again while
* this is true is undefined behavior - wait for the previous call's
* callback first.
*
* @return True if a save/load request is currently in progress.
*/
bool_t saveIsBusy(void);
/**
* Loads the save file for a given slot from persistent storage. Slow/async
* on some platforms (PSP's native save dialog spans multiple frames) - on
* others (Linux, Dolphin) onComplete is invoked before this call returns.
* See saveIsBusy().
* *
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @return An error code if the write fails. * @param onComplete Callback invoked with the result once loading finishes.
* @param user User data passed through to onComplete.
*/ */
errorret_t saveWrite(const uint8_t slot); void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user);
/**
* Writes the save file for a given slot to persistent storage. Slow/async
* on some platforms (PSP's native save dialog spans multiple frames) - on
* others (Linux, Dolphin) onComplete is invoked before this call returns.
* See saveIsBusy().
*
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @param onComplete Callback invoked with the result once writing finishes.
* @param user User data passed through to onComplete.
*/
void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user);
/** /**
* Deletes the save file for a given slot from persistent storage. * Deletes the save file for a given slot from persistent storage.
+65
View File
@@ -20,6 +20,44 @@
/** Maximum number of independent save slots supported. */ /** Maximum number of independent save slots supported. */
#define SAVE_FILE_COUNT_MAX 3 #define SAVE_FILE_COUNT_MAX 3
/**
* The save slot actually used for gameplay right now - there's no slot
* select/multi-save UX yet (SAVE_FILE_COUNT_MAX > 1 exists for later), so
* every part of the game that needs "the" save file (settings, the game
* menu's Save button, etc.) reads/writes this one slot.
*/
#define SAVE_ACTIVE_SLOT 0
/** Maximum length of a saved player name, including the null terminator. */
#define SAVE_PLAYER_NAME_MAX 32
/**
* Maximum number of global entities whose "collected" state can be
* tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here
* rather than tied to ENTITY_GLOBAL_LIST_COUNT, since savefile.h is a
* leaf header with no dependency on the entity system (and no reason to
* take one just for a size constant).
*/
#define SAVE_GLOBAL_ITEM_COUNT_MAX 64
/**
* Default gamepad deadzone for a save slot that's never actually been
* loaded from disk yet (see saveInit(), which stamps this onto every
* slot up front) - defined here, rather than by the input system, since
* the save file is now the single source of truth for this value (see
* savefile_t.deadzone) - nothing else stores or defaults it.
*/
#define SAVE_DEADZONE_DEFAULT 0.1f
/**
* Maximum number of story flags the save format can hold - see
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
* current flag count) rather than tied to STORY_FLAG_COUNT, since
* savefile.h is a leaf header with no dependency on generated story
* content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning.
*/
#define SAVE_STORY_FLAG_COUNT_MAX 128
typedef struct { typedef struct {
/** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */ /** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */
char_t header[SAVE_FILE_HEADER_SIZE]; char_t header[SAVE_FILE_HEADER_SIZE];
@@ -27,4 +65,31 @@ typedef struct {
uint32_t version; uint32_t version;
/** Runtime flag - true if this slot was successfully loaded or written. */ /** Runtime flag - true if this slot was successfully loaded or written. */
bool_t exists; bool_t exists;
/** The player's saved name. */
char_t playerName[SAVE_PLAYER_NAME_MAX];
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX];
/**
* User-configured gamepad deadzone (0.0f-1.0f) - the save file is the
* only place this lives; read it directly via saveGet(SAVE_ACTIVE_SLOT)
* ->deadzone rather than caching it anywhere else.
*/
float_t deadzone;
/**
* Story flag values, indexed by storyflag_t - the save file is the only
* place these live; read/write via storyFlagGet()/storyFlagSet() (see
* rpg/story/storyflag.h), not directly.
*/
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
} savefile_t; } savefile_t;
/**
* Callback invoked when an async saveWrite()/saveLoad() request completes.
* Declared here (rather than save.h) so platform save headers - which
* save.h's platform indirection pulls in before save.h finishes defining
* anything else - can reference it without a circular include.
*
* @param result Whether the request succeeded.
* @param user User data passed through from the original call.
*/
typedef void (*savecallback_t)(errorret_t result, void *user);
+16
View File
@@ -330,11 +330,27 @@ errorret_t saveStreamWriteDateImpl(
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) { errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
saveFileReadHeader(stream, file->header); saveFileReadHeader(stream, file->header);
saveFileReadVersion(stream, &file->version); saveFileReadVersion(stream, &file->version);
saveFileReadString(stream, file->playerName, SAVE_PLAYER_NAME_MAX);
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
saveFileReadBool(stream, &file->globalItemCollected[i]);
}
saveFileReadFloat(stream, &file->deadzone);
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
saveFileReadUInt8(stream, &file->storyFlags[i]);
}
errorOk(); errorOk();
} }
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) { errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
saveFileWriteHeader(stream, file->header); saveFileWriteHeader(stream, file->header);
saveFileWriteVersion(stream, &file->version); saveFileWriteVersion(stream, &file->version);
saveFileWriteString(stream, file->playerName, SAVE_PLAYER_NAME_MAX);
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
saveFileWriteBool(stream, &file->globalItemCollected[i]);
}
saveFileWriteFloat(stream, &file->deadzone);
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
saveFileWriteUInt8(stream, &file->storyFlags[i]);
}
errorOk(); errorOk();
} }
+1
View File
@@ -11,3 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Subdirs # Subdirs
add_subdirectory(overworld) add_subdirectory(overworld)
add_subdirectory(battle)
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
scenebattle.c
)
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "scenebattle.h"
#include "rpg/battle/battle.h"
errorret_t sceneBattleInit(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
battleUpdate();
errorOk();
}
errorret_t sceneBattleRender(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneBattleDispose(scenedata_t *sceneData) {
errorOk();
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "scene/scenebase.h"
typedef struct {
} scenebattle_t;
/**
* Initializes the battle scene. The battle itself (BATTLE global) is
* expected to already be started, e.g. by a StartBattle cutscene item.
*
* @param sceneData The scene data used for this scene.
* @return An error if the init failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleInit(scenedata_t *sceneData);
/**
* Updates the battle scene, ticking the battle simulation.
*
* @param sceneData The scene data used for this scene.
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleUpdate(scenedata_t *sceneData);
/**
* Renders the battle scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the render failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleRender(scenedata_t *sceneData);
/**
* Disposes the battle scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneBattleDispose(scenedata_t *sceneData);
+105 -55
View File
@@ -24,6 +24,8 @@
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h" #include "asset/loader/assetentry.h"
#include "util/memory.h"
errorret_t sceneOverworldInit(scenedata_t *sceneData) { errorret_t sceneOverworldInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorOk(); errorOk();
@@ -31,71 +33,127 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData) {
errorret_t sceneOverworldUpdate(scenedata_t *sceneData) { errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorOk(); errorOk();
} }
errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorret_t sceneOverworldRender(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorChain(displaySetState((displaystate_t){ sceneoverworld_t *overworld = &sceneData->overworld;
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL sceneOverworldCullUpdate(overworld);
}));
mat4 model, eye;
// Overworld camera
errorChain(shaderBind(&SHADER_UNLIT)); errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
));
// Model
glm_mat4_identity(model);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
// Camera projection
rpgCameraUpdateProjection(); rpgCameraUpdateProjection();
errorChain(shaderSetMatrix( errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection
)); ));
// Camera Eye rpgCameraUpdateEye();
float_t fov = glm_rad(RPG_CAMERA_FOV); errorChain(shaderSetMatrix(
float_t pixelsPerUnit = TILE_SIZE_PIXELS; &SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye
float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit; ));
float_t z = (worldH * 0.5f) / tanf(fov * 0.5f);
vec3 worldPosVec;
rpgCameraGetPosition(worldPosVec);
float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS);
glm_vec3_add(worldPosVec, (vec3){ 0.5f, 0.5f, 0.5f }, worldPosVec); // Chunk tiles
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksBase(overworld));
glm_lookat( // Entities
(vec3){
worldPosVec[0],
worldPosVec[1] + offset,
worldPosVec[2] + z
},
worldPosVec,
(vec3){ 0, 1, 0 }, // up
eye
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye));
// Base terrain meshes, drawn with normal depth testing.
errorChain(sceneOverworldDrawChunksBase());
// Entities are drawn with depth testing fully disabled so sloped tiles
// (ramps) never clip them; entity-vs-entity overlap falls back to
// array draw order instead of true depth.
errorChain(displaySetState((displaystate_t){ errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_CULL .flags = DISPLAY_STATE_FLAG_CULL
})); }));
// Entities
{
for(uint8_t i = 0; i < ENTITY_COUNT; i++) { for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
entity_t *ent = &ENTITIES[i]; entity_t *ent = &ENTITIES[i];
if(ent->type == ENTITY_TYPE_NULL) continue; if(ent->type == ENTITY_TYPE_NULL) continue;
errorChain(sceneOverworldDrawEntity(overworld, ent));
}
// Chunk props
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksProps(overworld));
// Weather effects
// errorChain(shaderSetMatrix(
// &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
// ));
// errorChain(shaderSetMatrix(
// &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
// ));
// errorChain(displaySetState((displaystate_t){
// .flags = DISPLAY_STATE_FLAG_BLEND
// }));
// spritebatchsprite_t skyboxSprite;
// memoryZero(&skyboxSprite, sizeof(spritebatchsprite_t));
// skyboxSprite.max[0] = (float_t)(SCREEN.width / SCREEN.scaleUi);
// skyboxSprite.max[1] = (float_t)(SCREEN.height / SCREEN.scaleUi);
// shadermaterial_t skyboxMaterial = {
// .unlit = { .color = color(0xFF, 0x00, 0x00, 0xFF/2), .texture = NULL }
// };
// errorChain(spriteBatchBuffer(&skyboxSprite, 1, &SHADER_UNLIT, skyboxMaterial));
// errorChain(spriteBatchFlush());
errorOk();
}
void sceneOverworldCullUpdate(sceneoverworld_t *overworld) {
rpgCameraGetPosition(overworld->cullTarget);
const float_t worldH =
(float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS;
const float_t worldW = worldH * SCREEN.aspect;
overworld->cullHalfRangeX =
(worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X;
overworld->cullHalfRangeY =
(worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y;
}
bool_t sceneOverworldChunkShouldRender(
const sceneoverworld_t *overworld,
const chunk_t *chunk
) {
worldpos_t worldPos;
chunkPosToWorldPos(&chunk->position, &worldPos);
const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f);
const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f);
if(fabsf(overworld->cullTarget[0] - chunkCenterX) >
overworld->cullHalfRangeX) return false;
if(fabsf(overworld->cullTarget[1] - chunkCenterY) >
overworld->cullHalfRangeY) return false;
return true;
}
bool_t sceneOverworldEntityShouldRender(
const sceneoverworld_t *overworld,
const entity_t *ent
) {
if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) >
overworld->cullHalfRangeX) return false;
if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) >
overworld->cullHalfRangeY) return false;
return true;
}
errorret_t sceneOverworldDrawEntity(
const sceneoverworld_t *overworld,
entity_t *ent
) {
if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk();
spritebatchsprite_t sprite; spritebatchsprite_t sprite;
glm_vec3_copy(ent->renderPosition, sprite.min); glm_vec3_copy(ent->renderPosition, sprite.min);
@@ -117,26 +175,17 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
shadermaterial_t material = { shadermaterial_t material = {
.unlit = { .color = color, .texture = NULL } .unlit = { .color = color, .texture = NULL }
}; };
spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material); errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
spriteBatchFlush(); errorChain(spriteBatchFlush());
}
}
// Other chunk meshes (trees, buildings, etc), drawn last with normal
// depth testing so they correctly occlude entities standing beneath
// them.
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
errorChain(sceneOverworldDrawChunksProps());
errorOk(); errorOk();
} }
errorret_t sceneOverworldDrawChunksBase() { errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = MAP.chunkOrder[i]; chunk_t *chunk = MAP.chunkOrder[i];
if(chunk == NULL) continue; if(chunk == NULL) continue;
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
if(chunk->meshCount == 0) continue; if(chunk->meshCount == 0) continue;
if(chunk->modelEntries[0] == NULL) continue; if(chunk->modelEntries[0] == NULL) continue;
if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue; if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue;
@@ -169,10 +218,11 @@ errorret_t sceneOverworldDrawChunksBase() {
errorOk(); errorOk();
} }
errorret_t sceneOverworldDrawChunksProps() { errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = MAP.chunkOrder[i]; chunk_t *chunk = MAP.chunkOrder[i];
if(chunk == NULL) continue; if(chunk == NULL) continue;
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
for(uint8_t m = 1; m < chunk->meshCount; m++) { for(uint8_t m = 1; m < chunk->meshCount; m++) {
if(chunk->modelEntries[m] == NULL) continue; if(chunk->modelEntries[m] == NULL) continue;
+90 -3
View File
@@ -7,11 +7,35 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
#include "rpg/overworld/chunk.h"
#include "rpg/entity/entity.h"
typedef struct { typedef struct {
// Cached per-frame chunk culling bounds, refreshed by
// sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender()
// does not need to recompute them for every chunk.
vec3 cullTarget;
float_t cullHalfRangeX;
float_t cullHalfRangeY;
} sceneoverworld_t; } sceneoverworld_t;
// Extra world-space margin, in tiles, added on top of the rough
// screen-size skin below - cheap insurance against the approximation
// culling a chunk that is actually still just in view. Y gets its own
// value since the camera's tilt makes the vertical check less exact.
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f
#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f
// The camera is tilted between the ground-depth (Y) and height (Z)
// axes, so a chunk's on-screen vertical extent also depends on its Z
// layers. Rather than projecting every corner, fold that into a
// generous skin margin on the depth check.
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \
((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X)
#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \
((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \
SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y)
/** /**
* Initialises the overworld scene. * Initialises the overworld scene.
* *
@@ -28,22 +52,85 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
*/ */
errorret_t sceneOverworldUpdate(scenedata_t *sceneData); errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
/**
* Refreshes the chunk culling bounds cached on the overworld scene data,
* from the current camera position and screen dimensions. Must be
* called once per frame before sceneOverworldChunkShouldRender().
*
* @param overworld The overworld scene data to update.
*/
void sceneOverworldCullUpdate(sceneoverworld_t *overworld);
/**
* Roughly checks whether a chunk falls within the visible screen area,
* based on chunk size and screen dimensions. Cheap approximation, not
* an exact frustum check - includes a generous skin margin to account
* for the camera's angle tilting height/depth onto the screen's
* vertical axis, so it may pass some chunks that are actually just out
* of view but will never wrongly cull one that is visible.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param chunk The chunk to check.
* @return true if the chunk should be rendered, false otherwise.
*/
bool_t sceneOverworldChunkShouldRender(
const sceneoverworld_t *overworld,
const chunk_t *chunk
);
/** /**
* Draws the base (tile) mesh of every loaded chunk, with normal depth * Draws the base (tile) mesh of every loaded chunk, with normal depth
* testing. Must be called before entities are rendered. * testing. Must be called before entities are rendered.
* *
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @return An error if drawing failed, or errorOk() on success. * @return An error if drawing failed, or errorOk() on success.
*/ */
errorret_t sceneOverworldDrawChunksBase(); errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld);
/** /**
* Draws every loaded chunk's additional meshes (trees, buildings, etc), * Draws every loaded chunk's additional meshes (trees, buildings, etc),
* with normal depth testing so they correctly occlude entities standing * with normal depth testing so they correctly occlude entities standing
* beneath them. Must be called after entities are rendered. * beneath them. Must be called after entities are rendered.
* *
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @return An error if drawing failed, or errorOk() on success. * @return An error if drawing failed, or errorOk() on success.
*/ */
errorret_t sceneOverworldDrawChunksProps(); errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld);
/**
* Roughly checks whether an entity falls within the visible screen
* area. Reuses the same cached chunk culling bounds, since an entity
* is just a point within that same space - deliberately cheap, a
* couple of subtractions and comparisons, since it runs per entity
* every frame and it is not worth spending more math than that to
* avoid drawing one that is slightly off-screen.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param ent The entity to check.
* @return true if the entity should be rendered, false otherwise.
*/
bool_t sceneOverworldEntityShouldRender(
const sceneoverworld_t *overworld,
const entity_t *ent
);
/**
* Draws a single entity as a sprite, colored by its facing direction.
* Skips drawing (without error) if the entity should not render.
*
* @param overworld The overworld scene data holding the cached culling
* bounds, as refreshed by sceneOverworldCullUpdate().
* @param ent The entity to draw.
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawEntity(
const sceneoverworld_t *overworld,
entity_t *ent
);
/** /**
* Renders the overworld scene. * Renders the overworld scene.
+26 -19
View File
@@ -58,6 +58,23 @@ errorret_t sceneUpdate(void) {
} }
errorret_t sceneRender(void) { errorret_t sceneRender(void) {
// Setup screen matrices for 3D rendering.
glm_mat4_identity(SCENE.screenIdentity);
glm_ortho(
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
0.1f, 100.0f,
SCENE.screenProj
);
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
SCENE.screenView
);
// Scene rendering // Scene rendering
if( if(
SCENE.current != SCENE_TYPE_NULL && SCENE.current != SCENE_TYPE_NULL &&
@@ -67,26 +84,16 @@ errorret_t sceneRender(void) {
} }
// UI Rendering // UI Rendering
mat4 proj, view, ident;
glm_mat4_identity(ident);
errorChain(shaderBind(&SHADER_UNLIT)); errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident)); errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity
glm_ortho( ));
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi), errorChain(shaderSetMatrix(
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f, &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj
0.1f, 100.0f, ));
proj errorChain(shaderSetMatrix(
); &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); ));
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
view
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
errorChain(displaySetState((displaystate_t){ errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND .flags = DISPLAY_STATE_FLAG_BLEND
+4
View File
@@ -6,12 +6,16 @@
*/ */
#pragma once #pragma once
#include "dusk.h"
#include "scenetype.h" #include "scenetype.h"
typedef struct { typedef struct {
scenetype_t current; scenetype_t current;
scenetype_t next; scenetype_t next;
scenedata_t data; scenedata_t data;
mat4 screenProj;
mat4 screenView;
mat4 screenIdentity;
} scene_t; } scene_t;
extern scene_t SCENE; extern scene_t SCENE;
+7
View File
@@ -16,5 +16,12 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
.render = sceneOverworldRender, .render = sceneOverworldRender,
.dispose = sceneOverworldDispose .dispose = sceneOverworldDispose
}, },
[SCENE_TYPE_BATTLE] = {
.init = sceneBattleInit,
.update = sceneBattleUpdate,
.render = sceneBattleRender,
.dispose = sceneBattleDispose
},
}; };
+3
View File
@@ -8,9 +8,11 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
#include "scene/overworld/sceneoverworld.h" #include "scene/overworld/sceneoverworld.h"
#include "scene/battle/scenebattle.h"
typedef union scenedata_u { typedef union scenedata_u {
sceneoverworld_t overworld; sceneoverworld_t overworld;
scenebattle_t battle;
} scenedata_t; } scenedata_t;
typedef errorret_t (*scenecallback_t)(scenedata_t *); typedef errorret_t (*scenecallback_t)(scenedata_t *);
@@ -26,6 +28,7 @@ typedef enum {
SCENE_TYPE_NULL, SCENE_TYPE_NULL,
SCENE_TYPE_OVERWORLD, SCENE_TYPE_OVERWORLD,
SCENE_TYPE_BATTLE,
SCENE_TYPE_COUNT SCENE_TYPE_COUNT
} scenetype_t; } scenetype_t;
+2
View File
@@ -11,3 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(game) add_subdirectory(game)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(battle)
add_subdirectory(backpack)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibackpack.c
)
+116
View File
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uibackpack.h"
#include "ui/frame/uiframe.h"
#include "rpg/item/backpack.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "assert/assert.h"
uibackpack_t UI_BACKPACK;
void uiBackpackTabChanged(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
const itemtype_t type = (itemtype_t)(index + 1);
const inventory_t *inventory = backpackGetInventory(type);
errorCatch(uiItemListSetItemStacks(
&UI_BACKPACK.itemList, inventory->storage, inventory->storageSize
));
}
void uiBackpackTabSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
uiItemListOpen(&UI_BACKPACK.itemList);
}
errorret_t uiBackpackInit(void) {
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
MENU_BEGIN(
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
uiBackpackTabSelected, NULL, uiBackpackTabChanged
);
for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) {
stringFormat(
UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
"Category %u", i + 1
);
MENU_TAB(UI_BACKPACK.tabLabels[i]);
}
MENU_END(UI_BACKPACK.tabs, menuIndex);
uiItemListInit(
&UI_BACKPACK.itemList,
UI_BACKPACK_ITEM_LIST_COLUMNS, UI_BACKPACK_ITEM_LIST_ROWS, 1,
NULL, NULL, NULL, NULL
);
errorOk();
}
errorret_t uiBackpackDraw(void) {
if(!uiMenuIsActive(&UI_BACKPACK.tabsMenu)) errorOk();
const float_t width = SCREEN.width;
const float_t height = SCREEN.height;
const float_t x = (float_t)SCREEN.scanX +
((float_t)SCREEN.scanWidth - width) * 0.5f;
const float_t y = (float_t)SCREEN.scanY +
((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
const float_t contentX = x + UI_FRAME_START_X;
const float_t contentY = y + UI_FRAME_START_Y;
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
errorChain(uiMenuDraw(
&UI_BACKPACK.tabsMenu,
contentX,
contentY,
contentWidth,
contentHeight
));
const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t listY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
errorChain(
uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth)
);
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiBackpackIsOpen(void) {
return uiMenuIsActive(&UI_BACKPACK.tabsMenu);
}
void uiBackpackOpen(void) {
uiMenuOpen(&UI_BACKPACK.tabsMenu);
}
void uiBackpackClose(void) {
uiMenuClose(&UI_BACKPACK.tabsMenu);
}
errorret_t uiBackpackDispose(void) {
errorOk();
}
+65
View File
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "ui/widget/uiitemlist.h"
#include "rpg/item/item.h"
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1)
#define UI_BACKPACK_TAB_LABEL_MAX 32
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
#define UI_BACKPACK_ITEM_LIST_ROWS 5
typedef struct {
uimenu_t tabsMenu;
uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT];
char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX];
uiitemlist_t itemList;
} uibackpack_t;
extern uibackpack_t UI_BACKPACK;
/**
* Initializes the backpack panel and its item type tabs.
*
* @return Any error that occurs.
*/
errorret_t uiBackpackInit(void);
/**
* Draws the backpack panel. No-op when not visible.
*
* @return Any error that occurs.
*/
errorret_t uiBackpackDraw(void);
/**
* Returns true when the backpack panel is currently open.
*
* @returns True if open.
*/
bool_t uiBackpackIsOpen(void);
/**
* Opens the backpack panel. No-op when already open.
*/
void uiBackpackOpen(void);
/**
* Closes the backpack panel. No-op when already closed.
*/
void uiBackpackClose(void);
/**
* Disposes of the backpack panel.
*
* @return Any error that occurs.
*/
errorret_t uiBackpackDispose(void);
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibattlemenu.c
)
+137
View File
@@ -0,0 +1,137 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uibattlemenu.h"
#include "ui/frame/uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
uibattlemenu_t UI_BATTLE_MENU;
void uiBattleMenuTargetSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
uiMenuClose(&UI_BATTLE_MENU.targetMenu);
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
}
void uiBattleMenuOpenTargets(void) {
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) return;
const battlefighterteam_t enemyTeam =
current->team == BATTLE_FIGHTER_TEAM_ALLY ?
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
MENU_BEGIN(
&UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems,
uiBattleMenuTargetSelected, NULL, NULL
);
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
battlefighter_t *candidate = &BATTLE.fighters[i];
if(candidate->team != enemyTeam) continue;
if(!battleFighterIsAlive(candidate)) continue;
stringFormat(
UI_BATTLE_MENU.targetLabels[menuIndex],
UI_BATTLE_MENU_TARGET_LABEL_MAX - 1,
"Enemy %u (%u/%u HP)",
candidate->id, candidate->health, candidate->healthMax
);
UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i;
MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]);
}
MENU_END(UI_BATTLE_MENU.targetItems, 1);
uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
}
void uiBattleMenuActionSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
switch(index) {
case UI_BATTLE_MENU_ACTION_INDEX_ATTACK:
uiBattleMenuOpenTargets();
break;
case UI_BATTLE_MENU_ACTION_INDEX_FLEE:
battlePlayerFlee();
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
break;
default:
break;
}
}
errorret_t uiBattleMenuInit(void) {
memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t));
MENU_BEGIN(
&UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems,
uiBattleMenuActionSelected, NULL, NULL
);
MENU_BUTTON("Attack");
MENU_BUTTON("Flee");
MENU_END(UI_BATTLE_MENU.actionItems, 1);
errorOk();
}
errorret_t uiBattleMenuUpdate(void) {
if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
battlefighter_t *current = battleGetCurrentFighter();
if(current == NULL) errorOk();
if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();
uiMenuOpen(&UI_BATTLE_MENU.actionMenu);
errorOk();
}
errorret_t uiBattleMenuDraw(void) {
if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
const float_t width = UI_BATTLE_MENU_WIDTH;
const float_t height = UI_BATTLE_MENU_HEIGHT;
const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height;
errorChain(uiFrameDraw(x, y, width, height));
errorChain(uiMenuDraw(
&UI_BATTLE_MENU.actionMenu,
x + UI_FRAME_START_X,
y + UI_FRAME_START_Y,
width - (UI_FRAME_START_X * 2),
height - (UI_FRAME_START_Y * 2)
));
if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) {
const float_t targetY = y - height;
errorChain(uiFrameDraw(x, targetY, width, height));
errorChain(uiMenuDraw(
&UI_BATTLE_MENU.targetMenu,
x + UI_FRAME_START_X,
targetY + UI_FRAME_START_Y,
width - (UI_FRAME_START_X * 2),
height - (UI_FRAME_START_Y * 2)
));
}
errorChain(spriteBatchFlush());
errorOk();
}
+57
View File
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "rpg/battle/battle.h"
#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2
#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0
#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1
#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_COUNT_MAX
#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32
#define UI_BATTLE_MENU_WIDTH 160.0f
#define UI_BATTLE_MENU_HEIGHT 96.0f
typedef struct {
uimenu_t actionMenu;
uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT];
uimenu_t targetMenu;
uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT]
[UI_BATTLE_MENU_TARGET_LABEL_MAX];
} uibattlemenu_t;
extern uibattlemenu_t UI_BATTLE_MENU;
/**
* Initializes the battle action/target menus.
*
* @return Any error that occurs.
*/
errorret_t uiBattleMenuInit(void);
/**
* Updates the battle menu: opens the action menu whenever it becomes a
* player-controlled fighter's turn.
*
* @return Any error that occurs.
*/
errorret_t uiBattleMenuUpdate(void);
/**
* Draws the battle action menu, and the target menu above it when
* open. No-op when the action menu isn't active.
*
* @return Any error that occurs.
*/
errorret_t uiBattleMenuDraw(void);
+110 -3
View File
@@ -7,14 +7,88 @@
#include "uigamemenu.h" #include "uigamemenu.h"
#include "ui/frame/uiframe.h" #include "ui/frame/uiframe.h"
#include "ui/frame/uiconfirm.h"
#include "ui/frame/settings/uisettings.h" #include "ui/frame/settings/uisettings.h"
#include "ui/frame/backpack/uibackpack.h"
#include "ui/rpg/textbox/uitextboxmain.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "save/save.h"
#include "error/error.h"
#include "util/string.h"
#define UI_GAME_MENU_INDEX_CHARACTERS 0 #define UI_GAME_MENU_INDEX_CHARACTERS 0
#define UI_GAME_MENU_INDEX_SETTINGS 1 #define UI_GAME_MENU_INDEX_ITEMS 1
#define UI_GAME_MENU_INDEX_SETTINGS 2
#define UI_GAME_MENU_INDEX_SAVE 3
static void uiGameMenuSaveWriteComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) {
// Generously sized - stringFormat asserts (crashes) rather than
// truncating if the message doesn't fit, so this must comfortably fit
// the longest platform save-error message plus this prefix.
char_t msg[256];
stringFormat(
msg, sizeof(msg), "Save failed: %s", result.state->message
);
errorCatch(result);
uiTextboxMainSetText(msg);
return;
}
uiTextboxMainSetText("Game saved.");
}
static void uiGameMenuSaveCreateConfirmed(const bool_t confirmed, void *user) {
if(!confirmed) {
uiTextboxMainSetText("Save cancelled.");
return;
}
saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
}
// Determines whether there's actually save data to overwrite (not just
// whether the medium is present) by attempting a real load first - this is
// what lets a fresh memory card/stick, with no prior save on it yet, be
// told apart from one that already has our data on it. Cheap either way
// (a single sector/file read), and correct on every platform without any
// platform-specific UI code - saveExists() already reflects each
// platform's own notion of "found something."
static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) {
char_t msg[256];
stringFormat(msg, sizeof(msg), "Can't save: %s", result.state->message);
errorCatch(result);
uiTextboxMainSetText(msg);
return;
}
if(saveExists(SAVE_ACTIVE_SLOT)) {
saveWrite(SAVE_ACTIVE_SLOT, uiGameMenuSaveWriteComplete, NULL);
return;
}
uiConfirmOpen(
"No save data found. Create a new save?",
uiGameMenuSaveCreateConfirmed,
NULL
);
}
static void uiGameMenuSave(void) {
if(!saveIsAvailable()) {
uiTextboxMainSetText("Can't save - no save device found.");
return;
}
if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up.
saveLoad(SAVE_ACTIVE_SLOT, uiGameMenuSaveCheckComplete, NULL);
}
uigamemenu_t UI_GAME_MENU; uigamemenu_t UI_GAME_MENU;
@@ -23,17 +97,50 @@ void uiGameMenuSelected(
const uint8_t index, const uint8_t index,
const uimenuitem_t *item const uimenuitem_t *item
) { ) {
if(index == UI_GAME_MENU_INDEX_ITEMS) uiBackpackOpen();
if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen(); if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen();
if(index == UI_GAME_MENU_INDEX_SAVE) uiGameMenuSave();
} }
errorret_t uiGameMenuInit(void) { errorret_t uiGameMenuInit(void) {
memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.characters",
0,
UI_GAME_MENU.charactersLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.items",
0,
UI_GAME_MENU.itemsLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.settings",
0,
UI_GAME_MENU.settingsLabel,
UI_GAME_MENU_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.game_menu.save",
0,
UI_GAME_MENU.saveLabel,
UI_GAME_MENU_LABEL_MAX
));
MENU_BEGIN( MENU_BEGIN(
&UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL
); );
MENU_BUTTON("Characters"); MENU_BUTTON(UI_GAME_MENU.charactersLabel);
MENU_BUTTON("Settings"); MENU_BUTTON(UI_GAME_MENU.itemsLabel);
MENU_BUTTON(UI_GAME_MENU.settingsLabel);
MENU_BUTTON(UI_GAME_MENU.saveLabel);
MENU_END(UI_GAME_MENU.items, 1); MENU_END(UI_GAME_MENU.items, 1);
+6 -1
View File
@@ -9,12 +9,17 @@
#include "error/error.h" #include "error/error.h"
#include "ui/widget/uimenu.h" #include "ui/widget/uimenu.h"
#define UI_GAME_MENU_ITEM_COUNT 2 #define UI_GAME_MENU_ITEM_COUNT 4
#define UI_GAME_MENU_WIDTH 150.0f #define UI_GAME_MENU_WIDTH 150.0f
#define UI_GAME_MENU_LABEL_MAX 32
typedef struct { typedef struct {
uimenu_t menu; uimenu_t menu;
uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT]; uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT];
char_t charactersLabel[UI_GAME_MENU_LABEL_MAX];
char_t itemsLabel[UI_GAME_MENU_LABEL_MAX];
char_t settingsLabel[UI_GAME_MENU_LABEL_MAX];
char_t saveLabel[UI_GAME_MENU_LABEL_MAX];
} uigamemenu_t; } uigamemenu_t;
extern uigamemenu_t UI_GAME_MENU; extern uigamemenu_t UI_GAME_MENU;
+5 -5
View File
@@ -11,7 +11,7 @@
#include "util/memory.h" #include "util/memory.h"
#include "locale/localemanager.h" #include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/locale/assetlocaleloader.h"
#include "input/input.h" #include "save/save.h"
void uiSettingsInputSelected( void uiSettingsInputSelected(
const uimenu_t *menu, const uimenu_t *menu,
@@ -39,7 +39,7 @@ errorret_t uiSettingsInputInit(uisettingsdata_t *data) {
UI_SETTINGS_INPUT_LABEL_MAX UI_SETTINGS_INPUT_LABEL_MAX
)); ));
MENU_SLIDER_FLOAT( MENU_SLIDER_FLOAT(
input->deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f input->deadzoneLabel, SAVE_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
); );
#else #else
MENU_LABEL("No input settings yet"); MENU_LABEL("No input settings yet");
@@ -55,14 +55,14 @@ void uiSettingsInputLoad(void) {
#ifdef DUSK_INPUT_GAMEPAD #ifdef DUSK_INPUT_GAMEPAD
uiSliderSetFloat( uiSliderSetFloat(
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider, &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
INPUT.deadzone saveGet(SAVE_ACTIVE_SLOT)->deadzone
); );
#endif #endif
} }
void uiSettingsInputApply(void) { void uiSettingsInputApply(void) {
#ifdef DUSK_INPUT_GAMEPAD #ifdef DUSK_INPUT_GAMEPAD
INPUT.deadzone = uiSliderGetFloat( saveGet(SAVE_ACTIVE_SLOT)->deadzone = uiSliderGetFloat(
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
); );
#endif #endif
@@ -73,7 +73,7 @@ bool_t uiSettingsInputHasChanges(void) {
#ifdef DUSK_INPUT_GAMEPAD #ifdef DUSK_INPUT_GAMEPAD
if(uiSliderGetFloat( if(uiSliderGetFloat(
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
) != INPUT.deadzone) return true; ) != saveGet(SAVE_ACTIVE_SLOT)->deadzone) return true;
#endif #endif
return false; return false;
+2 -2
View File
@@ -12,8 +12,8 @@
#define UI_FRAME_BORDER_WIDTH 6 #define UI_FRAME_BORDER_WIDTH 6
#define UI_FRAME_BORDER_HEIGHT 6 #define UI_FRAME_BORDER_HEIGHT 6
#define UI_FRAME_PADDING_X 4 #define UI_FRAME_PADDING_X 2
#define UI_FRAME_PADDING_Y 4 #define UI_FRAME_PADDING_Y 2
#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X) #define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y) #define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
#define UI_FRAME_TILE_WIDTH 1 #define UI_FRAME_TILE_WIDTH 1

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