From c8d9d6b792b046c8a9814de7cdea5b9ee4855a19 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 09:57:24 -0700 Subject: [PATCH] rendering/game: make player model semi-transparent in ghost form Add GhostStateCallback to GameHandler, fired when PLAYER_FLAGS_GHOST transitions on or off in UPDATE_OBJECT / login detection. Add setInstanceOpacity() to CharacterRenderer to directly set opacity without disturbing fade-in state. Application wires the callback to set opacity 0.5 on ghost entry and 1.0 on resurrect. --- include/game/game_handler.hpp | 5 +++++ include/rendering/character_renderer.hpp | 1 + src/core/application.cpp | 10 ++++++++++ src/game/game_handler.cpp | 3 +++ src/rendering/character_renderer.cpp | 9 +++++++++ 5 files changed, 28 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 3824dbf9..944c1e63 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -624,6 +624,10 @@ public: using StandStateCallback = std::function; void setStandStateCallback(StandStateCallback cb) { standStateCallback_ = std::move(cb); } + // Ghost state callback — fired when player enters or leaves ghost (spirit) form + using GhostStateCallback = std::function; + void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); } + // Melee swing callback (for driving animation/SFX) using MeleeSwingCallback = std::function; void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); } @@ -2256,6 +2260,7 @@ private: NpcAggroCallback npcAggroCallback_; NpcRespawnCallback npcRespawnCallback_; StandStateCallback standStateCallback_; + GhostStateCallback ghostStateCallback_; MeleeSwingCallback meleeSwingCallback_; SpellCastAnimCallback spellCastAnimCallback_; NpcSwingCallback npcSwingCallback_; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index f516b3a4..2b400998 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -78,6 +78,7 @@ public: void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation); void moveInstanceTo(uint32_t instanceId, const glm::vec3& destination, float durationSeconds); void startFadeIn(uint32_t instanceId, float durationSeconds); + void setInstanceOpacity(uint32_t instanceId, float opacity); const pipeline::M2Model* getModelData(uint32_t modelId) const; void setActiveGeosets(uint32_t instanceId, const std::unordered_set& geosets); void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, VkTexture* texture); diff --git a/src/core/application.cpp b/src/core/application.cpp index 0d7e1e8c..5065cc68 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2794,6 +2794,16 @@ void Application::setupUICallbacks() { } }); + // Ghost state callback — make player semi-transparent when in spirit form + gameHandler->setGhostStateCallback([this](bool isGhost) { + if (!renderer) return; + auto* cr = renderer->getCharacterRenderer(); + if (!cr) return; + uint32_t charInstId = renderer->getCharacterInstanceId(); + if (charInstId == 0) return; + cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f); + }); + // Stand state animation callback — map server stand state to M2 animation on player // and sync camera sit flag so movement is blocked while sitting gameHandler->setStandStateCallback([this](uint8_t standState) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index eb459ebd..13a2f708 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7749,6 +7749,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { releasedSpirit_ = true; playerDead_ = true; LOG_INFO("Player logged in as ghost (PLAYER_FLAGS)"); + if (ghostStateCallback_) ghostStateCallback_(true); } } // Determine hostility from faction template for online creatures @@ -8212,12 +8213,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (!wasGhost && nowGhost) { releasedSpirit_ = true; LOG_INFO("Player entered ghost form (PLAYER_FLAGS)"); + if (ghostStateCallback_) ghostStateCallback_(true); } else if (wasGhost && !nowGhost) { releasedSpirit_ = false; playerDead_ = false; repopPending_ = false; resurrectPending_ = false; LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)"); + if (ghostStateCallback_) ghostStateCallback_(false); } } } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index c16fc99c..5683af91 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -2876,6 +2876,15 @@ void CharacterRenderer::startFadeIn(uint32_t instanceId, float durationSeconds) it->second.fadeInDuration = durationSeconds; } +void CharacterRenderer::setInstanceOpacity(uint32_t instanceId, float opacity) { + auto it = instances.find(instanceId); + if (it != instances.end()) { + it->second.opacity = std::clamp(opacity, 0.0f, 1.0f); + // Cancel any fade-in in progress to avoid overwriting the new opacity + it->second.fadeInDuration = 0.0f; + } +} + void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unordered_set& geosets) { auto it = instances.find(instanceId); if (it != instances.end()) {