diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 61a7424d..a9cda295 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -72,14 +72,10 @@ errorret_t engineUpdate(void) { errorChain(uiTextboxUpdate()); physicsManagerUpdate(); errorChain(displayUpdate()); - - if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; - - // Scene update occurs last because only after rendering would we want to do - // scene switching, refer to sceneSet() for information. errorChain(cutsceneUpdate()); errorChain(sceneUpdate()); + if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; errorOk(); } diff --git a/src/dusk/entity/component/CMakeLists.txt b/src/dusk/entity/component/CMakeLists.txt index 4558fc62..fd6f1625 100644 --- a/src/dusk/entity/component/CMakeLists.txt +++ b/src/dusk/entity/component/CMakeLists.txt @@ -4,6 +4,7 @@ # https://opensource.org/licenses/MIT add_subdirectory(display) +add_subdirectory(overworld) add_subdirectory(physics) add_subdirectory(script) add_subdirectory(trigger) \ No newline at end of file diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c index 94877930..387171a7 100644 --- a/src/dusk/entity/component/display/entityrenderable.c +++ b/src/dusk/entity/component/display/entityrenderable.c @@ -11,7 +11,14 @@ #include "display/mesh/cube.h" #include "display/spritebatch/spritebatch.h" -errorret_t entityRenderableDrawDefault() { +errorret_t entityRenderableDrawDefault( + const entityid_t entityId, + const componentid_t componentId, + void *user +) { + (void)entityId; + (void)componentId; + (void)user; errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE)); errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL)); return meshDraw(&CUBE_MESH_SIMPLE, 0, -1); @@ -25,6 +32,7 @@ void entityRenderableInit( entityId, componentId, COMPONENT_TYPE_RENDERABLE ); r->draw = entityRenderableDrawDefault; + r->drawUser = NULL; } void entityRenderableDispose( @@ -37,12 +45,18 @@ void entityRenderableDispose( void entityRenderableSetDraw( const entityid_t entityId, const componentid_t componentId, - errorret_t (*draw)(void) + errorret_t (*draw)( + const entityid_t entityId, + const componentid_t componentId, + void *user + ), + void *user ) { entityrenderable_t *r = componentGetData( entityId, componentId, COMPONENT_TYPE_RENDERABLE ); r->draw = draw; + r->drawUser = user; } errorret_t entityRenderableDraw( @@ -52,5 +66,5 @@ errorret_t entityRenderableDraw( entityrenderable_t *r = componentGetData( entityId, componentId, COMPONENT_TYPE_RENDERABLE ); - return r->draw(); -} \ No newline at end of file + return r->draw(entityId, componentId, r->drawUser); +} diff --git a/src/dusk/entity/component/display/entityrenderable.h b/src/dusk/entity/component/display/entityrenderable.h index 1e4f578b..a0a36a55 100644 --- a/src/dusk/entity/component/display/entityrenderable.h +++ b/src/dusk/entity/component/display/entityrenderable.h @@ -12,7 +12,12 @@ #include "display/spritebatch/spritebatch.h" typedef struct { - errorret_t (*draw)(void); + errorret_t (*draw)( + const entityid_t entityId, + const componentid_t componentId, + void *user + ); + void *drawUser; } entityrenderable_t; /** @@ -48,7 +53,12 @@ void entityRenderableDispose( void entityRenderableSetDraw( const entityid_t entityId, const componentid_t componentId, - errorret_t (*draw)(void) + errorret_t (*draw)( + const entityid_t entityId, + const componentid_t componentId, + void *user + ), + void *user ); /** diff --git a/src/dusk/entity/component/overworld/CMakeLists.txt b/src/dusk/entity/component/overworld/CMakeLists.txt new file mode 100644 index 00000000..dab461f5 --- /dev/null +++ b/src/dusk/entity/component/overworld/CMakeLists.txt @@ -0,0 +1,12 @@ +# 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 + entityinteractable.c + entityoverworld.c + entityoverworldcamera.c + entityplayer.c +) diff --git a/src/dusk/entity/component/overworld/entityinteractable.c b/src/dusk/entity/component/overworld/entityinteractable.c new file mode 100644 index 00000000..dc74f606 --- /dev/null +++ b/src/dusk/entity/component/overworld/entityinteractable.c @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityinteractable.h" +#include "entity/entitymanager.h" + +void entityInteractableInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityinteractable_t *inter = entityInteractableGet(entityId, componentId); + inter->onInteract = NULL; + inter->user = NULL; +} + +entityinteractable_t * entityInteractableGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(entityId, componentId, COMPONENT_TYPE_INTERACTABLE); +} + +void entityInteractableSetCallback( + const entityid_t entityId, + const componentid_t componentId, + void (*onInteract)( + const entityid_t entityId, + const componentid_t componentId, + void *user + ), + void *user +) { + entityinteractable_t *inter = entityInteractableGet(entityId, componentId); + inter->onInteract = onInteract; + inter->user = user; +} + +void entityInteractableTrigger( + const entityid_t entityId, + const componentid_t componentId +) { + entityinteractable_t *inter = entityInteractableGet(entityId, componentId); + if(inter->onInteract == NULL) return; + inter->onInteract(entityId, componentId, inter->user); +} diff --git a/src/dusk/entity/component/overworld/entityinteractable.h b/src/dusk/entity/component/overworld/entityinteractable.h new file mode 100644 index 00000000..ec7440d1 --- /dev/null +++ b/src/dusk/entity/component/overworld/entityinteractable.h @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +typedef struct { + void (*onInteract)( + const entityid_t entityId, + const componentid_t componentId, + void *user + ); + void *user; +} entityinteractable_t; + +/** + * Initializes the interactable component, clearing the callback and user pointer. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityInteractableInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Returns a pointer to the interactable component data. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @return Pointer to the entityinteractable_t data. + */ +entityinteractable_t * entityInteractableGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the callback invoked when this interactable is triggered. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @param onInteract Function called on interaction, or NULL to clear. + * @param user Arbitrary pointer forwarded to the callback. + */ +void entityInteractableSetCallback( + const entityid_t entityId, + const componentid_t componentId, + void (*onInteract)( + const entityid_t entityId, + const componentid_t componentId, + void *user + ), + void *user +); + +/** + * Fires the interactable's callback if one is set. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityInteractableTrigger( + const entityid_t entityId, + const componentid_t componentId +); diff --git a/src/dusk/entity/component/overworld/entityoverworld.c b/src/dusk/entity/component/overworld/entityoverworld.c new file mode 100644 index 00000000..d95b7618 --- /dev/null +++ b/src/dusk/entity/component/overworld/entityoverworld.c @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityoverworld.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityrenderable.h" +#include "display/shader/shaderunlit.h" +#include "display/mesh/cube.h" + +void entityOverworldInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityoverworld_t *ow = entityOverworldGet(entityId, componentId); + ow->type = OVERWORLD_ENTITY_TYPE_NPC; + ow->facing = FACING_DIR_DOWN; + ow->renderCompId = entityGetComponent(entityId, COMPONENT_TYPE_RENDERABLE); + if(ow->renderCompId != COMPONENT_ID_INVALID) { + entityRenderableSetDraw(entityId, ow->renderCompId, entityOverworldDraw, NULL); + } + ow->physCompId = entityGetComponent(entityId, COMPONENT_TYPE_PHYSICS); +} + +entityoverworld_t * entityOverworldGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(entityId, componentId, COMPONENT_TYPE_OVERWORLD); +} + +void entityOverworldSetType( + const entityid_t entityId, + const componentid_t componentId, + const entityoverworldtype_t type +) { + entityOverworldGet(entityId, componentId)->type = type; +} + +errorret_t entityOverworldDraw( + const entityid_t entityId, + const componentid_t componentId, + void *user +) { + (void)componentId; (void)user; + + componentid_t owCompId = entityGetComponent(entityId, COMPONENT_TYPE_OVERWORLD); + entityoverworld_t *ow = entityOverworldGet(entityId, owCompId); + + color_t col = ow->type == OVERWORLD_ENTITY_TYPE_PLAYER ? COLOR_WHITE : COLOR_BLUE; + errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, col)); + errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL)); + return meshDraw(&CUBE_MESH_SIMPLE, 0, -1); +} diff --git a/src/dusk/entity/component/overworld/entityoverworld.h b/src/dusk/entity/component/overworld/entityoverworld.h new file mode 100644 index 00000000..6aa0a1db --- /dev/null +++ b/src/dusk/entity/component/overworld/entityoverworld.h @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "entity/entitybase.h" +#include "overworld/facingdir.h" + +typedef enum { + OVERWORLD_ENTITY_TYPE_PLAYER = 0, + OVERWORLD_ENTITY_TYPE_NPC = 1, +} entityoverworldtype_t; + +typedef struct { + entityoverworldtype_t type; + facingdir_t facing; + componentid_t renderCompId; + componentid_t physCompId; +} entityoverworld_t; + +/** + * Initializes the overworld component, wiring up the draw callback if a + * renderable component is already present on the entity. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityOverworldInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Returns a pointer to the overworld component data. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @return Pointer to the entityoverworld_t data. + */ +entityoverworld_t * entityOverworldGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the overworld entity type. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @param type The type to assign. + */ +void entityOverworldSetType( + const entityid_t entityId, + const componentid_t componentId, + const entityoverworldtype_t type +); + +/** + * Draw callback registered on the renderable component. + * + * @param entityId The owning entity. + * @param componentId The renderable component's ID. + * @param user Unused. + * @return Error result. + */ +errorret_t entityOverworldDraw( + const entityid_t entityId, + const componentid_t componentId, + void *user +); diff --git a/src/dusk/entity/component/overworld/entityoverworldcamera.c b/src/dusk/entity/component/overworld/entityoverworldcamera.c new file mode 100644 index 00000000..42222982 --- /dev/null +++ b/src/dusk/entity/component/overworld/entityoverworldcamera.c @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityoverworldcamera.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/display/entitycamera.h" + +void entityOverworldCameraInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityoverworldcamera_t *cam = entityOverworldCameraGet(entityId, componentId); + cam->targetEntityId = ENTITY_ID_INVALID; + cam->targetPosCompId = COMPONENT_ID_INVALID; + glm_vec3_zero(cam->targetOffset); + glm_vec3_zero(cam->eyeOffset); + cam->scale = 1.0f; +} + +entityoverworldcamera_t * entityOverworldCameraGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(entityId, componentId, COMPONENT_TYPE_OVERWORLD_CAMERA); +} + +void entityOverworldCameraSetTarget( + const entityid_t entityId, + const componentid_t componentId, + const entityid_t targetEntityId, + const componentid_t targetPosCompId +) { + entityoverworldcamera_t *cam = entityOverworldCameraGet(entityId, componentId); + cam->targetEntityId = targetEntityId; + cam->targetPosCompId = targetPosCompId; +} + +void entityOverworldCameraUpdate( + const entityid_t entityId, + const componentid_t componentId +) { + entityoverworldcamera_t *cam = entityOverworldCameraGet(entityId, componentId); + + vec3 targetPos; + entityPositionGetWorldPosition( + cam->targetEntityId, cam->targetPosCompId, targetPos + ); + + vec3 center = { + targetPos[0] + cam->targetOffset[0], + targetPos[1] + cam->targetOffset[1], + targetPos[2] + cam->targetOffset[2] + }; + + componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION); + componentid_t camComp = entityGetComponent(entityId, COMPONENT_TYPE_CAMERA); + entityCameraLookAtPixelPerfect( + entityId, posComp, camComp, center, cam->eyeOffset, cam->scale + ); +} diff --git a/src/dusk/entity/component/overworld/entityoverworldcamera.h b/src/dusk/entity/component/overworld/entityoverworldcamera.h new file mode 100644 index 00000000..cdc8c2b9 --- /dev/null +++ b/src/dusk/entity/component/overworld/entityoverworldcamera.h @@ -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 "entity/entitybase.h" + +typedef struct { + entityid_t targetEntityId; + componentid_t targetPosCompId; + vec3 targetOffset; + vec3 eyeOffset; + float_t scale; +} entityoverworldcamera_t; + +/** + * Initializes the overworld camera component. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityOverworldCameraInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Returns a pointer to the overworld camera component data. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @return Pointer to the entityoverworldcamera_t data. + */ +entityoverworldcamera_t * entityOverworldCameraGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the entity and position component the camera will follow. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @param targetEntityId Entity to follow. + * @param targetPosCompId Position component on the target entity. + */ +void entityOverworldCameraSetTarget( + const entityid_t entityId, + const componentid_t componentId, + const entityid_t targetEntityId, + const componentid_t targetPosCompId +); + +/** + * Updates the camera position to track the target entity. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityOverworldCameraUpdate( + const entityid_t entityId, + const componentid_t componentId +); diff --git a/src/dusk/entity/component/overworld/entityplayer.c b/src/dusk/entity/component/overworld/entityplayer.c new file mode 100644 index 00000000..a70cec3b --- /dev/null +++ b/src/dusk/entity/component/overworld/entityplayer.c @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityplayer.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/physics/entityphysics.h" +#include "entity/component/overworld/entityoverworld.h" +#include "entity/component/overworld/entityinteractable.h" +#include "input/input.h" + +void entityPlayerInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityplayer_t *player = entityPlayerGet(entityId, componentId); + player->speed = ENTITY_PLAYER_SPEED; + player->runSpeed = ENTITY_PLAYER_RUN_SPEED; +} + +entityplayer_t * entityPlayerGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(entityId, componentId, COMPONENT_TYPE_PLAYER); +} + +void entityPlayerUpdate( + const entityid_t entityId, + const componentid_t componentId +) { + entityplayer_t *player = entityPlayerGet(entityId, componentId); + + vec2 dir; + inputAngle2D( + INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, + INPUT_ACTION_UP, INPUT_ACTION_DOWN, + dir + ); + + float_t speed = ( + inputIsDown(INPUT_ACTION_CANCEL) ? player->runSpeed : player->speed + ); + + componentid_t owCompId = entityGetComponent(entityId, COMPONENT_TYPE_OVERWORLD); + entityoverworld_t *ow = entityOverworldGet(entityId, owCompId); + + if(ow && glm_vec2_norm(dir) > 0.0f) { + if(fabsf(dir[0]) >= fabsf(dir[1])) { + ow->facing = dir[0] > 0.0f ? FACING_DIR_RIGHT : FACING_DIR_LEFT; + } else { + ow->facing = dir[1] > 0.0f ? FACING_DIR_DOWN : FACING_DIR_UP; + } + } + + vec3 vel; + entityPhysicsGetVelocity(entityId, ow->physCompId, vel); + vel[0] = dir[0] * speed; + vel[2] = dir[1] * speed; + entityPhysicsSetVelocity(entityId, ow->physCompId, vel); + + if(!inputPressed(INPUT_ACTION_ACCEPT)) return; + + vec3 playerPos; + componentid_t playerPosCompId = entityGetComponent(entityId, COMPONENT_TYPE_POSITION); + if(playerPosCompId == COMPONENT_ID_INVALID) return; + entityPositionGetWorldPosition(entityId, playerPosCompId, playerPos); + + vec2 facingDir; + facingDirToVec2(ow ? ow->facing : FACING_DIR_DOWN, facingDir); + + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue; + if(i == entityId) continue; + + componentid_t interComp = entityGetComponent(i, COMPONENT_TYPE_INTERACTABLE); + if(interComp == COMPONENT_ID_INVALID) continue; + + componentid_t posComp = entityGetComponent(i, COMPONENT_TYPE_POSITION); + if(posComp == COMPONENT_ID_INVALID) continue; + + vec3 targetPos; + entityPositionGetWorldPosition(i, posComp, targetPos); + + vec2 toTarget = { + targetPos[0] - playerPos[0], + targetPos[2] - playerPos[2], + }; + float_t forward = glm_vec2_dot(facingDir, toTarget); + if(forward <= 0.0f || forward > ENTITY_PLAYER_INTERACT_RANGE) continue; + + float_t lateral = fabsf( + facingDir[0] * toTarget[1] - facingDir[1] * toTarget[0] + ); + if(lateral > ENTITY_PLAYER_INTERACT_LATERAL) continue; + + entityInteractableTrigger(i, interComp); + } +} diff --git a/src/dusk/entity/component/overworld/entityplayer.h b/src/dusk/entity/component/overworld/entityplayer.h new file mode 100644 index 00000000..4669e29f --- /dev/null +++ b/src/dusk/entity/component/overworld/entityplayer.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "entity/entitybase.h" + +#define ENTITY_PLAYER_SPEED 4.0f +#define ENTITY_PLAYER_RUN_SPEED 8.0f +#define ENTITY_PLAYER_INTERACT_RANGE 1.5f +#define ENTITY_PLAYER_INTERACT_LATERAL 0.6f + +typedef struct { + float_t speed; + float_t runSpeed; +} entityplayer_t; + +/** + * Initializes the player component. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityPlayerInit(const entityid_t entityId, const componentid_t componentId); + +/** + * Returns a pointer to the player component data. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + * @return Pointer to the entityplayer_t data. + */ +entityplayer_t * entityPlayerGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Reads input, moves the player, updates facing direction, and checks for + * interactable entities in front of the player when accept is pressed. + * + * @param entityId The owning entity. + * @param componentId This component's ID. + */ +void entityPlayerUpdate( + const entityid_t entityId, + const componentid_t componentId +); diff --git a/src/dusk/entity/componentlist.h b/src/dusk/entity/componentlist.h index 4c7367c2..b6d66e29 100644 --- a/src/dusk/entity/componentlist.h +++ b/src/dusk/entity/componentlist.h @@ -10,6 +10,10 @@ #include "entity/component/display/entityrenderable.h" #include "entity/component/physics/entityphysics.h" #include "entity/component/trigger/entitytrigger.h" +#include "entity/component/overworld/entityoverworld.h" +#include "entity/component/overworld/entityplayer.h" +#include "entity/component/overworld/entityinteractable.h" +#include "entity/component/overworld/entityoverworldcamera.h" // Name (Uppercase) // Structure @@ -22,3 +26,7 @@ X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL) X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose) X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose) X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL) +X(OVERWORLD, entityoverworld_t, overworld, entityOverworldInit, NULL) +X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL) +X(INTERACTABLE, entityinteractable_t, interactable, entityInteractableInit, NULL) +X(OVERWORLD_CAMERA, entityoverworldcamera_t, overworldCamera, entityOverworldCameraInit, NULL) diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c index d74e15d4..08efa668 100644 --- a/src/dusk/entity/entity.c +++ b/src/dusk/entity/entity.c @@ -83,7 +83,7 @@ void entityDisposeDeep(const entityid_t entityId) { void entityUpdate(const entityid_t entityId) { entity_t *ent = &ENTITY_MANAGER.entities[entityId]; for(uint8_t i = 0; i < ent->updateCount; i++) { - ent->onUpdate[i](entityId); + ent->onUpdate[i](entityId, ent->updateComponentId[i], ent->updateUser[i]); } } @@ -92,7 +92,7 @@ void entityDispose(const entityid_t entityId) { entity_t *ent = &ENTITY_MANAGER.entities[entityId]; for(uint8_t i = 0; i < ent->disposeCount; i++) { - ent->onDispose[i](entityId); + ent->onDispose[i](entityId, ent->disposeComponentId[i], ent->disposeUser[i]); } for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) { @@ -108,13 +108,21 @@ void entityDispose(const entityid_t entityId) { ent->state = 0; } -void entityUpdateAdd(const entityid_t entityId, const entitycallback_t callback) { +void entityUpdateAdd( + const entityid_t entityId, + const entitycallback_t callback, + const componentid_t componentId, + void *user +) { entity_t *ent = &ENTITY_MANAGER.entities[entityId]; assertTrue( ent->updateCount < ENTITY_UPDATE_CALLBACK_COUNT_MAX, "Entity update callback slots full" ); - ent->onUpdate[ent->updateCount++] = callback; + ent->onUpdate[ent->updateCount] = callback; + ent->updateComponentId[ent->updateCount] = componentId; + ent->updateUser[ent->updateCount] = user; + ent->updateCount++; } void entityUpdateRemove(const entityid_t entityId, const entitycallback_t callback) { @@ -123,19 +131,29 @@ void entityUpdateRemove(const entityid_t entityId, const entitycallback_t callba if(ent->onUpdate[i] != callback) continue; ent->updateCount--; for(uint8_t j = i; j < ent->updateCount; j++) { - ent->onUpdate[j] = ent->onUpdate[j + 1]; + ent->onUpdate[j] = ent->onUpdate[j + 1]; + ent->updateComponentId[j] = ent->updateComponentId[j + 1]; + ent->updateUser[j] = ent->updateUser[j + 1]; } return; } } -void entityDisposeAdd(const entityid_t entityId, const entitycallback_t callback) { +void entityDisposeAdd( + const entityid_t entityId, + const entitycallback_t callback, + const componentid_t componentId, + void *user +) { entity_t *ent = &ENTITY_MANAGER.entities[entityId]; assertTrue( ent->disposeCount < ENTITY_DISPOSE_CALLBACK_COUNT_MAX, "Entity dispose callback slots full" ); - ent->onDispose[ent->disposeCount++] = callback; + ent->onDispose[ent->disposeCount] = callback; + ent->disposeComponentId[ent->disposeCount] = componentId; + ent->disposeUser[ent->disposeCount] = user; + ent->disposeCount++; } void entityDisposeRemove(const entityid_t entityId, const entitycallback_t callback) { @@ -144,7 +162,9 @@ void entityDisposeRemove(const entityid_t entityId, const entitycallback_t callb if(ent->onDispose[i] != callback) continue; ent->disposeCount--; for(uint8_t j = i; j < ent->disposeCount; j++) { - ent->onDispose[j] = ent->onDispose[j + 1]; + ent->onDispose[j] = ent->onDispose[j + 1]; + ent->disposeComponentId[j] = ent->disposeComponentId[j + 1]; + ent->disposeUser[j] = ent->disposeUser[j + 1]; } return; } diff --git a/src/dusk/entity/entity.h b/src/dusk/entity/entity.h index 0e2acd80..b1eeafff 100644 --- a/src/dusk/entity/entity.h +++ b/src/dusk/entity/entity.h @@ -13,14 +13,22 @@ #define ENTITY_UPDATE_CALLBACK_COUNT_MAX 5 #define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5 -typedef void (*entitycallback_t)(const entityid_t entityId); +typedef void (*entitycallback_t)( + const entityid_t entityId, + const componentid_t componentId, + void *user +); typedef struct { uint8_t state; uint8_t updateCount; uint8_t disposeCount; entitycallback_t onUpdate[ENTITY_UPDATE_CALLBACK_COUNT_MAX]; + componentid_t updateComponentId[ENTITY_UPDATE_CALLBACK_COUNT_MAX]; + void *updateUser[ENTITY_UPDATE_CALLBACK_COUNT_MAX]; entitycallback_t onDispose[ENTITY_DISPOSE_CALLBACK_COUNT_MAX]; + componentid_t disposeComponentId[ENTITY_DISPOSE_CALLBACK_COUNT_MAX]; + void *disposeUser[ENTITY_DISPOSE_CALLBACK_COUNT_MAX]; } entity_t; /** @@ -85,7 +93,12 @@ void entityDisposeDeep(const entityid_t entityId); * @param entityId The entity to register on. * @param callback The function to call. */ -void entityUpdateAdd(const entityid_t entityId, const entitycallback_t callback); +void entityUpdateAdd( + const entityid_t entityId, + const entitycallback_t callback, + const componentid_t componentId, + void *user +); /** * Removes a previously registered update callback. @@ -102,7 +115,12 @@ void entityUpdateRemove(const entityid_t entityId, const entitycallback_t callba * @param entityId The entity to register on. * @param callback The function to call. */ -void entityDisposeAdd(const entityid_t entityId, const entitycallback_t callback); +void entityDisposeAdd( + const entityid_t entityId, + const entitycallback_t callback, + const componentid_t componentId, + void *user +); /** * Removes a previously registered dispose callback. diff --git a/src/dusk/entity/entitybase.h b/src/dusk/entity/entitybase.h index c05979b6..5ac19bda 100644 --- a/src/dusk/entity/entitybase.h +++ b/src/dusk/entity/entitybase.h @@ -8,11 +8,11 @@ #pragma once #include "dusk.h" -#define ENTITY_COUNT_MAX 100 -#define ENTITY_COMPONENT_COUNT_MAX 24 +#define ENTITY_COUNT_MAX 64 +#define ENTITY_COMPONENT_COUNT_MAX 16 -#define ENTITY_ID_INVALID 0xFF -#define COMPONENT_ID_INVALID 0xFF +#define ENTITY_ID_INVALID 0xFF +#define COMPONENT_ID_INVALID 0xFF typedef uint8_t entityid_t; typedef uint8_t componentid_t; diff --git a/src/dusk/overworld/CMakeLists.txt b/src/dusk/overworld/CMakeLists.txt index 5523d4d9..49ec3703 100644 --- a/src/dusk/overworld/CMakeLists.txt +++ b/src/dusk/overworld/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + facingdir.c maptypes.c map.c mapchunk.c diff --git a/src/dusk/overworld/facingdir.c b/src/dusk/overworld/facingdir.c new file mode 100644 index 00000000..8b99c82d --- /dev/null +++ b/src/dusk/overworld/facingdir.c @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "facingdir.h" + +void facingDirToVec2(facingdir_t facing, vec2 dest) { + switch(facing) { + case FACING_DIR_UP: dest[0] = 0.0f; dest[1] = -1.0f; return; + case FACING_DIR_LEFT: dest[0] = -1.0f; dest[1] = 0.0f; return; + case FACING_DIR_RIGHT: dest[0] = 1.0f; dest[1] = 0.0f; return; + default: dest[0] = 0.0f; dest[1] = 1.0f; return; + } +} diff --git a/src/dusk/overworld/facingdir.h b/src/dusk/overworld/facingdir.h new file mode 100644 index 00000000..5ecd3e13 --- /dev/null +++ b/src/dusk/overworld/facingdir.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef enum { + FACING_DIR_DOWN = 0, + FACING_DIR_UP = 1, + FACING_DIR_LEFT = 2, + FACING_DIR_RIGHT = 3, + FACING_DIR_SOUTH = FACING_DIR_DOWN, + FACING_DIR_NORTH = FACING_DIR_UP, + FACING_DIR_WEST = FACING_DIR_LEFT, + FACING_DIR_EAST = FACING_DIR_RIGHT, +} facingdir_t; + +/** + * Converts a facing direction to a normalized XZ vec2. + * + * @param facing The facing direction. + * @param dest Output vec2 — [0] is X, [1] is Z. + */ +void facingDirToVec2(facingdir_t facing, vec2 dest); diff --git a/src/dusk/scene/overworld/CMakeLists.txt b/src/dusk/scene/overworld/CMakeLists.txt index 12dda41f..a76dda08 100644 --- a/src/dusk/scene/overworld/CMakeLists.txt +++ b/src/dusk/scene/overworld/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC overworldground.c + overworldnpc.c overworldplayer.c overworldscene.c ) diff --git a/src/dusk/scene/overworld/overworldground.c b/src/dusk/scene/overworld/overworldground.c index 6411247d..93bf5e01 100644 --- a/src/dusk/scene/overworld/overworldground.c +++ b/src/dusk/scene/overworld/overworldground.c @@ -14,7 +14,14 @@ #define OVERWORLD_GROUND_SIZE 20.0f -static errorret_t overworldGroundDraw(void) { +static errorret_t overworldGroundDraw( + const entityid_t entityId, + const componentid_t componentId, + void *user +) { + (void)entityId; + (void)componentId; + (void)user; errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_MAGENTA)); errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL)); return meshDraw(&PLANE_MESH_SIMPLE, 0, -1); @@ -33,7 +40,7 @@ void overworldGroundAdd(overworldground_t *ground) { vec3 scale = { OVERWORLD_GROUND_SIZE * 2.0f, 1.0f, OVERWORLD_GROUND_SIZE * 2.0f }; entityPositionSetLocalPosition(ground->entityId, ground->posCompId, pos); entityPositionSetLocalScale(ground->entityId, ground->posCompId, scale); - entityRenderableSetDraw(ground->entityId, renderComp, overworldGroundDraw); + entityRenderableSetDraw(ground->entityId, renderComp, overworldGroundDraw, NULL); // Separate physics entity centered on the finite ground area. // The visual entity's position is its corner {-size, 0, -size}, not its diff --git a/src/dusk/scene/overworld/overworldnpc.c b/src/dusk/scene/overworld/overworldnpc.c new file mode 100644 index 00000000..259112be --- /dev/null +++ b/src/dusk/scene/overworld/overworldnpc.c @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "overworldnpc.h" +#include "entity/entitymanager.h" +#include "entity/component/physics/entityphysics.h" +#include "entity/component/trigger/entitytrigger.h" +#include "entity/component/overworld/entityoverworld.h" +#include "entity/component/overworld/entityinteractable.h" + +void overworldNpcAdd(overworldnpc_t *npc, vec3 position) { + npc->entityId = entityManagerAdd(); + npc->posCompId = entityAddComponent(npc->entityId, COMPONENT_TYPE_POSITION); + (void)entityAddComponent(npc->entityId, COMPONENT_TYPE_RENDERABLE); + (void)entityAddComponent(npc->entityId, COMPONENT_TYPE_PHYSICS); + npc->overworldCompId = entityAddComponent(npc->entityId, COMPONENT_TYPE_OVERWORLD); + npc->triggerCompId = entityAddComponent(npc->entityId, COMPONENT_TYPE_TRIGGER); + npc->interactableCompId = entityAddComponent( + npc->entityId, COMPONENT_TYPE_INTERACTABLE + ); + + entityPositionSetLocalPosition(npc->entityId, npc->posCompId, position); + + componentid_t physCompId = entityOverworldGet(npc->entityId, npc->overworldCompId)->physCompId; + entityPhysicsSetBodyType(npc->entityId, physCompId, PHYSICS_BODY_STATIC); + physicsshape_t shape = { + .type = PHYSICS_SHAPE_CAPSULE, + .data.capsule = { .radius = 0.4f, .halfHeight = 0.1f } + }; + entityPhysicsSetShape(npc->entityId, physCompId, shape); + + vec3 min = { + position[0] - OVERWORLD_NPC_INTERACT_RANGE, + position[1] - OVERWORLD_NPC_INTERACT_RANGE, + position[2] - OVERWORLD_NPC_INTERACT_RANGE + }; + vec3 max = { + position[0] + OVERWORLD_NPC_INTERACT_RANGE, + position[1] + OVERWORLD_NPC_INTERACT_RANGE, + position[2] + OVERWORLD_NPC_INTERACT_RANGE + }; + entityTriggerSetBounds(npc->entityId, npc->triggerCompId, min, max); +} diff --git a/src/dusk/scene/overworld/overworldnpc.h b/src/dusk/scene/overworld/overworldnpc.h new file mode 100644 index 00000000..8482485a --- /dev/null +++ b/src/dusk/scene/overworld/overworldnpc.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +#define OVERWORLD_NPC_INTERACT_RANGE 1.5f + +typedef struct { + entityid_t entityId; + componentid_t posCompId; + componentid_t overworldCompId; + componentid_t triggerCompId; + componentid_t interactableCompId; +} overworldnpc_t; + +/** + * Creates the NPC entity at the given world position. + * + * @param npc The NPC state to initialize. + * @param position World-space position to spawn the NPC at. + */ +void overworldNpcAdd(overworldnpc_t *npc, vec3 position); diff --git a/src/dusk/scene/overworld/overworldplayer.c b/src/dusk/scene/overworld/overworldplayer.c index 6787a891..e12db90c 100644 --- a/src/dusk/scene/overworld/overworldplayer.c +++ b/src/dusk/scene/overworld/overworldplayer.c @@ -8,43 +8,31 @@ #include "overworldplayer.h" #include "entity/entitymanager.h" #include "entity/component/physics/entityphysics.h" -#include "input/input.h" - -#define OVERWORLD_PLAYER_SPEED 4.0f -#define OVERWORLD_PLAYER_RUN_SPEED 8.0f +#include "entity/component/overworld/entityoverworld.h" +#include "entity/component/overworld/entityplayer.h" void overworldPlayerAdd(overworldplayer_t *player) { player->entityId = entityManagerAdd(); player->posCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_POSITION); (void)entityAddComponent(player->entityId, COMPONENT_TYPE_RENDERABLE); - player->physCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_PHYSICS); + (void)entityAddComponent(player->entityId, COMPONENT_TYPE_PHYSICS); + componentid_t owCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_OVERWORLD); + entityOverworldSetType(player->entityId, owCompId, OVERWORLD_ENTITY_TYPE_PLAYER); + player->playerCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_PLAYER); vec3 pos = { 0.0f, 0.5f, 0.0f }; entityPositionSetLocalPosition(player->entityId, player->posCompId, pos); - entityPhysicsSetBodyType(player->entityId, player->physCompId, PHYSICS_BODY_DYNAMIC); + componentid_t physCompId = entityOverworldGet(player->entityId, owCompId)->physCompId; + entityPhysicsSetBodyType(player->entityId, physCompId, PHYSICS_BODY_DYNAMIC); physicsshape_t shape = { .type = PHYSICS_SHAPE_CAPSULE, .data.capsule = { .radius = 0.4f, .halfHeight = 0.1f } }; - entityPhysicsSetShape(player->entityId, player->physCompId, shape); + entityPhysicsSetShape(player->entityId, physCompId, shape); + entityPhysicsGet(player->entityId, physCompId)->gravityScale = 1.0f; } void overworldPlayerUpdate(overworldplayer_t *player) { - vec2 dir; - inputAngle2D( - INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, - INPUT_ACTION_UP, INPUT_ACTION_DOWN, - dir - ); - - float_t speed = inputIsDown(INPUT_ACTION_CANCEL) - ? OVERWORLD_PLAYER_RUN_SPEED - : OVERWORLD_PLAYER_SPEED; - - vec3 vel; - entityPhysicsGetVelocity(player->entityId, player->physCompId, vel); - vel[0] = dir[0] * speed; - vel[2] = dir[1] * speed; - entityPhysicsSetVelocity(player->entityId, player->physCompId, vel); + entityPlayerUpdate(player->entityId, player->playerCompId); } diff --git a/src/dusk/scene/overworld/overworldplayer.h b/src/dusk/scene/overworld/overworldplayer.h index b8ae2c1e..b81da646 100644 --- a/src/dusk/scene/overworld/overworldplayer.h +++ b/src/dusk/scene/overworld/overworldplayer.h @@ -11,7 +11,7 @@ typedef struct { entityid_t entityId; componentid_t posCompId; - componentid_t physCompId; + componentid_t playerCompId; } overworldplayer_t; /** diff --git a/src/dusk/scene/overworld/overworldscene.c b/src/dusk/scene/overworld/overworldscene.c index 13e5cdc9..0f1d5ac5 100644 --- a/src/dusk/scene/overworld/overworldscene.c +++ b/src/dusk/scene/overworld/overworldscene.c @@ -8,60 +8,60 @@ #include "overworldscene.h" #include "overworldplayer.h" #include "overworldground.h" +#include "overworldnpc.h" #include "console/console.h" #include "entity/entitymanager.h" -#include "entity/component/physics/entityphysics.h" +#include "entity/component/overworld/entityinteractable.h" +#include "entity/component/overworld/entityoverworldcamera.h" #include "scene/scene.h" #define OVERWORLD (SCENE.data.overworld) +static void overworldSceneNpcInteract( + const entityid_t entityId, + const componentid_t componentId, + void *user +) { + (void)entityId; (void)componentId; (void)user; + consolePrint("NPC interacted with!"); +} + void overworldSceneInit(void) { consolePrint("Overworld scene initialized"); - OVERWORLD.cameraEntityId = entityManagerAdd(); - OVERWORLD.cameraPosCompId = entityAddComponent( - OVERWORLD.cameraEntityId, COMPONENT_TYPE_POSITION - ); - OVERWORLD.cameraCamCompId = entityAddComponent( - OVERWORLD.cameraEntityId, COMPONENT_TYPE_CAMERA - ); - overworldGroundAdd(&OVERWORLD.ground); overworldPlayerAdd(&OVERWORLD.player); - OVERWORLD.refCubeEntityId = entityManagerAdd(); - componentid_t refPosComp = entityAddComponent( - OVERWORLD.refCubeEntityId, COMPONENT_TYPE_POSITION - ); - (void)entityAddComponent(OVERWORLD.refCubeEntityId, COMPONENT_TYPE_RENDERABLE); - vec3 refPos = { 3.0f, 0.5f, 3.0f }; - entityPositionSetLocalPosition(OVERWORLD.refCubeEntityId, refPosComp, refPos); + vec3 npcPos = { 3.0f, 0.5f, 3.0f }; + overworldNpcAdd(&OVERWORLD.npc, npcPos); - componentid_t refPhysComp = entityAddComponent( - OVERWORLD.refCubeEntityId, COMPONENT_TYPE_PHYSICS + OVERWORLD.cameraEntityId = entityManagerAdd(); + (void)entityAddComponent(OVERWORLD.cameraEntityId, COMPONENT_TYPE_POSITION); + (void)entityAddComponent(OVERWORLD.cameraEntityId, COMPONENT_TYPE_CAMERA); + OVERWORLD.cameraOverworldCompId = entityAddComponent( + OVERWORLD.cameraEntityId, COMPONENT_TYPE_OVERWORLD_CAMERA + ); + entityOverworldCameraSetTarget( + OVERWORLD.cameraEntityId, OVERWORLD.cameraOverworldCompId, + OVERWORLD.player.entityId, OVERWORLD.player.posCompId + ); + entityoverworldcamera_t *camData = entityOverworldCameraGet( + OVERWORLD.cameraEntityId, OVERWORLD.cameraOverworldCompId + ); + glm_vec3_copy((vec3){ 0.5f, 0.5f, 0.5f }, camData->targetOffset); + glm_vec3_copy((vec3){ 0.0f, 0.0f, 5.0f }, camData->eyeOffset); + camData->scale = 32.0f; + entityInteractableSetCallback( + OVERWORLD.npc.entityId, OVERWORLD.npc.interactableCompId, + overworldSceneNpcInteract, NULL ); - entityPhysicsSetBodyType(OVERWORLD.refCubeEntityId, refPhysComp, PHYSICS_BODY_STATIC); - physicsshape_t refShape = { - .type = PHYSICS_SHAPE_CAPSULE, - .data.capsule = { .radius = 0.4f, .halfHeight = 0.1f } - }; - entityPhysicsSetShape(OVERWORLD.refCubeEntityId, refPhysComp, refShape); } errorret_t overworldSceneUpdate(void) { overworldPlayerUpdate(&OVERWORLD.player); - vec3 pos; - entityPositionGetLocalPosition( - OVERWORLD.player.entityId, OVERWORLD.player.posCompId, pos - ); - vec3 center = { pos[0] + 0.5f, pos[1] + 0.5f, pos[2] + 0.5f }; - vec3 eyeOffset = { 0.0f, 0.0f, 5.0f }; - entityCameraLookAtPixelPerfect( - OVERWORLD.cameraEntityId, - OVERWORLD.cameraPosCompId, - OVERWORLD.cameraCamCompId, - center, eyeOffset, 32.0f + entityOverworldCameraUpdate( + OVERWORLD.cameraEntityId, OVERWORLD.cameraOverworldCompId ); errorOk(); @@ -69,12 +69,15 @@ errorret_t overworldSceneUpdate(void) { void overworldSceneDispose(void) { OVERWORLD.cameraEntityId = ENTITY_ID_INVALID; - OVERWORLD.cameraPosCompId = COMPONENT_ID_INVALID; - OVERWORLD.cameraCamCompId = COMPONENT_ID_INVALID; + OVERWORLD.cameraOverworldCompId = COMPONENT_ID_INVALID; OVERWORLD.ground.entityId = ENTITY_ID_INVALID; OVERWORLD.ground.posCompId = COMPONENT_ID_INVALID; OVERWORLD.ground.floorEntityId = ENTITY_ID_INVALID; OVERWORLD.player.entityId = ENTITY_ID_INVALID; OVERWORLD.player.posCompId = COMPONENT_ID_INVALID; - OVERWORLD.refCubeEntityId = ENTITY_ID_INVALID; + OVERWORLD.npc.entityId = ENTITY_ID_INVALID; + OVERWORLD.npc.posCompId = COMPONENT_ID_INVALID; + OVERWORLD.npc.overworldCompId = COMPONENT_ID_INVALID; + OVERWORLD.npc.triggerCompId = COMPONENT_ID_INVALID; + OVERWORLD.npc.interactableCompId = COMPONENT_ID_INVALID; } diff --git a/src/dusk/scene/overworld/overworldscene.h b/src/dusk/scene/overworld/overworldscene.h index 8bcfa436..826e2f4b 100644 --- a/src/dusk/scene/overworld/overworldscene.h +++ b/src/dusk/scene/overworld/overworldscene.h @@ -10,14 +10,14 @@ #include "entity/entitybase.h" #include "scene/overworld/overworldplayer.h" #include "scene/overworld/overworldground.h" +#include "scene/overworld/overworldnpc.h" typedef struct { entityid_t cameraEntityId; - componentid_t cameraPosCompId; - componentid_t cameraCamCompId; + componentid_t cameraOverworldCompId; overworldplayer_t player; overworldground_t ground; - entityid_t refCubeEntityId; + overworldnpc_t npc; } overworldscene_t; void overworldSceneInit(void);