mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
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:
parent
4ad835271e
commit
3e85941cbc
@ -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()
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
#include "mxtypes.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
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<std::string, AnimCache> m_animCacheMap;
|
||||
|
||||
// Ride animation (vehicle-specific, not cached globally)
|
||||
LegoAnim* m_rideAnim;
|
||||
LegoROI** m_rideRoiMap;
|
||||
MxU32 m_rideRoiMapSize;
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include "misc.h"
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "extensions/multiplayer/websockettransport.h"
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
using namespace Extensions;
|
||||
@ -124,3 +125,42 @@ bool Extensions::IsMultiplayerRejected()
|
||||
{
|
||||
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
|
||||
|
||||
@ -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<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)
|
||||
{
|
||||
auto it = m_remotePlayers.find(p_peerId);
|
||||
|
||||
@ -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<LegoAnimPresenter*>(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<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;
|
||||
}
|
||||
|
||||
LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user