Compare commits
10 Commits
alpha-0.0.2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb020c36c1 | |||
| f17b0bfcfb | |||
| 2a85c9503f | |||
| 182428d6d6 | |||
| 8181a28557 | |||
| 88aed11d98 | |||
| 67010592b8 | |||
| dd22d6424a | |||
| e53775b97f | |||
| d326f6c1ac |
@@ -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.
Binary file not shown.
@@ -15,3 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
@@ -39,4 +39,16 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.loadAsync = assetJsonLoaderAsync,
|
||||
.dispose = assetJsonDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_CHUNK] = {
|
||||
.loadSync = assetChunkLoaderSync,
|
||||
.loadAsync = assetChunkLoaderAsync,
|
||||
.dispose = assetChunkDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_DMF] = {
|
||||
.loadSync = assetDmfLoaderSync,
|
||||
.loadAsync = assetDmfLoaderAsync,
|
||||
.dispose = assetDmfDispose
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "asset/loader/display/assettilesetloader.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/dmf/assetdmfloader.h"
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
@@ -20,6 +22,8 @@ typedef enum {
|
||||
ASSET_LOADER_TYPE_TILESET,
|
||||
ASSET_LOADER_TYPE_LOCALE,
|
||||
ASSET_LOADER_TYPE_JSON,
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_DMF,
|
||||
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
@@ -30,6 +34,8 @@ typedef union {
|
||||
assettilesetloaderinput_t tileset;
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
assetchunkloaderinput_t chunk;
|
||||
assetdmfloaderinput_t dmf;
|
||||
} assetloaderinput_t;
|
||||
|
||||
typedef union {
|
||||
@@ -38,6 +44,8 @@ typedef union {
|
||||
assettilesetloaderloading_t tileset;
|
||||
assetlocaleloaderloading_t locale;
|
||||
assetjsonloaderloading_t json;
|
||||
assetchunkloaderloading_t chunk;
|
||||
assetdmfloaderloading_t dmf;
|
||||
} assetloaderloading_t;
|
||||
|
||||
typedef union {
|
||||
@@ -46,6 +54,8 @@ typedef union {
|
||||
assettilesetoutput_t tileset;
|
||||
assetlocaleoutput_t locale;
|
||||
assetjsonoutput_t json;
|
||||
assetchunkoutput_t chunk;
|
||||
assetdmfoutput_t dmf;
|
||||
} 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."
|
||||
);
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
uint8_t nameLen = 0;
|
||||
while(
|
||||
data[offset + nameLen] != '\0' &&
|
||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
||||
) {
|
||||
nameLen++;
|
||||
}
|
||||
memoryCopy(out->meshNames[m], data + offset, nameLen);
|
||||
out->meshNames[m][nameLen] = '\0';
|
||||
offset += nameLen + 1;
|
||||
|
||||
memoryCopy(
|
||||
out->meshOffsets[m], data + offset, sizeof(vec3)
|
||||
);
|
||||
offset += sizeof(vec3);
|
||||
}
|
||||
|
||||
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,67 @@
|
||||
/**
|
||||
* 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 3
|
||||
|
||||
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;
|
||||
char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
} 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 with
|
||||
* tile data and DMF mesh names.
|
||||
*
|
||||
* @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);
|
||||
@@ -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
|
||||
assetdmfloader.c
|
||||
)
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetdmfloader.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 assetDmfLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.dmf.state != ASSET_DMF_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.dmf.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.dmf.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 DMF file."
|
||||
);
|
||||
|
||||
loading->loading.dmf.data = data;
|
||||
loading->loading.dmf.state = ASSET_DMF_LOADING_STATE_CREATE_MESH;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetDmfLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_DMF, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
switch(loading->loading.dmf.state) {
|
||||
case ASSET_DMF_LOADING_STATE_INITIAL:
|
||||
loading->loading.dmf.state = ASSET_DMF_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
break;
|
||||
|
||||
case ASSET_DMF_LOADING_STATE_CREATE_MESH:
|
||||
break;
|
||||
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.dmf.data;
|
||||
assertNotNull(data, "DMF data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'M' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid DMF file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_DMF_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported DMF version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(data + 8));
|
||||
assetdmfoutput_t *out = &loading->entry->data.dmf;
|
||||
|
||||
if(vertCount == 0) {
|
||||
memoryFree(data);
|
||||
loading->loading.dmf.data = NULL;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
out->vertices = memoryAllocate(vertCount * sizeof(meshvertex_t));
|
||||
memoryCopy(
|
||||
out->vertices, data + 12, vertCount * sizeof(meshvertex_t)
|
||||
);
|
||||
memoryFree(data);
|
||||
loading->loading.dmf.data = NULL;
|
||||
|
||||
errorret_t ret = meshInit(
|
||||
&out->mesh,
|
||||
MESH_PRIMITIVE_TYPE_TRIANGLES,
|
||||
(int32_t)vertCount,
|
||||
out->vertices
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||
if(errorIsNotOk(ret)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
meshDispose(&out->mesh);
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetDmfDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_DMF, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetdmfoutput_t *out = &entry->data.dmf;
|
||||
if(out->vertices != NULL) {
|
||||
errorChain(meshDispose(&out->mesh));
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/assetfile.h"
|
||||
#include "display/mesh/mesh.h"
|
||||
|
||||
#define ASSET_DMF_FILE_VERSION 1
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetdmfloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_DMF_LOADING_STATE_INITIAL,
|
||||
ASSET_DMF_LOADING_STATE_READ_FILE,
|
||||
ASSET_DMF_LOADING_STATE_CREATE_MESH,
|
||||
ASSET_DMF_LOADING_STATE_DONE
|
||||
} assetdmfloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetdmfloadingstate_t state;
|
||||
uint8_t *data;
|
||||
} assetdmfloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
mesh_t mesh;
|
||||
meshvertex_t *vertices;
|
||||
} assetdmfoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for DMF mesh assets. Reads the raw DMF 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.
|
||||
*/
|
||||
errorret_t assetDmfLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for DMF mesh assets. Parses the DMF binary read by
|
||||
* the async phase, then initializes and flushes the mesh to the GPU.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetDmfLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for DMF mesh assets. Disposes the mesh and frees the vertex
|
||||
* buffer.
|
||||
*
|
||||
* @param entry Asset entry containing the DMF data to dispose.
|
||||
* @return Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetDmfDispose(assetentry_t *entry);
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -214,3 +216,31 @@ uint8_t entityGetAvailable() {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -111,3 +113,12 @@ entity_t *entityGetAt(const worldpos_t pos);
|
||||
* @return The index of an available entity, or 0xFF if none are available.
|
||||
*/
|
||||
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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
)
|
||||
+12
-6
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,13 +33,24 @@ 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;
|
||||
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);
|
||||
|
||||
@@ -11,3 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
worldpos.c
|
||||
tile.c
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 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/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_MESH_NAME_MAX 64
|
||||
#define CHUNK_ENTITY_COUNT_MAX 10
|
||||
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct chunk_s {
|
||||
chunkpos_t position;
|
||||
tile_t tiles[CHUNK_TILE_COUNT];
|
||||
|
||||
meshvertex_t vertices[CHUNK_VERTEX_COUNT];
|
||||
uint32_t vertCount;
|
||||
mesh_t mesh;
|
||||
color_t testColor;
|
||||
uint8_t meshCount;
|
||||
char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
assetentry_t *meshEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
// uint8_t meshCount;
|
||||
// meshvertex_t vertices[CHUNK_VERTEX_COUNT_MAX];
|
||||
// mesh_t meshes[CHUNK_MESH_COUNT_MAX];
|
||||
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
||||
} chunk_t;
|
||||
|
||||
|
||||
+76
-143
@@ -9,6 +9,7 @@
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "util/string.h"
|
||||
|
||||
@@ -17,18 +18,6 @@ map_t MAP;
|
||||
errorret_t mapInit() {
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
|
||||
// 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
|
||||
));
|
||||
}
|
||||
|
||||
// Perform "initial load"
|
||||
MAP.loaded = true;
|
||||
int32_t i = 0;
|
||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
@@ -52,61 +41,6 @@ bool_t mapIsLoaded() {
|
||||
return MAP.loaded;
|
||||
}
|
||||
|
||||
// errorret_t mapLoad(const char_t *path, const chunkpos_t position) {
|
||||
// assertStrLenMin(path, 1, "Map file path cannot be empty");
|
||||
// assertStrLenMax(path, MAP_FILE_PATH_MAX - 1, "Map file path too long");
|
||||
|
||||
// if(stringCompare(MAP.filePath, path) == 0) {
|
||||
// // Same map, no need to reload
|
||||
// errorOk();
|
||||
// }
|
||||
|
||||
// chunkindex_t i;
|
||||
|
||||
// // Unload all loaded chunks
|
||||
// if(mapIsLoaded()) {
|
||||
// for(i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
// mapChunkUnload(&MAP.chunks[i]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Store the map file path
|
||||
// stringCopy(MAP.filePath, path, MAP_FILE_PATH_MAX);
|
||||
|
||||
// // Determine directory path (it is dirname)
|
||||
// stringCopy(MAP.dirPath, path, MAP_FILE_PATH_MAX);
|
||||
// char_t *last = stringFindLastChar(MAP.dirPath, '/');
|
||||
// if(last == NULL) errorThrow("Invalid map file path");
|
||||
|
||||
// // Store filename, sans extension
|
||||
// stringCopy(MAP.fileName, last + 1, MAP_FILE_PATH_MAX);
|
||||
// *last = '\0'; // Terminate to get directory path
|
||||
|
||||
// last = stringFindLastChar(MAP.fileName, '.');
|
||||
// if(last == NULL) errorThrow("Map file name has no extension");
|
||||
// *last = '\0'; // Terminate to remove extension
|
||||
|
||||
// // Reset map position
|
||||
// MAP.chunkPosition = position;
|
||||
|
||||
// // Perform "initial load"
|
||||
// i = 0;
|
||||
// for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||
// for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||
// for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||
// chunk_t *chunk = &MAP.chunks[i];
|
||||
// chunk->position.x = x + position.x;
|
||||
// chunk->position.y = y + position.y;
|
||||
// chunk->position.z = z + position.z;
|
||||
// MAP.chunkOrder[i] = chunk;
|
||||
// errorChain(mapChunkLoad(chunk));
|
||||
// i++;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// errorOk();
|
||||
// }
|
||||
|
||||
errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
|
||||
@@ -115,7 +49,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Determine which chunks remain loaded
|
||||
chunkindex_t chunksRemaining[MAP_CHUNK_COUNT] = {0};
|
||||
chunkindex_t chunksFreed[MAP_CHUNK_COUNT] = {0};
|
||||
|
||||
@@ -123,7 +56,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
uint32_t freedCount = 0;
|
||||
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
// Will this chunk remain loaded?
|
||||
chunk_t *chunk = &MAP.chunks[i];
|
||||
if(
|
||||
chunk->position.x >= newPos.x &&
|
||||
@@ -135,23 +67,18 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
chunk->position.z >= newPos.z &&
|
||||
chunk->position.z < newPos.z + MAP_CHUNK_DEPTH
|
||||
) {
|
||||
// Stays loaded
|
||||
chunksRemaining[remainingCount++] = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not remaining loaded
|
||||
chunksFreed[freedCount++] = i;
|
||||
}
|
||||
|
||||
// Unload the freed chunks
|
||||
for(chunkindex_t i = 0; i < freedCount; i++) {
|
||||
chunk_t *chunk = &MAP.chunks[chunksFreed[i]];
|
||||
mapChunkUnload(chunk);
|
||||
}
|
||||
|
||||
// This can probably be optimized later, for now we check each chunk and see
|
||||
// if it needs loading or not, and update the chunk order
|
||||
chunkindex_t orderIndex = 0;
|
||||
for(chunkunit_t zOff = 0; zOff < MAP_CHUNK_DEPTH; zOff++) {
|
||||
for(chunkunit_t yOff = 0; yOff < MAP_CHUNK_HEIGHT; yOff++) {
|
||||
@@ -160,7 +87,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
newPos.x + xOff, newPos.y + yOff, newPos.z + zOff
|
||||
};
|
||||
|
||||
// Is this chunk already loaded (was not unloaded earlier)?
|
||||
chunkindex_t chunkIndex = -1;
|
||||
for(chunkindex_t i = 0; i < remainingCount; i++) {
|
||||
chunk_t *chunk = &MAP.chunks[chunksRemaining[i]];
|
||||
@@ -169,9 +95,7 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Need to load this chunk
|
||||
if(chunkIndex == -1) {
|
||||
// Find a freed chunk to reuse
|
||||
chunkIndex = chunksFreed[--freedCount];
|
||||
chunk_t *chunk = &MAP.chunks[chunkIndex];
|
||||
chunk->position = newChunkPos;
|
||||
@@ -183,7 +107,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update map position
|
||||
MAP.chunkPosition = newPos;
|
||||
|
||||
errorOk();
|
||||
@@ -196,90 +119,100 @@ 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));
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void mapChunkUnload(chunk_t* chunk) {
|
||||
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]];
|
||||
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;
|
||||
}
|
||||
|
||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
||||
if(chunk->meshEntries[m] == NULL) continue;
|
||||
assetUnlockEntry(chunk->meshEntries[m]);
|
||||
chunk->meshEntries[m] = NULL;
|
||||
}
|
||||
chunk->meshCount = 0;
|
||||
}
|
||||
|
||||
errorret_t mapChunkLoad(chunk_t* chunk) {
|
||||
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;
|
||||
chunk->meshCount = 0;
|
||||
|
||||
if(chunk->position.z != 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
|
||||
};
|
||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
|
||||
assertNotNull(entry, "Failed to get chunk asset entry");
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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));
|
||||
uint8_t meshCount = entry->data.chunk.meshCount;
|
||||
for(uint8_t m = 0; m < meshCount; m++) {
|
||||
stringCopy(
|
||||
chunk->meshNames[m],
|
||||
entry->data.chunk.meshNames[m],
|
||||
CHUNK_MESH_NAME_MAX
|
||||
);
|
||||
glm_vec3_copy(entry->data.chunk.meshOffsets[m], chunk->meshOffsets[m]);
|
||||
}
|
||||
assetUnlockEntry(entry);
|
||||
|
||||
for(uint8_t m = 0; m < meshCount; m++) {
|
||||
assetentry_t *meshEntry = assetLock(
|
||||
chunk->meshNames[m], ASSET_LOADER_TYPE_DMF, NULL
|
||||
);
|
||||
if(meshEntry == NULL) {
|
||||
for(uint8_t j = 0; j < m; j++) {
|
||||
assetUnlockEntry(chunk->meshEntries[j]);
|
||||
chunk->meshEntries[j] = NULL;
|
||||
}
|
||||
errorThrow("Failed to lock mesh: %s", chunk->meshNames[m]);
|
||||
}
|
||||
|
||||
ret = assetRequireLoaded(meshEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(meshEntry);
|
||||
for(uint8_t j = 0; j < m; j++) {
|
||||
assetUnlockEntry(chunk->meshEntries[j]);
|
||||
chunk->meshEntries[j] = NULL;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
chunk->meshEntries[m] = meshEntry;
|
||||
}
|
||||
chunk->meshCount = meshCount;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -304,7 +237,7 @@ chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) {
|
||||
return chunkPosToIndex(&relPos);
|
||||
}
|
||||
|
||||
chunk_t* mapGetChunk(const uint8_t index) {
|
||||
chunk_t *mapGetChunk(const uint8_t index) {
|
||||
if(index >= MAP_CHUNK_COUNT) return NULL;
|
||||
if(!mapIsLoaded()) return NULL;
|
||||
return MAP.chunkOrder[index];
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define MAP_FILE_PATH_MAX 128
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -13,15 +13,35 @@
|
||||
#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"
|
||||
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.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 +100,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);
|
||||
|
||||
spriteCount++;
|
||||
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;
|
||||
}
|
||||
|
||||
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 +135,48 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
errorret_t sceneOverworldDrawChunks() {
|
||||
shadermaterial_t chunkMaterial = {
|
||||
.unlit = {
|
||||
.color = COLOR_WHITE,
|
||||
.texture = &TEXTURE_CHUNK
|
||||
}
|
||||
};
|
||||
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
chunk_t *chunk = &MAP.chunks[i];
|
||||
if(chunk->meshCount == 0) continue;
|
||||
|
||||
worldpos_t wp;
|
||||
chunkPosToWorldPos(&chunk->position, &wp);
|
||||
vec3 wpf = { (float_t)wp.x, (float_t)wp.y, (float_t)wp.z };
|
||||
|
||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
||||
if(chunk->meshEntries[m] == NULL) continue;
|
||||
|
||||
vec3 pos;
|
||||
glm_vec3_add(wpf, chunk->meshOffsets[m], pos);
|
||||
mat4 model;
|
||||
glm_translate_make(model, pos);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
|
||||
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
|
||||
errorChain(meshDraw(&chunk->meshEntries[m]->data.dmf.mesh, 0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
// Restore identity model so subsequent renders (e.g. entities) are
|
||||
// not affected by the last chunk transform.
|
||||
mat4 identity;
|
||||
glm_mat4_identity(identity);
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, identity));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
|
||||
errorChain(textureDispose(&TEXTURE_CHUNK));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -28,6 +28,14 @@ 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.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
crypt.c
|
||||
endian.c
|
||||
memory.c
|
||||
random.c
|
||||
string.c
|
||||
math.c
|
||||
sort.c
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,196 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
"""
|
||||
Converts DCF chunk files (version 1 or 2) to version 3 and generates the
|
||||
companion DMF (Dusk Mesh Format) files that version 3 references.
|
||||
|
||||
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]
|
||||
|
||||
Version 3 format (after 8-byte header + tiles):
|
||||
uint8_t meshCount
|
||||
for each mesh:
|
||||
null-terminated string (path to companion .dmf asset)
|
||||
|
||||
DMF format:
|
||||
Bytes 0-3: DMF\x00
|
||||
Bytes 4-7: uint32_t version = 1 (little-endian)
|
||||
Bytes 8-11: uint32_t vertCount (little-endian)
|
||||
Bytes 12+: 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.
|
||||
DMF files are written to assets/meshes/ beside the chunks/ directory.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
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
|
||||
CHUNK_MESH_NAME_MAX = 64
|
||||
|
||||
TILE_SIZE = 4
|
||||
VERTEX_SIZE = 20
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 3
|
||||
DMF_VERSION = 1
|
||||
|
||||
|
||||
def read_dcf(path):
|
||||
"""Read a v1 or v2 DCF file. Returns (tiles, meshes) where meshes is a
|
||||
list of raw vertex byte strings, one per mesh."""
|
||||
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 not in (1, 2):
|
||||
raise ValueError(
|
||||
f"{path}: expected version 1 or 2, got {version}"
|
||||
)
|
||||
|
||||
offset = 8
|
||||
tiles_size = CHUNK_TILE_COUNT * TILE_SIZE
|
||||
tiles = data[offset:offset + tiles_size]
|
||||
offset += tiles_size
|
||||
|
||||
meshes = []
|
||||
|
||||
if version == 1:
|
||||
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")
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
else:
|
||||
mesh_count = data[offset]
|
||||
offset += 1
|
||||
for _ in range(mesh_count):
|
||||
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")
|
||||
offset += vert_count * VERTEX_SIZE
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
|
||||
return tiles, meshes
|
||||
|
||||
|
||||
def write_dmf(path, vertex_bytes):
|
||||
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
||||
buf = bytearray()
|
||||
buf += DMF_MAGIC
|
||||
buf += struct.pack('<I', DMF_VERSION)
|
||||
buf += struct.pack('<I', vert_count)
|
||||
buf += vertex_bytes
|
||||
with open(path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DMF {path}: '
|
||||
f'{vert_count} vertices, {len(buf)} bytes'
|
||||
)
|
||||
|
||||
|
||||
def write_v3(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
||||
"""Write a v3 DCF that references the given DMF asset paths.
|
||||
|
||||
mesh_offsets is an optional list of (x, y, z) tuples, one per mesh.
|
||||
Defaults to (0, 0, 0) for each mesh when omitted.
|
||||
"""
|
||||
mesh_count = len(mesh_names)
|
||||
if mesh_offsets is None:
|
||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||
|
||||
buf = bytearray()
|
||||
buf += FILE_MAGIC
|
||||
buf += b'\x00'
|
||||
buf += struct.pack('<I', VERSION_OUT)
|
||||
buf += tiles
|
||||
buf += struct.pack('<B', mesh_count)
|
||||
for name, offset in zip(mesh_names, mesh_offsets):
|
||||
encoded = name.encode('ascii')
|
||||
if len(encoded) >= CHUNK_MESH_NAME_MAX:
|
||||
raise ValueError(
|
||||
f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}"
|
||||
)
|
||||
buf += encoded + b'\x00'
|
||||
buf += struct.pack('<3f', offset[0], offset[1], offset[2])
|
||||
with open(dcf_path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DCF {dcf_path}: '
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es)'
|
||||
)
|
||||
|
||||
|
||||
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, meshes = read_dcf(src)
|
||||
print(
|
||||
f" tiles={CHUNK_TILE_COUNT}, "
|
||||
f"meshes={len(meshes)}, "
|
||||
f"total_verts="
|
||||
f"{sum(len(m) // VERTEX_SIZE for m in meshes)}"
|
||||
)
|
||||
|
||||
# Derive chunk base name from the DCF filename (e.g. "0_0_0" from
|
||||
# "0_0_0.dcf") to name DMF files "chunk_0_0_0_0.dmf" etc.
|
||||
base = os.path.splitext(os.path.basename(dst))[0]
|
||||
|
||||
# assets/meshes/ sits beside assets/chunks/ (one dir up from the DCF).
|
||||
meshes_dir = os.path.normpath(
|
||||
os.path.join(os.path.dirname(os.path.abspath(dst)), '..', 'meshes')
|
||||
)
|
||||
os.makedirs(meshes_dir, exist_ok=True)
|
||||
|
||||
print(f"Writing DMF files to {meshes_dir} ...")
|
||||
mesh_names = []
|
||||
for idx, verts in enumerate(meshes):
|
||||
dmf_filename = f'chunk_{base}_{idx}.dmf'
|
||||
dmf_path = os.path.join(meshes_dir, dmf_filename)
|
||||
write_dmf(dmf_path, verts)
|
||||
mesh_names.append(f'meshes/{dmf_filename}')
|
||||
|
||||
print(f"Writing {dst} ...")
|
||||
write_v3(dst, tiles, mesh_names)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
"""
|
||||
Writes DMF (Dusk Mesh Format) files.
|
||||
|
||||
DMF format:
|
||||
Bytes 0-3: DMF\x00 (magic)
|
||||
Bytes 4-7: uint32_t version = 1 (little-endian)
|
||||
Bytes 8-11: uint32_t vertCount (little-endian)
|
||||
Bytes 12+: meshvertex_t vertices[vertCount]
|
||||
Each vertex is 20 bytes: uv[2] + pos[3] (5 floats, LE)
|
||||
|
||||
Usage:
|
||||
python3 -m tools.asset.dmf <output.dmf> <vertices.bin>
|
||||
Reads raw vertex bytes from vertices.bin and writes a DMF file.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
MAGIC = b'DMF\x00'
|
||||
VERSION = 1
|
||||
VERTEX_SIZE = 20
|
||||
|
||||
|
||||
def write_dmf(path, vertex_bytes):
|
||||
if len(vertex_bytes) % VERTEX_SIZE != 0:
|
||||
raise ValueError(
|
||||
f"Vertex data size {len(vertex_bytes)} is not a "
|
||||
f"multiple of {VERTEX_SIZE}"
|
||||
)
|
||||
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
||||
buf = bytearray()
|
||||
buf += MAGIC
|
||||
buf += struct.pack('<I', VERSION)
|
||||
buf += struct.pack('<I', vert_count)
|
||||
buf += vertex_bytes
|
||||
with open(path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f'Wrote {path}: {vert_count} vertices, {len(buf)} bytes'
|
||||
)
|
||||
return vert_count
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if len(args) != 2:
|
||||
print(
|
||||
"Usage: python3 -m tools.asset.dmf "
|
||||
"<output.dmf> <vertices.bin>"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
dst = args[0]
|
||||
src = args[1]
|
||||
|
||||
with open(src, 'rb') as f:
|
||||
vertex_bytes = f.read()
|
||||
|
||||
write_dmf(dst, vertex_bytes)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user