diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c index 84a55c61..4b2b4a65 100644 --- a/src/dusk/rpg/entity/entity.c +++ b/src/dusk/rpg/entity/entity.c @@ -11,6 +11,7 @@ #include "time/time.h" #include "util/math.h" #include "rpg/overworld/map.h" +#include "rpg/overworld/maparea.h" #include "rpg/overworld/chunk.h" #include "rpg/overworld/tile.h" @@ -224,6 +225,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) { entity->animation = ENTITY_ANIM_WALK; entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking entityUpdateChunk(entity); + mapAreaCheckEntity(entity); } void entityRun(entity_t *entity, const entitydir_t direction) { diff --git a/src/dusk/rpg/overworld/CMakeLists.txt b/src/dusk/rpg/overworld/CMakeLists.txt index 3f1c93c5..a6c4f934 100644 --- a/src/dusk/rpg/overworld/CMakeLists.txt +++ b/src/dusk/rpg/overworld/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC chunk.c map.c + maparea.c worldpos.c tile.c tileshape.c diff --git a/src/dusk/rpg/overworld/maparea.c b/src/dusk/rpg/overworld/maparea.c new file mode 100644 index 00000000..2758065d --- /dev/null +++ b/src/dusk/rpg/overworld/maparea.c @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "maparea.h" +#include "assert/assert.h" +#include "util/math.h" +#include "rpg/overworld/map.h" + +maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX]; + +void mapAreaInit( + maparea_t *area, + const worldpos_t min, + const worldpos_t max, + const mapareacallback_t callback, + const mapareanotify_t notify +) { + assertNotNull(area, "Map area pointer cannot be NULL"); + assertNotNull(callback, "Map area callback cannot be NULL"); + + area->min.x = mathMin(min.x, max.x); + area->min.y = mathMin(min.y, max.y); + area->min.z = mathMin(min.z, max.z); + + area->max.x = mathMax(min.x, max.x); + area->max.y = mathMax(min.y, max.y); + area->max.z = mathMax(min.z, max.z); + + area->callback = callback; + area->notify = notify; +} + +bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) { + assertNotNull(area, "Map area pointer cannot be NULL"); + + return ( + position.x >= area->min.x && position.x <= area->max.x && + position.y >= area->min.y && position.y <= area->max.y && + position.z >= area->min.z && position.z <= area->max.z + ); +} + +bool_t mapAreaIsChunkOverlappingOrInside( + const maparea_t *area, + const chunk_t *chunk +) { + assertNotNull(area, "Map area pointer cannot be NULL"); + assertNotNull(chunk, "Chunk pointer cannot be NULL"); + + worldpos_t chunkMin, chunkMax; + chunkPosToWorldPos(&chunk->position, &chunkMin); + chunkMax.x = chunkMin.x + CHUNK_WIDTH - 1; + chunkMax.y = chunkMin.y + CHUNK_HEIGHT - 1; + chunkMax.z = chunkMin.z + CHUNK_DEPTH - 1; + + return ( + chunkMin.x <= area->max.x && area->min.x <= chunkMax.x && + chunkMin.y <= area->max.y && area->min.y <= chunkMax.y && + chunkMin.z <= area->max.z && area->min.z <= chunkMax.z + ); +} + +bool_t mapAreaCanUnload(const maparea_t *area) { + assertNotNull(area, "Map area pointer cannot be NULL"); + + for(chunkindex_t i = 0; i < MAP_LOADED_CHUNK_COUNT; i++) { + if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false; + } + + return true; +} + +bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity) { + assertNotNull(area, "Map area pointer cannot be NULL"); + assertNotNull(entity, "Entity pointer cannot be NULL"); + + switch(entity->type) { + case ENTITY_TYPE_PLAYER: + return (area->notify & MAP_AREA_NOTIFY_PLAYER) != 0; + + case ENTITY_TYPE_NPC: + return (area->notify & MAP_AREA_NOTIFY_NPC) != 0; + + default: + return false; + } +} + +uint8_t mapAreaAdd( + const worldpos_t min, + const worldpos_t max, + const mapareacallback_t callback, + const mapareanotify_t notify +) { + for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) { + if(MAP_AREAS[i].callback != NULL) continue; + mapAreaInit(&MAP_AREAS[i], min, max, callback, notify); + return i; + } + + assertUnreachable("No available map area slots"); + return 0xFF; +} + +void mapAreaRemove(const uint8_t id) { + assertTrue(id < MAP_AREA_COUNT_MAX, "Map area ID is out of range"); + MAP_AREAS[id].callback = NULL; +} + +void mapAreaCheckEntity(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + + for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) { + maparea_t *area = &MAP_AREAS[i]; + if(area->callback == NULL) continue; + if(!mapAreaIsInside(area, entity->position)) continue; + if(!mapAreaShouldNotify(area, entity)) continue; + area->callback(entity); + } +} diff --git a/src/dusk/rpg/overworld/maparea.h b/src/dusk/rpg/overworld/maparea.h new file mode 100644 index 00000000..b138d56c --- /dev/null +++ b/src/dusk/rpg/overworld/maparea.h @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "worldpos.h" + +typedef struct chunk_s chunk_t; +typedef struct entity_s entity_t; +typedef struct maparea_s maparea_t; + +#define MAP_AREA_COUNT_MAX 32 + +typedef uint8_t mapareanotify_t; + +#define MAP_AREA_NOTIFY_PLAYER ((mapareanotify_t)(1 << 0)) +#define MAP_AREA_NOTIFY_NPC ((mapareanotify_t)(1 << 1)) +#define MAP_AREA_NOTIFY_ALL ((mapareanotify_t)( \ + MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC \ +)) + +/** + * Callback invoked for a map area. + * + * @param entity Pointer to the entity associated with the callback. + */ +typedef void (*mapareacallback_t)(entity_t *entity); + +typedef struct maparea_s { + worldpos_t min; + worldpos_t max; + mapareacallback_t callback; + mapareanotify_t notify; +} maparea_t; + +extern maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX]; + +/** + * Initializes a map area with the given bounds, callback and notify flags. + * + * @param area Pointer to the map area to initialize. + * @param min The minimum world position of the area. + * @param max The maximum world position of the area. + * @param callback The callback to invoke for this area. Must not be NULL. + * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types + * should trigger the callback. + */ +void mapAreaInit( + maparea_t *area, + const worldpos_t min, + const worldpos_t max, + const mapareacallback_t callback, + const mapareanotify_t notify +); + +/** + * Checks whether a world position falls within a map area's bounds + * (inclusive on all axes). + * + * @param area Pointer to the map area to check. + * @param position The world position to check. + * @returns true if the position is inside the area. + */ +bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position); + +/** + * Checks whether a chunk's full footprint overlaps a map area's bounds, + * rather than just testing the chunk's origin corner. + * + * @param area Pointer to the map area to check. + * @param chunk Pointer to the chunk to check. + * @returns true if the chunk overlaps the area. + */ +bool_t mapAreaIsChunkOverlappingOrInside( + const maparea_t *area, + const chunk_t *chunk +); + +/** + * Checks whether a map area is safe to unload, i.e. no entity currently + * occupies a position within its bounds. + * + * @param area Pointer to the map area to check. + * @returns true if no entity is inside the area. + */ +bool_t mapAreaCanUnload(const maparea_t *area); + +/** + * Checks whether an entity's type is included in a map area's notify flags. + * + * @param area Pointer to the map area to check. + * @param entity Pointer to the entity to check. + * @returns true if the entity's type should trigger this area's callback. + */ +bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity); + +/** + * Adds a map area to the global MAP_AREAS list, in the first free slot. + * A slot is considered free if its callback is NULL. + * + * @param min The minimum world position of the area. + * @param max The maximum world position of the area. + * @param callback The callback to invoke for this area. Must not be NULL. + * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types + * should trigger the callback. + * @returns The ID of the newly added map area. + */ +uint8_t mapAreaAdd( + const worldpos_t min, + const worldpos_t max, + const mapareacallback_t callback, + const mapareanotify_t notify +); + +/** + * Removes a map area from the global MAP_AREAS list, freeing its slot by + * clearing its callback to NULL. + * + * @param id The ID of the map area to remove. + */ +void mapAreaRemove(const uint8_t id); + +/** + * Checks every active map area against an entity's current position, + * invoking each matching area's callback if the entity is inside it and + * its type is included in that area's notify flags. + * + * @param entity Pointer to the entity to check. + */ +void mapAreaCheckEntity(entity_t *entity); \ No newline at end of file diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index abf0e976..5b97ba4d 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -9,6 +9,7 @@ #include "entity/entity.h" #include "rpg/entity/npc/npcpath.h" #include "rpg/overworld/map.h" +#include "rpg/overworld/maparea.h" #include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/scene/testcutscene.h" #include "rpg/item/backpack.h" @@ -17,9 +18,15 @@ #include "util/memory.h" #include "util/string.h" #include "assert/assert.h" +#include "console/console.h" + +void rpgTestAreaCallback(entity_t *entity) { + consolePrint("rpgTestAreaCallback"); +} errorret_t rpgInit(void) { memoryZero(ENTITIES, sizeof(ENTITIES)); + memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); backpackInit(); cutsceneSystemInit(); @@ -41,6 +48,15 @@ errorret_t rpgInit(void) { mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 }); + // TEST: Create a test map area. + uint8_t areaIndex = mapAreaAdd( + (worldpos_t){ 11, 3, 0 }, + (worldpos_t){ 16, 9, 10 }, + rpgTestAreaCallback, + MAP_AREA_NOTIFY_ALL + ); + assertTrue(areaIndex != 0xFF, "No available map area slots!."); + // All Good! errorOk(); } diff --git a/src/dusk/rpg/rpg.h b/src/dusk/rpg/rpg.h index fcc1449a..9396c326 100644 --- a/src/dusk/rpg/rpg.h +++ b/src/dusk/rpg/rpg.h @@ -28,7 +28,7 @@ errorret_t rpgUpdate(void); /** * Dispose of the RPG subsystem. - * + * * @return An error code. */ errorret_t rpgDispose(void); \ No newline at end of file