From 3e85941cbc510892261b02c8b4d49586be29e735 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 03:06:48 +0000 Subject: [PATCH] Add animation protocol: walk/idle selection, emote triggers, WASM exports Implement the animation system from the Phase 1 plan: Protocol: Add walkAnimId/idleAnimId fields to PlayerStateMsg (2 extra bytes per 15Hz tick), add MSG_EMOTE (type 9) with EmoteMsg struct, and define shared animation lookup tables (walk: 6 anims, idle: 3, emote: 2). NetworkManager: Store local walk/idle animation indices, include them in every state broadcast, handle incoming MSG_EMOTE by dispatching to the target remote player's TriggerEmote(). Add SetWalkAnimation(), SetIdleAnimation(), SendEmote(), GetPlayerCount() public API. RemotePlayer: Replace per-animation raw pointers with AnimCache struct and lazy m_animCacheMap (name -> ROI map, built on first use, cleared on Despawn). UpdateFromNetwork() detects walk/idle ID changes and swaps the active animation cache. UpdateAnimation() now has three states: moving (configurable walk anim), emote (one-shot with duration tracking, interrupted by movement), and idle (configurable idle anim after 2.5s timeout). Add TriggerEmote() for one-shot emote playback. WASM exports: mp_set_walk_animation(), mp_set_idle_animation(), mp_trigger_emote(), mp_get_player_count() with EMSCRIPTEN_KEEPALIVE. CMakeLists.txt adds EXPORTED_FUNCTIONS and EXPORTED_RUNTIME_METHODS for Svelte ccall/cwrap access. https://claude.ai/code/session_01BEYdu8gXr1QmYwzRRgaEA6 --- CMakeLists.txt | 2 + .../extensions/multiplayer/networkmanager.h | 8 + .../include/extensions/multiplayer/protocol.h | 35 ++++ .../extensions/multiplayer/remoteplayer.h | 67 +++++- extensions/src/multiplayer.cpp | 40 ++++ extensions/src/multiplayer/networkmanager.cpp | 52 ++++- extensions/src/multiplayer/remoteplayer.cpp | 198 ++++++++++++------ 7 files changed, 334 insertions(+), 68 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 533b9957..41cb379b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ endif() if (EMSCRIPTEN) add_compile_options(-pthread) add_link_options(-sUSE_WEBGL2=1 -sMIN_WEBGL_VERSION=2 -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=2gb -sUSE_PTHREADS=1 -sPROXY_TO_PTHREAD=1 -sOFFSCREENCANVAS_SUPPORT=1 -sPTHREAD_POOL_SIZE_STRICT=0 -sFORCE_FILESYSTEM=1 -sWASMFS=1 -sEXIT_RUNTIME=1) + add_link_options("-sEXPORTED_FUNCTIONS=[\"_main\",\"_mp_set_walk_animation\",\"_mp_set_idle_animation\",\"_mp_trigger_emote\",\"_mp_get_player_count\"]") + add_link_options("-sEXPORTED_RUNTIME_METHODS=[\"ccall\",\"cwrap\"]") set(SDL_PTHREADS ON CACHE BOOL "Enable SDL pthreads" FORCE) endif() diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 89804686..b2bf5989 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -41,6 +41,11 @@ class NetworkManager : public MxCore { bool IsConnected() const; bool WasRejected() const; + void SetWalkAnimation(uint8_t p_index); + void SetIdleAnimation(uint8_t p_index); + void SendEmote(uint8_t p_emoteId); + int GetPlayerCount() const; + void OnWorldEnabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world); @@ -61,6 +66,7 @@ class NetworkManager : public MxCore { void HandleLeave(const PlayerLeaveMsg& p_msg); void HandleState(const PlayerStateMsg& p_msg); void HandleHostAssign(const HostAssignMsg& p_msg); + void HandleEmote(const EmoteMsg& p_msg); void RemoveRemotePlayer(uint32_t p_peerId); void RemoveAllRemotePlayers(); @@ -80,6 +86,8 @@ class NetworkManager : public MxCore { uint32_t m_sequence; uint32_t m_lastBroadcastTime; uint8_t m_lastValidActorId; + uint8_t m_localWalkAnimId; + uint8_t m_localIdleAnimId; bool m_inIsleWorld; bool m_registered; diff --git a/extensions/include/extensions/multiplayer/protocol.h b/extensions/include/extensions/multiplayer/protocol.h index c7a7574e..71035314 100644 --- a/extensions/include/extensions/multiplayer/protocol.h +++ b/extensions/include/extensions/multiplayer/protocol.h @@ -17,6 +17,7 @@ enum MessageType : uint8_t { MSG_WORLD_SNAPSHOT = 6, MSG_WORLD_EVENT = 7, MSG_WORLD_EVENT_REQUEST = 8, + MSG_EMOTE = 9, MSG_ASSIGN_ID = 0xFF }; @@ -76,6 +77,8 @@ struct PlayerStateMsg { float direction[3]; float up[3]; float speed; + uint8_t walkAnimId; // Index into walk animation table (0 = default) + uint8_t idleAnimId; // Index into idle animation table (0 = default) }; // Server -> all: announces which peer is the host @@ -116,8 +119,40 @@ struct WorldEventRequestMsg { uint8_t padding; // Alignment }; +// One-shot emote trigger, broadcast to all peers +struct EmoteMsg { + MessageHeader header; + uint8_t emoteId; // Index into emote table +}; + #pragma pack(pop) +// Walk animation table: index -> CNs name +static const char* const g_walkAnimNames[] = { + "CNs001xx", // 0: Normal (default) + "CNs002xx", // 1: Joyful + "CNs003xx", // 2: Gloomy + "CNs005xx", // 3: Leaning + "CNs006xx", // 4: Scared + "CNs007xx", // 5: Hyper +}; +static const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]); + +// Idle animation table: index -> CNs name +static const char* const g_idleAnimNames[] = { + "CNs008xx", // 0: Sway (default) + "CNs009xx", // 1: Groove + "CNs010xx", // 2: Excited +}; +static const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]); + +// Emote table: index -> CNs name +static const char* const g_emoteAnimNames[] = { + "CNs011xx", // 0: Wave + "CNs012xx", // 1: Hat Tip +}; +static const int g_emoteAnimCount = sizeof(g_emoteAnimNames) / sizeof(g_emoteAnimNames[0]); + // Validate actorId is a playable character (1-5, not brickster) inline bool IsValidActorId(uint8_t p_actorId) { diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index c93412bc..6619be03 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -4,6 +4,8 @@ #include "mxtypes.h" #include +#include +#include class LegoROI; class LegoWorld; @@ -32,9 +34,49 @@ class RemotePlayer { uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; } void SetVisible(bool p_visible); + void TriggerEmote(uint8_t p_emoteId); private: - void BuildWalkROIMap(LegoWorld* p_isleWorld); + // Cached ROI map entry for an animation + struct AnimCache { + LegoAnim* anim; + LegoROI** roiMap; + MxU32 roiMapSize; + + AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {} + ~AnimCache() + { + if (roiMap) { + delete[] roiMap; + } + } + + AnimCache(const AnimCache&) = delete; + AnimCache& operator=(const AnimCache&) = delete; + AnimCache(AnimCache&& p_other) noexcept + : anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize) + { + p_other.roiMap = nullptr; + p_other.roiMapSize = 0; + p_other.anim = nullptr; + } + AnimCache& operator=(AnimCache&& p_other) noexcept + { + if (this != &p_other) { + if (roiMap) { + delete[] roiMap; + } + anim = p_other.anim; + roiMap = p_other.roiMap; + roiMapSize = p_other.roiMapSize; + p_other.roiMap = nullptr; + p_other.roiMapSize = 0; + p_other.anim = nullptr; + } + return *this; + } + }; + void BuildROIMap( LegoAnim* p_anim, LegoROI* p_rootROI, @@ -42,6 +84,7 @@ class RemotePlayer { LegoROI**& p_roiMap, MxU32& p_roiMapSize ); + AnimCache* GetOrBuildAnimCache(const char* p_animName); void UpdateTransform(float p_deltaTime); void UpdateAnimation(float p_deltaTime); void UpdateVehicleState(); @@ -69,18 +112,26 @@ class RemotePlayer { float m_currentDirection[3]; float m_currentUp[3]; - LegoAnim* m_walkAnim; - LegoROI** m_walkRoiMap; - MxU32 m_walkRoiMapSize; + // Animation state + uint8_t m_walkAnimId; + uint8_t m_idleAnimId; + AnimCache* m_walkAnimCache; + AnimCache* m_idleAnimCache; float m_animTime; float m_idleTime; + float m_idleAnimTime; bool m_wasMoving; - LegoAnim* m_idleAnim; - LegoROI** m_idleRoiMap; - MxU32 m_idleRoiMapSize; - float m_idleAnimTime; + // Emote state + AnimCache* m_emoteAnimCache; + float m_emoteTime; + float m_emoteDuration; + bool m_emoteActive; + // ROI map cache: animation name -> cached ROI map (invalidated on world change) + std::map m_animCacheMap; + + // Ride animation (vehicle-specific, not cached globally) LegoAnim* m_rideAnim; LegoROI** m_rideRoiMap; MxU32 m_rideRoiMapSize; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index 05cf9a79..a05442f3 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -10,6 +10,7 @@ #include "misc.h" #ifdef __EMSCRIPTEN__ #include "extensions/multiplayer/websockettransport.h" +#include #endif using namespace Extensions; @@ -124,3 +125,42 @@ bool Extensions::IsMultiplayerRejected() { return Extension::Call(CheckRejected).value_or(FALSE); } + +#ifdef __EMSCRIPTEN__ +extern "C" { + +EMSCRIPTEN_KEEPALIVE void mp_set_walk_animation(int index) +{ + Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); + if (mgr) { + mgr->SetWalkAnimation(static_cast(index)); + } +} + +EMSCRIPTEN_KEEPALIVE void mp_set_idle_animation(int index) +{ + Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); + if (mgr) { + mgr->SetIdleAnimation(static_cast(index)); + } +} + +EMSCRIPTEN_KEEPALIVE void mp_trigger_emote(int index) +{ + Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); + if (mgr) { + mgr->SendEmote(static_cast(index)); + } +} + +EMSCRIPTEN_KEEPALIVE int mp_get_player_count() +{ + Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); + if (mgr) { + return mgr->GetPlayerCount(); + } + return 0; +} + +} // extern "C" +#endif diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index a6f3eb7e..51bf3ae3 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -30,7 +30,7 @@ void NetworkManager::SendMessage(const T& p_msg) NetworkManager::NetworkManager() : m_transport(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), m_lastBroadcastTime(0), - m_lastValidActorId(0), m_inIsleWorld(false), m_registered(false) + m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), m_inIsleWorld(false), m_registered(false) { } @@ -212,6 +212,8 @@ void NetworkManager::BroadcastLocalState() SDL_memcpy(msg.direction, dir, sizeof(msg.direction)); SDL_memcpy(msg.up, up, sizeof(msg.up)); msg.speed = speed; + msg.walkAnimId = m_localWalkAnimId; + msg.idleAnimId = m_localIdleAnimId; SendMessage(msg); } @@ -291,6 +293,13 @@ void NetworkManager::ProcessIncomingPackets() } break; } + case MSG_EMOTE: { + EmoteMsg msg; + if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_EMOTE) { + HandleEmote(msg); + } + break; + } default: break; } @@ -378,6 +387,47 @@ void NetworkManager::HandleHostAssign(const HostAssignMsg& p_msg) } } +void NetworkManager::SetWalkAnimation(uint8_t p_index) +{ + if (p_index < g_walkAnimCount) { + m_localWalkAnimId = p_index; + } +} + +void NetworkManager::SetIdleAnimation(uint8_t p_index) +{ + if (p_index < g_idleAnimCount) { + m_localIdleAnimId = p_index; + } +} + +void NetworkManager::SendEmote(uint8_t p_emoteId) +{ + if (p_emoteId >= g_emoteAnimCount) { + return; + } + + EmoteMsg msg{}; + msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++}; + msg.emoteId = p_emoteId; + SendMessage(msg); +} + +int NetworkManager::GetPlayerCount() const +{ + // +1 for the local player + return static_cast(m_remotePlayers.size()) + 1; +} + +void NetworkManager::HandleEmote(const EmoteMsg& p_msg) +{ + uint32_t peerId = p_msg.header.peerId; + auto it = m_remotePlayers.find(peerId); + if (it != m_remotePlayers.end()) { + it->second->TriggerEmote(p_msg.emoteId); + } +} + void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId) { auto it = m_remotePlayers.find(p_peerId); diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 4d22f636..4bdae635 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -39,10 +39,10 @@ static bool IsLargeVehicle(int8_t p_vehicleType) RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId) : m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()), - m_hasReceivedUpdate(false), m_walkAnim(nullptr), m_walkRoiMap(nullptr), m_walkRoiMapSize(0), m_animTime(0.0f), - m_idleTime(0.0f), m_wasMoving(false), m_idleAnim(nullptr), m_idleRoiMap(nullptr), m_idleRoiMapSize(0), - m_idleAnimTime(0.0f), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), - m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) + m_hasReceivedUpdate(false), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), + m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr), + m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_rideAnim(nullptr), m_rideRoiMap(nullptr), + m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) { SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId); @@ -92,15 +92,9 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) m_spawned = true; m_visible = false; - BuildWalkROIMap(p_isleWorld); - - MxCore* idlePresenter = p_isleWorld->Find("LegoAnimPresenter", "CNs008xx"); - if (idlePresenter) { - m_idleAnim = static_cast(idlePresenter)->GetAnimation(); - if (m_idleAnim) { - BuildROIMap(m_idleAnim, m_roi, nullptr, m_idleRoiMap, m_idleRoiMapSize); - } - } + // Build initial walk and idle animation caches + m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]); + m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]); } void RemotePlayer::Despawn() @@ -117,19 +111,13 @@ void RemotePlayer::Despawn() m_roi = nullptr; } - if (m_walkRoiMap) { - delete[] m_walkRoiMap; - m_walkRoiMap = nullptr; - m_walkRoiMapSize = 0; - } - if (m_idleRoiMap) { - delete[] m_idleRoiMap; - m_idleRoiMap = nullptr; - m_idleRoiMapSize = 0; - } + // Clear all cached animation ROI maps (anim pointers are world-owned, not ours) + m_animCacheMap.clear(); + m_walkAnimCache = nullptr; + m_idleAnimCache = nullptr; + m_emoteAnimCache = nullptr; + m_emoteActive = false; - m_walkAnim = nullptr; - m_idleAnim = nullptr; m_spawned = false; m_visible = false; } @@ -152,6 +140,18 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) SET3(m_currentUp, m_targetUp); m_hasReceivedUpdate = true; } + + // Swap walk animation if changed + if (p_msg.walkAnimId != m_walkAnimId && p_msg.walkAnimId < g_walkAnimCount) { + m_walkAnimId = p_msg.walkAnimId; + m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]); + } + + // Swap idle animation if changed + if (p_msg.idleAnimId != m_idleAnimId && p_msg.idleAnimId < g_idleAnimCount) { + m_idleAnimId = p_msg.idleAnimId; + m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]); + } } void RemotePlayer::Tick(float p_deltaTime) @@ -211,24 +211,62 @@ void RemotePlayer::SetVisible(bool p_visible) } } -void RemotePlayer::BuildWalkROIMap(LegoWorld* p_isleWorld) +RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName) { - if (!p_isleWorld) { - return; + if (!p_animName || !m_roi) { + return nullptr; } - MxCore* presenter = p_isleWorld->Find("LegoAnimPresenter", "CNs001xx"); + // Check if already cached + auto it = m_animCacheMap.find(p_animName); + if (it != m_animCacheMap.end()) { + return &it->second; + } + + // Look up the animation presenter in the current world + LegoWorld* world = CurrentWorld(); + if (!world) { + return nullptr; + } + + MxCore* presenter = world->Find("LegoAnimPresenter", p_animName); if (!presenter) { + return nullptr; + } + + LegoAnim* anim = static_cast(presenter)->GetAnimation(); + if (!anim) { + return nullptr; + } + + // Build and cache + AnimCache& cache = m_animCacheMap[p_animName]; + cache.anim = anim; + BuildROIMap(anim, m_roi, nullptr, cache.roiMap, cache.roiMapSize); + + return &cache; +} + +void RemotePlayer::TriggerEmote(uint8_t p_emoteId) +{ + if (p_emoteId >= g_emoteAnimCount || !m_spawned) { return; } - LegoAnimPresenter* animPresenter = static_cast(presenter); - m_walkAnim = animPresenter->GetAnimation(); - if (!m_walkAnim) { + // Only play emotes when stationary + if (m_targetSpeed > 0.01f) { return; } - BuildROIMap(m_walkAnim, m_roi, nullptr, m_walkRoiMap, m_walkRoiMapSize); + AnimCache* cache = GetOrBuildAnimCache(g_emoteAnimNames[p_emoteId]); + if (!cache || !cache->anim) { + return; + } + + m_emoteAnimCache = cache; + m_emoteTime = 0.0f; + m_emoteDuration = (float) cache->anim->GetDuration(); + m_emoteActive = true; } // Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime @@ -334,59 +372,101 @@ void RemotePlayer::UpdateTransform(float p_deltaTime) void RemotePlayer::UpdateAnimation(float p_deltaTime) { - LegoAnim* anim = nullptr; - if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { return; } - LegoROI** roiMap = nullptr; + // Determine the active walk/ride animation and its ROI map + LegoAnim* walkAnim = nullptr; + LegoROI** walkRoiMap = nullptr; + MxU32 walkRoiMapSize = 0; if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) { - anim = m_rideAnim; - roiMap = m_rideRoiMap; + walkAnim = m_rideAnim; + walkRoiMap = m_rideRoiMap; + walkRoiMapSize = m_rideRoiMapSize; } - else if (m_walkAnim && m_walkRoiMap) { - anim = m_walkAnim; - roiMap = m_walkRoiMap; - } - else { - return; + else if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) { + walkAnim = m_walkAnimCache->anim; + walkRoiMap = m_walkAnimCache->roiMap; + walkRoiMapSize = m_walkAnimCache->roiMapSize; } - MxU32 roiMapSize = (roiMap == m_walkRoiMap) ? m_walkRoiMapSize : m_rideRoiMapSize; - for (MxU32 i = 1; i < roiMapSize; i++) { - if (roiMap[i] != nullptr) { - roiMap[i]->SetVisibility(TRUE); + // Ensure visibility of all mapped ROIs + if (walkRoiMap) { + for (MxU32 i = 1; i < walkRoiMapSize; i++) { + if (walkRoiMap[i] != nullptr) { + walkRoiMap[i]->SetVisibility(TRUE); + } } } - for (MxU32 i = 1; i < m_idleRoiMapSize; i++) { - if (m_idleRoiMap[i] != nullptr) { - m_idleRoiMap[i]->SetVisibility(TRUE); + if (m_idleAnimCache && m_idleAnimCache->roiMap) { + for (MxU32 i = 1; i < m_idleAnimCache->roiMapSize; i++) { + if (m_idleAnimCache->roiMap[i] != nullptr) { + m_idleAnimCache->roiMap[i]->SetVisibility(TRUE); + } } } bool inVehicle = (m_currentVehicleType != VEHICLE_NONE); + bool isMoving = inVehicle || m_targetSpeed > 0.01f; + + // Movement interrupts emotes + if (isMoving && m_emoteActive) { + m_emoteActive = false; + m_emoteAnimCache = nullptr; + } + + if (isMoving) { + // Walking / riding + if (!walkAnim || !walkRoiMap) { + return; + } - if (inVehicle || m_targetSpeed > 0.01f) { if (m_targetSpeed > 0.01f) { m_animTime += p_deltaTime * 2000.0f; } - float duration = (float) anim->GetDuration(); + float duration = (float) walkAnim->GetDuration(); if (duration > 0.0f) { float timeInCycle = m_animTime - duration * floorf(m_animTime / duration); MxMatrix transform(m_roi->GetLocal2World()); - LegoTreeNode* root = anim->GetRoot(); + LegoTreeNode* root = walkAnim->GetRoot(); for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { - LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, roiMap); + LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, walkRoiMap); } } m_wasMoving = true; m_idleTime = 0.0f; m_idleAnimTime = 0.0f; } - else if (m_idleAnim && m_idleRoiMap) { + else if (m_emoteActive && m_emoteAnimCache && m_emoteAnimCache->anim && m_emoteAnimCache->roiMap) { + // Emote playback (one-shot) + m_emoteTime += p_deltaTime * 1000.0f; + + if (m_emoteTime >= m_emoteDuration) { + // Emote completed -- return to stationary flow + m_emoteActive = false; + m_emoteAnimCache = nullptr; + m_wasMoving = false; + m_idleTime = 0.0f; + m_idleAnimTime = 0.0f; + } + else { + MxMatrix transform(m_roi->GetLocal2World()); + LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot(); + for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { + LegoROI::ApplyAnimationTransformation( + root->GetChild(i), + transform, + (LegoTime) m_emoteTime, + m_emoteAnimCache->roiMap + ); + } + } + } + else if (m_idleAnimCache && m_idleAnimCache->anim && m_idleAnimCache->roiMap) { + // Idle animation if (m_wasMoving) { m_wasMoving = false; m_idleTime = 0.0f; @@ -400,18 +480,18 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) m_idleAnimTime += p_deltaTime * 1000.0f; } - float duration = (float) m_idleAnim->GetDuration(); + float duration = (float) m_idleAnimCache->anim->GetDuration(); if (duration > 0.0f) { float timeInCycle = m_idleAnimTime - duration * floorf(m_idleAnimTime / duration); MxMatrix transform(m_roi->GetLocal2World()); - LegoTreeNode* root = m_idleAnim->GetRoot(); + LegoTreeNode* root = m_idleAnimCache->anim->GetRoot(); for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { LegoROI::ApplyAnimationTransformation( root->GetChild(i), transform, (LegoTime) timeInCycle, - m_idleRoiMap + m_idleAnimCache->roiMap ); } }