diff --git a/GameModules/SparkGameOpenWorld/CMakeLists.txt b/GameModules/SparkGameOpenWorld/CMakeLists.txt new file mode 100644 index 00000000..baad675f --- /dev/null +++ b/GameModules/SparkGameOpenWorld/CMakeLists.txt @@ -0,0 +1,254 @@ +cmake_minimum_required(VERSION 3.25) + +# ================================================================ +# SparkGameOpenWorld - Open World Game Module DLL +# +# Showcases SparkEngine's open-world capabilities: +# - 8 biome regions with seamless streaming and origin rebasing +# - Survival player (stamina, hunger, fast travel, compass) +# - Points of interest, map discovery, landmarks +# - Wildlife ecology (herds, predator-prey, taming) +# - NPC settlements and player camp building +# - Resource gathering and crafting +# - Dynamic world events and random encounters +# - Full engine integration (weather, time, save, AI, music) +# +# Built as a shared library loaded by the engine at runtime. +# ================================================================ + +if(NOT CMAKE_PROJECT_NAME OR CMAKE_PROJECT_NAME STREQUAL "SparkGameOpenWorld") + project(SparkGameOpenWorld LANGUAGES CXX) + set(SPARK_GAME_OW_STANDALONE TRUE) +else() + set(SPARK_GAME_OW_STANDALONE FALSE) +endif() + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# --------------------------------------------------------------------- +# Standalone mode: locate SparkEngine +# --------------------------------------------------------------------- +if(SPARK_GAME_OW_STANDALONE) + if(NOT SPARK_ENGINE_DIR) + set(SPARK_ENGINE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." CACHE PATH + "Path to SparkEngine root directory") + endif() + + message(STATUS "SparkGameOpenWorld standalone build - Engine at: ${SPARK_ENGINE_DIR}") + + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + + foreach(config Debug Release RelWithDebInfo MinSizeRel) + string(TOUPPER ${config} CONFIG_UPPER) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_UPPER} ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_UPPER} ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_UPPER} ${CMAKE_BINARY_DIR}/lib) + endforeach() + + if(MSVC) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + add_compile_options(/W3 /MP /bigobj /wd4005 /wd4996 /wd4244 /wd4267) + add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS SPARK_PLATFORM_WINDOWS) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options(-Wall -Wextra -Wno-unused-parameter -fPIC) + endif() + + set(THIRDPARTY_INCLUDE_DIRS "") + if(EXISTS "${SPARK_ENGINE_DIR}/ThirdParty/ECS/entt/single_include") + list(APPEND THIRDPARTY_INCLUDE_DIRS "${SPARK_ENGINE_DIR}/ThirdParty/ECS/entt/single_include") + endif() + if(EXISTS "${SPARK_ENGINE_DIR}/ThirdParty/Physics/bullet3/src") + list(APPEND THIRDPARTY_INCLUDE_DIRS "${SPARK_ENGINE_DIR}/ThirdParty/Physics/bullet3/src") + endif() + if(EXISTS "${SPARK_ENGINE_DIR}/ThirdParty/UI/imgui") + list(APPEND THIRDPARTY_INCLUDE_DIRS "${SPARK_ENGINE_DIR}/ThirdParty/UI/imgui") + endif() + + set(ENGINE_SOURCE_DIR "${SPARK_ENGINE_DIR}/SparkEngine/Source") +else() + set(ENGINE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/SparkEngine/Source") +endif() + +# --------------------------------------------------------------------- +# Collect SparkGameOpenWorld source files +# --------------------------------------------------------------------- +file(GLOB_RECURSE SPARK_GAME_OW_SOURCES + CONFIGURE_DEPENDS + "Source/*.cpp" + "Source/*.h" + "Source/*.hpp" +) +list(FILTER SPARK_GAME_OW_SOURCES EXCLUDE REGEX ".*[Tt]est.*") +list(FILTER SPARK_GAME_OW_SOURCES EXCLUDE REGEX ".*[Ee]xample.*") + +# --------------------------------------------------------------------- +# Create the game as a SHARED library (DLL) +# --------------------------------------------------------------------- +add_library(SparkGameOpenWorld SHARED ${SPARK_GAME_OW_SOURCES}) + +target_compile_definitions(SparkGameOpenWorld PRIVATE SPARK_GAME_DLL SPARK_MODULE_DLL) + +# --------------------------------------------------------------------- +# Link against SparkEngineLib +# --------------------------------------------------------------------- +if(NOT SPARK_GAME_OW_STANDALONE AND TARGET SparkEngineLib) + if(WIN32) + target_link_libraries(SparkGameOpenWorld PRIVATE SparkEngineLib) + elseif(TARGET SparkEngineInterface) + target_link_libraries(SparkGameOpenWorld PRIVATE SparkEngineInterface) + endif() +endif() + +# --------------------------------------------------------------------- +# Include directories +# --------------------------------------------------------------------- +if(SPARK_GAME_OW_STANDALONE) + set(SPARK_SDK_INCLUDE_DIR "${SPARK_ENGINE_DIR}/SparkSDK/Include") +else() + set(SPARK_SDK_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/SparkSDK/Include") +endif() + +target_include_directories(SparkGameOpenWorld PRIVATE + "Source" + "${ENGINE_SOURCE_DIR}" + "${SPARK_SDK_INCLUDE_DIR}" + ${THIRDPARTY_INCLUDE_DIRS} +) + +# --------------------------------------------------------------------- +# Platform-specific libraries +# --------------------------------------------------------------------- +if(WIN32) + target_link_libraries(SparkGameOpenWorld PRIVATE + d3d11 dxgi d3dcompiler dxguid + kernel32 user32 gdi32 winspool + shell32 comdlg32 advapi32 + ole32 oleaut32 uuid + winmm + $<$:ws2_32> + $<$:wsock32> + $<$:crypt32> + $<$:wldap32> + $<$:normaliz> + ) + target_compile_definitions(SparkGameOpenWorld PRIVATE + WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS SPARK_PLATFORM_WINDOWS + ) +else() + find_package(Threads REQUIRED) + target_link_libraries(SparkGameOpenWorld PRIVATE + Threads::Threads + ${CMAKE_DL_LIBS} + ) + if(APPLE) + target_compile_definitions(SparkGameOpenWorld PRIVATE SPARK_PLATFORM_MACOS) + else() + target_compile_definitions(SparkGameOpenWorld PRIVATE SPARK_PLATFORM_LINUX) + endif() +endif() + +if(MSVC) + target_link_libraries(SparkGameOpenWorld PRIVATE legacy_stdio_definitions) +endif() + +# Link Jolt Physics if available +if(WIN32) + if(TARGET Jolt) + target_link_libraries(SparkGameOpenWorld PRIVATE Jolt) + endif() + if(TARGET miniz) + target_link_libraries(SparkGameOpenWorld PRIVATE miniz) + endif() + if(TARGET tinyobjloader) + target_link_libraries(SparkGameOpenWorld PRIVATE tinyobjloader) + endif() +endif() + +# Vulkan backend +if(SPARK_VULKAN_AVAILABLE) + target_compile_definitions(SparkGameOpenWorld PRIVATE SPARK_VULKAN_SUPPORT) + if(Vulkan_FOUND) + target_link_libraries(SparkGameOpenWorld PRIVATE Vulkan::Vulkan) + else() + target_include_directories(SparkGameOpenWorld PRIVATE ${Vulkan_INCLUDE_DIR}) + target_link_libraries(SparkGameOpenWorld PRIVATE ${Vulkan_LIBRARY}) + endif() +endif() + +# OpenGL backend +if(SPARK_OPENGL_AVAILABLE) + target_compile_definitions(SparkGameOpenWorld PRIVATE SPARK_OPENGL_SUPPORT) + target_link_libraries(SparkGameOpenWorld PRIVATE OpenGL::GL) + if(TARGET glad) + target_link_libraries(SparkGameOpenWorld PRIVATE glad) + endif() +endif() + +# Apply feature definitions +if(FEATURE_DEFINITIONS) + target_compile_definitions(SparkGameOpenWorld PRIVATE ${FEATURE_DEFINITIONS}) +endif() + +# --------------------------------------------------------------------- +# Post-build: Asset directories +# --------------------------------------------------------------------- +add_custom_command(TARGET SparkGameOpenWorld POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/Shaders" + COMMAND ${CMAKE_COMMAND} -E make_directory "$/Assets" + COMMENT "Creating SparkGameOpenWorld asset directory structure" +) + +# Copy shaders from engine +set(SHADER_SOURCE_DIR "${ENGINE_SOURCE_DIR}/../Shaders/HLSL") +if(NOT EXISTS "${SHADER_SOURCE_DIR}") + set(SHADER_SOURCE_DIR "${CMAKE_SOURCE_DIR}/SparkEngine/Shaders/HLSL") + if(SPARK_GAME_OW_STANDALONE) + set(SHADER_SOURCE_DIR "${SPARK_ENGINE_DIR}/SparkEngine/Shaders/HLSL") + endif() +endif() +if(EXISTS "${SHADER_SOURCE_DIR}") + file(GLOB_RECURSE SHADER_FILES "${SHADER_SOURCE_DIR}/*.hlsl") + foreach(SHADER_FILE ${SHADER_FILES}) + get_filename_component(SHADER_NAME ${SHADER_FILE} NAME) + add_custom_command(TARGET SparkGameOpenWorld POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${SHADER_FILE} + "$/Shaders/${SHADER_NAME}" + COMMENT "Copying shader ${SHADER_NAME}" + ) + endforeach() +endif() + +# --------------------------------------------------------------------- +# Visual Studio settings +# --------------------------------------------------------------------- +if(MSVC) + foreach(src ${SPARK_GAME_OW_SOURCES}) + get_filename_component(dir "${src}" DIRECTORY) + file(RELATIVE_PATH grp "${CMAKE_CURRENT_SOURCE_DIR}/Source" "${dir}") + string(REPLACE "/" "\\\\" grp "${grp}") + if(grp STREQUAL "") + source_group("Source Files" FILES "${src}") + else() + source_group("Source Files\\\\${grp}" FILES "${src}") + endif() + endforeach() + + if(SPARK_GAME_OW_STANDALONE) + set_property(TARGET SparkGameOpenWorld PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${SPARK_ENGINE_DIR}") + else() + set_property(TARGET SparkGameOpenWorld PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + endif() +endif() + +message(STATUS "SparkGameOpenWorld configured as SHARED LIBRARY (open world game module DLL)") +if(SPARK_GAME_OW_STANDALONE) + message(STATUS " Mode: Standalone build") + message(STATUS " Engine: ${SPARK_ENGINE_DIR}") +else() + message(STATUS " Mode: Sub-project of SparkEngine") +endif() diff --git a/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp b/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp new file mode 100644 index 00000000..d18a6230 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp @@ -0,0 +1,571 @@ +/** + * @file Main.cpp + * @brief SparkGameOpenWorld DLL - IModule implementation and exports + * + * Implements the SparkGameOpenWorldModule class and exports the CreateModule/ + * DestroyModule factory functions for the engine's ModuleManager. + */ + +#include "SparkGameOpenWorld.h" +#include "OWEngineSystems.h" +#include "World/OWWorldSetup.h" +#include "Player/OWPlayerSystem.h" +#include "Exploration/OWExplorationSystem.h" +#include "Wildlife/OWWildlifeSystem.h" +#include "Settlement/OWSettlementSystem.h" +#include "Gathering/OWGatheringSystem.h" +#include "Events/OWDynamicEventSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef SPARK_PLATFORM_WINDOWS +#include + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID) +{ + switch (reason) + { + case DLL_PROCESS_ATTACH: + DisableThreadLibraryCalls(hModule); + break; + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} +#endif + +// ============================================================================= +// Module exports +// ============================================================================= + +SPARK_IMPLEMENT_MODULE(SparkGameOpenWorldModule) + +// ============================================================================= +// SparkGameOpenWorldModule implementation +// ============================================================================= + +SparkGameOpenWorldModule::SparkGameOpenWorldModule() = default; + +SparkGameOpenWorldModule::~SparkGameOpenWorldModule() +{ + if (m_initialized) + OnUnload(); +} + +Spark::ModuleInfo SparkGameOpenWorldModule::GetModuleInfo() const +{ + Spark::ModuleInfo info{}; + info.name = "Spark Open World - Exploration Showcase"; + info.version = "1.0.0"; + info.sdkVersion = SPARK_SDK_VERSION; + info.loadOrder = 1008; + return info; +} + +bool SparkGameOpenWorldModule::OnLoad(Spark::IEngineContext* context) +{ + if (!context) + return false; + + m_context = context; + + auto& console = Spark::SimpleConsole::GetInstance(); + console.LogInfo("[OpenWorld] Loading Spark Open World module..."); + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open World module loading - initializing 8 subsystems"); + + // 1. World setup (regions, roads, streaming, origin rebasing) + m_worldSetup = std::make_unique(); + if (!m_worldSetup->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize world setup"); + return false; + } + + // 2. Player system (survival, stamina, fast travel, compass) + m_playerSystem = std::make_unique(); + if (!m_playerSystem->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize player system"); + return false; + } + + // 3. Exploration system (POIs, discovery, map fog) + m_explorationSystem = std::make_unique(); + if (!m_explorationSystem->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize exploration system"); + return false; + } + + // 4. Wildlife system (species, herds, predator-prey, taming) + m_wildlifeSystem = std::make_unique(); + if (!m_wildlifeSystem->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize wildlife system"); + return false; + } + + // 5. Settlement system (NPC villages, player camps) + m_settlementSystem = std::make_unique(); + if (!m_settlementSystem->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize settlement system"); + return false; + } + + // 6. Gathering system (resource nodes, harvesting, crafting) + m_gatheringSystem = std::make_unique(); + if (!m_gatheringSystem->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize gathering system"); + return false; + } + + // 7. Dynamic event system (world events, random encounters) + m_eventSystem = std::make_unique(); + if (!m_eventSystem->Initialize(context)) + { + console.LogError("[OpenWorld] Failed to initialize dynamic event system"); + return false; + } + + // 8. Engine systems integration (save, AI, cinematic, weather, music, events) + m_engineSystems = std::make_unique(); + if (!m_engineSystems->Initialize(context)) + { + console.LogWarning("[OpenWorld] Engine systems integration partially failed (non-fatal)"); + } + + RegisterConsoleCommands(); + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open World module loaded successfully - 8 subsystems active"); + console.LogInfo("[OpenWorld] Module loaded successfully (8 subsystems)"); + console.LogInfo("[OpenWorld] Regions: " + std::to_string(m_worldSetup->GetRegionCount()) + + " | POIs: " + std::to_string(m_explorationSystem->GetPOICount()) + + " | Species: " + std::to_string(m_wildlifeSystem->GetSpeciesCount()) + + " | Settlements: " + std::to_string(m_settlementSystem->GetSettlementCount()) + + " | Nodes: " + std::to_string(m_gatheringSystem->GetNodeCount()) + + " | Recipes: " + std::to_string(m_gatheringSystem->GetRecipeCount()) + + " | Events: " + std::to_string(m_eventSystem->GetTemplateCount())); + return true; +} + +void SparkGameOpenWorldModule::OnUnload() +{ + if (!m_initialized) + return; + + auto& console = Spark::SimpleConsole::GetInstance(); + console.LogInfo("[OpenWorld] Unloading Spark Open World module..."); + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open World module shutting down"); + + // Shutdown in reverse initialization order + if (m_engineSystems) + { + m_engineSystems->Shutdown(); + m_engineSystems.reset(); + } + if (m_eventSystem) + { + m_eventSystem->Shutdown(); + m_eventSystem.reset(); + } + if (m_gatheringSystem) + { + m_gatheringSystem->Shutdown(); + m_gatheringSystem.reset(); + } + if (m_settlementSystem) + { + m_settlementSystem->Shutdown(); + m_settlementSystem.reset(); + } + if (m_wildlifeSystem) + { + m_wildlifeSystem->Shutdown(); + m_wildlifeSystem.reset(); + } + if (m_explorationSystem) + { + m_explorationSystem->Shutdown(); + m_explorationSystem.reset(); + } + if (m_playerSystem) + { + m_playerSystem->Shutdown(); + m_playerSystem.reset(); + } + if (m_worldSetup) + { + m_worldSetup->Shutdown(); + m_worldSetup.reset(); + } + + m_context = nullptr; + m_initialized = false; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open World module unloaded"); + console.LogInfo("[OpenWorld] Module unloaded"); +} + +void SparkGameOpenWorldModule::OnUpdate(float deltaTime) +{ + if (!m_initialized || m_paused) + return; + + const auto& playerState = m_playerSystem->GetWorldState(); + + m_worldSetup->Update(deltaTime); + m_playerSystem->Update(deltaTime); + m_explorationSystem->Update(deltaTime, playerState.posX, playerState.posY, playerState.posZ); + m_wildlifeSystem->Update(deltaTime, playerState.posX, playerState.posZ, playerState.currentRegionId); + m_settlementSystem->Update(deltaTime); + m_gatheringSystem->Update(deltaTime); + m_eventSystem->Update(deltaTime, playerState.posX, playerState.posZ, playerState.currentRegionId); + m_engineSystems->Update(deltaTime); +} + +void SparkGameOpenWorldModule::OnFixedUpdate(float fixedDeltaTime) +{ + if (!m_initialized || m_paused) + return; + + m_playerSystem->FixedUpdate(fixedDeltaTime); +} + +void SparkGameOpenWorldModule::OnRender() +{ + if (!m_initialized) + return; +} + +void SparkGameOpenWorldModule::OnResize(int width, int height) +{ + (void)width; + (void)height; +} + +void SparkGameOpenWorldModule::OnPause() +{ + m_paused = true; +} + +void SparkGameOpenWorldModule::OnResume() +{ + m_paused = false; +} + +void SparkGameOpenWorldModule::OnImGui() +{ + if (!m_initialized) + return; + + m_worldSetup->RenderDebugUI(); + m_playerSystem->RenderDebugUI(); + m_explorationSystem->RenderDebugUI(); + m_wildlifeSystem->RenderDebugUI(); + m_settlementSystem->RenderDebugUI(); + m_gatheringSystem->RenderDebugUI(); + m_eventSystem->RenderDebugUI(); + m_engineSystems->RenderDebugUI(); +} + +void SparkGameOpenWorldModule::RegisterConsoleCommands() +{ + auto& console = Spark::SimpleConsole::GetInstance(); + + // --- Status --- + console.RegisterCommand( + "ow_status", + [this](const std::vector&) -> std::string + { + std::string s = "=== Spark Open World Status ===\n"; + s += "Regions: " + std::to_string(m_worldSetup->GetRegionCount()) + "\n"; + s += "POIs: " + std::to_string(m_explorationSystem->GetDiscoveredCount()) + "/" + + std::to_string(m_explorationSystem->GetPOICount()) + "\n"; + s += "Wildlife: " + std::to_string(m_wildlifeSystem->GetActiveAnimalCount()) + " active, " + + std::to_string(m_wildlifeSystem->GetTamedCount()) + " tamed\n"; + s += "Settlements: " + std::to_string(m_settlementSystem->GetSettlementCount()) + + " | Camps: " + std::to_string(m_settlementSystem->GetCampCount()) + "\n"; + s += "Nodes: " + std::to_string(m_gatheringSystem->GetNodeCount()) + + " | Recipes: " + std::to_string(m_gatheringSystem->GetRecipeCount()) + "\n"; + s += "Active Events: " + std::to_string(m_eventSystem->GetActiveEventCount()) + "\n"; + s += m_worldSetup->GetWorldStatusString() + "\n"; + return s; + }, + "Show open world module status", "OpenWorld"); + + // --- World --- + console.RegisterCommand( + "ow_regions", [this](const std::vector&) -> std::string + { return m_worldSetup->GetRegionListString(); }, "List world regions", "OpenWorld"); + + // --- Player --- + console.RegisterCommand( + "ow_player", [this](const std::vector&) -> std::string + { return m_playerSystem->GetStatusString(); }, "Show player status", "OpenWorld"); + + console.RegisterCommand( + "ow_eat", + [this](const std::vector&) -> std::string + { + m_playerSystem->Eat(30.0f); + return "Ate food (+30 hunger)"; + }, + "Eat food", "OpenWorld"); + + console.RegisterCommand( + "ow_drink", + [this](const std::vector&) -> std::string + { + m_playerSystem->Drink(40.0f); + return "Drank water (+40 thirst)"; + }, + "Drink water", "OpenWorld"); + + console.RegisterCommand( + "ow_teleport", + [this](const std::vector& args) -> std::string + { + if (args.size() < 3) + return "Usage: ow_teleport "; + try + { + float x = std::stof(args[0]); + float y = std::stof(args[1]); + float z = std::stof(args[2]); + m_playerSystem->SetPosition(x, y, z); + return "Teleported to (" + args[0] + ", " + args[1] + ", " + args[2] + ")"; + } + catch (...) + { + return "Invalid coordinates"; + } + }, + "Teleport player", "OpenWorld", "ow_teleport "); + + console.RegisterCommand( + "ow_fast_travel", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + { + std::string s = "Fast Travel Points:\n"; + for (const auto& pt : m_playerSystem->GetFastTravelPoints()) + s += " [" + std::to_string(pt.pointId) + "] " + pt.name + "\n"; + return s + "Usage: ow_fast_travel "; + } + try + { + uint32_t id = static_cast(std::stoi(args[0])); + return m_playerSystem->FastTravelTo(id) ? "Fast traveled!" : "Invalid point"; + } + catch (...) + { + return "Invalid point ID"; + } + }, + "Fast travel to a discovered point", "OpenWorld"); + + // --- Exploration --- + console.RegisterCommand( + "ow_explore", [this](const std::vector&) -> std::string + { return m_explorationSystem->GetExplorationString(); }, "Show exploration progress", "OpenWorld"); + + console.RegisterCommand( + "ow_pois", [this](const std::vector&) -> std::string + { return m_explorationSystem->GetPOIListString(); }, "List all points of interest", "OpenWorld"); + + // --- Wildlife --- + console.RegisterCommand( + "ow_wildlife", [this](const std::vector&) -> std::string + { return m_wildlifeSystem->GetWildlifeString(); }, "Show wildlife summary", "OpenWorld"); + + console.RegisterCommand( + "ow_species", [this](const std::vector&) -> std::string + { return m_wildlifeSystem->GetSpeciesListString(); }, "List animal species", "OpenWorld"); + + console.RegisterCommand( + "ow_tame", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_tame "; + try + { + uint32_t id = static_cast(std::stoi(args[0])); + return m_wildlifeSystem->TameAnimal(id) ? "Animal tamed!" + : "Cannot tame (invalid, dead, or untameable)"; + } + catch (...) + { + return "Invalid animal ID"; + } + }, + "Tame a wildlife animal", "OpenWorld"); + + // --- Settlements --- + console.RegisterCommand( + "ow_settlements", [this](const std::vector&) -> std::string + { return m_settlementSystem->GetSettlementListString(); }, "List settlements", "OpenWorld"); + + console.RegisterCommand( + "ow_camps", [this](const std::vector&) -> std::string + { return m_settlementSystem->GetCampListString(); }, "List player camps", "OpenWorld"); + + console.RegisterCommand( + "ow_place_camp", + [this](const std::vector& args) -> std::string + { + std::string name = args.empty() ? "Camp" : args[0]; + const auto& ps = m_playerSystem->GetWorldState(); + uint32_t id = m_settlementSystem->PlaceCamp(name, ps.posX, ps.posY, ps.posZ, ps.currentRegionId); + return "Camp '" + name + "' placed (id=" + std::to_string(id) + ")"; + }, + "Place a camp at current position", "OpenWorld"); + + console.RegisterCommand( + "ow_upgrade_camp", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_upgrade_camp "; + try + { + uint32_t id = static_cast(std::stoi(args[0])); + return m_settlementSystem->UpgradeCamp(id) ? "Camp upgraded!" : "Cannot upgrade (max tier or invalid)"; + } + catch (...) + { + return "Invalid camp ID"; + } + }, + "Upgrade a player camp", "OpenWorld"); + + // --- Gathering / Crafting --- + console.RegisterCommand( + "ow_nodes", [this](const std::vector&) -> std::string + { return m_gatheringSystem->GetNodeListString(); }, "List resource nodes", "OpenWorld"); + + console.RegisterCommand( + "ow_inventory", [this](const std::vector&) -> std::string + { return m_gatheringSystem->GetInventoryString(); }, "Show resource inventory", "OpenWorld"); + + console.RegisterCommand( + "ow_recipes", [this](const std::vector&) -> std::string + { return m_gatheringSystem->GetRecipeListString(); }, "List crafting recipes", "OpenWorld"); + + console.RegisterCommand( + "ow_harvest", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_harvest "; + try + { + uint32_t id = static_cast(std::stoi(args[0])); + uint32_t amount = m_gatheringSystem->HarvestNode(id); + return amount > 0 ? "Harvested " + std::to_string(amount) + " resources" : "Node depleted or invalid"; + } + catch (...) + { + return "Invalid node ID"; + } + }, + "Harvest a resource node", "OpenWorld"); + + console.RegisterCommand( + "ow_craft", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_craft "; + try + { + uint32_t id = static_cast(std::stoi(args[0])); + return m_gatheringSystem->Craft(id) ? "Crafted!" : "Cannot craft (missing resources or invalid)"; + } + catch (...) + { + return "Invalid recipe ID"; + } + }, + "Craft an item", "OpenWorld"); + + // --- Events --- + console.RegisterCommand( + "ow_events", [this](const std::vector&) -> std::string + { return m_eventSystem->GetActiveEventsString(); }, "Show active world events", "OpenWorld"); + + console.RegisterCommand( + "ow_event_types", [this](const std::vector&) -> std::string + { return m_eventSystem->GetEventListString(); }, "List event types", "OpenWorld"); + + console.RegisterCommand( + "ow_trigger_event", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_trigger_event "; + try + { + uint32_t tmplId = static_cast(std::stoi(args[0])); + const auto& ps = m_playerSystem->GetWorldState(); + uint32_t evtId = m_eventSystem->TriggerEvent(tmplId, ps.currentRegionId, ps.posX, ps.posZ); + return evtId > 0 ? "Event triggered (id=" + std::to_string(evtId) + ")" : "Failed to trigger"; + } + catch (...) + { + return "Invalid template ID"; + } + }, + "Trigger a world event", "OpenWorld"); + + // --- Engine integration --- + console.RegisterCommand( + "ow_save", + [this](const std::vector& args) -> std::string + { + std::string slot = args.empty() ? "slot1" : args[0]; + return m_engineSystems->SaveGame(slot); + }, + "Save game", "OpenWorld"); + + console.RegisterCommand( + "ow_load", + [this](const std::vector& args) -> std::string + { + std::string slot = args.empty() ? "slot1" : args[0]; + return m_engineSystems->LoadGame(slot); + }, + "Load game", "OpenWorld"); + + console.RegisterCommand( + "ow_weather", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_weather "; + return m_engineSystems->SetWeather(args[0]); + }, + "Set weather", "OpenWorld"); + + console.RegisterCommand( + "ow_time", + [this](const std::vector& args) -> std::string + { + if (args.empty()) + return "Usage: ow_time (0-24)"; + try + { + float hour = std::stof(args[0]); + return m_engineSystems->SetTime(hour); + } + catch (...) + { + return "Invalid hour"; + } + }, + "Set time of day", "OpenWorld"); +} diff --git a/GameModules/SparkGameOpenWorld/Source/Core/OWEngineSystems.cpp b/GameModules/SparkGameOpenWorld/Source/Core/OWEngineSystems.cpp new file mode 100644 index 00000000..84e9b8e7 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Core/OWEngineSystems.cpp @@ -0,0 +1,424 @@ +/** + * @file OWEngineSystems.cpp + * @brief Wires open world gameplay into engine subsystems + * + * Registers save serializers, wildlife behavior trees, cinematic sequences, + * biome-driven weather/time rules, dynamic music, and event subscriptions. + */ + +#include "OWEngineSystems.h" +#include "Enums/OpenWorldEnums.h" + +#include "Engine/SaveSystem/SaveSystem.h" +#include "Engine/AI/AISystem.h" +#include "Engine/AI/BehaviorTree.h" +#include "Engine/Cinematic/Sequencer.h" +#include "Graphics/WeatherSystem.h" +#include "Engine/World/TimeOfDaySystem.h" +#include "Audio/MusicManager.h" +#include "Engine/Events/EventSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +namespace OpenWorld +{ + + OWEngineSystems::~OWEngineSystems() + { + if (m_initialized) + Shutdown(); + } + + bool OWEngineSystems::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + RegisterSaveSerializers(); + RegisterBehaviorTrees(); + RegisterCinematicSequences(); + SetupWeatherAndTimeOfDay(); + RegisterMusicTracks(); + SubscribeToEvents(); + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world engine systems integration initialized (6 subsystems)"); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Engine systems wired (6 subsystems)"); + return true; + } + + void OWEngineSystems::Update([[maybe_unused]] float deltaTime) + { + if (!m_initialized) + return; + } + + void OWEngineSystems::Shutdown() + { + if (!m_initialized) + return; + + m_eventHandles.clear(); + m_context = nullptr; + m_initialized = false; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Engine systems shut down"); + } + + void OWEngineSystems::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + // Engine system debug is rendered by the engine's own panels +#endif + } + + // ========================================================================= + // Save System + // ========================================================================= + + void OWEngineSystems::RegisterSaveSerializers() + { + auto* saveSystem = m_context->GetSaveSystem(); + if (!saveSystem) + return; + + auto& registry = Spark::ComponentSerializerRegistry::GetInstance(); + + auto registerPlaceholder = [&](const std::string& typeName) + { + registry.Register( + typeName, + [typeName](const void*) -> Spark::SerializedComponent + { + Spark::SerializedComponent sc; + sc.typeName = typeName; + sc.properties["placeholder"] = typeName; + return sc; + }, + []([[maybe_unused]] World& world, [[maybe_unused]] EntityID entity, + [[maybe_unused]] const Spark::SerializedComponent& data) {}); + }; + + registerPlaceholder("OWPlayerSurvival"); // Health, stamina, hunger, thirst, temp + registerPlaceholder("OWExplorationState"); // Discovered POIs, secrets, XP + registerPlaceholder("OWResourceInventory"); // Gathered materials + registerPlaceholder("OWCampData"); // Player camp positions and tiers + registerPlaceholder("OWWildlifeTamed"); // Tamed animal companions + registerPlaceholder("OWEventProgress"); // Completed world events + + saveSystem->SetMaxAutoSaves(3); + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world registered 6 save serializers"); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Registered 6 save serializers"); + } + + std::string OWEngineSystems::SaveGame(const std::string& slotName) + { + auto* saveSystem = m_context->GetSaveSystem(); + if (!saveSystem) + return "Save system not available"; + + return "Save to slot '" + slotName + "' requested (save system wired)"; + } + + std::string OWEngineSystems::LoadGame(const std::string& slotName) + { + auto* saveSystem = m_context->GetSaveSystem(); + if (!saveSystem) + return "Save system not available"; + + if (!saveSystem->SaveExists(slotName)) + return "No save found in slot '" + slotName + "'"; + + return "Load from slot '" + slotName + "' requested (save system wired)"; + } + + // ========================================================================= + // AI / Behavior Trees + // ========================================================================= + + void OWEngineSystems::RegisterBehaviorTrees() + { + auto* aiSystem = m_context->GetAI(); + if (!aiSystem) + return; + + using namespace Spark::AI; + + // Herbivore: graze, flee from threats, herd movement + { + auto grazerTree = + FPSBehaviors::CreatePatrolBehavior({{0.0f, 0.0f, 0.0f}, {20.0f, 0.0f, 10.0f}, {-10.0f, 0.0f, 20.0f}}); + aiSystem->RegisterBehavior("ow_herbivore", std::move(grazerTree)); + } + + // Predator: stalk, chase, attack, retreat when wounded + { + AIAgentConfig predatorConfig; + predatorConfig.detectionRange = 40.0f; + predatorConfig.attackRange = 5.0f; + predatorConfig.moveSpeed = 7.0f; + predatorConfig.accuracy = 0.8f; + predatorConfig.reactionTime = 0.15f; + auto predatorTree = FPSBehaviors::CreateCombatBehavior(predatorConfig); + aiSystem->RegisterBehavior("ow_predator", std::move(predatorTree)); + } + + // Settlement guard: patrol route, investigate, defend + { + AIAgentConfig guardConfig; + guardConfig.detectionRange = 30.0f; + guardConfig.attackRange = 10.0f; + guardConfig.moveSpeed = 4.0f; + guardConfig.accuracy = 0.7f; + guardConfig.reactionTime = 0.3f; + auto guardTree = FPSBehaviors::CreateCombatBehavior(guardConfig); + aiSystem->RegisterBehavior("ow_guard", std::move(guardTree)); + } + + // Merchant: stand at post, greet players + { + auto merchantTree = FPSBehaviors::CreateGuardBehavior({0.0f, 0.0f, 0.0f}, 5.0f); + aiSystem->RegisterBehavior("ow_merchant", std::move(merchantTree)); + } + + // Bandit: aggressive patrol, ambush tactics + { + AIAgentConfig banditConfig; + banditConfig.detectionRange = 35.0f; + banditConfig.attackRange = 12.0f; + banditConfig.moveSpeed = 5.5f; + banditConfig.accuracy = 0.6f; + banditConfig.reactionTime = 0.2f; + banditConfig.canUseCover = true; + auto banditTree = FPSBehaviors::CreateCombatBehavior(banditConfig); + aiSystem->RegisterBehavior("ow_bandit", std::move(banditTree)); + } + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world registered 5 behavior trees"); + Spark::SimpleConsole::GetInstance().LogInfo( + "[OpenWorld] Registered 5 behavior trees (herbivore, predator, guard, merchant, bandit)"); + } + + // ========================================================================= + // Cinematic Sequences + // ========================================================================= + + void OWEngineSystems::RegisterCinematicSequences() + { + auto* cinematic = m_context->GetCinematic(); + if (!cinematic) + return; + + using namespace Spark::Cinematic; + + // Opening: dawn breaks over the Emerald Meadows + { + auto* seq = cinematic->CreateSequence("ow_opening"); + + auto* cam = seq->AddCameraTrack("opening_camera"); + cam->AddKeyframe({0.0f, {0.0f, 80.0f, -500.0f}, {0.0f, 0.0f, 0.0f}, 65.0f, 0.0f}); + cam->AddKeyframe({4.0f, {0.0f, 40.0f, -200.0f}, {0.0f, 5.0f, 100.0f}, 55.0f, 0.0f}); + cam->AddKeyframe({8.0f, {50.0f, 15.0f, -50.0f}, {0.0f, 5.0f, 0.0f}, 50.0f, 0.0f}); + cam->AddKeyframe({12.0f, {0.0f, 8.0f, 0.0f}, {0.0f, 5.0f, 10.0f}, 60.0f, 0.0f}); + + auto* subs = seq->AddSubtitleTrack("opening_subs"); + subs->AddSubtitle({1.0f, 4.0f, "The world stretches before you, vast and uncharted.", "Narrator"}); + subs->AddSubtitle({5.0f, 8.0f, "Every horizon holds a secret. Every path, a choice.", "Narrator"}); + subs->AddSubtitle({9.0f, 12.0f, "Your journey begins in the Emerald Meadows.", "Narrator"}); + + auto* fade = seq->AddFadeTrack("opening_fade"); + fade->AddKeyframe({0.0f, 1.0f, {0.0f, 0.0f, 0.0f}}); + fade->AddKeyframe({1.5f, 0.0f, {0.0f, 0.0f, 0.0f}}); + fade->AddKeyframe({11.0f, 0.0f, {0.0f, 0.0f, 0.0f}}); + fade->AddKeyframe({12.0f, 0.0f, {0.0f, 0.0f, 0.0f}}); + + auto* events = seq->AddEventTrack("opening_events"); + events->AddCue({0.5f, "ow_play_music", "meadow_dawn"}); + events->AddCue({12.0f, "ow_enable_input", ""}); + } + + // Dragon sighting event cinematic + { + auto* seq = cinematic->CreateSequence("ow_dragon_sighting"); + + auto* cam = seq->AddCameraTrack("dragon_camera"); + cam->AddKeyframe({0.0f, {0.0f, 10.0f, 0.0f}, {100.0f, 50.0f, 200.0f}, 70.0f, 0.0f}); + cam->AddKeyframe({3.0f, {0.0f, 15.0f, 0.0f}, {200.0f, 80.0f, 100.0f}, 60.0f, 0.0f}); + cam->AddKeyframe({6.0f, {0.0f, 10.0f, 0.0f}, {300.0f, 100.0f, -50.0f}, 50.0f, 0.0f}); + + auto* subs = seq->AddSubtitleTrack("dragon_subs"); + subs->AddSubtitle({1.0f, 3.0f, "A shadow passes overhead...", ""}); + subs->AddSubtitle({3.5f, 6.0f, "The ancient dragon circles the peaks, watching.", "Narrator"}); + + auto* events = seq->AddEventTrack("dragon_events"); + events->AddCue({0.0f, "ow_play_music", "dragon_theme"}); + events->AddCue({6.0f, "ow_resume_gameplay", ""}); + } + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Registered 2 cinematic sequences"); + } + + // ========================================================================= + // Weather / Time of Day + // ========================================================================= + + void OWEngineSystems::SetupWeatherAndTimeOfDay() + { + auto* weather = m_context->GetWeather(); + auto* timeOfDay = m_context->GetTimeOfDay(); + + // Start at dawn with clear weather + if (weather) + weather->SetWeather(Spark::WeatherType::Clear, 0.0f, 0.0f); + + if (timeOfDay) + { + timeOfDay->SetTimeOfDay(6.0f); // 6:00 AM — dawn + timeOfDay->SetTimeScale(40.0f); // 1 real second = 40 game seconds (~36 min day cycle) + } + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Weather and time configured (6:00 AM dawn, clear)"); + } + + std::string OWEngineSystems::SetWeather(const std::string& weatherName) + { + auto* weather = m_context->GetWeather(); + if (!weather) + return "Weather system not available"; + + Spark::WeatherType type = Spark::WeatherType::Clear; + if (weatherName == "clear") + type = Spark::WeatherType::Clear; + else if (weatherName == "cloudy") + type = Spark::WeatherType::Cloudy; + else if (weatherName == "rain") + type = Spark::WeatherType::Rain; + else if (weatherName == "snow") + type = Spark::WeatherType::Snow; + else if (weatherName == "fog") + type = Spark::WeatherType::Fog; + else if (weatherName == "storm") + type = Spark::WeatherType::Storm; + else + return "Unknown weather: " + weatherName; + + weather->SetWeather(type, -1.0f, 5.0f); + return "Weather set to " + weatherName + " (5s transition)"; + } + + std::string OWEngineSystems::SetTime(float hour) + { + auto* timeOfDay = m_context->GetTimeOfDay(); + if (!timeOfDay) + return "Time-of-day system not available"; + + if (hour < 0.0f || hour >= 24.0f) + return "Hour must be in [0, 24)"; + + timeOfDay->SetTimeOfDay(hour); + return "Time set to " + timeOfDay->GetTimeString(); + } + + std::string OWEngineSystems::GetWeatherStatus() const + { + auto* weather = m_context->GetWeather(); + if (!weather) + return "Weather system not available"; + + return "Weather active (query via engine context)"; + } + + std::string OWEngineSystems::GetAbilitySummary() const + { + return "Open world uses survival mechanics instead of ability systems"; + } + + // ========================================================================= + // Music + // ========================================================================= + + void OWEngineSystems::RegisterMusicTracks() + { + auto* music = m_context->GetMusic(); + if (!music) + return; + + using Spark::Audio::MusicTrack; + + // Exploration tracks per biome + music->RegisterTrack({"meadow_dawn", "Assets/Audio/Music/ow_meadow_dawn.ogg", 80.0f, 0.0f, -1.0f, true, ""}); + music->RegisterTrack({"forest_depths", "Assets/Audio/Music/ow_forest.ogg", 70.0f, 0.0f, -1.0f, true, ""}); + music->RegisterTrack({"mountain_winds", "Assets/Audio/Music/ow_mountain.ogg", 65.0f, 0.0f, -1.0f, true, ""}); + music->RegisterTrack({"desert_heat", "Assets/Audio/Music/ow_desert.ogg", 75.0f, 0.0f, -1.0f, true, ""}); + music->RegisterTrack({"tundra_silence", "Assets/Audio/Music/ow_tundra.ogg", 55.0f, 0.0f, -1.0f, true, ""}); + music->RegisterTrack({"coastal_breeze", "Assets/Audio/Music/ow_coast.ogg", 85.0f, 0.0f, -1.0f, true, ""}); + + // Action / event tracks + music->RegisterTrack({"combat_tension", "Assets/Audio/Music/ow_combat.ogg", 120.0f, 0.0f, -1.0f, true, ""}); + music->RegisterTrack({"dragon_theme", "Assets/Audio/Music/ow_dragon.ogg", 140.0f, 0.0f, -1.0f, false, ""}); + + // Settlement / safe area + music->RegisterTrack({"village_hearth", "Assets/Audio/Music/ow_village.ogg", 90.0f, 0.0f, -1.0f, true, ""}); + + // Dynamic music state + Spark::Audio::DynamicMusicState dynamicState; + dynamicState.explorationTrack = "meadow_dawn"; + dynamicState.lowThreatTrack = "forest_depths"; + dynamicState.combatTrack = "combat_tension"; + dynamicState.bossFightTrack = "dragon_theme"; + dynamicState.transitionDuration = 3.0f; + music->SetDynamicMusicState(dynamicState); + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Registered 9 music tracks with dynamic transitions"); + } + + // ========================================================================= + // Event Bus + // ========================================================================= + + void OWEngineSystems::SubscribeToEvents() + { + auto* eventBus = m_context->GetEventBus(); + if (!eventBus) + return; + + // Weather changes affect survival (temperature, visibility) + m_eventHandles.push_back(eventBus->Subscribe( + [](const Spark::WeatherChangedEvent& evt) + { + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Weather changed to type " + + std::to_string(evt.newType) + + " (intensity=" + std::to_string(evt.intensity) + ")"); + })); + + // Time of day transitions (dawn/dusk affect wildlife behavior) + m_eventHandles.push_back(eventBus->Subscribe( + [](const Spark::TimeOfDayChangedEvent& evt) + { + if (evt.currentHour >= 6.0f && evt.previousHour < 6.0f) + { + Spark::SimpleConsole::GetInstance().LogInfo( + "[OpenWorld] Dawn — diurnal wildlife active, nocturnal retreating"); + } + else if (evt.currentHour >= 20.0f && evt.previousHour < 20.0f) + { + Spark::SimpleConsole::GetInstance().LogInfo( + "[OpenWorld] Dusk — nocturnal predators emerging, increased danger"); + } + })); + + // Entity killed (quest/hunting tracking) + m_eventHandles.push_back(eventBus->Subscribe( + [](const Spark::EntityKilledEvent& evt) + { + Spark::SimpleConsole::GetInstance().LogInfo( + "[OpenWorld] Entity killed: " + std::to_string(evt.entityId) + " (" + evt.cause + ")"); + })); + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world subscribed to 3 engine events"); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Subscribed to 3 engine events"); + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Core/OWEngineSystems.h b/GameModules/SparkGameOpenWorld/Source/Core/OWEngineSystems.h new file mode 100644 index 00000000..363d9cf1 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Core/OWEngineSystems.h @@ -0,0 +1,62 @@ +/** + * @file OWEngineSystems.h + * @brief Wires open world gameplay into engine subsystems + * @author Spark Engine Team + * @date 2026 + * + * OWEngineSystems registers open-world-specific data with engine infrastructure: + * save serializers, wildlife behavior trees, cinematic sequences, biome-driven + * weather/time-of-day rules, dynamic music, and event bus subscriptions. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Utils/EventBus.h" + +#include +#include + +namespace OpenWorld +{ + + /** + * @brief Bridges open world game logic with engine subsystems + * + * Constructed and owned by SparkGameOpenWorldModule. On Initialize() it + * registers open-world assets and handlers with save, AI, cinematic, + * weather, music, and event systems. + */ + class OWEngineSystems + { + public: + OWEngineSystems() = default; + ~OWEngineSystems(); + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime); + void Shutdown(); + void RenderDebugUI(); + + // Console command helpers + std::string SaveGame(const std::string& slotName); + std::string LoadGame(const std::string& slotName); + std::string SetWeather(const std::string& weatherName); + std::string SetTime(float hour); + std::string GetWeatherStatus() const; + std::string GetAbilitySummary() const; + + private: + void RegisterSaveSerializers(); + void RegisterBehaviorTrees(); + void RegisterCinematicSequences(); + void SetupWeatherAndTimeOfDay(); + void RegisterMusicTracks(); + void SubscribeToEvents(); + + Spark::IEngineContext* m_context = nullptr; + bool m_initialized = false; + std::vector m_eventHandles; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Core/SparkGameOpenWorld.h b/GameModules/SparkGameOpenWorld/Source/Core/SparkGameOpenWorld.h new file mode 100644 index 00000000..a34142b2 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Core/SparkGameOpenWorld.h @@ -0,0 +1,79 @@ +/** + * @file SparkGameOpenWorld.h + * @brief Open world game module - large-world exploration showcase + * @author Spark Engine Team + * @date 2026 + * + * SparkGameOpenWorld is a game module that showcases SparkEngine's open-world + * capabilities: 8 biome regions with seamless streaming, survival mechanics, + * wildlife ecology, settlement building, resource gathering and crafting, + * dynamic world events, and full engine subsystem integration. + * + * Implements the Spark::IModule interface for the module system. + */ + +#pragma once + +#include "Spark/SparkSDK.h" +#include + +namespace OpenWorld +{ + class OWWorldSetup; + class OWPlayerSystem; + class OWExplorationSystem; + class OWWildlifeSystem; + class OWSettlementSystem; + class OWGatheringSystem; + class OWDynamicEventSystem; + class OWEngineSystems; +} // namespace OpenWorld + +/** + * @brief Game module showcasing open-world gameplay on SparkEngine + * + * Wires up 8 subsystems covering world streaming, player survival, + * exploration/discovery, wildlife ecology, NPC settlements, resource + * gathering/crafting, dynamic events, and engine integration. + */ +class SparkGameOpenWorldModule : public Spark::IModule +{ + public: + SparkGameOpenWorldModule(); + ~SparkGameOpenWorldModule() override; + + // --- Spark::IModule interface --- + Spark::ModuleInfo GetModuleInfo() const override; + bool OnLoad(Spark::IEngineContext* context) override; + void OnUnload() override; + void OnUpdate(float deltaTime) override; + void OnFixedUpdate(float fixedDeltaTime) override; + void OnRender() override; + void OnResize(int width, int height) override; + void OnPause() override; + void OnResume() override; + void OnImGui() override; + + private: + void RegisterConsoleCommands(); + + Spark::IEngineContext* m_context{nullptr}; + bool m_initialized{false}; + bool m_paused{false}; + + std::unique_ptr m_worldSetup; + std::unique_ptr m_playerSystem; + std::unique_ptr m_explorationSystem; + std::unique_ptr m_wildlifeSystem; + std::unique_ptr m_settlementSystem; + std::unique_ptr m_gatheringSystem; + std::unique_ptr m_eventSystem; + std::unique_ptr m_engineSystems; +}; + +// Module exports +extern "C" +{ + SPARK_MODULE_API Spark::IModule* CreateModule(); + SPARK_MODULE_API void DestroyModule(Spark::IModule* mod); +} diff --git a/GameModules/SparkGameOpenWorld/Source/Enums/OpenWorldEnums.h b/GameModules/SparkGameOpenWorld/Source/Enums/OpenWorldEnums.h new file mode 100644 index 00000000..42cf7640 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Enums/OpenWorldEnums.h @@ -0,0 +1,225 @@ +/** + * @file OpenWorldEnums.h + * @brief Enumerations for open world gameplay systems + * @author Spark Engine Team + * @date 2026 + * + * Contains all enum types used by the open world game module: biomes, + * wildlife, resources, settlements, exploration, player survival, + * dynamic events, and crafting. + */ + +#pragma once + +#include + +namespace OpenWorld +{ + + /// @brief World biome types affecting terrain, weather, wildlife, and resources + enum class Biome : uint8_t + { + Grasslands = 0, + Forest = 1, + Mountains = 2, + Desert = 3, + Tundra = 4, + Swamp = 5, + Coast = 6, + Volcanic = 7, + Count = 8 + }; + + /// @brief Wildlife species categories + enum class AnimalType : uint8_t + { + Deer = 0, + Wolf = 1, + Bear = 2, + Boar = 3, + Eagle = 4, + Horse = 5, + Rabbit = 6, + Elk = 7, + Fox = 8, + Bison = 9, + MountainLion = 10, + Snake = 11, + Count = 12 + }; + + /// @brief Wildlife behavioral state + enum class AnimalBehavior : uint8_t + { + Grazing = 0, + Roaming = 1, + Fleeing = 2, + Stalking = 3, + Attacking = 4, + Sleeping = 5, + Following = 6, ///< Tamed, following player + Count = 7 + }; + + /// @brief Wildlife diet classification (drives predator-prey interactions) + enum class DietType : uint8_t + { + Herbivore = 0, + Carnivore = 1, + Omnivore = 2, + Count = 3 + }; + + /// @brief Harvestable resource categories + enum class ResourceType : uint8_t + { + Wood = 0, + Stone = 1, + Iron = 2, + Herbs = 3, + Fiber = 4, + Hide = 5, + Meat = 6, + Water = 7, + Gold = 8, + Crystal = 9, + Clay = 10, + Count = 11 + }; + + /// @brief Crafting recipe categories + enum class CraftingCategory : uint8_t + { + Tools = 0, + Weapons = 1, + Armor = 2, + Consumables = 3, + Building = 4, + Alchemy = 5, + Count = 6 + }; + + /// @brief Point of interest types on the world map + enum class POIType : uint8_t + { + Landmark = 0, + Ruins = 1, + Cave = 2, + Shrine = 3, + Viewpoint = 4, + Camp = 5, + Dungeon = 6, + Village = 7, + FastTravel = 8, + TreasureCache = 9, + Count = 10 + }; + + /// @brief Discovery state for map fog / exploration progress + enum class DiscoveryState : uint8_t + { + Undiscovered = 0, + Revealed = 1, ///< Seen from viewpoint but not visited + Visited = 2, + Completed = 3, ///< All objectives/secrets found + Count = 4 + }; + + /// @brief Settlement types in the world + enum class SettlementType : uint8_t + { + Village = 0, + Town = 1, + Outpost = 2, + Farmstead = 3, + PlayerCamp = 4, + BanditCamp = 5, + Count = 6 + }; + + /// @brief NPC roles within settlements + enum class NPCRole : uint8_t + { + Merchant = 0, + Blacksmith = 1, + Innkeeper = 2, + Guard = 3, + Farmer = 4, + Hunter = 5, + Elder = 6, + Wanderer = 7, + Count = 8 + }; + + /// @brief Player survival stat categories + enum class SurvivalStat : uint8_t + { + Health = 0, + Stamina = 1, + Hunger = 2, + Thirst = 3, + Temperature = 4, + Count = 5 + }; + + /// @brief Dynamic world event types + enum class WorldEventType : uint8_t + { + BanditRaid = 0, + WildlifeStampede = 1, + MerchantCaravan = 2, + WeatherStorm = 3, + VolcanicEruption = 4, + DragonSighting = 5, + HarvestFestival = 6, + AncientAwakening = 7, + Count = 8 + }; + + /// @brief Event lifecycle state + enum class EventState : uint8_t + { + Dormant = 0, + Approaching = 1, + Active = 2, + Resolving = 3, + Completed = 4, + Count = 5 + }; + + /// @brief Temperature range affecting survival + enum class TemperatureZone : uint8_t + { + Freezing = 0, + Cold = 1, + Temperate = 2, + Warm = 3, + Hot = 4, + Scorching = 5, + Count = 6 + }; + + /// @brief Time-of-day periods that affect gameplay + enum class DayPeriod : uint8_t + { + Dawn = 0, + Morning = 1, + Midday = 2, + Afternoon = 3, + Dusk = 4, + Night = 5, + Count = 6 + }; + + /// @brief Player camp upgrade tiers + enum class CampTier : uint8_t + { + Campfire = 0, ///< Basic fire, no shelter + Lean_To = 1, ///< Basic shelter from rain + Tent = 2, ///< Full weather protection, storage + Cabin = 3, ///< Permanent structure, crafting stations + Homestead = 4, ///< Full base with all facilities + Count = 5 + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Events/OWDynamicEventSystem.cpp b/GameModules/SparkGameOpenWorld/Source/Events/OWDynamicEventSystem.cpp new file mode 100644 index 00000000..658297e6 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Events/OWDynamicEventSystem.cpp @@ -0,0 +1,345 @@ +/** + * @file OWDynamicEventSystem.cpp + * @brief Dynamic world events and random encounters + */ + +#include "OWDynamicEventSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include +#include + +namespace OpenWorld +{ + + bool OWDynamicEventSystem::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + DefineEventTemplates(); + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Dynamic event system initialized: %zu event types", + m_templates.size()); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Events: " + std::to_string(m_templates.size()) + + " event types registered"); + return true; + } + + void OWDynamicEventSystem::DefineEventTemplates() + { + m_templates.clear(); + + m_templates.push_back({1, + "Bandit Raid", + "A group of bandits attacks a nearby settlement", + WorldEventType::BanditRaid, + 120.0f, + 300.0f, + 2, + 150, + {Biome::Grasslands, Biome::Forest, Biome::Coast}}); + + m_templates.push_back({2, + "Wildlife Stampede", + "A herd of animals stampedes through the area", + WorldEventType::WildlifeStampede, + 60.0f, + 600.0f, + 1, + 75, + {Biome::Grasslands, Biome::Tundra}}); + + m_templates.push_back({3, + "Merchant Caravan", + "A traveling merchant needs escort to the next settlement", + WorldEventType::MerchantCaravan, + 180.0f, + 400.0f, + 1, + 200, + {Biome::Grasslands, Biome::Forest, Biome::Desert, Biome::Coast}}); + + m_templates.push_back({4, + "Severe Storm", + "A dangerous storm system rolls in, affecting visibility and travel", + WorldEventType::WeatherStorm, + 90.0f, + 500.0f, + 3, + 100, + {Biome::Mountains, Biome::Tundra, Biome::Coast, Biome::Swamp}}); + + m_templates.push_back({5, + "Volcanic Eruption", + "The caldera rumbles and spews lava flows across the region", + WorldEventType::VolcanicEruption, + 300.0f, + 1800.0f, + 8, + 500, + {Biome::Volcanic}}); + + m_templates.push_back({6, + "Dragon Sighting", + "A legendary dragon has been spotted soaring over the region", + WorldEventType::DragonSighting, + 45.0f, + 3600.0f, + 5, + 1000, + {Biome::Mountains, Biome::Volcanic}}); + + m_templates.push_back({7, + "Harvest Festival", + "A settlement celebrates the harvest with games and trades", + WorldEventType::HarvestFestival, + 240.0f, + 1200.0f, + 1, + 50, + {Biome::Grasslands, Biome::Forest}}); + + m_templates.push_back({8, + "Ancient Awakening", + "An ancient guardian stirs from slumber deep underground", + WorldEventType::AncientAwakening, + 180.0f, + 2400.0f, + 7, + 750, + {Biome::Desert, Biome::Mountains, Biome::Volcanic}}); + } + + void OWDynamicEventSystem::Update(float deltaTime, float playerX, float playerZ, uint32_t playerRegionId) + { + if (!m_initialized) + return; + + // Tick cooldowns + for (auto& [id, remaining] : m_cooldowns) + { + remaining = std::max(0.0f, remaining - deltaTime); + } + + // Check for new random events periodically + m_eventCheckTimer += deltaTime; + if (m_eventCheckTimer >= 30.0f) // Check every 30 seconds + { + m_eventCheckTimer = 0.0f; + CheckForNewEvents(playerX, playerZ, playerRegionId); + } + + UpdateActiveEvents(deltaTime, playerX, playerZ); + } + + void OWDynamicEventSystem::CheckForNewEvents([[maybe_unused]] float playerX, [[maybe_unused]] float playerZ, + uint32_t playerRegionId) + { + // Simple random event spawning near the player's region + for (const auto& tmpl : m_templates) + { + // Check cooldown + auto cdIt = m_cooldowns.find(tmpl.templateId); + if (cdIt != m_cooldowns.end() && cdIt->second > 0.0f) + continue; + + // Limit concurrent events + if (m_activeEvents.size() >= 3) + break; + + // Simplified biome check: use a deterministic "random" based on world time + bool biomeMatch = false; + for (auto b : tmpl.validBiomes) + { + if (static_cast(b) + 1 == playerRegionId) + { + biomeMatch = true; + break; + } + } + if (!biomeMatch) + continue; + + // Pseudo-random spawn chance (deterministic for reproducibility) + uint32_t hash = (m_nextEventId * 31 + playerRegionId * 17 + tmpl.templateId * 13) % 100; + if (hash < 85) // 15% chance per eligible template per check + continue; + + TriggerEvent(tmpl.templateId, playerRegionId, playerX + static_cast(hash), playerZ); + break; // One event per check cycle + } + } + + uint32_t OWDynamicEventSystem::TriggerEvent(uint32_t templateId, uint32_t regionId, float x, float z) + { + const WorldEventTemplate* tmpl = nullptr; + for (const auto& t : m_templates) + { + if (t.templateId == templateId) + { + tmpl = &t; + break; + } + } + if (!tmpl) + return 0; + + ActiveWorldEvent evt{}; + evt.eventId = m_nextEventId++; + evt.templateId = templateId; + evt.name = tmpl->name; + evt.type = tmpl->type; + evt.state = EventState::Approaching; + evt.posX = x; + evt.posZ = z; + evt.regionId = regionId; + evt.timeRemaining = tmpl->duration; + evt.totalDuration = tmpl->duration; + + m_activeEvents[evt.eventId] = evt; + m_cooldowns[templateId] = tmpl->cooldown; + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] EVENT: " + tmpl->name + " in Region " + + std::to_string(regionId) + " (id=" + std::to_string(evt.eventId) + + ")"); + return evt.eventId; + } + + bool OWDynamicEventSystem::JoinEvent(uint32_t eventId) + { + auto it = m_activeEvents.find(eventId); + if (it == m_activeEvents.end() || it->second.state == EventState::Completed) + return false; + + it->second.playerParticipating = true; + it->second.state = EventState::Active; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Joined event: " + it->second.name); + return true; + } + + void OWDynamicEventSystem::UpdateActiveEvents(float deltaTime, [[maybe_unused]] float playerX, + [[maybe_unused]] float playerZ) + { + std::vector completed; + + for (auto& [id, evt] : m_activeEvents) + { + evt.timeRemaining -= deltaTime; + + // State transitions based on remaining time + float progress = 1.0f - (evt.timeRemaining / evt.totalDuration); + if (progress > 0.9f) + evt.state = EventState::Resolving; + else if (progress > 0.1f && evt.state == EventState::Approaching) + evt.state = EventState::Active; + + if (evt.timeRemaining <= 0.0f) + completed.push_back(id); + } + + for (uint32_t id : completed) + CompleteEvent(id); + } + + void OWDynamicEventSystem::CompleteEvent(uint32_t eventId) + { + auto it = m_activeEvents.find(eventId); + if (it == m_activeEvents.end()) + return; + + it->second.state = EventState::Completed; + m_totalEventsCompleted++; + + std::string result = it->second.playerParticipating ? "PARTICIPATED" : "MISSED"; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Event completed: " + it->second.name + " (" + result + + ")"); + + m_activeEvents.erase(it); + } + + std::string OWDynamicEventSystem::GetEventListString() const + { + std::ostringstream ss; + ss << "=== World Event Types ===\n"; + for (const auto& t : m_templates) + { + ss << "[" << t.templateId << "] " << t.name << " (dur:" << t.duration << "s, cd:" << t.cooldown << "s)\n"; + ss << " " << t.description << "\n"; + } + return ss.str(); + } + + std::string OWDynamicEventSystem::GetActiveEventsString() const + { + std::ostringstream ss; + ss << "=== Active Events ===\n"; + if (m_activeEvents.empty()) + { + ss << "No active events\n"; + return ss.str(); + } + for (const auto& [id, evt] : m_activeEvents) + { + const char* stateStr = "???"; + switch (evt.state) + { + case EventState::Approaching: + stateStr = "Approaching"; + break; + case EventState::Active: + stateStr = "Active"; + break; + case EventState::Resolving: + stateStr = "Resolving"; + break; + default: + break; + } + ss << "[" << id << "] " << evt.name << " (" << stateStr << ") " << evt.timeRemaining << "s remaining"; + if (evt.playerParticipating) + ss << " [JOINED]"; + ss << "\n"; + } + ss << "Total completed: " << m_totalEventsCompleted << "\n"; + return ss.str(); + } + + void OWDynamicEventSystem::Shutdown() + { + if (!m_initialized) + return; + + m_templates.clear(); + m_activeEvents.clear(); + m_cooldowns.clear(); + m_initialized = false; + } + + void OWDynamicEventSystem::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Dynamic Events")) + return; + + ImGui::Text("Event Types: %zu | Active: %zu | Completed: %u", m_templates.size(), m_activeEvents.size(), + m_totalEventsCompleted); + ImGui::Separator(); + + for (const auto& [id, evt] : m_activeEvents) + { + float progress = 1.0f - (evt.timeRemaining / evt.totalDuration); + ImGui::ProgressBar(progress, ImVec2(-1, 0), evt.name.c_str()); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Events/OWDynamicEventSystem.h b/GameModules/SparkGameOpenWorld/Source/Events/OWDynamicEventSystem.h new file mode 100644 index 00000000..bcdf3e3b --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Events/OWDynamicEventSystem.h @@ -0,0 +1,97 @@ +/** + * @file OWDynamicEventSystem.h + * @brief Dynamic world events, random encounters, and emergent gameplay + * @author Spark Engine Team + * @date 2026 + * + * Spawns time-limited world events that create emergent gameplay: bandit raids + * on settlements, wildlife stampedes, merchant caravans to escort, weather- + * driven disasters, and mythical creature sightings. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief Template defining a world event type + struct WorldEventTemplate + { + uint32_t templateId = 0; + std::string name; + std::string description; + WorldEventType type = WorldEventType::BanditRaid; + float duration = 120.0f; ///< Seconds the event lasts + float cooldown = 600.0f; ///< Minimum seconds between occurrences + int minDangerLevel = 1; ///< Region must be at least this dangerous + uint32_t xpReward = 100; + std::vector validBiomes; ///< Biomes where this event can spawn + }; + + /// @brief Runtime state for an active world event + struct ActiveWorldEvent + { + uint32_t eventId = 0; + uint32_t templateId = 0; + std::string name; + WorldEventType type = WorldEventType::BanditRaid; + EventState state = EventState::Dormant; + float posX = 0.0f; + float posZ = 0.0f; + uint32_t regionId = 0; + float timeRemaining = 0.0f; + float totalDuration = 0.0f; + bool playerParticipating = false; + }; + + /** + * @brief Dynamic world event spawning and lifecycle management + */ + class OWDynamicEventSystem + { + public: + OWDynamicEventSystem() = default; + ~OWDynamicEventSystem() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime, float playerX, float playerZ, uint32_t playerRegionId); + void Shutdown(); + void RenderDebugUI(); + + size_t GetTemplateCount() const { return m_templates.size(); } + size_t GetActiveEventCount() const { return m_activeEvents.size(); } + + /// @brief Force-spawn a specific event type in a region + uint32_t TriggerEvent(uint32_t templateId, uint32_t regionId, float x, float z); + + /// @brief Player joins an active event + bool JoinEvent(uint32_t eventId); + + std::string GetEventListString() const; + std::string GetActiveEventsString() const; + + private: + void DefineEventTemplates(); + void CheckForNewEvents(float playerX, float playerZ, uint32_t playerRegionId); + void UpdateActiveEvents(float deltaTime, float playerX, float playerZ); + void CompleteEvent(uint32_t eventId); + + Spark::IEngineContext* m_context{nullptr}; + std::vector m_templates; + std::unordered_map m_activeEvents; + std::unordered_map m_cooldowns; ///< templateId -> remaining cooldown + uint32_t m_nextEventId{1}; + float m_eventCheckTimer{0.0f}; + uint32_t m_totalEventsCompleted{0}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Exploration/OWExplorationSystem.cpp b/GameModules/SparkGameOpenWorld/Source/Exploration/OWExplorationSystem.cpp new file mode 100644 index 00000000..2d713344 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Exploration/OWExplorationSystem.cpp @@ -0,0 +1,344 @@ +/** + * @file OWExplorationSystem.cpp + * @brief POI discovery, map fog, and exploration progress tracking + */ + +#include "OWExplorationSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include +#include + +namespace OpenWorld +{ + + bool OWExplorationSystem::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + DefinePointsOfInterest(); + + // Initialize all POIs as undiscovered + for (const auto& poi : m_pois) + { + m_progress[poi.poiId] = {poi.poiId, DiscoveryState::Undiscovered, false}; + } + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Exploration system initialized with %zu POIs", m_pois.size()); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Exploration: " + std::to_string(m_pois.size()) + + " points of interest"); + return true; + } + + void OWExplorationSystem::DefinePointsOfInterest() + { + m_pois.clear(); + uint32_t id = 1; + + // --- Emerald Meadows (Region 1) --- + m_pois.push_back({id++, "Shepherd's Overlook", "A hilltop with a panoramic view of the meadows", + POIType::Viewpoint, 500.0f, 60.0f, -300.0f, 20.0f, 1, 100, false}); + m_pois.push_back({id++, "Old Stone Well", "An ancient well said to grant wishes", POIType::Shrine, -400.0f, + 2.0f, 600.0f, 15.0f, 1, 50, true}); + m_pois.push_back({id++, "Windmill Ruins", "The remains of a once-great flour mill", POIType::Ruins, 800.0f, + 10.0f, 100.0f, 25.0f, 1, 75, false}); + m_pois.push_back({id++, "Meadow Crossroads", "A major junction with a weathered signpost", POIType::FastTravel, + 0.0f, 5.0f, 0.0f, 30.0f, 1, 25, false}); + + // --- Ironwood Forest (Region 2) --- + m_pois.push_back({id++, "Hollow Oak", "A massive hollowed-out oak tree, ancient and eerie", POIType::Landmark, + 3000.0f, 15.0f, -500.0f, 20.0f, 2, 75, true}); + m_pois.push_back({id++, "Moonstone Cave", "A cave with glowing crystal formations", POIType::Cave, 4000.0f, + -10.0f, 1000.0f, 15.0f, 2, 150, true}); + m_pois.push_back({id++, "Ranger's Watchtower", "A tall watchtower offering forest views", POIType::Viewpoint, + 3500.0f, 80.0f, 500.0f, 20.0f, 2, 100, false}); + m_pois.push_back({id++, "Wolf Den", "The lair of the forest wolf pack", POIType::Dungeon, 2500.0f, -5.0f, + -1500.0f, 25.0f, 2, 200, true}); + + // --- Stormcrest Mountains (Region 3) --- + m_pois.push_back({id++, "Eagle's Perch", "The highest reachable peak in the range", POIType::Viewpoint, 1000.0f, + 700.0f, 5000.0f, 20.0f, 3, 200, false}); + m_pois.push_back({id++, "Dwarven Mine", "Abandoned mine shafts with rich ore veins", POIType::Dungeon, 500.0f, + 250.0f, 3500.0f, 25.0f, 3, 250, true}); + m_pois.push_back({id++, "Frozen Shrine", "An ice-encrusted altar to a forgotten god", POIType::Shrine, -500.0f, + 400.0f, 4500.0f, 15.0f, 3, 150, true}); + + // --- Ashwind Desert (Region 4) --- + m_pois.push_back({id++, "Sandstone Colossus", "A half-buried statue of immense proportions", POIType::Landmark, + 7000.0f, 30.0f, -2000.0f, 30.0f, 4, 150, false}); + m_pois.push_back({id++, "Oasis of Respite", "A rare water source amid the dunes", POIType::FastTravel, 6000.0f, + 5.0f, -1000.0f, 25.0f, 4, 100, false}); + m_pois.push_back({id++, "Buried Temple", "Ancient ruins emerging from shifting sands", POIType::Dungeon, + 8000.0f, -20.0f, -3000.0f, 25.0f, 4, 300, true}); + m_pois.push_back({id++, "Scorpion Caves", "A network of tunnels beneath the desert", POIType::Cave, 5500.0f, + -30.0f, -2500.0f, 20.0f, 4, 175, true}); + + // --- Frosthollow Tundra (Region 5) --- + m_pois.push_back({id++, "Mammoth Graveyard", "A field of ancient bones jutting from the ice", POIType::Landmark, + -3500.0f, 10.0f, 5000.0f, 30.0f, 5, 150, true}); + m_pois.push_back({id++, "Ice Caverns", "Crystalline ice caves with aurora reflections", POIType::Cave, -4000.0f, + -15.0f, 4000.0f, 20.0f, 5, 200, true}); + m_pois.push_back({id++, "Frost Watchtower", "A crumbling tower on the tundra's edge", POIType::Viewpoint, + -2000.0f, 50.0f, 3000.0f, 20.0f, 5, 100, false}); + + // --- Mistveil Swamp (Region 6) --- + m_pois.push_back({id++, "Witch's Hut", "A stilted shack deep in the bog", POIType::Landmark, -3500.0f, 3.0f, + -1000.0f, 15.0f, 6, 100, true}); + m_pois.push_back({id++, "Sunken Ruins", "Crumbling walls half-swallowed by the marsh", POIType::Ruins, -4000.0f, + -5.0f, 500.0f, 25.0f, 6, 150, true}); + m_pois.push_back({id++, "Bog Shrine", "A moss-covered altar oozing with swamp water", POIType::Shrine, -3000.0f, + 1.0f, -2000.0f, 15.0f, 6, 75, false}); + + // --- Sunbreak Coast (Region 7) --- + m_pois.push_back({id++, "Lighthouse Point", "A working lighthouse overlooking the sea", POIType::Viewpoint, + 0.0f, 40.0f, -6000.0f, 20.0f, 7, 100, false}); + m_pois.push_back({id++, "Smuggler's Cove", "A hidden inlet used by coastal smugglers", POIType::Cave, -2000.0f, + 2.0f, -5500.0f, 20.0f, 7, 175, true}); + m_pois.push_back({id++, "Port Market", "A bustling trading hub on the waterfront", POIType::FastTravel, 1000.0f, + 3.0f, -5000.0f, 30.0f, 7, 50, false}); + m_pois.push_back({id++, "Shipwreck Beach", "The wreckage of an old galleon", POIType::TreasureCache, 2500.0f, + 1.0f, -7000.0f, 25.0f, 7, 200, true}); + + // --- Cinderforge Caldera (Region 8) --- + m_pois.push_back({id++, "Obsidian Spire", "A towering natural obsidian formation", POIType::Landmark, 500.0f, + 350.0f, 9000.0f, 30.0f, 8, 250, false}); + m_pois.push_back({id++, "Forge of the Ancients", "A volcanic forge of immense power", POIType::Dungeon, 0.0f, + 200.0f, 10000.0f, 25.0f, 8, 500, true}); + m_pois.push_back({id++, "Lava Tunnels", "Cooled lava tubes leading deep underground", POIType::Cave, -500.0f, + 150.0f, 8500.0f, 20.0f, 8, 300, true}); + } + + void OWExplorationSystem::Update(float deltaTime, float playerX, float playerY, float playerZ) + { + if (!m_initialized) + return; + + (void)deltaTime; + CheckDiscovery(playerX, playerY, playerZ); + } + + void OWExplorationSystem::CheckDiscovery(float playerX, float playerY, float playerZ) + { + for (const auto& poi : m_pois) + { + auto& prog = m_progress[poi.poiId]; + if (prog.state >= DiscoveryState::Visited) + continue; + + float dx = playerX - poi.x; + float dy = playerY - poi.y; + float dz = playerZ - poi.z; + float distSq = dx * dx + dy * dy + dz * dz; + float radiusSq = poi.discoveryRadius * poi.discoveryRadius; + + if (distSq <= radiusSq) + { + DiscoverPOI(poi.poiId); + } + } + } + + void OWExplorationSystem::DiscoverPOI(uint32_t poiId) + { + auto it = m_progress.find(poiId); + if (it == m_progress.end() || it->second.state >= DiscoveryState::Visited) + return; + + it->second.state = DiscoveryState::Visited; + + // Find the POI for reward + for (const auto& poi : m_pois) + { + if (poi.poiId == poiId) + { + m_totalXPEarned += poi.xpReward; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Discovered: " + poi.name + " (+" + + std::to_string(poi.xpReward) + " XP)"); + break; + } + } + } + + void OWExplorationSystem::RevealPOI(uint32_t poiId) + { + auto it = m_progress.find(poiId); + if (it == m_progress.end() || it->second.state >= DiscoveryState::Revealed) + return; + + it->second.state = DiscoveryState::Revealed; + } + + void OWExplorationSystem::FindSecret(uint32_t poiId) + { + auto it = m_progress.find(poiId); + if (it == m_progress.end()) + return; + + if (!it->second.secretFound) + { + it->second.secretFound = true; + it->second.state = DiscoveryState::Completed; + m_totalXPEarned += 50; // Bonus XP for secret + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Secret found at POI " + std::to_string(poiId) + + " (+50 XP)"); + } + } + + size_t OWExplorationSystem::GetDiscoveredCount() const + { + size_t count = 0; + for (const auto& [id, prog] : m_progress) + { + if (prog.state >= DiscoveryState::Visited) + ++count; + } + return count; + } + + float OWExplorationSystem::GetOverallCompletion() const + { + if (m_pois.empty()) + return 0.0f; + + size_t completed = 0; + for (const auto& [id, prog] : m_progress) + { + if (prog.state == DiscoveryState::Completed) + ++completed; + else if (prog.state == DiscoveryState::Visited) + ++completed; // Visited counts as partial completion for non-secret POIs + } + return static_cast(completed) / static_cast(m_pois.size()) * 100.0f; + } + + RegionExploration OWExplorationSystem::GetRegionExploration(uint32_t regionId) const + { + RegionExploration result{}; + result.regionId = regionId; + + for (const auto& poi : m_pois) + { + if (poi.regionId != regionId) + continue; + + result.totalPOIs++; + auto it = m_progress.find(poi.poiId); + if (it != m_progress.end()) + { + if (it->second.state >= DiscoveryState::Visited) + result.discoveredPOIs++; + if (it->second.state == DiscoveryState::Completed) + result.completedPOIs++; + } + } + + if (result.totalPOIs > 0) + result.completionPercent = + static_cast(result.discoveredPOIs) / static_cast(result.totalPOIs) * 100.0f; + + return result; + } + + std::string OWExplorationSystem::GetExplorationString() const + { + std::ostringstream ss; + ss << "=== Exploration Progress ===\n"; + ss << "Discovered: " << GetDiscoveredCount() << "/" << m_pois.size() << "\n"; + ss << "Completion: " << GetOverallCompletion() << "%\n"; + ss << "Total XP: " << m_totalXPEarned << "\n"; + return ss.str(); + } + + std::string OWExplorationSystem::GetPOIListString() const + { + std::ostringstream ss; + ss << "=== Points of Interest ===\n"; + for (const auto& poi : m_pois) + { + auto it = m_progress.find(poi.poiId); + std::string state = "???"; + if (it != m_progress.end()) + { + switch (it->second.state) + { + case DiscoveryState::Undiscovered: + state = "Undiscovered"; + break; + case DiscoveryState::Revealed: + state = "Revealed"; + break; + case DiscoveryState::Visited: + state = "Visited"; + break; + case DiscoveryState::Completed: + state = "Completed"; + break; + default: + break; + } + } + ss << "[" << poi.poiId << "] " << poi.name << " (" << state << ")\n"; + } + return ss.str(); + } + + void OWExplorationSystem::Shutdown() + { + if (!m_initialized) + return; + + m_pois.clear(); + m_progress.clear(); + m_initialized = false; + } + + void OWExplorationSystem::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Exploration")) + return; + + ImGui::Text("POIs: %zu | Discovered: %zu | XP: %u", m_pois.size(), GetDiscoveredCount(), m_totalXPEarned); + ImGui::Text("Overall Completion: %.1f%%", GetOverallCompletion()); + ImGui::Separator(); + + for (const auto& poi : m_pois) + { + auto it = m_progress.find(poi.poiId); + if (it == m_progress.end()) + continue; + + const char* stateStr = "???"; + switch (it->second.state) + { + case DiscoveryState::Undiscovered: + stateStr = "Undiscovered"; + break; + case DiscoveryState::Revealed: + stateStr = "Revealed"; + break; + case DiscoveryState::Visited: + stateStr = "Visited"; + break; + case DiscoveryState::Completed: + stateStr = "Completed"; + break; + default: + break; + } + + ImGui::Text("[%u] %s (%s)", poi.poiId, poi.name.c_str(), stateStr); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Exploration/OWExplorationSystem.h b/GameModules/SparkGameOpenWorld/Source/Exploration/OWExplorationSystem.h new file mode 100644 index 00000000..9c567394 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Exploration/OWExplorationSystem.h @@ -0,0 +1,97 @@ +/** + * @file OWExplorationSystem.h + * @brief Points of interest, map discovery, landmarks, and exploration rewards + * @author Spark Engine Team + * @date 2026 + * + * Manages the player's exploration progress: POI discovery, map fog reveal, + * viewpoint unlocks, discovery XP, and completion tracking per region. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief A point of interest in the world + struct PointOfInterest + { + uint32_t poiId = 0; + std::string name; + std::string description; + POIType type = POIType::Landmark; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float discoveryRadius = 30.0f; ///< How close player must be to discover + uint32_t regionId = 0; + uint32_t xpReward = 50; + bool hasSecret = false; ///< Contains hidden collectible + }; + + /// @brief Per-POI discovery tracking + struct POIProgress + { + uint32_t poiId = 0; + DiscoveryState state = DiscoveryState::Undiscovered; + bool secretFound = false; + }; + + /// @brief Per-region exploration summary + struct RegionExploration + { + uint32_t regionId = 0; + uint32_t totalPOIs = 0; + uint32_t discoveredPOIs = 0; + uint32_t completedPOIs = 0; + float completionPercent = 0.0f; + }; + + /** + * @brief Exploration and discovery tracking system + */ + class OWExplorationSystem + { + public: + OWExplorationSystem() = default; + ~OWExplorationSystem() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime, float playerX, float playerY, float playerZ); + void Shutdown(); + void RenderDebugUI(); + + size_t GetPOICount() const { return m_pois.size(); } + size_t GetDiscoveredCount() const; + float GetOverallCompletion() const; + RegionExploration GetRegionExploration(uint32_t regionId) const; + std::string GetExplorationString() const; + std::string GetPOIListString() const; + + /// @brief Manually reveal a POI (e.g., from viewpoint or NPC hint) + void RevealPOI(uint32_t poiId); + + /// @brief Mark a POI's secret as found + void FindSecret(uint32_t poiId); + + private: + void DefinePointsOfInterest(); + void CheckDiscovery(float playerX, float playerY, float playerZ); + void DiscoverPOI(uint32_t poiId); + + Spark::IEngineContext* m_context{nullptr}; + std::vector m_pois; + std::unordered_map m_progress; + uint32_t m_totalXPEarned{0}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Gathering/OWGatheringSystem.cpp b/GameModules/SparkGameOpenWorld/Source/Gathering/OWGatheringSystem.cpp new file mode 100644 index 00000000..4bea7397 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Gathering/OWGatheringSystem.cpp @@ -0,0 +1,468 @@ +/** + * @file OWGatheringSystem.cpp + * @brief Resource nodes, harvesting, and crafting recipes + */ + +#include "OWGatheringSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include + +namespace OpenWorld +{ + + static const char* ResourceName(ResourceType type) + { + switch (type) + { + case ResourceType::Wood: + return "Wood"; + case ResourceType::Stone: + return "Stone"; + case ResourceType::Iron: + return "Iron"; + case ResourceType::Herbs: + return "Herbs"; + case ResourceType::Fiber: + return "Fiber"; + case ResourceType::Hide: + return "Hide"; + case ResourceType::Meat: + return "Meat"; + case ResourceType::Water: + return "Water"; + case ResourceType::Gold: + return "Gold"; + case ResourceType::Crystal: + return "Crystal"; + case ResourceType::Clay: + return "Clay"; + default: + return "Unknown"; + } + } + + bool OWGatheringSystem::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + DefineResourceNodes(); + DefineCraftingRecipes(); + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Gathering system initialized: %zu nodes, %zu recipes", m_nodes.size(), + m_recipes.size()); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Gathering: " + std::to_string(m_nodes.size()) + + " nodes, " + std::to_string(m_recipes.size()) + " recipes"); + return true; + } + + void OWGatheringSystem::DefineResourceNodes() + { + m_nodes.clear(); + uint32_t id = 1; + + auto addNode = [&](const std::string& name, ResourceType type, float x, float y, float z, uint32_t region, + uint32_t yield, float respawn) + { + m_nodes[id] = {id, name, type, x, y, z, region, yield, yield, respawn, 0.0f, false}; + ++id; + }; + + // Emerald Meadows (Region 1) + addNode("Oak Tree", ResourceType::Wood, 200.0f, 5.0f, 100.0f, 1, 5, 120.0f); + addNode("Meadow Herbs", ResourceType::Herbs, -100.0f, 2.0f, 300.0f, 1, 3, 90.0f); + addNode("River Water", ResourceType::Water, 0.0f, 0.0f, -200.0f, 1, 10, 60.0f); + addNode("Tall Grass", ResourceType::Fiber, 400.0f, 3.0f, -50.0f, 1, 4, 80.0f); + addNode("Field Stone", ResourceType::Stone, -300.0f, 5.0f, 500.0f, 1, 4, 150.0f); + + // Ironwood Forest (Region 2) + addNode("Ironwood Tree", ResourceType::Wood, 3000.0f, 10.0f, -200.0f, 2, 8, 180.0f); + addNode("Forest Mushrooms", ResourceType::Herbs, 2500.0f, 5.0f, 500.0f, 2, 4, 100.0f); + addNode("Vine Fiber", ResourceType::Fiber, 3500.0f, 8.0f, 100.0f, 2, 5, 90.0f); + addNode("Mossy Rock", ResourceType::Stone, 4000.0f, 12.0f, -500.0f, 2, 5, 150.0f); + + // Stormcrest Mountains (Region 3) + addNode("Iron Vein", ResourceType::Iron, 500.0f, 300.0f, 3500.0f, 3, 6, 300.0f); + addNode("Crystal Deposit", ResourceType::Crystal, -200.0f, 400.0f, 4500.0f, 3, 3, 600.0f); + addNode("Granite Boulder", ResourceType::Stone, 800.0f, 250.0f, 3000.0f, 3, 8, 200.0f); + addNode("Mountain Herbs", ResourceType::Herbs, 100.0f, 350.0f, 4000.0f, 3, 3, 120.0f); + + // Ashwind Desert (Region 4) + addNode("Sandstone Slab", ResourceType::Stone, 6500.0f, 5.0f, -1500.0f, 4, 6, 180.0f); + addNode("Gold Nuggets", ResourceType::Gold, 7500.0f, -10.0f, -2500.0f, 4, 2, 900.0f); + addNode("Desert Clay", ResourceType::Clay, 5500.0f, 0.0f, -500.0f, 4, 5, 150.0f); + addNode("Cactus Water", ResourceType::Water, 6000.0f, 3.0f, -1000.0f, 4, 3, 120.0f); + + // Frosthollow Tundra (Region 5) + addNode("Frozen Timber", ResourceType::Wood, -3500.0f, 5.0f, 5000.0f, 5, 4, 200.0f); + addNode("Ice Stone", ResourceType::Stone, -4500.0f, 10.0f, 4000.0f, 5, 6, 180.0f); + addNode("Tundra Lichen", ResourceType::Herbs, -2500.0f, 3.0f, 3500.0f, 5, 2, 150.0f); + + // Mistveil Swamp (Region 6) + addNode("Bogwood", ResourceType::Wood, -3500.0f, 2.0f, -800.0f, 6, 4, 120.0f); + addNode("Marsh Reeds", ResourceType::Fiber, -4000.0f, 0.0f, 200.0f, 6, 6, 80.0f); + addNode("Rare Herbs", ResourceType::Herbs, -3000.0f, 1.0f, -1500.0f, 6, 2, 180.0f); + addNode("Bog Clay", ResourceType::Clay, -4500.0f, -2.0f, 0.0f, 6, 5, 150.0f); + + // Sunbreak Coast (Region 7) + addNode("Driftwood", ResourceType::Wood, 500.0f, 1.0f, -5500.0f, 7, 3, 100.0f); + addNode("Coastal Fiber", ResourceType::Fiber, -1000.0f, 2.0f, -6000.0f, 7, 4, 90.0f); + addNode("Shell Deposits", ResourceType::Gold, 2000.0f, 0.0f, -6500.0f, 7, 1, 600.0f); + + // Cinderforge Caldera (Region 8) + addNode("Obsidian Shard", ResourceType::Stone, 500.0f, 200.0f, 9000.0f, 8, 4, 300.0f); + addNode("Volcanic Iron", ResourceType::Iron, -500.0f, 180.0f, 8500.0f, 8, 8, 240.0f); + addNode("Fire Crystal", ResourceType::Crystal, 0.0f, 250.0f, 10000.0f, 8, 2, 900.0f); + } + + void OWGatheringSystem::DefineCraftingRecipes() + { + m_recipes.clear(); + uint32_t id = 1; + + // Tools + m_recipes.push_back({id++, + "Stone Axe", + "A crude but effective chopping tool", + CraftingCategory::Tools, + {{ResourceType::Wood, 2}, {ResourceType::Stone, 3}}, + 5.0f, + false}); + m_recipes.push_back({id++, + "Iron Pickaxe", + "A sturdy mining tool", + CraftingCategory::Tools, + {{ResourceType::Wood, 3}, {ResourceType::Iron, 4}}, + 8.0f, + true}); + m_recipes.push_back({id++, + "Fishing Rod", + "For catching fish at water sources", + CraftingCategory::Tools, + {{ResourceType::Wood, 2}, {ResourceType::Fiber, 3}}, + 5.0f, + false}); + + // Weapons + m_recipes.push_back({id++, + "Wooden Spear", + "A simple pointed weapon", + CraftingCategory::Weapons, + {{ResourceType::Wood, 4}, {ResourceType::Fiber, 1}}, + 6.0f, + false}); + m_recipes.push_back({id++, + "Iron Sword", + "A reliable combat blade", + CraftingCategory::Weapons, + {{ResourceType::Iron, 5}, {ResourceType::Wood, 2}, {ResourceType::Hide, 1}}, + 12.0f, + true}); + m_recipes.push_back({id++, + "Hunting Bow", + "A bow for ranged hunting and combat", + CraftingCategory::Weapons, + {{ResourceType::Wood, 3}, {ResourceType::Fiber, 4}}, + 8.0f, + false}); + m_recipes.push_back({id++, + "Crystal Blade", + "A weapon infused with crystal energy", + CraftingCategory::Weapons, + {{ResourceType::Iron, 6}, {ResourceType::Crystal, 3}, {ResourceType::Gold, 1}}, + 20.0f, + true}); + + // Armor + m_recipes.push_back({id++, + "Leather Vest", + "Basic torso protection", + CraftingCategory::Armor, + {{ResourceType::Hide, 5}, {ResourceType::Fiber, 2}}, + 10.0f, + false}); + m_recipes.push_back({id++, + "Iron Chainmail", + "Medium armor with good protection", + CraftingCategory::Armor, + {{ResourceType::Iron, 8}, {ResourceType::Hide, 2}}, + 15.0f, + true}); + m_recipes.push_back({id++, + "Fur Cloak", + "Provides warmth in cold biomes", + CraftingCategory::Armor, + {{ResourceType::Hide, 4}, {ResourceType::Fiber, 3}}, + 8.0f, + false}); + + // Consumables + m_recipes.push_back({id++, + "Herbal Potion", + "Restores health over time", + CraftingCategory::Consumables, + {{ResourceType::Herbs, 3}, {ResourceType::Water, 1}}, + 5.0f, + false}); + m_recipes.push_back({id++, + "Cooked Meat", + "Satisfying meal that restores hunger", + CraftingCategory::Consumables, + {{ResourceType::Meat, 2}}, + 3.0f, + false}); + m_recipes.push_back({id++, + "Purified Water", + "Clean drinking water", + CraftingCategory::Consumables, + {{ResourceType::Water, 2}}, + 2.0f, + false}); + m_recipes.push_back({id++, + "Antidote", + "Cures poison effects", + CraftingCategory::Consumables, + {{ResourceType::Herbs, 4}, {ResourceType::Water, 1}}, + 6.0f, + false}); + + // Building + m_recipes.push_back({id++, + "Campfire Kit", + "Materials for a basic camp", + CraftingCategory::Building, + {{ResourceType::Wood, 5}, {ResourceType::Stone, 3}}, + 8.0f, + false}); + m_recipes.push_back({id++, + "Lean-To Frame", + "Upgrade materials for camp shelter", + CraftingCategory::Building, + {{ResourceType::Wood, 10}, {ResourceType::Fiber, 5}}, + 12.0f, + false}); + m_recipes.push_back({id++, + "Tent Canvas", + "Fabric for a proper tent", + CraftingCategory::Building, + {{ResourceType::Fiber, 12}, {ResourceType::Hide, 4}}, + 15.0f, + false}); + m_recipes.push_back({id++, + "Cabin Logs", + "Heavy timber for a permanent structure", + CraftingCategory::Building, + {{ResourceType::Wood, 25}, {ResourceType::Stone, 10}, {ResourceType::Iron, 5}}, + 30.0f, + true}); + + // Alchemy + m_recipes.push_back({id++, + "Warmth Tonic", + "Temporarily raises body temperature", + CraftingCategory::Alchemy, + {{ResourceType::Herbs, 2}, {ResourceType::Water, 1}, {ResourceType::Crystal, 1}}, + 8.0f, + true}); + m_recipes.push_back({id++, + "Stamina Elixir", + "Boosts stamina recovery", + CraftingCategory::Alchemy, + {{ResourceType::Herbs, 3}, {ResourceType::Crystal, 1}}, + 10.0f, + true}); + } + + void OWGatheringSystem::Update(float deltaTime) + { + if (!m_initialized) + return; + + UpdateNodeRespawns(deltaTime); + } + + void OWGatheringSystem::UpdateNodeRespawns(float deltaTime) + { + for (auto& [id, node] : m_nodes) + { + if (!node.isDepleted) + continue; + + node.respawnTimer += deltaTime; + if (node.respawnTimer >= node.respawnTime) + { + node.isDepleted = false; + node.currentYield = node.maxYield; + node.respawnTimer = 0.0f; + } + } + } + + uint32_t OWGatheringSystem::HarvestNode(uint32_t nodeId) + { + auto it = m_nodes.find(nodeId); + if (it == m_nodes.end() || it->second.isDepleted) + return 0; + + auto& node = it->second; + uint32_t harvested = std::min(node.currentYield, 1u); + node.currentYield -= harvested; + + if (node.currentYield == 0) + { + node.isDepleted = true; + node.respawnTimer = 0.0f; + } + + m_inventory.Add(node.resource, harvested); + m_totalHarvested += harvested; + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Harvested " + std::to_string(harvested) + "x " + + std::string(ResourceName(node.resource)) + " from " + node.name); + return harvested; + } + + void OWGatheringSystem::AddResource(ResourceType type, uint32_t amount) + { + m_inventory.Add(type, amount); + } + + bool OWGatheringSystem::CanCraft(uint32_t recipeId) const + { + for (const auto& recipe : m_recipes) + { + if (recipe.recipeId != recipeId) + continue; + + for (const auto& ing : recipe.ingredients) + { + if (m_inventory.Get(ing.resource) < ing.amount) + return false; + } + return true; + } + return false; + } + + bool OWGatheringSystem::Craft(uint32_t recipeId) + { + if (!CanCraft(recipeId)) + return false; + + for (const auto& recipe : m_recipes) + { + if (recipe.recipeId != recipeId) + continue; + + for (const auto& ing : recipe.ingredients) + m_inventory.Spend(ing.resource, ing.amount); + + m_totalCrafted++; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Crafted: " + recipe.resultName); + return true; + } + return false; + } + + std::string OWGatheringSystem::GetNodeListString() const + { + std::ostringstream ss; + ss << "=== Resource Nodes ===\n"; + for (const auto& [id, node] : m_nodes) + { + ss << "[" << id << "] " << node.name << " (" << ResourceName(node.resource) << ")"; + if (node.isDepleted) + ss << " [DEPLETED]"; + else + ss << " [" << node.currentYield << "/" << node.maxYield << "]"; + ss << "\n"; + } + return ss.str(); + } + + std::string OWGatheringSystem::GetRecipeListString() const + { + std::ostringstream ss; + ss << "=== Crafting Recipes ===\n"; + for (const auto& r : m_recipes) + { + ss << "[" << r.recipeId << "] " << r.resultName; + if (r.requiresCraftingStation) + ss << " [Station]"; + ss << " — "; + for (size_t i = 0; i < r.ingredients.size(); ++i) + { + if (i > 0) + ss << ", "; + ss << r.ingredients[i].amount << "x " << ResourceName(r.ingredients[i].resource); + } + ss << "\n"; + } + return ss.str(); + } + + std::string OWGatheringSystem::GetInventoryString() const + { + std::ostringstream ss; + ss << "=== Resource Inventory ===\n"; + for (const auto& [type, count] : m_inventory.resources) + { + if (count > 0) + ss << ResourceName(type) << ": " << count << "\n"; + } + ss << "Total Harvested: " << m_totalHarvested << " | Total Crafted: " << m_totalCrafted << "\n"; + return ss.str(); + } + + void OWGatheringSystem::Shutdown() + { + if (!m_initialized) + return; + + m_nodes.clear(); + m_recipes.clear(); + m_initialized = false; + } + + void OWGatheringSystem::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Gathering & Crafting")) + return; + + ImGui::Text("Nodes: %zu | Recipes: %zu | Harvested: %u | Crafted: %u", m_nodes.size(), m_recipes.size(), + m_totalHarvested, m_totalCrafted); + ImGui::Separator(); + + if (ImGui::TreeNode("Inventory")) + { + for (const auto& [type, count] : m_inventory.resources) + { + if (count > 0) + ImGui::Text("%s: %u", ResourceName(type), count); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Recipes")) + { + for (const auto& r : m_recipes) + { + bool canCraft = CanCraft(r.recipeId); + ImGui::Text("[%u] %s %s", r.recipeId, r.resultName.c_str(), canCraft ? "[CAN CRAFT]" : ""); + } + ImGui::TreePop(); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Gathering/OWGatheringSystem.h b/GameModules/SparkGameOpenWorld/Source/Gathering/OWGatheringSystem.h new file mode 100644 index 00000000..19a2e330 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Gathering/OWGatheringSystem.h @@ -0,0 +1,129 @@ +/** + * @file OWGatheringSystem.h + * @brief Resource nodes, harvesting, and crafting recipes + * @author Spark Engine Team + * @date 2026 + * + * Manages harvestable resource nodes scattered across biomes, a player + * inventory of raw materials, and a crafting system that converts + * gathered resources into tools, weapons, armor, and consumables. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief A harvestable resource node in the world + struct ResourceNode + { + uint32_t nodeId = 0; + std::string name; + ResourceType resource = ResourceType::Wood; + float posX = 0.0f; + float posY = 0.0f; + float posZ = 0.0f; + uint32_t regionId = 0; + uint32_t maxYield = 5; ///< How many units before depleted + uint32_t currentYield = 5; + float respawnTime = 300.0f; ///< Seconds until refilled + float respawnTimer = 0.0f; + bool isDepleted = false; + }; + + /// @brief A crafting ingredient requirement + struct CraftingIngredient + { + ResourceType resource = ResourceType::Wood; + uint32_t amount = 1; + }; + + /// @brief A craftable item recipe + struct CraftingRecipe + { + uint32_t recipeId = 0; + std::string resultName; + std::string description; + CraftingCategory category = CraftingCategory::Tools; + std::vector ingredients; + float craftTime = 5.0f; ///< Seconds to craft + bool requiresCraftingStation = false; + }; + + /// @brief Player resource inventory (raw material counts) + struct ResourceInventory + { + std::unordered_map resources; + + uint32_t Get(ResourceType type) const + { + auto it = resources.find(type); + return it != resources.end() ? it->second : 0; + } + void Add(ResourceType type, uint32_t amount) { resources[type] += amount; } + bool Spend(ResourceType type, uint32_t amount) + { + if (Get(type) < amount) + return false; + resources[type] -= amount; + return true; + } + }; + + /** + * @brief Resource gathering and crafting system + */ + class OWGatheringSystem + { + public: + OWGatheringSystem() = default; + ~OWGatheringSystem() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime); + void Shutdown(); + void RenderDebugUI(); + + size_t GetNodeCount() const { return m_nodes.size(); } + size_t GetRecipeCount() const { return m_recipes.size(); } + const ResourceInventory& GetInventory() const { return m_inventory; } + + /// @brief Harvest a resource node, adding resources to inventory + uint32_t HarvestNode(uint32_t nodeId); + + /// @brief Add resources directly (e.g., from hunting) + void AddResource(ResourceType type, uint32_t amount); + + /// @brief Check if the player can craft a recipe + bool CanCraft(uint32_t recipeId) const; + + /// @brief Craft an item, consuming resources + bool Craft(uint32_t recipeId); + + std::string GetNodeListString() const; + std::string GetRecipeListString() const; + std::string GetInventoryString() const; + + private: + void DefineResourceNodes(); + void DefineCraftingRecipes(); + void UpdateNodeRespawns(float deltaTime); + + Spark::IEngineContext* m_context{nullptr}; + std::unordered_map m_nodes; + std::vector m_recipes; + ResourceInventory m_inventory; + uint32_t m_totalHarvested{0}; + uint32_t m_totalCrafted{0}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Player/OWPlayerSystem.cpp b/GameModules/SparkGameOpenWorld/Source/Player/OWPlayerSystem.cpp new file mode 100644 index 00000000..f2148570 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Player/OWPlayerSystem.cpp @@ -0,0 +1,260 @@ +/** + * @file OWPlayerSystem.cpp + * @brief Player survival mechanics, fast travel, and compass + */ + +#include "OWPlayerSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include +#include +#include + +namespace OpenWorld +{ + + bool OWPlayerSystem::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + // Start in the Emerald Meadows center + m_worldState.posX = 0.0f; + m_worldState.posY = 5.0f; + m_worldState.posZ = 0.0f; + m_worldState.yaw = 0.0f; + m_worldState.currentRegionId = 1; + + // Full survival stats + m_survival = SurvivalState{}; + + // Register starting fast travel point + UnlockFastTravel({1, "Meadow Campfire", 0.0f, 5.0f, 0.0f, 1}); + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world player system initialized"); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Player system initialized"); + return true; + } + + void OWPlayerSystem::Update(float deltaTime) + { + if (!m_initialized) + return; + + m_survivalTickTimer += deltaTime; + if (m_survivalTickTimer >= 1.0f) + { + UpdateSurvival(m_survivalTickTimer); + m_survivalTickTimer = 0.0f; + } + } + + void OWPlayerSystem::FixedUpdate(float fixedDeltaTime) + { + if (!m_initialized) + return; + + // Stamina recovery/drain + if (m_worldState.isSprinting) + { + m_survival.stamina = std::max(0.0f, m_survival.stamina - 15.0f * fixedDeltaTime); + if (m_survival.stamina <= 0.0f) + m_worldState.isSprinting = false; + } + else + { + m_survival.stamina = std::min(m_survival.maxStamina, m_survival.stamina + 8.0f * fixedDeltaTime); + } + } + + void OWPlayerSystem::Shutdown() + { + if (!m_initialized) + return; + + m_fastTravelPoints.clear(); + m_unlockedPointIds.clear(); + m_initialized = false; + } + + void OWPlayerSystem::UpdateSurvival(float deltaTime) + { + // Hunger and thirst decrease over time + constexpr float hungerRate = 0.5f; // per second + constexpr float thirstRate = 0.7f; + + m_survival.hunger = std::max(0.0f, m_survival.hunger - hungerRate * deltaTime); + m_survival.thirst = std::max(0.0f, m_survival.thirst - thirstRate * deltaTime); + + UpdateTemperature(deltaTime); + ApplySurvivalEffects(deltaTime); + } + + void OWPlayerSystem::UpdateTemperature([[maybe_unused]] float deltaTime) + { + // Temperature is influenced by biome base temp and clothing warmth + // This is a simplified model — real implementation would query the + // world setup for biome temperature at current position + float targetTemp = 20.0f; // Default temperate + float adjustment = (m_survival.warmth - 0.5f) * 10.0f; + targetTemp += adjustment; + + // Lerp toward target + float diff = targetTemp - m_survival.temperature; + m_survival.temperature += diff * 0.1f; + } + + void OWPlayerSystem::ApplySurvivalEffects(float deltaTime) + { + // Starvation damage + if (m_survival.hunger <= 0.0f) + m_survival.health = std::max(0.0f, m_survival.health - 2.0f * deltaTime); + + // Dehydration damage + if (m_survival.thirst <= 0.0f) + m_survival.health = std::max(0.0f, m_survival.health - 3.0f * deltaTime); + + // Temperature extremes + if (m_survival.temperature < 0.0f) + m_survival.health = std::max(0.0f, m_survival.health - 1.5f * deltaTime); + else if (m_survival.temperature > 45.0f) + m_survival.health = std::max(0.0f, m_survival.health - 1.5f * deltaTime); + + // Passive health regen when well-fed and hydrated + if (m_survival.hunger > 50.0f && m_survival.thirst > 50.0f) + m_survival.health = std::min(m_survival.maxHealth, m_survival.health + 1.0f * deltaTime); + } + + void OWPlayerSystem::Eat(float nutritionValue) + { + m_survival.hunger = std::min(100.0f, m_survival.hunger + nutritionValue); + } + + void OWPlayerSystem::Drink(float hydrationValue) + { + m_survival.thirst = std::min(100.0f, m_survival.thirst + hydrationValue); + } + + void OWPlayerSystem::TakeDamage(float amount) + { + m_survival.health = std::max(0.0f, m_survival.health - amount); + } + + void OWPlayerSystem::Heal(float amount) + { + m_survival.health = std::min(m_survival.maxHealth, m_survival.health + amount); + } + + void OWPlayerSystem::SetPosition(float x, float y, float z) + { + m_worldState.posX = x; + m_worldState.posY = y; + m_worldState.posZ = z; + } + + void OWPlayerSystem::SetFacing(float yaw) + { + m_worldState.yaw = std::fmod(yaw, 360.0f); + if (m_worldState.yaw < 0.0f) + m_worldState.yaw += 360.0f; + } + + std::string OWPlayerSystem::GetCompassDirection() const + { + float y = m_worldState.yaw; + if (y >= 337.5f || y < 22.5f) + return "N"; + if (y < 67.5f) + return "NE"; + if (y < 112.5f) + return "E"; + if (y < 157.5f) + return "SE"; + if (y < 202.5f) + return "S"; + if (y < 247.5f) + return "SW"; + if (y < 292.5f) + return "W"; + return "NW"; + } + + void OWPlayerSystem::UnlockFastTravel(const FastTravelPoint& point) + { + if (m_unlockedPointIds.count(point.pointId)) + return; + + m_fastTravelPoints.push_back(point); + m_unlockedPointIds.insert(point.pointId); + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Fast travel unlocked: " + point.name); + } + + bool OWPlayerSystem::FastTravelTo(uint32_t pointId) + { + for (const auto& pt : m_fastTravelPoints) + { + if (pt.pointId == pointId) + { + SetPosition(pt.x, pt.y, pt.z); + m_worldState.currentRegionId = pt.regionId; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Fast traveled to " + pt.name); + return true; + } + } + return false; + } + + std::string OWPlayerSystem::GetStatusString() const + { + std::ostringstream ss; + ss << "=== Player Status ===\n"; + ss << "Position: (" << m_worldState.posX << ", " << m_worldState.posY << ", " << m_worldState.posZ << ")\n"; + ss << "Facing: " << GetCompassDirection() << " (" << m_worldState.yaw << " deg)\n"; + ss << "Region: " << m_worldState.currentRegionId << "\n"; + ss << "Health: " << m_survival.health << "/" << m_survival.maxHealth << "\n"; + ss << "Stamina: " << m_survival.stamina << "/" << m_survival.maxStamina << "\n"; + ss << "Hunger: " << m_survival.hunger << "/100\n"; + ss << "Thirst: " << m_survival.thirst << "/100\n"; + ss << "Temperature: " << m_survival.temperature << "C\n"; + ss << "Fast Travel Points: " << m_fastTravelPoints.size() << "\n"; + return ss.str(); + } + + void OWPlayerSystem::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Open World Player")) + return; + + ImGui::Text("Position: (%.1f, %.1f, %.1f)", m_worldState.posX, m_worldState.posY, m_worldState.posZ); + ImGui::Text("Facing: %s (%.0f deg)", GetCompassDirection().c_str(), m_worldState.yaw); + ImGui::Text("Region: %u", m_worldState.currentRegionId); + ImGui::Separator(); + + ImGui::ProgressBar(m_survival.health / m_survival.maxHealth, ImVec2(-1, 0), "Health"); + ImGui::ProgressBar(m_survival.stamina / m_survival.maxStamina, ImVec2(-1, 0), "Stamina"); + ImGui::ProgressBar(m_survival.hunger / 100.0f, ImVec2(-1, 0), "Hunger"); + ImGui::ProgressBar(m_survival.thirst / 100.0f, ImVec2(-1, 0), "Thirst"); + ImGui::Text("Temperature: %.1fC | Warmth: %.1f", m_survival.temperature, m_survival.warmth); + + if (!m_fastTravelPoints.empty() && ImGui::TreeNode("Fast Travel")) + { + for (const auto& pt : m_fastTravelPoints) + { + ImGui::Text("[%u] %s (%.0f, %.0f, %.0f)", pt.pointId, pt.name.c_str(), pt.x, pt.y, pt.z); + } + ImGui::TreePop(); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Player/OWPlayerSystem.h b/GameModules/SparkGameOpenWorld/Source/Player/OWPlayerSystem.h new file mode 100644 index 00000000..a8f807f4 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Player/OWPlayerSystem.h @@ -0,0 +1,116 @@ +/** + * @file OWPlayerSystem.h + * @brief Open world player: survival stats, stamina, fast travel, compass + * @author Spark Engine Team + * @date 2026 + * + * Manages the player character in an open world context: survival mechanics + * (health, stamina, hunger, thirst, temperature), fast travel between + * discovered points, compass/bearing data, and player state persistence. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief Player survival state + struct SurvivalState + { + float health = 100.0f; + float maxHealth = 100.0f; + float stamina = 100.0f; + float maxStamina = 100.0f; + float hunger = 100.0f; ///< 100 = full, 0 = starving + float thirst = 100.0f; ///< 100 = hydrated, 0 = dehydrated + float temperature = 20.0f; ///< Current body temperature (Celsius) + float warmth = 0.5f; ///< Clothing/shelter warmth factor (0-1) + }; + + /// @brief Player world position and movement state + struct PlayerWorldState + { + float posX = 0.0f; + float posY = 0.0f; + float posZ = 0.0f; + float yaw = 0.0f; ///< Facing direction in degrees (0 = north) + float speed = 0.0f; + bool isSprinting = false; + bool isSwimming = false; + bool isClimbing = false; + bool isInShelter = false; + uint32_t currentRegionId = 0; + }; + + /// @brief A fast travel destination the player has unlocked + struct FastTravelPoint + { + uint32_t pointId = 0; + std::string name; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + uint32_t regionId = 0; + }; + + /** + * @brief Player survival, movement, fast travel, and compass system + */ + class OWPlayerSystem + { + public: + OWPlayerSystem() = default; + ~OWPlayerSystem() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime); + void FixedUpdate(float fixedDeltaTime); + void Shutdown(); + void RenderDebugUI(); + + // --- Survival --- + const SurvivalState& GetSurvivalState() const { return m_survival; } + void Eat(float nutritionValue); + void Drink(float hydrationValue); + void TakeDamage(float amount); + void Heal(float amount); + bool IsAlive() const { return m_survival.health > 0.0f; } + + // --- Position / Movement --- + const PlayerWorldState& GetWorldState() const { return m_worldState; } + void SetPosition(float x, float y, float z); + void SetFacing(float yaw); + float GetCompassBearing() const { return m_worldState.yaw; } + std::string GetCompassDirection() const; + + // --- Fast Travel --- + void UnlockFastTravel(const FastTravelPoint& point); + bool FastTravelTo(uint32_t pointId); + const std::vector& GetFastTravelPoints() const { return m_fastTravelPoints; } + size_t GetFastTravelCount() const { return m_fastTravelPoints.size(); } + + std::string GetStatusString() const; + + private: + void UpdateSurvival(float deltaTime); + void UpdateTemperature(float deltaTime); + void ApplySurvivalEffects(float deltaTime); + + Spark::IEngineContext* m_context{nullptr}; + SurvivalState m_survival; + PlayerWorldState m_worldState; + std::vector m_fastTravelPoints; + std::unordered_set m_unlockedPointIds; + float m_survivalTickTimer{0.0f}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Settlement/OWSettlementSystem.cpp b/GameModules/SparkGameOpenWorld/Source/Settlement/OWSettlementSystem.cpp new file mode 100644 index 00000000..e8a1d42b --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Settlement/OWSettlementSystem.cpp @@ -0,0 +1,413 @@ +/** + * @file OWSettlementSystem.cpp + * @brief NPC settlements and player camp management + */ + +#include "OWSettlementSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include +#include + +namespace OpenWorld +{ + + bool OWSettlementSystem::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + DefineSettlements(); + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Settlement system initialized: %zu settlements", + m_settlements.size()); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Settlements: " + std::to_string(m_settlements.size()) + + " | NPCs: " + std::to_string(GetTotalNPCCount())); + return true; + } + + void OWSettlementSystem::DefineSettlements() + { + m_settlements.clear(); + uint32_t npcId = 1; + + // Meadowbrook — starting village in Emerald Meadows + { + Settlement s{}; + s.settlementId = 1; + s.name = "Meadowbrook"; + s.description = "A friendly farming village at the heart of the meadows"; + s.type = SettlementType::Village; + s.regionId = 1; + s.centerX = 100.0f; + s.centerY = 5.0f; + s.centerZ = -100.0f; + s.radius = 150.0f; + s.hasMerchant = true; + s.hasBlacksmith = true; + s.hasInn = true; + s.npcs = { + {npcId++, "Marta the Merchant", NPCRole::Merchant, 0.8f}, + {npcId++, "Gorrik the Smith", NPCRole::Blacksmith, 0.7f}, + {npcId++, "Old Hilda", NPCRole::Innkeeper, 0.9f}, + {npcId++, "Captain Brynn", NPCRole::Guard, 0.6f}, + {npcId++, "Farmer Jorik", NPCRole::Farmer, 0.8f}, + {npcId++, "Elder Aldric", NPCRole::Elder, 0.9f}, + }; + m_settlements.push_back(s); + } + + // Timberhold — forest outpost in Ironwood Forest + { + Settlement s{}; + s.settlementId = 2; + s.name = "Timberhold"; + s.description = "A logging outpost surrounded by towering ironwood trees"; + s.type = SettlementType::Outpost; + s.regionId = 2; + s.centerX = 2800.0f; + s.centerY = 15.0f; + s.centerZ = 0.0f; + s.radius = 80.0f; + s.hasMerchant = true; + s.hasBlacksmith = false; + s.hasInn = false; + s.npcs = { + {npcId++, "Theren the Woodsman", NPCRole::Hunter, 0.6f}, + {npcId++, "Lina the Trader", NPCRole::Merchant, 0.7f}, + {npcId++, "Sentry Dael", NPCRole::Guard, 0.5f}, + }; + m_settlements.push_back(s); + } + + // Highpass — mountain town in Stormcrest Mountains + { + Settlement s{}; + s.settlementId = 3; + s.name = "Highpass"; + s.description = "A fortified mountain town guarding the only pass through the peaks"; + s.type = SettlementType::Town; + s.regionId = 3; + s.centerX = 200.0f; + s.centerY = 300.0f; + s.centerZ = 3000.0f; + s.radius = 120.0f; + s.hasMerchant = true; + s.hasBlacksmith = true; + s.hasInn = true; + s.npcs = { + {npcId++, "Ingrid the Armorer", NPCRole::Blacksmith, 0.6f}, + {npcId++, "Ulfar the Supplier", NPCRole::Merchant, 0.5f}, + {npcId++, "Bruni the Innkeeper", NPCRole::Innkeeper, 0.7f}, + {npcId++, "Watch Commander Sigrun", NPCRole::Guard, 0.4f}, + {npcId++, "Elder Thorvald", NPCRole::Elder, 0.7f}, + }; + m_settlements.push_back(s); + } + + // Dusthaven — desert oasis settlement + { + Settlement s{}; + s.settlementId = 4; + s.name = "Dusthaven"; + s.description = "A shaded oasis settlement where desert traders converge"; + s.type = SettlementType::Town; + s.regionId = 4; + s.centerX = 6000.0f; + s.centerY = 10.0f; + s.centerZ = -1000.0f; + s.radius = 100.0f; + s.hasMerchant = true; + s.hasBlacksmith = true; + s.hasInn = true; + s.npcs = { + {npcId++, "Zara the Jeweler", NPCRole::Merchant, 0.6f}, + {npcId++, "Tamir the Bladesmith", NPCRole::Blacksmith, 0.5f}, + {npcId++, "Keeper Amara", NPCRole::Innkeeper, 0.8f}, + {npcId++, "Desert Ranger Khalid", NPCRole::Guard, 0.5f}, + }; + m_settlements.push_back(s); + } + + // Frosthome — tundra survival camp + { + Settlement s{}; + s.settlementId = 5; + s.name = "Frosthome"; + s.description = "A hardy encampment of fur-clad hunters braving the frozen wastes"; + s.type = SettlementType::Outpost; + s.regionId = 5; + s.centerX = -3000.0f; + s.centerY = 15.0f; + s.centerZ = 4000.0f; + s.radius = 60.0f; + s.hasMerchant = true; + s.hasBlacksmith = false; + s.hasInn = false; + s.npcs = { + {npcId++, "Ymir the Trapper", NPCRole::Hunter, 0.5f}, + {npcId++, "Helga the Fur Trader", NPCRole::Merchant, 0.6f}, + }; + m_settlements.push_back(s); + } + + // Bogwatch — swamp farmstead + { + Settlement s{}; + s.settlementId = 6; + s.name = "Bogwatch"; + s.description = "A cluster of stilted huts perched above the murky swamp waters"; + s.type = SettlementType::Farmstead; + s.regionId = 6; + s.centerX = -3200.0f; + s.centerY = 5.0f; + s.centerZ = -500.0f; + s.radius = 50.0f; + s.hasMerchant = false; + s.hasBlacksmith = false; + s.hasInn = false; + s.npcs = { + {npcId++, "Marsh Warden Fen", NPCRole::Guard, 0.4f}, + {npcId++, "Herbalist Moss", NPCRole::Farmer, 0.7f}, + }; + m_settlements.push_back(s); + } + + // Port Sunbreak — coastal trade hub + { + Settlement s{}; + s.settlementId = 7; + s.name = "Port Sunbreak"; + s.description = "A prosperous port city with a bustling marketplace and harbor"; + s.type = SettlementType::Town; + s.regionId = 7; + s.centerX = 1000.0f; + s.centerY = 5.0f; + s.centerZ = -5000.0f; + s.radius = 200.0f; + s.hasMerchant = true; + s.hasBlacksmith = true; + s.hasInn = true; + s.npcs = { + {npcId++, "Captain Reva", NPCRole::Merchant, 0.7f}, + {npcId++, "Dock Master Finn", NPCRole::Merchant, 0.6f}, + {npcId++, "Coral the Smith", NPCRole::Blacksmith, 0.6f}, + {npcId++, "Harbor Guard Orin", NPCRole::Guard, 0.5f}, + {npcId++, "Wanderer Sable", NPCRole::Wanderer, 0.8f}, + {npcId++, "Tavern Keep Rossi", NPCRole::Innkeeper, 0.7f}, + }; + m_settlements.push_back(s); + } + + // Bandit camps scattered in dangerous regions + { + Settlement s{}; + s.settlementId = 8; + s.name = "Blackthorn Camp"; + s.description = "A fortified bandit hideout in the forest depths"; + s.type = SettlementType::BanditCamp; + s.regionId = 2; + s.centerX = 4500.0f; + s.centerY = 10.0f; + s.centerZ = -2000.0f; + s.radius = 40.0f; + s.npcs = { + {npcId++, "Bandit Leader Grix", NPCRole::Guard, 0.0f}, + {npcId++, "Bandit Lookout", NPCRole::Guard, 0.0f}, + }; + m_settlements.push_back(s); + } + } + + void OWSettlementSystem::Update([[maybe_unused]] float deltaTime) + { + if (!m_initialized) + return; + + // NPC AI updates, disposition changes, shop restocking would happen here + } + + void OWSettlementSystem::Shutdown() + { + if (!m_initialized) + return; + + m_settlements.clear(); + m_camps.clear(); + m_initialized = false; + } + + size_t OWSettlementSystem::GetTotalNPCCount() const + { + size_t total = 0; + for (const auto& s : m_settlements) + total += s.npcs.size(); + return total; + } + + const Settlement* OWSettlementSystem::GetSettlement(uint32_t id) const + { + for (const auto& s : m_settlements) + { + if (s.settlementId == id) + return &s; + } + return nullptr; + } + + const Settlement* OWSettlementSystem::GetNearestSettlement(float x, float z) const + { + const Settlement* nearest = nullptr; + float bestDist = std::numeric_limits::max(); + + for (const auto& s : m_settlements) + { + float dx = x - s.centerX; + float dz = z - s.centerZ; + float dist = std::sqrt(dx * dx + dz * dz); + if (dist < bestDist) + { + bestDist = dist; + nearest = &s; + } + } + return nearest; + } + + uint32_t OWSettlementSystem::PlaceCamp(const std::string& name, float x, float y, float z, uint32_t regionId) + { + PlayerCamp camp{}; + camp.campId = m_nextCampId++; + camp.name = name; + camp.tier = CampTier::Campfire; + camp.posX = x; + camp.posY = y; + camp.posZ = z; + camp.regionId = regionId; + camp.storageCapacity = 10; + + m_camps[camp.campId] = camp; + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Camp '" + name + + "' placed (id=" + std::to_string(camp.campId) + ")"); + return camp.campId; + } + + bool OWSettlementSystem::UpgradeCamp(uint32_t campId) + { + auto it = m_camps.find(campId); + if (it == m_camps.end()) + return false; + + auto& camp = it->second; + if (camp.tier >= CampTier::Homestead) + return false; + + camp.tier = static_cast(static_cast(camp.tier) + 1); + camp.storageCapacity += 10; + + // Unlock features per tier + if (camp.tier >= CampTier::Lean_To) + camp.hasCookingFire = true; + if (camp.tier >= CampTier::Tent) + camp.hasStorageChest = true; + if (camp.tier >= CampTier::Cabin) + camp.hasCraftingStation = true; + + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Camp '" + camp.name + "' upgraded to tier " + + std::to_string(static_cast(camp.tier))); + return true; + } + + std::string OWSettlementSystem::GetSettlementListString() const + { + std::ostringstream ss; + ss << "=== Settlements ===\n"; + for (const auto& s : m_settlements) + { + ss << "[" << s.settlementId << "] " << s.name << " (Region " << s.regionId << ")\n"; + ss << " " << s.description << "\n"; + ss << " NPCs: " << s.npcs.size(); + if (s.hasMerchant) + ss << " | Merchant"; + if (s.hasBlacksmith) + ss << " | Blacksmith"; + if (s.hasInn) + ss << " | Inn"; + ss << "\n"; + } + return ss.str(); + } + + std::string OWSettlementSystem::GetCampListString() const + { + std::ostringstream ss; + ss << "=== Player Camps ===\n"; + if (m_camps.empty()) + { + ss << "No camps placed\n"; + return ss.str(); + } + for (const auto& [id, camp] : m_camps) + { + ss << "[" << id << "] " << camp.name << " (Tier " << static_cast(camp.tier) << ")\n"; + ss << " Position: (" << camp.posX << ", " << camp.posY << ", " << camp.posZ << ")\n"; + ss << " Storage: " << camp.storageCapacity << " slots"; + if (camp.hasCookingFire) + ss << " | Cooking"; + if (camp.hasCraftingStation) + ss << " | Crafting"; + if (camp.hasStorageChest) + ss << " | Chest"; + ss << "\n"; + } + return ss.str(); + } + + void OWSettlementSystem::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Settlements")) + return; + + ImGui::Text("Settlements: %zu | NPCs: %zu | Player Camps: %zu", m_settlements.size(), GetTotalNPCCount(), + m_camps.size()); + ImGui::Separator(); + + for (const auto& s : m_settlements) + { + ImGui::PushID(static_cast(s.settlementId)); + if (ImGui::TreeNode(s.name.c_str())) + { + ImGui::Text("Region: %u | Type: %d | NPCs: %zu", s.regionId, static_cast(s.type), s.npcs.size()); + ImGui::TextWrapped("%s", s.description.c_str()); + for (const auto& npc : s.npcs) + { + ImGui::Text(" %s (Disposition: %.1f)", npc.name.c_str(), npc.disposition); + } + ImGui::TreePop(); + } + ImGui::PopID(); + } + + if (!m_camps.empty() && ImGui::TreeNode("Player Camps")) + { + for (const auto& [id, camp] : m_camps) + { + ImGui::Text("[%u] %s (Tier %d, Storage %u)", id, camp.name.c_str(), static_cast(camp.tier), + camp.storageCapacity); + } + ImGui::TreePop(); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Settlement/OWSettlementSystem.h b/GameModules/SparkGameOpenWorld/Source/Settlement/OWSettlementSystem.h new file mode 100644 index 00000000..34d776e1 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Settlement/OWSettlementSystem.h @@ -0,0 +1,108 @@ +/** + * @file OWSettlementSystem.h + * @brief NPC settlements, merchants, and player camp building + * @author Spark Engine Team + * @date 2026 + * + * Manages populated areas in the open world: NPC villages and outposts with + * merchants and services, and a player camp system with progressive upgrades + * from a basic campfire to a full homestead. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief An NPC inhabitant of a settlement + struct SettlementNPC + { + uint32_t npcId = 0; + std::string name; + NPCRole role = NPCRole::Wanderer; + float disposition = 0.5f; ///< 0 = hostile, 1 = friendly + }; + + /// @brief A populated settlement in the world + struct Settlement + { + uint32_t settlementId = 0; + std::string name; + std::string description; + SettlementType type = SettlementType::Village; + uint32_t regionId = 0; + float centerX = 0.0f; + float centerY = 0.0f; + float centerZ = 0.0f; + float radius = 100.0f; + bool hasMerchant = false; + bool hasBlacksmith = false; + bool hasInn = false; + std::vector npcs; + }; + + /// @brief Player-built camp with progressive upgrades + struct PlayerCamp + { + uint32_t campId = 0; + std::string name; + CampTier tier = CampTier::Campfire; + float posX = 0.0f; + float posY = 0.0f; + float posZ = 0.0f; + uint32_t regionId = 0; + bool hasCraftingStation = false; + bool hasStorageChest = false; + bool hasCookingFire = false; + uint32_t storageCapacity = 10; + }; + + /** + * @brief Settlement and player camp management system + */ + class OWSettlementSystem + { + public: + OWSettlementSystem() = default; + ~OWSettlementSystem() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime); + void Shutdown(); + void RenderDebugUI(); + + size_t GetSettlementCount() const { return m_settlements.size(); } + size_t GetCampCount() const { return m_camps.size(); } + size_t GetTotalNPCCount() const; + + const Settlement* GetSettlement(uint32_t id) const; + const Settlement* GetNearestSettlement(float x, float z) const; + + /// @brief Place a new player camp at the given position + uint32_t PlaceCamp(const std::string& name, float x, float y, float z, uint32_t regionId); + + /// @brief Upgrade an existing camp to the next tier + bool UpgradeCamp(uint32_t campId); + + std::string GetSettlementListString() const; + std::string GetCampListString() const; + + private: + void DefineSettlements(); + + Spark::IEngineContext* m_context{nullptr}; + std::vector m_settlements; + std::unordered_map m_camps; + uint32_t m_nextCampId{1}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Wildlife/OWWildlifeSystem.cpp b/GameModules/SparkGameOpenWorld/Source/Wildlife/OWWildlifeSystem.cpp new file mode 100644 index 00000000..84168746 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Wildlife/OWWildlifeSystem.cpp @@ -0,0 +1,521 @@ +/** + * @file OWWildlifeSystem.cpp + * @brief Wildlife ecology, herd behavior, predator-prey, taming + */ + +#include "OWWildlifeSystem.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include +#include +#include + +namespace OpenWorld +{ + + bool OWWildlifeSystem::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + DefineSpecies(); + + // Spawn initial wildlife for starting region + SpawnRegionWildlife(1); // Emerald Meadows + SpawnRegionWildlife(2); // Ironwood Forest + + m_initialized = true; + SPARK_LOG_INFO(Spark::LogCategory::Game, "Wildlife system initialized: %zu species, %zu active", + m_species.size(), m_animals.size()); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Wildlife: " + std::to_string(m_species.size()) + + " species, " + std::to_string(m_animals.size()) + " spawned"); + return true; + } + + void OWWildlifeSystem::DefineSpecies() + { + m_species.clear(); + + m_species.push_back({AnimalType::Deer, + "Deer", + DietType::Herbivore, + 40.0f, + 6.0f, + 25.0f, + 0.0f, + 10.0f, + false, + true, + true, + 3, + 8, + {Biome::Grasslands, Biome::Forest}, + {ResourceType::Meat, ResourceType::Hide}}); + + m_species.push_back({AnimalType::Wolf, + "Wolf", + DietType::Carnivore, + 60.0f, + 7.0f, + 35.0f, + 15.0f, + 9.0f, + false, + false, + true, + 3, + 6, + {Biome::Forest, Biome::Tundra, Biome::Mountains}, + {ResourceType::Hide}}); + + m_species.push_back({AnimalType::Bear, + "Bear", + DietType::Omnivore, + 150.0f, + 4.0f, + 20.0f, + 30.0f, + 6.0f, + false, + false, + false, + 1, + 1, + {Biome::Forest, Biome::Mountains, Biome::Tundra}, + {ResourceType::Meat, ResourceType::Hide}}); + + m_species.push_back({AnimalType::Boar, + "Boar", + DietType::Omnivore, + 50.0f, + 5.0f, + 15.0f, + 10.0f, + 7.0f, + false, + false, + true, + 2, + 5, + {Biome::Forest, Biome::Swamp}, + {ResourceType::Meat, ResourceType::Hide}}); + + m_species.push_back({AnimalType::Eagle, + "Eagle", + DietType::Carnivore, + 20.0f, + 12.0f, + 50.0f, + 5.0f, + 15.0f, + false, + false, + false, + 1, + 2, + {Biome::Mountains, Biome::Coast, Biome::Desert}, + {}}); + + m_species.push_back({AnimalType::Horse, + "Horse", + DietType::Herbivore, + 80.0f, + 10.0f, + 30.0f, + 0.0f, + 14.0f, + false, + true, + true, + 3, + 7, + {Biome::Grasslands}, + {}}); + + m_species.push_back({AnimalType::Rabbit, + "Rabbit", + DietType::Herbivore, + 10.0f, + 8.0f, + 15.0f, + 0.0f, + 12.0f, + false, + false, + false, + 1, + 3, + {Biome::Grasslands, Biome::Forest}, + {ResourceType::Meat}}); + + m_species.push_back({AnimalType::Elk, + "Elk", + DietType::Herbivore, + 100.0f, + 7.0f, + 30.0f, + 8.0f, + 10.0f, + false, + false, + true, + 4, + 10, + {Biome::Mountains, Biome::Tundra, Biome::Forest}, + {ResourceType::Meat, ResourceType::Hide}}); + + m_species.push_back({AnimalType::Fox, + "Fox", + DietType::Omnivore, + 25.0f, + 8.0f, + 20.0f, + 3.0f, + 11.0f, + true, + false, + false, + 1, + 2, + {Biome::Grasslands, Biome::Forest, Biome::Swamp}, + {ResourceType::Hide}}); + + m_species.push_back({AnimalType::Bison, + "Bison", + DietType::Herbivore, + 200.0f, + 5.0f, + 20.0f, + 12.0f, + 7.0f, + false, + false, + true, + 5, + 15, + {Biome::Tundra, Biome::Grasslands}, + {ResourceType::Meat, ResourceType::Hide}}); + + m_species.push_back({AnimalType::MountainLion, + "Mountain Lion", + DietType::Carnivore, + 80.0f, + 9.0f, + 40.0f, + 25.0f, + 12.0f, + true, + false, + false, + 1, + 1, + {Biome::Mountains}, + {ResourceType::Hide}}); + + m_species.push_back({AnimalType::Snake, + "Snake", + DietType::Carnivore, + 15.0f, + 3.0f, + 10.0f, + 8.0f, + 5.0f, + false, + false, + false, + 1, + 1, + {Biome::Desert, Biome::Swamp}, + {}}); + } + + void OWWildlifeSystem::SpawnRegionWildlife(uint32_t regionId) + { + // Spawn a representative population for the region + for (const auto& species : m_species) + { + bool native = false; + for (auto b : species.nativeBiomes) + { + // Simple check: biome index matches region convention + // Real implementation would query OWWorldSetup for region biome + if (static_cast(b) + 1 == regionId || regionId <= 2) + { + native = true; + break; + } + } + if (!native) + continue; + + uint32_t count = species.formsHerds ? species.herdSizeMin + 2 : 1; + + // Create a herd if applicable + uint32_t herdId = 0; + if (species.formsHerds && count > 1) + { + herdId = m_nextHerdId++; + Herd herd{}; + herd.herdId = herdId; + herd.type = species.type; + herd.regionId = regionId; + herd.centerX = static_cast(regionId * 1000); + herd.centerZ = static_cast(regionId * 500); + m_herds[herdId] = herd; + } + + for (uint32_t i = 0; i < count; ++i) + { + AnimalInstance animal{}; + animal.instanceId = m_nextInstanceId++; + animal.type = species.type; + animal.health = species.health; + animal.behavior = + (species.diet == DietType::Carnivore) ? AnimalBehavior::Roaming : AnimalBehavior::Grazing; + animal.herdId = herdId; + animal.regionId = regionId; + // Spread positions around region center + animal.posX = static_cast(regionId * 1000) + static_cast(i * 50); + animal.posZ = static_cast(regionId * 500) + static_cast(i * 30); + + m_animals[animal.instanceId] = animal; + + if (herdId > 0) + m_herds[herdId].memberIds.push_back(animal.instanceId); + } + } + } + + void OWWildlifeSystem::Update(float deltaTime, float playerX, float playerZ, + [[maybe_unused]] uint32_t currentRegionId) + { + if (!m_initialized) + return; + + UpdateBehaviors(deltaTime, playerX, playerZ); + UpdatePredatorPrey(deltaTime); + RespawnCheck(deltaTime); + } + + void OWWildlifeSystem::UpdateBehaviors(float deltaTime, float playerX, float playerZ) + { + for (auto& [id, animal] : m_animals) + { + if (!animal.isAlive) + continue; + + // Find species info + const AnimalSpecies* species = nullptr; + for (const auto& s : m_species) + { + if (s.type == animal.type) + { + species = &s; + break; + } + } + if (!species) + continue; + + if (animal.isTamed) + { + // Follow player + float dx = playerX - animal.posX; + float dz = playerZ - animal.posZ; + float dist = std::sqrt(dx * dx + dz * dz); + if (dist > 5.0f) + { + float speed = species->speed * deltaTime; + animal.posX += (dx / dist) * speed; + animal.posZ += (dz / dist) * speed; + } + animal.behavior = AnimalBehavior::Following; + continue; + } + + // Distance to player + float dx = playerX - animal.posX; + float dz = playerZ - animal.posZ; + float distToPlayer = std::sqrt(dx * dx + dz * dz); + + // Herbivores flee from player if too close + if (species->diet == DietType::Herbivore && distToPlayer < species->detectionRange) + { + animal.behavior = AnimalBehavior::Fleeing; + float speed = species->fleeSpeed * deltaTime; + animal.posX -= (dx / distToPlayer) * speed; + animal.posZ -= (dz / distToPlayer) * speed; + } + // Carnivores stalk player if in range + else if (species->diet == DietType::Carnivore && species->attackDamage > 0.0f && + distToPlayer < species->detectionRange) + { + animal.behavior = AnimalBehavior::Stalking; + if (distToPlayer > 5.0f) + { + float speed = species->speed * deltaTime; + animal.posX += (dx / distToPlayer) * speed; + animal.posZ += (dz / distToPlayer) * speed; + } + } + else + { + // Idle behavior: slow wander + animal.behavior = + (species->diet == DietType::Herbivore) ? AnimalBehavior::Grazing : AnimalBehavior::Roaming; + animal.posX += (static_cast((id * 7) % 11) - 5.0f) * 0.1f * deltaTime; + animal.posZ += (static_cast((id * 13) % 11) - 5.0f) * 0.1f * deltaTime; + } + } + } + + void OWWildlifeSystem::UpdatePredatorPrey([[maybe_unused]] float deltaTime) + { + // Simplified: carnivores near herbivores trigger flee response + // Real implementation would use SpatialGrid for efficient queries + } + + void OWWildlifeSystem::RespawnCheck(float deltaTime) + { + m_respawnTimer += deltaTime; + if (m_respawnTimer < 60.0f) + return; + m_respawnTimer = 0.0f; + + // Remove dead animals and count per region + std::vector toRemove; + for (const auto& [id, animal] : m_animals) + { + if (!animal.isAlive) + toRemove.push_back(id); + } + for (uint32_t id : toRemove) + m_animals.erase(id); + } + + bool OWWildlifeSystem::TameAnimal(uint32_t instanceId) + { + auto it = m_animals.find(instanceId); + if (it == m_animals.end() || !it->second.isAlive || it->second.isTamed) + return false; + + // Check if species is tameable + for (const auto& species : m_species) + { + if (species.type == it->second.type && species.isTameable) + { + it->second.isTamed = true; + it->second.behavior = AnimalBehavior::Following; + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Tamed " + species.name + + " (id=" + std::to_string(instanceId) + ")"); + return true; + } + } + return false; + } + + std::vector> OWWildlifeSystem::HuntAnimal(uint32_t instanceId) + { + std::vector> drops; + + auto it = m_animals.find(instanceId); + if (it == m_animals.end() || !it->second.isAlive) + return drops; + + it->second.isAlive = false; + it->second.health = 0.0f; + + for (const auto& species : m_species) + { + if (species.type == it->second.type) + { + for (auto res : species.drops) + drops.emplace_back(res, 1); + break; + } + } + + return drops; + } + + size_t OWWildlifeSystem::GetTamedCount() const + { + size_t count = 0; + for (const auto& [id, animal] : m_animals) + { + if (animal.isTamed) + ++count; + } + return count; + } + + std::string OWWildlifeSystem::GetWildlifeString() const + { + std::ostringstream ss; + ss << "=== Wildlife ===\n"; + ss << "Species: " << m_species.size() << " | Active: " << m_animals.size(); + ss << " | Herds: " << m_herds.size() << " | Tamed: " << GetTamedCount() << "\n"; + return ss.str(); + } + + std::string OWWildlifeSystem::GetSpeciesListString() const + { + std::ostringstream ss; + ss << "=== Animal Species ===\n"; + for (const auto& s : m_species) + { + ss << s.name << " (" + << (s.diet == DietType::Herbivore ? "Herb" + : s.diet == DietType::Carnivore ? "Carn" + : "Omni") + << ") HP:" << s.health << " Spd:" << s.speed; + if (s.isTameable) + ss << " [Tameable]"; + if (s.formsHerds) + ss << " [Herd:" << s.herdSizeMin << "-" << s.herdSizeMax << "]"; + ss << "\n"; + } + return ss.str(); + } + + void OWWildlifeSystem::Shutdown() + { + if (!m_initialized) + return; + + m_species.clear(); + m_animals.clear(); + m_herds.clear(); + m_initialized = false; + } + + void OWWildlifeSystem::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Wildlife")) + return; + + ImGui::Text("Species: %zu | Active: %zu | Herds: %zu | Tamed: %zu", m_species.size(), m_animals.size(), + m_herds.size(), GetTamedCount()); + ImGui::Separator(); + + if (ImGui::TreeNode("Species")) + { + for (const auto& s : m_species) + { + ImGui::Text("%s | HP:%.0f | Spd:%.0f | Dmg:%.0f%s", s.name.c_str(), s.health, s.speed, s.attackDamage, + s.isTameable ? " [Tameable]" : ""); + } + ImGui::TreePop(); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/Wildlife/OWWildlifeSystem.h b/GameModules/SparkGameOpenWorld/Source/Wildlife/OWWildlifeSystem.h new file mode 100644 index 00000000..2d64d22d --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/Wildlife/OWWildlifeSystem.h @@ -0,0 +1,117 @@ +/** + * @file OWWildlifeSystem.h + * @brief Wildlife ecology: animal spawning, herds, predator-prey, taming + * @author Spark Engine Team + * @date 2026 + * + * Manages the open world's animal population: species templates with biome + * affinity, herd formation, predator-prey AI interactions, day/night + * activity cycles, and a taming system for mounts and companions. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief Template defining an animal species + struct AnimalSpecies + { + AnimalType type = AnimalType::Deer; + std::string name; + DietType diet = DietType::Herbivore; + float health = 50.0f; + float speed = 5.0f; + float detectionRange = 30.0f; + float attackDamage = 0.0f; ///< 0 for passive animals + float fleeSpeed = 8.0f; + bool isNocturnal = false; + bool isTameable = false; + bool formsHerds = false; + uint32_t herdSizeMin = 1; + uint32_t herdSizeMax = 1; + std::vector nativeBiomes; + std::vector drops; ///< Resources dropped when hunted + }; + + /// @brief Runtime state for a single spawned animal + struct AnimalInstance + { + uint32_t instanceId = 0; + AnimalType type = AnimalType::Deer; + float posX = 0.0f; + float posY = 0.0f; + float posZ = 0.0f; + float health = 50.0f; + AnimalBehavior behavior = AnimalBehavior::Grazing; + uint32_t herdId = 0; ///< 0 = solitary + uint32_t regionId = 0; + bool isTamed = false; + bool isAlive = true; + }; + + /// @brief A group of animals moving together + struct Herd + { + uint32_t herdId = 0; + AnimalType type = AnimalType::Deer; + std::vector memberIds; + float centerX = 0.0f; + float centerZ = 0.0f; + uint32_t regionId = 0; + }; + + /** + * @brief Wildlife population, ecology, and taming system + */ + class OWWildlifeSystem + { + public: + OWWildlifeSystem() = default; + ~OWWildlifeSystem() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime, float playerX, float playerZ, uint32_t currentRegionId); + void Shutdown(); + void RenderDebugUI(); + + size_t GetSpeciesCount() const { return m_species.size(); } + size_t GetActiveAnimalCount() const { return m_animals.size(); } + size_t GetHerdCount() const { return m_herds.size(); } + size_t GetTamedCount() const; + + /// @brief Attempt to tame an animal (returns true if successful) + bool TameAnimal(uint32_t instanceId); + + /// @brief Hunt an animal, returning its resource drops + std::vector> HuntAnimal(uint32_t instanceId); + + std::string GetWildlifeString() const; + std::string GetSpeciesListString() const; + + private: + void DefineSpecies(); + void SpawnRegionWildlife(uint32_t regionId); + void UpdateBehaviors(float deltaTime, float playerX, float playerZ); + void UpdatePredatorPrey(float deltaTime); + void RespawnCheck(float deltaTime); + + Spark::IEngineContext* m_context{nullptr}; + std::vector m_species; + std::unordered_map m_animals; + std::unordered_map m_herds; + uint32_t m_nextInstanceId{1}; + uint32_t m_nextHerdId{1}; + float m_respawnTimer{0.0f}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/World/OWWorldSetup.cpp b/GameModules/SparkGameOpenWorld/Source/World/OWWorldSetup.cpp new file mode 100644 index 00000000..4dc9a5ee --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/World/OWWorldSetup.cpp @@ -0,0 +1,386 @@ +/** + * @file OWWorldSetup.cpp + * @brief Open world biome regions, road network, and streaming registration + */ + +#include "OWWorldSetup.h" +#include "Utils/SparkConsole.h" +#include "Utils/LogMacros.h" +#include "Engine/Streaming/SeamlessAreaManager.h" + +#ifdef ENABLE_EDITOR +#include +#endif + +#include +#include + +namespace OpenWorld +{ + + bool OWWorldSetup::Initialize(Spark::IEngineContext* context) + { + if (!context) + return false; + + m_context = context; + + DefineBiomeRegions(); + DefineRoadNetwork(); + RegisterAreasWithStreaming(); + ConfigureOriginRebasing(); + + m_initialized = true; + return true; + } + + void OWWorldSetup::DefineBiomeRegions() + { + m_regions.clear(); + + // Region 1: Emerald Meadows — central grasslands, starting area + { + BiomeRegion r{}; + r.regionId = 1; + r.name = "Emerald Meadows"; + r.description = "Rolling grasslands dotted with wildflowers and gentle streams"; + r.biome = Biome::Grasslands; + r.boundsMinX = -2000.0f; + r.boundsMinY = -10.0f; + r.boundsMinZ = -2000.0f; + r.boundsMaxX = 2000.0f; + r.boundsMaxY = 150.0f; + r.boundsMaxZ = 2000.0f; + r.baseTemperature = 20.0f; + r.elevationMin = 0.0f; + r.elevationMax = 80.0f; + r.dangerLevel = 1; + r.abundantResources = {ResourceType::Herbs, ResourceType::Fiber, ResourceType::Water}; + r.nativeWildlife = {AnimalType::Deer, AnimalType::Rabbit, AnimalType::Horse, AnimalType::Fox}; + r.connectedRegions = {2, 3, 5, 6}; + m_regions.push_back(r); + } + + // Region 2: Ironwood Forest — dense woodland, moderate danger + { + BiomeRegion r{}; + r.regionId = 2; + r.name = "Ironwood Forest"; + r.description = "An ancient forest with towering ironwood trees and dappled sunlight"; + r.biome = Biome::Forest; + r.boundsMinX = 2000.0f; + r.boundsMinY = -20.0f; + r.boundsMinZ = -3000.0f; + r.boundsMaxX = 6000.0f; + r.boundsMaxY = 300.0f; + r.boundsMaxZ = 3000.0f; + r.baseTemperature = 15.0f; + r.elevationMin = 10.0f; + r.elevationMax = 200.0f; + r.dangerLevel = 3; + r.abundantResources = {ResourceType::Wood, ResourceType::Herbs, ResourceType::Fiber}; + r.nativeWildlife = {AnimalType::Wolf, AnimalType::Bear, AnimalType::Deer, AnimalType::Boar, + AnimalType::Fox}; + r.connectedRegions = {1, 3, 4}; + m_regions.push_back(r); + } + + // Region 3: Stormcrest Mountains — high altitude, harsh + { + BiomeRegion r{}; + r.regionId = 3; + r.name = "Stormcrest Mountains"; + r.description = "Jagged peaks shrouded in mist, where only the hardiest survive"; + r.biome = Biome::Mountains; + r.boundsMinX = -1000.0f; + r.boundsMinY = 100.0f; + r.boundsMinZ = 2000.0f; + r.boundsMaxX = 4000.0f; + r.boundsMaxY = 800.0f; + r.boundsMaxZ = 7000.0f; + r.baseTemperature = 2.0f; + r.elevationMin = 200.0f; + r.elevationMax = 800.0f; + r.dangerLevel = 6; + r.abundantResources = {ResourceType::Stone, ResourceType::Iron, ResourceType::Crystal}; + r.nativeWildlife = {AnimalType::Eagle, AnimalType::MountainLion, AnimalType::Elk}; + r.connectedRegions = {1, 2, 8}; + m_regions.push_back(r); + } + + // Region 4: Ashwind Desert — arid expanse, extreme heat + { + BiomeRegion r{}; + r.regionId = 4; + r.name = "Ashwind Desert"; + r.description = "A scorching desert of red sand dunes and ancient ruins half-buried in ash"; + r.biome = Biome::Desert; + r.boundsMinX = 4000.0f; + r.boundsMinY = -50.0f; + r.boundsMinZ = -5000.0f; + r.boundsMaxX = 10000.0f; + r.boundsMaxY = 200.0f; + r.boundsMaxZ = 1000.0f; + r.baseTemperature = 42.0f; + r.elevationMin = -20.0f; + r.elevationMax = 150.0f; + r.dangerLevel = 5; + r.abundantResources = {ResourceType::Stone, ResourceType::Gold, ResourceType::Clay}; + r.nativeWildlife = {AnimalType::Snake, AnimalType::Eagle}; + r.connectedRegions = {2, 7}; + m_regions.push_back(r); + } + + // Region 5: Frosthollow Tundra — frozen north, extreme cold + { + BiomeRegion r{}; + r.regionId = 5; + r.name = "Frosthollow Tundra"; + r.description = "An endless frozen plain where blizzards rage and mammoth bones lie exposed"; + r.biome = Biome::Tundra; + r.boundsMinX = -6000.0f; + r.boundsMinY = -10.0f; + r.boundsMinZ = 2000.0f; + r.boundsMaxX = -1000.0f; + r.boundsMaxY = 200.0f; + r.boundsMaxZ = 8000.0f; + r.baseTemperature = -15.0f; + r.elevationMin = 0.0f; + r.elevationMax = 120.0f; + r.dangerLevel = 7; + r.abundantResources = {ResourceType::Hide, ResourceType::Stone, ResourceType::Water}; + r.nativeWildlife = {AnimalType::Bison, AnimalType::Wolf, AnimalType::Elk, AnimalType::Bear}; + r.connectedRegions = {1, 8}; + m_regions.push_back(r); + } + + // Region 6: Mistveil Swamp — wetlands, poison, disease + { + BiomeRegion r{}; + r.regionId = 6; + r.name = "Mistveil Swamp"; + r.description = "Fetid marshlands where visibility is poor and the ground cannot be trusted"; + r.biome = Biome::Swamp; + r.boundsMinX = -5000.0f; + r.boundsMinY = -30.0f; + r.boundsMinZ = -4000.0f; + r.boundsMaxX = -2000.0f; + r.boundsMaxY = 50.0f; + r.boundsMaxZ = 2000.0f; + r.baseTemperature = 25.0f; + r.elevationMin = -20.0f; + r.elevationMax = 30.0f; + r.dangerLevel = 4; + r.abundantResources = {ResourceType::Herbs, ResourceType::Clay, ResourceType::Fiber}; + r.nativeWildlife = {AnimalType::Snake, AnimalType::Boar, AnimalType::Fox}; + r.connectedRegions = {1, 7}; + m_regions.push_back(r); + } + + // Region 7: Sunbreak Coast — temperate coastline, trade hub + { + BiomeRegion r{}; + r.regionId = 7; + r.name = "Sunbreak Coast"; + r.description = "A warm coastline with white cliffs, sandy beaches, and a bustling port"; + r.biome = Biome::Coast; + r.boundsMinX = -4000.0f; + r.boundsMinY = -5.0f; + r.boundsMinZ = -8000.0f; + r.boundsMaxX = 4000.0f; + r.boundsMaxY = 80.0f; + r.boundsMaxZ = -4000.0f; + r.baseTemperature = 24.0f; + r.elevationMin = 0.0f; + r.elevationMax = 60.0f; + r.dangerLevel = 2; + r.abundantResources = {ResourceType::Water, ResourceType::Fiber, ResourceType::Gold}; + r.nativeWildlife = {AnimalType::Eagle, AnimalType::Deer, AnimalType::Rabbit}; + r.connectedRegions = {4, 6}; + m_regions.push_back(r); + } + + // Region 8: Cinderforge Caldera — volcanic, endgame + { + BiomeRegion r{}; + r.regionId = 8; + r.name = "Cinderforge Caldera"; + r.description = "A volcanic hellscape of lava flows, obsidian spires, and toxic vents"; + r.biome = Biome::Volcanic; + r.boundsMinX = -2000.0f; + r.boundsMinY = 0.0f; + r.boundsMinZ = 7000.0f; + r.boundsMaxX = 3000.0f; + r.boundsMaxY = 600.0f; + r.boundsMaxZ = 12000.0f; + r.baseTemperature = 55.0f; + r.elevationMin = 50.0f; + r.elevationMax = 600.0f; + r.dangerLevel = 10; + r.abundantResources = {ResourceType::Iron, ResourceType::Crystal, ResourceType::Stone}; + r.nativeWildlife = {}; // Too hostile for wildlife + r.connectedRegions = {3, 5}; + m_regions.push_back(r); + } + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world defined %zu biome regions", m_regions.size()); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Defined " + std::to_string(m_regions.size()) + + " biome regions"); + } + + void OWWorldSetup::DefineRoadNetwork() + { + m_roads.clear(); + + auto addRoad = [&](uint32_t id, const std::string& name, uint32_t from, uint32_t to, float safety, bool main) + { m_roads.push_back({id, name, from, to, safety, main}); }; + + addRoad(1, "Meadow Trail", 1, 2, 0.8f, true); + addRoad(2, "Mountain Pass", 1, 3, 0.5f, true); + addRoad(3, "Frostbound Road", 1, 5, 0.4f, false); + addRoad(4, "Marshway", 1, 6, 0.3f, false); + addRoad(5, "Timber Road", 2, 3, 0.6f, true); + addRoad(6, "Scorched Path", 2, 4, 0.3f, false); + addRoad(7, "Coastal Highway", 4, 7, 0.7f, true); + addRoad(8, "Bogwalk", 6, 7, 0.4f, false); + addRoad(9, "Summit Trail", 3, 8, 0.2f, false); + addRoad(10, "Frozen Ridge", 5, 8, 0.1f, false); + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world defined %zu roads", m_roads.size()); + } + + void OWWorldSetup::RegisterAreasWithStreaming() + { + auto& streamingMgr = Spark::Streaming::SeamlessAreaManager::GetInstance(); + + for (const auto& region : m_regions) + { + Spark::Streaming::AreaDefinition def{}; + def.areaId = region.regionId; + def.name = region.name; + def.boundsMin = {region.boundsMinX, region.boundsMinY, region.boundsMinZ}; + def.boundsMax = {region.boundsMaxX, region.boundsMaxY, region.boundsMaxZ}; + def.scenePath = "Assets/Scenes/OpenWorld/" + region.name + ".scene"; + def.priority = (region.dangerLevel <= 2) ? 2 : 1; + + streamingMgr.RegisterArea(def); + } + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world areas registered with SeamlessAreaManager"); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Registered areas with SeamlessAreaManager"); + } + + void OWWorldSetup::ConfigureOriginRebasing() + { + // Open world spans ~20km — rebasing is critical for precision + m_originSystem.SetRebasingThreshold(4000.0f); + m_originSystem.SetEnabled(true); + + SPARK_LOG_INFO(Spark::LogCategory::Game, "Open world origin rebasing enabled (threshold: 4000m)"); + Spark::SimpleConsole::GetInstance().LogInfo("[OpenWorld] Origin rebasing enabled (threshold: 4000m)"); + } + + void OWWorldSetup::Update(float deltaTime) + { + if (!m_initialized) + return; + + m_worldTime += deltaTime; + + auto& streamingMgr = Spark::Streaming::SeamlessAreaManager::GetInstance(); + streamingMgr.Update(deltaTime); + } + + void OWWorldSetup::Shutdown() + { + if (!m_initialized) + return; + + m_regions.clear(); + m_roads.clear(); + m_initialized = false; + } + + const BiomeRegion* OWWorldSetup::GetRegion(uint32_t regionId) const + { + for (const auto& r : m_regions) + { + if (r.regionId == regionId) + return &r; + } + return nullptr; + } + + const BiomeRegion* OWWorldSetup::GetRegionAtPosition(float x, float z) const + { + for (const auto& r : m_regions) + { + if (x >= r.boundsMinX && x <= r.boundsMaxX && z >= r.boundsMinZ && z <= r.boundsMaxZ) + return &r; + } + return nullptr; + } + + std::string OWWorldSetup::GetRegionListString() const + { + std::ostringstream ss; + ss << "=== Open World Regions ===\n"; + for (const auto& r : m_regions) + { + ss << "[" << r.regionId << "] " << r.name << " (" << static_cast(r.biome) << ")"; + ss << " Danger:" << r.dangerLevel << " Temp:" << r.baseTemperature << "C\n"; + ss << " " << r.description << "\n"; + ss << " Wildlife: " << r.nativeWildlife.size(); + ss << " | Resources: " << r.abundantResources.size(); + ss << " | Connections: "; + for (size_t i = 0; i < r.connectedRegions.size(); ++i) + { + if (i > 0) + ss << ", "; + const auto* connected = GetRegion(r.connectedRegions[i]); + ss << (connected ? connected->name : "?"); + } + ss << "\n"; + } + return ss.str(); + } + + std::string OWWorldSetup::GetWorldStatusString() const + { + std::ostringstream ss; + ss << "World Time: " << static_cast(m_worldTime) << "s"; + ss << " | Regions: " << m_regions.size(); + ss << " | Roads: " << m_roads.size(); + return ss.str(); + } + + void OWWorldSetup::RenderDebugUI() + { +#ifdef ENABLE_EDITOR + if (!ImGui::CollapsingHeader("Open World Map")) + return; + + ImGui::Text("World Time: %.1f s", m_worldTime); + ImGui::Text("Regions: %zu | Roads: %zu", m_regions.size(), m_roads.size()); + ImGui::Separator(); + + for (const auto& r : m_regions) + { + ImGui::PushID(static_cast(r.regionId)); + if (ImGui::TreeNode(r.name.c_str())) + { + ImGui::Text("Biome: %d | Danger: %d | Temp: %.0fC", static_cast(r.biome), r.dangerLevel, + r.baseTemperature); + ImGui::TextWrapped("%s", r.description.c_str()); + ImGui::Text("Bounds: (%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f)", r.boundsMinX, r.boundsMinY, r.boundsMinZ, + r.boundsMaxX, r.boundsMaxY, r.boundsMaxZ); + ImGui::Text("Wildlife: %zu species | Resources: %zu types", r.nativeWildlife.size(), + r.abundantResources.size()); + ImGui::TreePop(); + } + ImGui::PopID(); + } +#endif + } + +} // namespace OpenWorld diff --git a/GameModules/SparkGameOpenWorld/Source/World/OWWorldSetup.h b/GameModules/SparkGameOpenWorld/Source/World/OWWorldSetup.h new file mode 100644 index 00000000..f6fcba06 --- /dev/null +++ b/GameModules/SparkGameOpenWorld/Source/World/OWWorldSetup.h @@ -0,0 +1,100 @@ +/** + * @file OWWorldSetup.h + * @brief Open world map with 8 biome regions, seamless streaming, and origin rebasing + * @author Spark Engine Team + * @date 2026 + * + * Defines a large contiguous open world divided into 8 biome regions, each with + * distinct terrain, climate, wildlife, and resources. Integrates with + * SeamlessAreaManager for predictive streaming and WorldOriginSystem for + * floating-point precision at large distances. + */ + +#pragma once + +#include "Spark/IEngineContext.h" +#include "Engine/World/WorldOriginSystem.h" +#include "Enums/OpenWorldEnums.h" + +#include +#include +#include +#include + +namespace OpenWorld +{ + + /// @brief Defines a single biome region in the open world + struct BiomeRegion + { + uint32_t regionId = 0; + std::string name; + std::string description; + Biome biome = Biome::Grasslands; + float boundsMinX = 0.0f; + float boundsMinY = 0.0f; + float boundsMinZ = 0.0f; + float boundsMaxX = 0.0f; + float boundsMaxY = 0.0f; + float boundsMaxZ = 0.0f; + float baseTemperature = 20.0f; ///< Celsius, base temperature for this biome + float elevationMin = 0.0f; + float elevationMax = 100.0f; + int dangerLevel = 1; ///< 1-10 difficulty rating + std::vector abundantResources; + std::vector nativeWildlife; + std::vector connectedRegions; ///< Adjacent biome IDs + }; + + /// @brief Road or path connecting two regions + struct WorldRoad + { + uint32_t roadId = 0; + std::string name; + uint32_t fromRegion = 0; + uint32_t toRegion = 0; + float safetyRating = 0.5f; ///< 0 = dangerous, 1 = fully patrolled + bool isMainRoad = false; + }; + + /** + * @brief World region registry, terrain configuration, and streaming setup + * + * Manages the open world's 8 biome regions, road network between them, + * seamless area streaming via SeamlessAreaManager, and coordinate-space + * management via WorldOriginSystem. + */ + class OWWorldSetup + { + public: + OWWorldSetup() = default; + ~OWWorldSetup() = default; + + bool Initialize(Spark::IEngineContext* context); + void Update(float deltaTime); + void Shutdown(); + void RenderDebugUI(); + + size_t GetRegionCount() const { return m_regions.size(); } + size_t GetRoadCount() const { return m_roads.size(); } + const std::vector& GetRegions() const { return m_regions; } + const BiomeRegion* GetRegion(uint32_t regionId) const; + const BiomeRegion* GetRegionAtPosition(float x, float z) const; + std::string GetRegionListString() const; + std::string GetWorldStatusString() const; + + private: + void DefineBiomeRegions(); + void DefineRoadNetwork(); + void RegisterAreasWithStreaming(); + void ConfigureOriginRebasing(); + + Spark::IEngineContext* m_context{nullptr}; + std::vector m_regions; + std::vector m_roads; + Spark::World::WorldOriginSystem m_originSystem; + float m_worldTime{0.0f}; + bool m_initialized{false}; + }; + +} // namespace OpenWorld diff --git a/wiki/Home.md b/wiki/Home.md index a94b5ecf..f4d929a4 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -149,5 +149,5 @@ SparkEngine is licensed under the [MIT License](https://github.com/Krilliac/Spar | Test files | 243 | | Test cases | 3119+ | | Wiki pages | 88 | -| *Last synced* | *2026-04-03 04:02* | +| *Last synced* | *2026-04-03 05:32* |