9 Commits

Author SHA1 Message Date
YourWishes f17b0bfcfb Tiles 2026-06-27 08:50:55 -05:00
YourWishes 2a85c9503f npc interact turn to face player 2026-06-27 06:30:45 -05:00
YourWishes 182428d6d6 Merge branch 'cutscene'
Build Dusk / build-linux (push) Successful in 5m53s
Build Dusk / run-tests (push) Failing after 16m6s
Build Dusk / build-psp (push) Successful in 3m38s
Build Dusk / build-knulli (push) Successful in 3m50s
Build Dusk / build-gamecube (push) Successful in 3m7s
Build Dusk / build-gamecube-iso (push) Successful in 3m22s
Build Dusk / build-wii (push) Successful in 3m58s
Build Dusk / build-wii-iso (push) Successful in 2m57s
2026-06-26 19:43:30 -05:00
YourWishes 8181a28557 Bunch of stuff done 2026-06-26 19:42:34 -05:00
YourWishes 88aed11d98 Blocked path 2026-06-26 14:42:35 -05:00
YourWishes 67010592b8 Fixing some performance 2026-06-26 14:41:30 -05:00
YourWishes dd22d6424a testing some performance stuff 2026-06-26 14:29:55 -05:00
YourWishes e53775b97f Fixed player turn bug 2026-06-26 14:24:13 -05:00
YourWishes d326f6c1ac NPC movements 2026-06-26 14:21:48 -05:00
52 changed files with 1456 additions and 307 deletions
+23
View File
@@ -423,6 +423,29 @@ 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`.
Binary file not shown.
+2 -1
View File
@@ -14,4 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Subdirs
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(json)
add_subdirectory(chunk)
+6
View File
@@ -39,4 +39,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.loadAsync = assetJsonLoaderAsync,
.dispose = assetJsonDispose
},
[ASSET_LOADER_TYPE_CHUNK] = {
.loadSync = assetChunkLoaderSync,
.loadAsync = assetChunkLoaderAsync,
.dispose = assetChunkDispose
},
};
+5
View File
@@ -11,6 +11,7 @@
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/chunk/assetchunkloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
@@ -20,6 +21,7 @@ typedef enum {
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_CHUNK,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
@@ -30,6 +32,7 @@ typedef union {
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
assetchunkloaderinput_t chunk;
} assetloaderinput_t;
typedef union {
@@ -38,6 +41,7 @@ typedef union {
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
assetchunkloaderloading_t chunk;
} assetloaderloading_t;
typedef union {
@@ -46,6 +50,7 @@ typedef union {
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetchunkoutput_t chunk;
} assetloaderoutput_t;
typedef struct assetloading_s assetloading_t;
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetchunkloader.c
)
@@ -0,0 +1,126 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetchunkloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.chunk.data, "Data already defined?");
assetfile_t *file = &loading->loading.chunk.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
assertTrue(
file->lastRead == file->size,
"Failed to read entire chunk file."
);
loading->loading.chunk.data = data;
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetChunkLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.chunk.state) {
case ASSET_CHUNK_LOADING_STATE_INITIAL:
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_CHUNK_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.chunk.data;
assertNotNull(data, "Chunk data should have been loaded by now.");
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid chunk file header");
}
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
if(version != ASSET_CHUNK_FILE_VERSION) {
memoryFree(data);
assetLoaderErrorThrow(
loading, "Unsupported chunk file version %u", version
);
}
assetchunkoutput_t *out = &loading->entry->data.chunk;
size_t offset = 8;
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
memoryCopy(out->tiles, data + offset, tileSize);
offset += tileSize;
out->meshCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
out->meshCount <= CHUNK_MESH_COUNT_MAX,
"Chunk mesh count exceeds maximum."
);
uint32_t poolOffset = 0;
for(uint8_t m = 0; m < out->meshCount; m++) {
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(data + offset));
offset += sizeof(uint32_t);
assertTrue(
poolOffset + vertCount <= CHUNK_VERTEX_COUNT,
"Chunk vertex data exceeds pool."
);
out->meshVertCounts[m] = vertCount;
memoryCopy(
&out->vertices[poolOffset],
data + offset,
vertCount * sizeof(meshvertex_t)
);
offset += vertCount * sizeof(meshvertex_t);
poolOffset += vertCount;
}
memoryFree(data);
loading->loading.chunk.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetChunkDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk();
}
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 2
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assetchunkloaderinput_t;
typedef enum {
ASSET_CHUNK_LOADING_STATE_INITIAL,
ASSET_CHUNK_LOADING_STATE_READ_FILE,
ASSET_CHUNK_LOADING_STATE_PARSE,
ASSET_CHUNK_LOADING_STATE_DONE
} assetchunkloadingstate_t;
typedef struct {
assetfile_t file;
assetchunkloadingstate_t state;
uint8_t *data;
} assetchunkloaderloading_t;
typedef struct {
tile_t tiles[CHUNK_TILE_COUNT];
uint8_t meshCount;
uint32_t meshVertCounts[CHUNK_MESH_COUNT_MAX];
meshvertex_t vertices[CHUNK_VERTEX_COUNT];
} assetchunkoutput_t;
/**
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
* the loading buffer so the sync phase can parse without blocking the
* main thread on I/O.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for chunk assets. Validates the DCF binary previously
* read by the async phase and populates the output assetchunkoutput_t.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetChunkLoaderSync(assetloading_t *loading);
/**
* Disposer for chunk assets.
*
* @param entry Asset entry containing the chunk data to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetChunkDispose(assetentry_t *entry);
+5 -4
View File
@@ -7,9 +7,10 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entity.c
entityanim.c
entitydir.c
entityinteract.c
npc.c
player.c
)
)
add_subdirectory(anim)
add_subdirectory(interact)
add_subdirectory(npc)
+13
View File
@@ -0,0 +1,13 @@
# 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
entityanim.c
entityanimidle.c
entityanimturn.c
entityanimwalk.c
entityanimrun.c
)
+27
View File
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "time/time.h"
const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
[ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
[ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
[ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
[ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
};
void entityAnimUpdate(entity_t *entity) {
if(entity->animation != ENTITY_ANIM_IDLE) {
entity->animTime -= TIME.delta;
if(entity->animTime <= 0) {
entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0;
}
}
ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
}
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "entityanimidle.h"
#include "entityanimturn.h"
#include "entityanimwalk.h"
#include "entityanimrun.h"
typedef struct entity_s entity_t;
typedef enum {
ENTITY_ANIM_IDLE,
ENTITY_ANIM_TURN,
ENTITY_ANIM_WALK,
ENTITY_ANIM_RUN,
ENTITY_ANIM_COUNT
} entityanim_t;
typedef struct {
/** Updates the render position for this animation state. */
void (*update)(entity_t *entity);
} entityanimcallback_t;
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
/**
* Updates the entity animation timer and render position.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimUpdate(entity_t *entity);
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
void entityAnimIdleUpdate(entity_t *entity) {
entity->renderPosition[0] = (float_t)entity->position.x;
entity->renderPosition[1] = (float_t)entity->position.y;
entity->renderPosition[2] = (float_t)entity->position.z;
}
+18
View File
@@ -0,0 +1,18 @@
/**
* 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 entity_s entity_t;
/**
* Updates render position for the idle animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimIdleUpdate(entity_t *entity);
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
void entityAnimRunUpdate(entity_t *entity) {
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
(float_t)entity->position.x - (float_t)entity->lastPosition.x
);
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
(float_t)entity->position.y - (float_t)entity->lastPosition.y
);
entity->renderPosition[2] = (float_t)entity->lastPosition.z + t * (
(float_t)entity->position.z - (float_t)entity->lastPosition.z
);
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "time/time.h"
typedef struct entity_s entity_t;
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
/**
* Updates render position for the run animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimRunUpdate(entity_t *entity);
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
void entityAnimTurnUpdate(entity_t *entity) {
entity->renderPosition[0] = (float_t)entity->position.x;
entity->renderPosition[1] = (float_t)entity->position.y;
entity->renderPosition[2] = (float_t)entity->position.z;
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "time/time.h"
typedef struct entity_s entity_t;
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(2)
/**
* Updates render position for the turn animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimTurnUpdate(entity_t *entity);
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
void entityAnimWalkUpdate(entity_t *entity) {
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
(float_t)entity->position.x - (float_t)entity->lastPosition.x
);
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
(float_t)entity->position.y - (float_t)entity->lastPosition.y
);
entity->renderPosition[2] = (float_t)entity->lastPosition.z + t * (
(float_t)entity->position.z - (float_t)entity->lastPosition.z
);
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "time/time.h"
typedef struct entity_s entity_t;
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(10)
/**
* Updates render position for the walk animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimWalkUpdate(entity_t *entity);
+30
View File
@@ -12,6 +12,7 @@
#include "util/math.h"
#include "rpg/cutscene/cutscenemode.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/chunk.h"
#include "rpg/overworld/tile.h"
entity_t ENTITIES[ENTITY_COUNT];
@@ -28,6 +29,7 @@ void entityInit(entity_t *entity, const entitytype_t type) {
memoryZero(entity, sizeof(entity_t));
entity->id = (uint8_t)(entity - ENTITIES);
entity->type = type;
entity->chunkIndex = 0xFF;
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
}
@@ -213,4 +215,32 @@ uint8_t entityGetAvailable() {
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
return 0xFF;
}
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
assertNotNull(entity, "Entity pointer cannot be NULL");
if(entity->chunkIndex != 0xFF) {
chunk_t *old = mapGetChunk(entity->chunkIndex);
if(old != NULL) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(old->entities[i] != entity->id) continue;
old->entities[i] = 0xFF;
break;
}
}
}
entity->chunkIndex = chunkIndex;
if(chunkIndex != 0xFF) {
chunk_t *next = mapGetChunk(chunkIndex);
if(next != NULL) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(next->entities[i] != 0xFF) continue;
next->entities[i] = entity->id;
break;
}
}
}
}
+16 -5
View File
@@ -7,10 +7,10 @@
#pragma once
#include "entitydir.h"
#include "entityanim.h"
#include "entityinteract.h"
#include "anim/entityanim.h"
#include "interact/entityinteract.h"
#include "entitytype.h"
#include "npc.h"
#include "npc/npc.h"
typedef struct map_s map_t;
@@ -29,6 +29,8 @@ typedef struct entity_s {
float_t animTime;
entityinteract_t interact;
uint8_t chunkIndex;
} entity_t;
extern entity_t ENTITIES[ENTITY_COUNT];
@@ -107,7 +109,16 @@ entity_t *entityGetAt(const worldpos_t pos);
/**
* Gets an available entity index.
*
*
* @return The index of an available entity, or 0xFF if none are available.
*/
uint8_t entityGetAvailable();
uint8_t entityGetAvailable();
/**
* Assigns an entity to a chunk, removing it from its current chunk first.
* Pass 0xFF as chunkIndex to detach the entity from any chunk.
*
* @param entity Pointer to the entity.
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
*/
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex);
-41
View File
@@ -1,41 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entity.h"
#include "time/time.h"
void entityAnimUpdate(entity_t *entity) {
if(entity->animation != ENTITY_ANIM_IDLE) {
entity->animTime -= TIME.delta;
if(entity->animTime <= 0) {
entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0;
}
}
if(
entity->animation == ENTITY_ANIM_WALK ||
entity->animation == ENTITY_ANIM_RUN
) {
float_t duration = entity->animation == ENTITY_ANIM_WALK ?
ENTITY_ANIM_WALK_DURATION : ENTITY_ANIM_RUN_DURATION;
float_t t = 1.0f - (entity->animTime / duration);
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
(float_t)entity->position.x - (float_t)entity->lastPosition.x
);
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
(float_t)entity->position.y - (float_t)entity->lastPosition.y
);
entity->renderPosition[2] = (float_t)entity->lastPosition.z + t * (
(float_t)entity->position.z - (float_t)entity->lastPosition.z
);
} else {
entity->renderPosition[0] = (float_t)entity->position.x;
entity->renderPosition[1] = (float_t)entity->position.y;
entity->renderPosition[2] = (float_t)entity->position.z;
}
}
-29
View File
@@ -1,29 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "time/time.h"
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(2)
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(6)
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(3)
typedef struct entity_s entity_t;
typedef enum {
ENTITY_ANIM_IDLE,
ENTITY_ANIM_TURN,
ENTITY_ANIM_WALK,
ENTITY_ANIM_RUN,
} entityanim_t;
/**
* Updates the entity's animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimUpdate(entity_t *entity);
+2 -11
View File
@@ -7,7 +7,7 @@
#pragma once
#include "rpg/entity/player.h"
#include "npc.h"
#include "npc/npc.h"
typedef enum {
ENTITY_TYPE_NULL,
@@ -33,18 +33,10 @@ typedef struct {
void (*init)(entity_t *entity);
/**
* Movement callback for the entity type.
* Movement callback for the entity type. Gated by cutscene input.
* @param entity Pointer to the entity to move.
*/
void (*movement)(entity_t *entity);
/**
* Interaction callback for the entity type.
* @param player Pointer to the player entity.
* @param entity Pointer to the entity to interact with.
* @return True if the entity handled the interaction, false otherwise.
*/
bool_t (*interact)(entity_t *player, entity_t *entity);
} entitycallback_t;
static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
@@ -58,6 +50,5 @@ static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
[ENTITY_TYPE_NPC] = {
.init = npcInit,
.movement = npcMovement,
.interact = npcInteract
}
};
@@ -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
entityinteract.c
)
@@ -5,7 +5,7 @@
* https://opensource.org/licenses/MIT
*/
#include "entity.h"
#include "rpg/entity/entity.h"
#include "assert/assert.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/uitextboxmain.h"
@@ -21,11 +21,20 @@ void entityInteractWith(entity_t *player, entity_t *target) {
"Interact cutscene pointer cannot be NULL"
);
cutsceneSystemStartCutscene(target->interact.data.cutscene);
return;
break;
case ENTITY_INTERACT_PRINT:
uiTextboxMainSetText(target->interact.data.message);
return;
// If NPC turn to face player.
if(target->type == ENTITY_TYPE_NPC) {
target->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING;
target->animation = ENTITY_ANIM_IDLE;
entityTurn(target, entityDirGetOpposite(player->direction));
}
// entityTurn(player, player->direction); // Redundant (for now)
break;
case ENTITY_INTERACT_NULL:
break;
@@ -34,7 +43,4 @@ void entityInteractWith(entity_t *player, entity_t *target) {
assertUnreachable("Unknown entity interact type");
break;
}
if(ENTITY_CALLBACKS[target->type].interact == NULL) return;
ENTITY_CALLBACKS[target->type].interact(player, target);
}
}
@@ -16,8 +16,11 @@ typedef struct entity_s entity_t;
*/
typedef enum {
ENTITY_INTERACT_NULL = 0,
ENTITY_INTERACT_CUTSCENE,
ENTITY_INTERACT_PRINT,
ENTITY_INTERACT_CALLBACK,
ENTITY_INTERACT_COUNT
} entityinteracttype_t;
@@ -27,6 +30,7 @@ typedef enum {
typedef union {
const cutscene_t *cutscene;
const char_t *message;
void (*callback)(entity_t *player, entity_t *target);
} entityinteractdata_t;
/**
-31
View File
@@ -1,31 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entity.h"
#include "assert/assert.h"
#include "rpg/cutscene/scene/testcutscene.h"
#include "rpg/rpgtextbox.h"
void npcInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
}
void npcMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
}
bool_t npcInteract(entity_t *player, entity_t *npc) {
assertNotNull(player, "Player entity pointer cannot be NULL");
assertNotNull(npc, "NPC entity pointer cannot be NULL");
cutsceneSystemStartCutscene(&TEST_CUTSCENE);
// rpgTextboxShow(RPG_TEXTBOX_POS_BOTTOM, "Hello World!");
return false;
};
-37
View File
@@ -1,37 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct entity_s entity_t;
typedef struct {
void *nothing;
} npc_t;
/**
* Initializes an NPC entity.
*
* @param entity Pointer to the entity structure to initialize.
*/
void npcInit(entity_t *entity);
/**
* Updates an NPC entity.
*
* @param entity Pointer to the entity structure to update.
*/
void npcMovement(entity_t *entity);
/**
* Handles interaction with an NPC entity.
*
* @param player Pointer to the player entity.
* @param npc Pointer to the NPC entity.
*/
bool_t npcInteract(entity_t *player, entity_t *npc);
+13
View File
@@ -0,0 +1,13 @@
# 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
npc.c
npcturn.c
npcwalk.c
npcpath.c
)
+59
View File
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "assert/assert.h"
#include "rpg/cutscene/scene/testcutscene.h"
#include "rpg/rpgtextbox.h"
const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT] = {
[NPC_MOVE_TYPE_NULL] = { 0 },
[NPC_MOVE_TYPE_RANDOM_TURN] = {
npcRandomTurnInit,
npcRandomTurnMovement
},
[NPC_MOVE_TYPE_RANDOM_WALK] = {
npcRandomWalkInit,
npcRandomWalkMovement
},
[NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK] = {
npcRandomTurnAndWalkInit,
npcRandomTurnAndWalkMovement
},
[NPC_MOVE_TYPE_PATH] = {
npcPathInit,
npcPathMovement
},
};
void npcInit(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
}
void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) {
assertNotNull(entity, "Entity pointer cannot be NULL");
npc_t *npc = &entity->data.npc;
npc->moveType = moveType;
if(NPC_MOVE_CALLBACKS[moveType].init != NULL) {
NPC_MOVE_CALLBACKS[moveType].init(npc);
}
}
void npcMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
npc_t *npc = &entity->data.npc;
if(npc->interactState != NPC_INTERACT_STATE_NONE) return;
const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType];
if(cb->movement != NULL) cb->movement(entity);
}
+81
View File
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "npcturn.h"
#include "npcwalk.h"
#include "npcpath.h"
typedef struct entity_s entity_t;
typedef enum {
NPC_INTERACT_STATE_NONE,
NPC_INTERACT_STATE_CONVERSING,
NPC_INTERACT_STATE_COUNT
} npcinteractstate_t;
typedef enum {
NPC_MOVE_TYPE_NULL,
NPC_MOVE_TYPE_RANDOM_TURN,
NPC_MOVE_TYPE_RANDOM_WALK,
NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK,
NPC_MOVE_TYPE_PATH,
NPC_MOVE_TYPE_COUNT
} npcmovetype_t;
typedef union {
npcrandomturn_t randomTurn;
npcrandomwalk_t randomWalk;
npcrandomturnandwalk_t randomTurnAndWalk;
npcpath_t path;
} npcmovedata_t;
typedef struct npc_s {
npcinteractstate_t interactState;
npcmovetype_t moveType;
npcmovedata_t moveData;
} npc_t;
typedef struct {
/** Called once when the move type is set. */
void (*init)(npc_t *npc);
/** Called each movement tick. */
void (*movement)(entity_t *entity);
} npcmovecallback_t;
extern const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT];
/**
* Initializes an NPC entity.
*
* @param entity Pointer to the entity structure to initialize.
*/
void npcInit(entity_t *entity);
/**
* Sets the movement type for an NPC entity.
*
* @param entity Pointer to the entity structure.
* @param moveType The movement type to set.
*/
void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType);
/**
* Movement callback for an NPC entity. Gated by cutscene input.
*
* @param entity Pointer to the entity structure to update.
*/
void npcMovement(entity_t *entity);
/**
* Free movement callback for an NPC entity. Runs always-run move types
* regardless of cutscene state.
*
* @param entity Pointer to the entity structure to update.
*/
void npcFreeMovement(entity_t *entity);
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "npc.h"
#include "rpg/entity/entity.h"
#include "rpg/overworld/worldpos.h"
void npcPathInit(npc_t *npc) {
npcpath_t *path = &npc->moveData.path;
path->count = 0;
path->index = 0;
}
void npcPathMovement(entity_t *entity) {
npcpath_t *path = &entity->data.npc.moveData.path;
if(path->count == 0) return;
if(!entityCanWalk(entity)) return;
// Advance past any waypoints already reached (including the current one).
worldpos_t *target = &path->positions[path->index];
if(worldPosIsEqual(entity->position, *target)) {
path->index = (path->index + 1) % path->count;
target = &path->positions[path->index];
// New target is the same tile - nothing to do this tick
if(worldPosIsEqual(entity->position, *target)) return;
}
entitydir_t dir;
worldpos_t pos = entity->position;
if(pos.x != target->x) {
dir = pos.x < target->x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
} else if(pos.y != target->y) {
dir = pos.y < target->y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
} else {
// x and y match but z differs - step to correct z via ramp logic
dir = pos.z < target->z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
}
entityWalk(entity, dir);
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "rpg/overworld/worldpos.h"
typedef struct npc_s npc_t;
typedef struct entity_s entity_t;
/** Maximum number of waypoints in an NPC path. */
#define NPC_PATH_COUNT_MAX 8
typedef struct {
worldpos_t positions[NPC_PATH_COUNT_MAX];
uint8_t count;
uint8_t index;
} npcpath_t;
/**
* Initializes the path movement data for an NPC.
*
* @param npc Pointer to the NPC to initialize.
*/
void npcPathInit(npc_t *npc);
/**
* Movement tick for an NPC following a path.
*
* @param entity Pointer to the entity to update.
*/
void npcPathMovement(entity_t *entity);
+27
View File
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "npc.h"
#include "rpg/entity/entity.h"
#include "util/random.h"
#include "time/time.h"
#include <stdlib.h>
void npcRandomTurnInit(npc_t *npc) {
npcrandomturn_t *turn = &npc->moveData.randomTurn;
turn->frequencyMin = NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT;
turn->frequencyMax = NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT;
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
}
void npcRandomTurnMovement(entity_t *entity) {
npcrandomturn_t *turn = &entity->data.npc.moveData.randomTurn;
turn->timer -= TIME.delta;
if(turn->timer > 0.0f) return;
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
}
+36
View File
@@ -0,0 +1,36 @@
/**
* 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 npc_s npc_t;
typedef struct entity_s entity_t;
/** Default min/max seconds between NPC random-turn ticks. */
#define NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT 2.0f
#define NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT 4.0f
typedef struct {
float_t frequencyMin;
float_t frequencyMax;
float_t timer;
} npcrandomturn_t;
/**
* Initializes the random-turn movement data for an NPC.
*
* @param npc Pointer to the NPC to initialize.
*/
void npcRandomTurnInit(npc_t *npc);
/**
* Movement tick for an NPC using random-turn movement.
*
* @param entity Pointer to the entity to update.
*/
void npcRandomTurnMovement(entity_t *entity);
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "npc.h"
#include "rpg/entity/entity.h"
#include "util/random.h"
#include "time/time.h"
#include <stdlib.h>
void npcRandomWalkInit(npc_t *npc) {
npcrandomwalk_t *walk = &npc->moveData.randomWalk;
walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
}
void npcRandomWalkMovement(entity_t *entity) {
npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
walk->timer -= TIME.delta;
if(walk->timer > 0.0f) return;
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
}
void npcRandomTurnAndWalkInit(npc_t *npc) {
npcRandomTurnInit(npc);
npcrandomturnandwalk_t *tw = &npc->moveData.randomTurnAndWalk;
tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
}
void npcRandomTurnAndWalkMovement(entity_t *entity) {
npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
tw->turn.timer -= TIME.delta;
if(tw->turn.timer <= 0.0f) {
tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
}
tw->walk.timer -= TIME.delta;
if(tw->walk.timer <= 0.0f) {
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "npcturn.h"
/** Default min/max seconds between NPC random-walk ticks. */
#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
typedef struct {
float_t frequencyMin;
float_t frequencyMax;
float_t timer;
} npcrandomwalk_t;
typedef struct {
npcrandomturn_t turn;
npcrandomwalk_t walk;
} npcrandomturnandwalk_t;
/**
* Initializes the random-walk movement data for an NPC.
*
* @param npc Pointer to the NPC to initialize.
*/
void npcRandomWalkInit(npc_t *npc);
/**
* Movement tick for an NPC using random-walk movement.
*
* @param entity Pointer to the entity to update.
*/
void npcRandomWalkMovement(entity_t *entity);
/**
* Initializes the random-turn-and-walk movement data for an NPC.
*
* @param npc Pointer to the NPC to initialize.
*/
void npcRandomTurnAndWalkInit(npc_t *npc);
/**
* Movement tick for an NPC using random-turn-and-walk movement.
*
* @param entity Pointer to the entity to update.
*/
void npcRandomTurnAndWalkMovement(entity_t *entity);
+14 -3
View File
@@ -33,14 +33,25 @@ void playerInput(entity_t *entity) {
// Can player act?
if(UI_FOCUS.count > 0) return;
// Turn
// Turn - only if not already holding the direction we face
const playerinputdirmap_t *dirMap = PLAYER_INPUT_DIR_MAP;
bool_t holdingFaced = false;
do {
if(!inputIsDown(dirMap->action)) continue;
if(entity->direction == dirMap->direction) continue;
return entityTurn(entity, dirMap->direction);
if(entity->direction != dirMap->direction) continue;
holdingFaced = true;
break;
} while((++dirMap)->action != 0xFF);
if(!holdingFaced) {
dirMap = PLAYER_INPUT_DIR_MAP;
do {
if(!inputIsDown(dirMap->action)) continue;
if(entity->direction == dirMap->direction) continue;
return entityTurn(entity, dirMap->direction);
} while((++dirMap)->action != 0xFF);
}
// Walk / Run
bool_t running = inputIsDown(INPUT_ACTION_CANCEL);
dirMap = PLAYER_INPUT_DIR_MAP;
+2 -1
View File
@@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
map.c
worldpos.c
tile.c
)
)
+9 -12
View File
@@ -1,6 +1,6 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
@@ -8,25 +8,22 @@
#pragma once
#include "rpg/overworld/tile.h"
#include "worldpos.h"
#include "display/mesh/quad.h"
#include "display/mesh/mesh.h"
#include "display/spritebatch/spritebatch.h"
// #define CHUNK_MESH_COUNT_MAX 3
#define CHUNK_VERTEX_COUNT (QUAD_VERTEX_COUNT * CHUNK_WIDTH * CHUNK_HEIGHT * 2)
#define CHUNK_MESH_COUNT_MAX 10
#define CHUNK_VERTEX_COUNT 8192
#define CHUNK_ENTITY_COUNT_MAX 10
typedef struct chunk_s {
chunkpos_t position;
tile_t tiles[CHUNK_TILE_COUNT];
uint8_t meshCount;
uint32_t meshVertCounts[CHUNK_MESH_COUNT_MAX];
meshvertex_t vertices[CHUNK_VERTEX_COUNT];
uint32_t vertCount;
mesh_t mesh;
color_t testColor;
// uint8_t meshCount;
// meshvertex_t vertices[CHUNK_VERTEX_COUNT_MAX];
// mesh_t meshes[CHUNK_MESH_COUNT_MAX];
mesh_t meshes[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
} chunk_t;
+63 -71
View File
@@ -20,12 +20,14 @@ errorret_t mapInit() {
// Setup chunk meshes
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
errorChain(meshInit(
&chunk->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
CHUNK_VERTEX_COUNT,
chunk->vertices
));
for(uint8_t j = 0; j < CHUNK_MESH_COUNT_MAX; j++) {
errorChain(meshInit(
&chunk->meshes[j],
MESH_PRIMITIVE_TYPE_TRIANGLES,
CHUNK_VERTEX_COUNT,
chunk->vertices
));
}
}
// Perform "initial load"
@@ -196,90 +198,80 @@ void mapUpdate() {
errorret_t mapDispose() {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
mapChunkUnload(&MAP.chunks[i]);
errorChain(meshDispose(&MAP.chunks[i].mesh));
for(uint8_t j = 0; j < CHUNK_MESH_COUNT_MAX; j++) {
errorChain(meshDispose(&MAP.chunks[i].meshes[j]));
}
}
errorOk();
}
void mapChunkUnload(chunk_t* chunk) {
uint8_t chunkIndex = (uint8_t)(chunk - MAP.chunks);
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(chunk->entities[i] == 0xFF) break;
if(chunk->entities[i] == 0xFF) continue;
entity_t *entity = &ENTITIES[chunk->entities[i]];
entity->type = ENTITY_TYPE_NULL;
assertTrue(
entity->chunkIndex == chunkIndex,
"Entity chunk index does not match chunk"
);
if(entity->type == ENTITY_TYPE_PLAYER) {
entitySetChunk(entity, 0xFF);
} else {
entity->type = ENTITY_TYPE_NULL;
}
}
chunk->vertCount = 0;
chunk->meshCount = 0;
}
errorret_t mapChunkLoad(chunk_t* chunk) {
if(!mapIsLoaded()) errorThrow("No map loaded");
color_t color = COLOR_WHITE;
if(chunk->position.y % 2 == 0) {
if(chunk->position.x % 2 == 0) {
color = COLOR_BLACK;
} else {
color = COLOR_WHITE;
}
} else {
if(chunk->position.x % 2 == 0) {
color = COLOR_WHITE;
} else {
color = COLOR_BLACK;
}
}
// if(chunk->position.x == 0 && chunk->position.y == 0 && chunk->position.z == 0) {
// color = COLOR_RED;
// }
chunk->testColor = color;
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
chunk->vertCount = 0;
if(chunk->position.z != 0) {
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
chunk->meshCount = 0;
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
);
if(!assetFileExists(name)) {
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles));
errorOk();
}
// Set Chunk sprites.
vec3 spriteMin = {
chunk->position.x * CHUNK_WIDTH,
chunk->position.y * CHUNK_HEIGHT,
chunk->position.z * CHUNK_DEPTH
};
spritebatchsprite_t sprites[CHUNK_TILE_COUNT];
uint32_t i = 0;
for(uint8_t x = 0; x < CHUNK_WIDTH; x++) {
for(uint8_t y = 0; y < CHUNK_HEIGHT; y++) {
glm_vec3_copy(spriteMin, sprites[i].min);
glm_vec3_add(
sprites[i].min,
(vec3){ x, y, 0 },
sprites[i].min
);
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
assertNotNull(entry, "Failed to get chunk asset entry");
glm_vec3_copy(sprites[i].min, sprites[i].max);
glm_vec3_add(
sprites[i].max,
(vec3){ 1, 1, 0 },
sprites[i].max
);
glm_vec2_copy((vec2){ 0, 0 }, sprites[i].uvMin);
glm_vec2_copy((vec2){ 1, 1 }, sprites[i].uvMax);
i++;
}
errorret_t ret = assetRequireLoaded(entry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(entry);
return ret;
}
chunk->vertCount = i * QUAD_VERTEX_COUNT;
spriteBatchBufferToMesh(
sprites,
i,
chunk->vertices,
chunk->vertCount
);
errorChain(meshFlush(&chunk->mesh, 0, chunk->vertCount));
memoryCopy(chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles));
memoryCopy(
chunk->vertices, entry->data.chunk.vertices, sizeof(chunk->vertices)
);
memoryCopy(
chunk->meshVertCounts,
entry->data.chunk.meshVertCounts,
sizeof(chunk->meshVertCounts)
);
uint8_t meshCount = entry->data.chunk.meshCount;
assetUnlockEntry(entry);
if(meshCount == 0) errorOk();
chunk->meshCount = meshCount;
for(uint8_t m = 0; m < meshCount; m++) {
if(chunk->meshVertCounts[m] == 0) continue;
errorChain(meshFlush(
&chunk->meshes[m], 0, (int32_t)chunk->meshVertCounts[m]
));
}
errorOk();
}
+4 -4
View File
@@ -12,13 +12,13 @@
#define CHUNK_WIDTH 16
#define CHUNK_HEIGHT 16
#define CHUNK_DEPTH 8
#define CHUNK_DEPTH 32
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH)
#define MAP_CHUNK_WIDTH 4
#define MAP_CHUNK_HEIGHT 4
#define MAP_CHUNK_DEPTH 3
#define MAP_CHUNK_WIDTH 5
#define MAP_CHUNK_HEIGHT 3
#define MAP_CHUNK_DEPTH 1
#define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH)
#define ENTITY_COUNT 32
+20
View File
@@ -37,13 +37,33 @@ errorret_t rpgInit(void) {
entityInit(ent, ENTITY_TYPE_PLAYER);
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
RPG_CAMERA.followEntity.followEntityId = ent->id;
{
chunkpos_t cp;
worldPosToChunkPos(&ent->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(ent, (uint8_t)ci);
}
uint8_t npcIndex = entityGetAvailable();
entity_t *npc = &ENTITIES[npcIndex];
entityInit(npc, ENTITY_TYPE_NPC);
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
npc->position = (worldpos_t){ 3, 3, 0 };
npc->interact.type = ENTITY_INTERACT_PRINT;
npc->interact.data.message = "hello world";
{
chunkpos_t cp;
worldPosToChunkPos(&npc->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(npc, (uint8_t)ci);
}
npcpath_t *path = &npc->data.npc.moveData.path;
path->positions[0] = (worldpos_t){ 3, 3, 0 };
path->positions[1] = (worldpos_t){ 10, 3, 0 };
path->positions[2] = (worldpos_t){ 10, 10, 0 };
path->positions[3] = (worldpos_t){ 3, 10, 0 };
path->count = 4;
// All Good!
errorOk();
+76 -49
View File
@@ -13,15 +13,32 @@
#include "display/screen/screen.h"
#include "display/shader/shaderunlit.h"
#include "display/spritebatch/spritebatch.h"
#include "display/texture/texture.h"
#include "rpg/overworld/map.h"
#include "rpg/entity/entity.h"
#include "rpg/rpgcamera.h"
#define TEXTURE_CHUNK_SIZE 16
static texture_t TEXTURE_CHUNK;
static color_t TEXTURE_CHUNK_PIXELS[TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE];
errorret_t sceneOverworldInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
for(uint32_t i = 0; i < TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE; i++) {
uint8_t r = (uint8_t)((i & 7) * 36);
uint8_t g = (uint8_t)(((i >> 3) & 7) * 36);
uint8_t b = (uint8_t)((i >> 6) * 85);
TEXTURE_CHUNK_PIXELS[i] = color4b(r, g, b, 255);
}
errorChain(textureInit(
&TEXTURE_CHUNK,
TEXTURE_CHUNK_SIZE, TEXTURE_CHUNK_SIZE,
TEXTURE_FORMAT_RGBA,
(texturedata_t){ .rgbaColors = TEXTURE_CHUNK_PIXELS }
));
errorOk();
}
@@ -80,64 +97,34 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye));
// Chunks
{
shadermaterial_t chunkMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = NULL
}
};
uint32_t i = 0;
for(uint8_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
for(uint8_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
for(uint8_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->vertCount == 0) {
i++;
continue;
}
chunkMaterial.unlit.color = chunk->testColor;
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
errorChain(meshDraw(&chunk->mesh, 0, chunk->vertCount));
i++;
}
}
}
}
errorChain(sceneOverworldDrawChunks());
// Entities
{
uint8_t spriteCount = 0;
spritebatchsprite_t sprites[ENTITY_COUNT];
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
entity_t *ent = &ENTITIES[i];
if(ent->type == ENTITY_TYPE_NULL) continue;
glm_vec3_copy(ent->renderPosition, sprites[spriteCount].min);
glm_vec3_copy(ent->renderPosition, sprites[spriteCount].max);
glm_vec3_add(
sprites[spriteCount].max,
(vec3){ 1, 1, 0 },
sprites[spriteCount].max
);
spritebatchsprite_t sprite;
glm_vec3_copy(ent->renderPosition, sprite.min);
glm_vec3_copy(ent->renderPosition, sprite.max);
glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max);
glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin);
glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax);
glm_vec2_copy((vec2){ 0, 0 }, sprites[spriteCount].uvMin);
glm_vec2_copy((vec2){ 1, 1 }, sprites[spriteCount].uvMax);
color_t color;
switch(ent->direction) {
case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break;
case ENTITY_DIR_EAST: color = COLOR_RED; break;
case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break;
case ENTITY_DIR_WEST: color = COLOR_BLUE; break;
default: color = COLOR_CYAN; break;
}
spriteCount++;
}
if(spriteCount) {
shadermaterial_t material = {
.unlit = {
.color = COLOR_CYAN,
.texture = NULL
}
.unlit = { .color = color, .texture = NULL }
};
// material.unlit.texture = &TEXTURE_TEST;
spriteBatchBuffer(sprites, spriteCount, &SHADER_UNLIT, material);
spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material);
spriteBatchFlush();
}
}
@@ -145,10 +132,50 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
errorOk();
}
errorret_t sceneOverworldDrawChunks() {
shadermaterial_t chunkMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_CHUNK
}
};
// Pass 1: draw all base meshes with the shared chunk texture (no mid-loop
// texture swaps).
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->meshCount == 0) continue;
if(chunk->meshVertCounts[0] == 0) continue;
errorChain(meshDraw(
&chunk->meshes[0], 0, (int32_t)chunk->meshVertCounts[0]
));
}
// Pass 2: draw each chunk's additional meshes (indices 1..meshCount-1).
// Vertices are packed sequentially in the pool, so accumulate the offset.
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
uint32_t vertOffset = chunk->meshVertCounts[0];
for(uint8_t m = 1; m < chunk->meshCount; m++) {
if(chunk->meshVertCounts[m] > 0) {
errorChain(meshDraw(
&chunk->meshes[m],
(int32_t)vertOffset,
(int32_t)chunk->meshVertCounts[m]
));
}
vertOffset += chunk->meshVertCounts[m];
}
}
errorOk();
}
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorChain(textureDispose(&TEXTURE_CHUNK));
errorOk();
}
+9 -1
View File
@@ -28,9 +28,17 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
*/
errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
/**
* Draws all loaded chunks in two passes: base meshes first (shared texture,
* no binds between chunks), then each chunk's additional meshes.
*
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawChunks();
/**
* Renders the overworld scene.
*
*
* @param sceneData The scene data used for this scene.
* @return An error if the render failed, or errorOk() if it succeeded.
*/
+1
View File
@@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
crypt.c
endian.c
memory.c
random.c
string.c
math.c
sort.c
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "random.h"
#include <stdlib.h>
float_t randomFloat(const float_t min, const float_t max) {
return min + ((float_t)rand() / (float_t)RAND_MAX) * (max - min);
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
/**
* Returns a random float between min (inclusive) and max (exclusive).
*
* @param min Lower bound.
* @param max Upper bound.
* @returns A random float in [min, max).
*/
float_t randomFloat(const float_t min, const float_t max);
+120
View File
@@ -0,0 +1,120 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
"""
Converts DCF chunk files from version 1 to version 2.
Version 1 format (after 8-byte header + tiles):
uint32_t vertCount
meshvertex_t vertices[vertCount]
Version 2 format (after 8-byte header + tiles):
uint8_t meshCount
for each mesh:
uint32_t vertCount
meshvertex_t vertices[vertCount]
Usage:
python3 -m tools.asset.chunk <input.dcf> [output.dcf]
If output is omitted the input file is updated in place.
"""
import struct
import sys
import os
# Must match src/dusk/rpg/overworld/chunk.h
CHUNK_WIDTH = 16
CHUNK_HEIGHT = 16
CHUNK_DEPTH = 32
CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 8192
CHUNK_MESH_COUNT_MAX = 10
CHUNK_VERTEX_COUNT = 8192
# C enum (int) = 4 bytes; meshvertex_t = uv[2]+pos[3] floats = 20 bytes
TILE_SIZE = 4
VERTEX_SIZE = 20 # 2 floats UV + 3 floats pos, MESH_ENABLE_COLOR=0
FILE_MAGIC = b'DCF'
VERSION_IN = 1
VERSION_OUT = 2
def read_v1(path):
with open(path, 'rb') as f:
data = f.read()
if data[:3] != FILE_MAGIC:
raise ValueError(f"{path}: not a DCF file")
version = struct.unpack_from('<I', data, 4)[0]
if version != VERSION_IN:
raise ValueError(f"{path}: expected version {VERSION_IN}, got {version}")
offset = 8
tiles_bytes = CHUNK_TILE_COUNT * TILE_SIZE
tiles = data[offset:offset + tiles_bytes]
offset += tiles_bytes
vert_count = struct.unpack_from('<I', data, offset)[0]
offset += 4
verts = data[offset:offset + vert_count * VERTEX_SIZE]
if len(verts) != vert_count * VERTEX_SIZE:
raise ValueError(f"{path}: truncated vertex data")
return tiles, vert_count, verts
def write_v2(path, tiles, vert_count, verts):
if vert_count > CHUNK_VERTEX_COUNT:
print(
f" Warning: {vert_count} vertices exceeds pool "
f"({CHUNK_VERTEX_COUNT}); truncating."
)
vert_count = CHUNK_VERTEX_COUNT
verts = verts[:vert_count * VERTEX_SIZE]
mesh_count = 1 if vert_count > 0 else 0
buf = bytearray()
buf += FILE_MAGIC
buf += b'\x00'
buf += struct.pack('<I', VERSION_OUT)
buf += tiles
buf += struct.pack('<B', mesh_count)
if mesh_count > 0:
buf += struct.pack('<I', vert_count)
buf += verts
with open(path, 'wb') as f:
f.write(buf)
print(
f" Wrote {path}: version {VERSION_OUT}, "
f"{mesh_count} mesh(es), {vert_count} vertices."
)
def main():
args = sys.argv[1:]
if not args:
print("Usage: python3 -m tools.asset.chunk <input.dcf> [output.dcf]")
sys.exit(1)
src = args[0]
dst = args[1] if len(args) > 1 else src
print(f"Reading {src} ...")
tiles, vert_count, verts = read_v1(src)
print(f" tiles={CHUNK_TILE_COUNT}, vertices={vert_count}")
print(f"Writing {dst} ...")
write_v2(dst, tiles, vert_count, verts)
if __name__ == '__main__':
main()
+160
View File
@@ -0,0 +1,160 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
"""
Generates chunk 0_0_0.dcf with a small hill in the centre.
Hill layout (tile coordinates, 0-based):
y=5: . . . . . . N N . . . . . . . . RAMP_NORTH (south slope)
y=6: . . . . . E H H W . . . . . . . Hill top (H), RAMP_EAST/WEST
y=7: . . . . . E H H W . . . . . . .
y=8: . . . . . . S S . . . . . . . . RAMP_SOUTH (north slope)
x=6 x=7
"""
import struct, os
# Must match src/dusk/rpg/overworld/chunk.h and tile.h
CHUNK_WIDTH = 16
CHUNK_HEIGHT = 16
CHUNK_DEPTH = 32
CHUNK_W_F = float(CHUNK_WIDTH)
TILE_NULL = 0
TILE_GROUND = 1
TILE_RAMP_NORTH = 2
TILE_RAMP_SOUTH = 3
TILE_RAMP_EAST = 4
TILE_RAMP_WEST = 5
TILE_SIZE = 4 # sizeof(tile_t) = sizeof(int)
VERT_SIZE = 20 # sizeof(meshvertex_t): uv[2] + pos[3] floats
FILE_VER = 2
# Hill geometry parameters
HILL_X = frozenset({6, 7})
HILL_Y = frozenset({6, 7})
HILL_H = 1.0
def tile_idx(cx, cy, cz):
return cz * CHUNK_WIDTH * CHUNK_HEIGHT + cy * CHUNK_WIDTH + cx
def make_vert(u, v, px, py, pz):
return struct.pack('<5f', u, v, px, py, pz)
def quad_verts(cx, cy, z_sw, z_se, z_ne, z_nw):
"""
Build 6 vertices (2 triangles) for a tile quad.
Heights at each corner: SW=south-west, SE=south-east,
NE=north-east, NW=north-west.
UV formula (verified against existing DCF data):
u = (cy + within_x) / CHUNK_WIDTH where within_x in {0,1}
v = (cx + within_y) / CHUNK_HEIGHT where within_y in {0,1}
"""
u0 = cy / CHUNK_W_F
u1 = (cy + 1) / CHUNK_W_F
v0 = cx / CHUNK_W_F
v1 = (cx + 1) / CHUNK_W_F
x0, x1 = float(cx), float(cx + 1)
y0, y1 = float(cy), float(cy + 1)
SW = make_vert(u0, v0, x0, y0, float(z_sw))
SE = make_vert(u1, v0, x1, y0, float(z_se))
NE = make_vert(u1, v1, x1, y1, float(z_ne))
NW = make_vert(u0, v1, x0, y1, float(z_nw))
return SW + SE + NE + SW + NE + NW
def flat(cx, cy, z):
return quad_verts(cx, cy, z, z, z, z)
def ramp_north(cx, cy):
return quad_verts(cx, cy, 0, 0, HILL_H, HILL_H)
def ramp_south(cx, cy):
return quad_verts(cx, cy, HILL_H, HILL_H, 0, 0)
def ramp_east(cx, cy):
return quad_verts(cx, cy, 0, HILL_H, HILL_H, 0)
def ramp_west(cx, cy):
return quad_verts(cx, cy, HILL_H, 0, 0, HILL_H)
def generate():
tiles = [TILE_GROUND] * (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH)
ramps_n = frozenset((cx, 5) for cx in HILL_X)
ramps_s = frozenset((cx, 8) for cx in HILL_X)
ramps_e = frozenset((5, cy) for cy in HILL_Y)
ramps_w = frozenset((8, cy) for cy in HILL_Y)
for cx, cy in ramps_n:
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_NORTH
for cx, cy in ramps_s:
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_SOUTH
for cx, cy in ramps_e:
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_EAST
for cx, cy in ramps_w:
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_WEST
for cx in HILL_X:
for cy in HILL_Y:
tiles[tile_idx(cx, cy, 1)] = TILE_GROUND
verts = bytearray()
for cx in range(CHUNK_WIDTH):
for cy in range(CHUNK_HEIGHT):
pos = (cx, cy)
if cx in HILL_X and cy in HILL_Y:
continue
if pos in ramps_n:
verts += ramp_north(cx, cy)
elif pos in ramps_s:
verts += ramp_south(cx, cy)
elif pos in ramps_e:
verts += ramp_east(cx, cy)
elif pos in ramps_w:
verts += ramp_west(cx, cy)
else:
verts += flat(cx, cy, 0)
for cx in sorted(HILL_X):
for cy in sorted(HILL_Y):
verts += flat(cx, cy, HILL_H)
vert_count = len(verts) // VERT_SIZE
tile_bytes = struct.pack(f'<{len(tiles)}i', *tiles)
buf = bytearray()
buf += b'DCF\x00'
buf += struct.pack('<I', FILE_VER)
buf += tile_bytes
buf += struct.pack('<B', 1)
buf += struct.pack('<I', vert_count)
buf += verts
return buf, vert_count
if __name__ == '__main__':
out = os.path.join(
os.path.dirname(__file__), '..', '..', '..', 'assets', 'chunks',
'0_0_0.dcf'
)
out = os.path.normpath(out)
buf, vert_count = generate()
with open(out, 'wb') as f:
f.write(buf)
print(f'Wrote {out}: {vert_count} vertices, {len(buf)} bytes')