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
This commit is contained in:
Claude 2026-03-02 03:06:48 +00:00
parent 4ad835271e
commit 3e85941cbc
No known key found for this signature in database
7 changed files with 334 additions and 68 deletions

View File

@ -15,6 +15,8 @@ endif()
if (EMSCRIPTEN) if (EMSCRIPTEN)
add_compile_options(-pthread) 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(-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) set(SDL_PTHREADS ON CACHE BOOL "Enable SDL pthreads" FORCE)
endif() endif()

View File

@ -41,6 +41,11 @@ class NetworkManager : public MxCore {
bool IsConnected() const; bool IsConnected() const;
bool WasRejected() 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 OnWorldEnabled(LegoWorld* p_world);
void OnWorldDisabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world);
@ -61,6 +66,7 @@ class NetworkManager : public MxCore {
void HandleLeave(const PlayerLeaveMsg& p_msg); void HandleLeave(const PlayerLeaveMsg& p_msg);
void HandleState(const PlayerStateMsg& p_msg); void HandleState(const PlayerStateMsg& p_msg);
void HandleHostAssign(const HostAssignMsg& p_msg); void HandleHostAssign(const HostAssignMsg& p_msg);
void HandleEmote(const EmoteMsg& p_msg);
void RemoveRemotePlayer(uint32_t p_peerId); void RemoveRemotePlayer(uint32_t p_peerId);
void RemoveAllRemotePlayers(); void RemoveAllRemotePlayers();
@ -80,6 +86,8 @@ class NetworkManager : public MxCore {
uint32_t m_sequence; uint32_t m_sequence;
uint32_t m_lastBroadcastTime; uint32_t m_lastBroadcastTime;
uint8_t m_lastValidActorId; uint8_t m_lastValidActorId;
uint8_t m_localWalkAnimId;
uint8_t m_localIdleAnimId;
bool m_inIsleWorld; bool m_inIsleWorld;
bool m_registered; bool m_registered;

View File

@ -17,6 +17,7 @@ enum MessageType : uint8_t {
MSG_WORLD_SNAPSHOT = 6, MSG_WORLD_SNAPSHOT = 6,
MSG_WORLD_EVENT = 7, MSG_WORLD_EVENT = 7,
MSG_WORLD_EVENT_REQUEST = 8, MSG_WORLD_EVENT_REQUEST = 8,
MSG_EMOTE = 9,
MSG_ASSIGN_ID = 0xFF MSG_ASSIGN_ID = 0xFF
}; };
@ -76,6 +77,8 @@ struct PlayerStateMsg {
float direction[3]; float direction[3];
float up[3]; float up[3];
float speed; 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 // Server -> all: announces which peer is the host
@ -116,8 +119,40 @@ struct WorldEventRequestMsg {
uint8_t padding; // Alignment 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) #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) // Validate actorId is a playable character (1-5, not brickster)
inline bool IsValidActorId(uint8_t p_actorId) inline bool IsValidActorId(uint8_t p_actorId)
{ {

View File

@ -4,6 +4,8 @@
#include "mxtypes.h" #include "mxtypes.h"
#include <cstdint> #include <cstdint>
#include <map>
#include <string>
class LegoROI; class LegoROI;
class LegoWorld; class LegoWorld;
@ -32,9 +34,49 @@ class RemotePlayer {
uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; } uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; }
void SetVisible(bool p_visible); void SetVisible(bool p_visible);
void TriggerEmote(uint8_t p_emoteId);
private: 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( void BuildROIMap(
LegoAnim* p_anim, LegoAnim* p_anim,
LegoROI* p_rootROI, LegoROI* p_rootROI,
@ -42,6 +84,7 @@ class RemotePlayer {
LegoROI**& p_roiMap, LegoROI**& p_roiMap,
MxU32& p_roiMapSize MxU32& p_roiMapSize
); );
AnimCache* GetOrBuildAnimCache(const char* p_animName);
void UpdateTransform(float p_deltaTime); void UpdateTransform(float p_deltaTime);
void UpdateAnimation(float p_deltaTime); void UpdateAnimation(float p_deltaTime);
void UpdateVehicleState(); void UpdateVehicleState();
@ -69,18 +112,26 @@ class RemotePlayer {
float m_currentDirection[3]; float m_currentDirection[3];
float m_currentUp[3]; float m_currentUp[3];
LegoAnim* m_walkAnim; // Animation state
LegoROI** m_walkRoiMap; uint8_t m_walkAnimId;
MxU32 m_walkRoiMapSize; uint8_t m_idleAnimId;
AnimCache* m_walkAnimCache;
AnimCache* m_idleAnimCache;
float m_animTime; float m_animTime;
float m_idleTime; float m_idleTime;
float m_idleAnimTime;
bool m_wasMoving; bool m_wasMoving;
LegoAnim* m_idleAnim; // Emote state
LegoROI** m_idleRoiMap; AnimCache* m_emoteAnimCache;
MxU32 m_idleRoiMapSize; float m_emoteTime;
float m_idleAnimTime; float m_emoteDuration;
bool m_emoteActive;
// ROI map cache: animation name -> cached ROI map (invalidated on world change)
std::map<std::string, AnimCache> m_animCacheMap;
// Ride animation (vehicle-specific, not cached globally)
LegoAnim* m_rideAnim; LegoAnim* m_rideAnim;
LegoROI** m_rideRoiMap; LegoROI** m_rideRoiMap;
MxU32 m_rideRoiMapSize; MxU32 m_rideRoiMapSize;

View File

@ -10,6 +10,7 @@
#include "misc.h" #include "misc.h"
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
#include "extensions/multiplayer/websockettransport.h" #include "extensions/multiplayer/websockettransport.h"
#include <emscripten.h>
#endif #endif
using namespace Extensions; using namespace Extensions;
@ -124,3 +125,42 @@ bool Extensions::IsMultiplayerRejected()
{ {
return Extension<MultiplayerExt>::Call(CheckRejected).value_or(FALSE); return Extension<MultiplayerExt>::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<uint8_t>(index));
}
}
EMSCRIPTEN_KEEPALIVE void mp_set_idle_animation(int index)
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
mgr->SetIdleAnimation(static_cast<uint8_t>(index));
}
}
EMSCRIPTEN_KEEPALIVE void mp_trigger_emote(int index)
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
mgr->SendEmote(static_cast<uint8_t>(index));
}
}
EMSCRIPTEN_KEEPALIVE int mp_get_player_count()
{
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
if (mgr) {
return mgr->GetPlayerCount();
}
return 0;
}
} // extern "C"
#endif

View File

@ -30,7 +30,7 @@ void NetworkManager::SendMessage(const T& p_msg)
NetworkManager::NetworkManager() NetworkManager::NetworkManager()
: m_transport(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), m_lastBroadcastTime(0), : 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.direction, dir, sizeof(msg.direction));
SDL_memcpy(msg.up, up, sizeof(msg.up)); SDL_memcpy(msg.up, up, sizeof(msg.up));
msg.speed = speed; msg.speed = speed;
msg.walkAnimId = m_localWalkAnimId;
msg.idleAnimId = m_localIdleAnimId;
SendMessage(msg); SendMessage(msg);
} }
@ -291,6 +293,13 @@ void NetworkManager::ProcessIncomingPackets()
} }
break; break;
} }
case MSG_EMOTE: {
EmoteMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_EMOTE) {
HandleEmote(msg);
}
break;
}
default: default:
break; 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<int>(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) void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
{ {
auto it = m_remotePlayers.find(p_peerId); auto it = m_remotePlayers.find(p_peerId);

View File

@ -39,10 +39,10 @@ static bool IsLargeVehicle(int8_t p_vehicleType)
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId) 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_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_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_hasReceivedUpdate(false), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr),
m_idleTime(0.0f), m_wasMoving(false), m_idleAnim(nullptr), m_idleRoiMap(nullptr), m_idleRoiMapSize(0), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr),
m_idleAnimTime(0.0f), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_rideAnim(nullptr), m_rideRoiMap(nullptr),
m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) 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); 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_spawned = true;
m_visible = false; m_visible = false;
BuildWalkROIMap(p_isleWorld); // Build initial walk and idle animation caches
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
MxCore* idlePresenter = p_isleWorld->Find("LegoAnimPresenter", "CNs008xx"); m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
if (idlePresenter) {
m_idleAnim = static_cast<LegoAnimPresenter*>(idlePresenter)->GetAnimation();
if (m_idleAnim) {
BuildROIMap(m_idleAnim, m_roi, nullptr, m_idleRoiMap, m_idleRoiMapSize);
}
}
} }
void RemotePlayer::Despawn() void RemotePlayer::Despawn()
@ -117,19 +111,13 @@ void RemotePlayer::Despawn()
m_roi = nullptr; m_roi = nullptr;
} }
if (m_walkRoiMap) { // Clear all cached animation ROI maps (anim pointers are world-owned, not ours)
delete[] m_walkRoiMap; m_animCacheMap.clear();
m_walkRoiMap = nullptr; m_walkAnimCache = nullptr;
m_walkRoiMapSize = 0; m_idleAnimCache = nullptr;
} m_emoteAnimCache = nullptr;
if (m_idleRoiMap) { m_emoteActive = false;
delete[] m_idleRoiMap;
m_idleRoiMap = nullptr;
m_idleRoiMapSize = 0;
}
m_walkAnim = nullptr;
m_idleAnim = nullptr;
m_spawned = false; m_spawned = false;
m_visible = false; m_visible = false;
} }
@ -152,6 +140,18 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
SET3(m_currentUp, m_targetUp); SET3(m_currentUp, m_targetUp);
m_hasReceivedUpdate = true; 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) 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) { if (!p_animName || !m_roi) {
return; 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) { if (!presenter) {
return nullptr;
}
LegoAnim* anim = static_cast<LegoAnimPresenter*>(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; return;
} }
LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter); // Only play emotes when stationary
m_walkAnim = animPresenter->GetAnimation(); if (m_targetSpeed > 0.01f) {
if (!m_walkAnim) {
return; 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 // 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) void RemotePlayer::UpdateAnimation(float p_deltaTime)
{ {
LegoAnim* anim = nullptr;
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
return; 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) { if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) {
anim = m_rideAnim; walkAnim = m_rideAnim;
roiMap = m_rideRoiMap; walkRoiMap = m_rideRoiMap;
walkRoiMapSize = m_rideRoiMapSize;
} }
else if (m_walkAnim && m_walkRoiMap) { else if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) {
anim = m_walkAnim; walkAnim = m_walkAnimCache->anim;
roiMap = m_walkRoiMap; walkRoiMap = m_walkAnimCache->roiMap;
} walkRoiMapSize = m_walkAnimCache->roiMapSize;
else {
return;
} }
MxU32 roiMapSize = (roiMap == m_walkRoiMap) ? m_walkRoiMapSize : m_rideRoiMapSize; // Ensure visibility of all mapped ROIs
for (MxU32 i = 1; i < roiMapSize; i++) { if (walkRoiMap) {
if (roiMap[i] != nullptr) { for (MxU32 i = 1; i < walkRoiMapSize; i++) {
roiMap[i]->SetVisibility(TRUE); if (walkRoiMap[i] != nullptr) {
walkRoiMap[i]->SetVisibility(TRUE);
} }
} }
for (MxU32 i = 1; i < m_idleRoiMapSize; i++) { }
if (m_idleRoiMap[i] != nullptr) { if (m_idleAnimCache && m_idleAnimCache->roiMap) {
m_idleRoiMap[i]->SetVisibility(TRUE); 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 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) { if (m_targetSpeed > 0.01f) {
m_animTime += p_deltaTime * 2000.0f; m_animTime += p_deltaTime * 2000.0f;
} }
float duration = (float) anim->GetDuration(); float duration = (float) walkAnim->GetDuration();
if (duration > 0.0f) { if (duration > 0.0f) {
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration); float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
MxMatrix transform(m_roi->GetLocal2World()); MxMatrix transform(m_roi->GetLocal2World());
LegoTreeNode* root = anim->GetRoot(); LegoTreeNode* root = walkAnim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { 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_wasMoving = true;
m_idleTime = 0.0f; m_idleTime = 0.0f;
m_idleAnimTime = 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) { if (m_wasMoving) {
m_wasMoving = false; m_wasMoving = false;
m_idleTime = 0.0f; m_idleTime = 0.0f;
@ -400,18 +480,18 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
m_idleAnimTime += p_deltaTime * 1000.0f; m_idleAnimTime += p_deltaTime * 1000.0f;
} }
float duration = (float) m_idleAnim->GetDuration(); float duration = (float) m_idleAnimCache->anim->GetDuration();
if (duration > 0.0f) { if (duration > 0.0f) {
float timeInCycle = m_idleAnimTime - duration * floorf(m_idleAnimTime / duration); float timeInCycle = m_idleAnimTime - duration * floorf(m_idleAnimTime / duration);
MxMatrix transform(m_roi->GetLocal2World()); MxMatrix transform(m_roi->GetLocal2World());
LegoTreeNode* root = m_idleAnim->GetRoot(); LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation( LegoROI::ApplyAnimationTransformation(
root->GetChild(i), root->GetChild(i),
transform, transform,
(LegoTime) timeInCycle, (LegoTime) timeInCycle,
m_idleRoiMap m_idleAnimCache->roiMap
); );
} }
} }