mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Add emote sound effects (#14)
* Add optional sound effects to emotes Replace the raw 2D string emote table with a struct-based EmoteEntry/EmotePhase table so each emote phase can declare an optional 3D spatialized sound effect. The sound plays via LegoCacheSoundManager when the emote phase starts, positioned at the character's ROI. No extra network sync needed since sounds trigger locally when TriggerEmote is called on each client. Adds "crash5" sound to the BNsDis01 (disassemble) emote phase. https://claude.ai/code/session_013XkwJJZ4RbNcBMFESuPTfJ * Extract animation/asset tables from protocol.h into animdata.h Move EmotePhase, EmoteEntry structs, animation table externs (walk/idle/emote/vehicle), and helpers (IsMultiPartEmote, IsLargeVehicle, DetectVehicleType) into a dedicated animdata.h/cpp pair. This keeps protocol.h focused on wire-format concerns (message structs, serialization, enums) while animdata.h owns game asset data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1a8c6c70ea
commit
630368c89f
@ -532,6 +532,7 @@ if (ISLE_EXTENSIONS)
|
||||
extensions/src/siloader.cpp
|
||||
extensions/src/textureloader.cpp
|
||||
extensions/src/multiplayer.cpp
|
||||
extensions/src/multiplayer/animdata.cpp
|
||||
extensions/src/multiplayer/animutils.cpp
|
||||
extensions/src/multiplayer/characteranimator.cpp
|
||||
extensions/src/multiplayer/charactercloner.cpp
|
||||
|
||||
49
extensions/include/extensions/multiplayer/animdata.h
Normal file
49
extensions/include/extensions/multiplayer/animdata.h
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class LegoPathActor;
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
// Animation and vehicle tables (defined in animdata.cpp)
|
||||
extern const char* const g_walkAnimNames[];
|
||||
extern const int g_walkAnimCount;
|
||||
|
||||
extern const char* const g_idleAnimNames[];
|
||||
extern const int g_idleAnimCount;
|
||||
|
||||
// Per-phase emote data: animation name and optional sound effect.
|
||||
struct EmotePhase {
|
||||
const char* anim; // Animation name (nullptr = unused phase)
|
||||
const char* sound; // Sound key for LegoCacheSoundManager (nullptr = silent)
|
||||
};
|
||||
|
||||
// Emote table entry: two phases (phase 1 = primary, phase 2 = recovery for multi-part emotes).
|
||||
struct EmoteEntry {
|
||||
EmotePhase phases[2];
|
||||
};
|
||||
|
||||
extern const EmoteEntry g_emoteEntries[];
|
||||
extern const int g_emoteAnimCount;
|
||||
|
||||
// Returns true if the emote is a multi-part stateful emote (has a phase-2 animation).
|
||||
inline bool IsMultiPartEmote(uint8_t p_emoteId)
|
||||
{
|
||||
return p_emoteId < g_emoteAnimCount && g_emoteEntries[p_emoteId].phases[1].anim != nullptr;
|
||||
}
|
||||
|
||||
extern const char* const g_vehicleROINames[VEHICLE_COUNT];
|
||||
extern const char* const g_rideAnimNames[VEHICLE_COUNT];
|
||||
extern const char* const g_rideVehicleROINames[VEHICLE_COUNT];
|
||||
|
||||
// Returns true if the vehicle type has no ride animation (model swap instead)
|
||||
bool IsLargeVehicle(int8_t p_vehicleType);
|
||||
|
||||
// Detect the vehicle type of a given actor, or VEHICLE_NONE if not a vehicle
|
||||
int8_t DetectVehicleType(LegoPathActor* p_actor);
|
||||
|
||||
} // namespace Multiplayer
|
||||
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "extensions/multiplayer/animdata.h"
|
||||
#include "extensions/multiplayer/animutils.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "mxgeometry/mxmatrix.h"
|
||||
#include "mxtypes.h"
|
||||
|
||||
|
||||
@ -5,8 +5,6 @@
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
class LegoPathActor;
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
@ -155,33 +153,6 @@ struct CustomizeMsg {
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// Animation and vehicle tables (defined in protocol.cpp)
|
||||
extern const char* const g_walkAnimNames[];
|
||||
extern const int g_walkAnimCount;
|
||||
|
||||
extern const char* const g_idleAnimNames[];
|
||||
extern const int g_idleAnimCount;
|
||||
|
||||
// Emote animation table: [emoteId][phase]. Phase 0 = primary, phase 1 = phase-2 (nullptr for one-shot).
|
||||
extern const char* const g_emoteAnims[][2];
|
||||
extern const int g_emoteAnimCount;
|
||||
|
||||
// Returns true if the emote is a multi-part stateful emote (has a phase-2 animation).
|
||||
inline bool IsMultiPartEmote(uint8_t p_emoteId)
|
||||
{
|
||||
return p_emoteId < g_emoteAnimCount && g_emoteAnims[p_emoteId][1] != nullptr;
|
||||
}
|
||||
|
||||
extern const char* const g_vehicleROINames[VEHICLE_COUNT];
|
||||
extern const char* const g_rideAnimNames[VEHICLE_COUNT];
|
||||
extern const char* const g_rideVehicleROINames[VEHICLE_COUNT];
|
||||
|
||||
// Returns true if the vehicle type has no ride animation (model swap instead)
|
||||
bool IsLargeVehicle(int8_t p_vehicleType);
|
||||
|
||||
// Detect the vehicle type of a given actor, or VEHICLE_NONE if not a vehicle
|
||||
int8_t DetectVehicleType(LegoPathActor* p_actor);
|
||||
|
||||
// Validate actorId is a playable character (1-5, not brickster)
|
||||
inline bool IsValidActorId(uint8_t p_actorId)
|
||||
{
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
#include "extensions/multiplayer/characteranimator.h"
|
||||
#include "extensions/multiplayer/customizestate.h"
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
#include "mxgeometry/mxgeometry3d.h"
|
||||
#include "mxtypes.h"
|
||||
|
||||
|
||||
78
extensions/src/multiplayer/animdata.cpp
Normal file
78
extensions/src/multiplayer/animdata.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
#include "extensions/multiplayer/animdata.h"
|
||||
|
||||
#include "legopathactor.h"
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
const char* const g_walkAnimNames[] = {
|
||||
"CNs001xx", // 0: Normal (default)
|
||||
"CNs002xx", // 1: Joyful
|
||||
"CNs003xx", // 2: Gloomy
|
||||
"CNs005xx", // 3: Leaning
|
||||
"CNs006xx", // 4: Scared
|
||||
"CNs007xx", // 5: Hyper
|
||||
};
|
||||
const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
|
||||
|
||||
const char* const g_idleAnimNames[] = {
|
||||
"CNs008xx", // 0: Sway (default)
|
||||
"CNs009xx", // 1: Groove
|
||||
"CNs010xx", // 2: Excited
|
||||
};
|
||||
const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
|
||||
|
||||
// Emote table. Each entry has two phases: {anim, sound}.
|
||||
// Phase 2 anim is nullptr for one-shot emotes; non-null makes it a multi-part stateful emote.
|
||||
const EmoteEntry g_emoteEntries[] = {
|
||||
{{{"CNs011xx", nullptr}, {nullptr, nullptr}}}, // 0: Wave (one-shot)
|
||||
{{{"CNs012xx", nullptr}, {nullptr, nullptr}}}, // 1: Hat Tip (one-shot)
|
||||
{{{"BNsDis01", "crash5"}, {"BNsAss01", nullptr}}}, // 2: Disassemble / Reassemble (multi-part)
|
||||
};
|
||||
const int g_emoteAnimCount = sizeof(g_emoteEntries) / sizeof(g_emoteEntries[0]);
|
||||
|
||||
// Vehicle model names (LOD names). The helicopter is a compound ROI ("copter")
|
||||
// with no standalone LOD; use its body part instead.
|
||||
const char* const g_vehicleROINames[VEHICLE_COUNT] =
|
||||
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
|
||||
|
||||
// Ride animation names for small vehicles (NULL = large vehicle, no ride anim)
|
||||
const char* const g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
|
||||
|
||||
// Vehicle variant ROI names used in ride animations
|
||||
const char* const g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
||||
|
||||
bool IsLargeVehicle(int8_t p_vehicleType)
|
||||
{
|
||||
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
||||
}
|
||||
|
||||
int8_t DetectVehicleType(LegoPathActor* p_actor)
|
||||
{
|
||||
static const struct {
|
||||
const char* className;
|
||||
int8_t vehicleType;
|
||||
} vehicleMap[] = {
|
||||
{"Helicopter", VEHICLE_HELICOPTER},
|
||||
{"Jetski", VEHICLE_JETSKI},
|
||||
{"DuneBuggy", VEHICLE_DUNEBUGGY},
|
||||
{"Bike", VEHICLE_BIKE},
|
||||
{"SkateBoard", VEHICLE_SKATEBOARD},
|
||||
{"Motorcycle", VEHICLE_MOTOCYCLE},
|
||||
{"TowTrack", VEHICLE_TOWTRACK},
|
||||
{"Ambulance", VEHICLE_AMBULANCE},
|
||||
};
|
||||
|
||||
if (!p_actor) {
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
for (const auto& entry : vehicleMap) {
|
||||
if (p_actor->IsA(entry.className)) {
|
||||
return entry.vehicleType;
|
||||
}
|
||||
}
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
} // namespace Multiplayer
|
||||
@ -5,7 +5,9 @@
|
||||
#include "extensions/multiplayer/charactercustomizer.h"
|
||||
#include "extensions/multiplayer/namebubblerenderer.h"
|
||||
#include "legoanimpresenter.h"
|
||||
#include "legocachesoundmanager.h"
|
||||
#include "legocharactermanager.h"
|
||||
#include "legosoundmanager.h"
|
||||
#include "legovideomanager.h"
|
||||
#include "legoworld.h"
|
||||
#include "misc.h"
|
||||
@ -237,7 +239,7 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i
|
||||
if (IsMultiPartEmote(p_emoteId)) {
|
||||
if (m_frozenEmoteId == (int8_t) p_emoteId) {
|
||||
// Phase 2: play the recovery animation to unfreeze
|
||||
AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteAnims[p_emoteId][1]);
|
||||
AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteEntries[p_emoteId].phases[1].anim);
|
||||
if (!cache || !cache->anim) {
|
||||
return;
|
||||
}
|
||||
@ -250,6 +252,11 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i
|
||||
m_emoteDuration = (float) cache->anim->GetDuration();
|
||||
m_emoteActive = true;
|
||||
|
||||
const char* sound = g_emoteEntries[p_emoteId].phases[1].sound;
|
||||
if (sound) {
|
||||
SoundManager()->GetCacheSoundManager()->Play(sound, p_roi->GetName(), FALSE);
|
||||
}
|
||||
|
||||
if (m_config.saveEmoteTransform) {
|
||||
m_emoteParentTransform = m_frozenParentTransform;
|
||||
}
|
||||
@ -268,7 +275,7 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i
|
||||
}
|
||||
}
|
||||
|
||||
AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteAnims[p_emoteId][0]);
|
||||
AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteEntries[p_emoteId].phases[0].anim);
|
||||
if (!cache || !cache->anim) {
|
||||
return;
|
||||
}
|
||||
@ -281,6 +288,11 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i
|
||||
m_emoteDuration = (float) cache->anim->GetDuration();
|
||||
m_emoteActive = true;
|
||||
|
||||
const char* sound = g_emoteEntries[p_emoteId].phases[0].sound;
|
||||
if (sound) {
|
||||
SoundManager()->GetCacheSoundManager()->Play(sound, p_roi->GetName(), FALSE);
|
||||
}
|
||||
|
||||
// Save clean transform to prevent scale accumulation during emote
|
||||
if (m_config.saveEmoteTransform) {
|
||||
m_emoteParentTransform = p_roi->GetLocal2World();
|
||||
@ -371,7 +383,7 @@ void CharacterAnimator::InitAnimCaches(LegoROI* p_roi)
|
||||
void CharacterAnimator::SetFrozenEmoteId(int8_t p_emoteId, LegoROI* p_roi)
|
||||
{
|
||||
if (p_emoteId >= 0 && p_emoteId < g_emoteAnimCount && IsMultiPartEmote((uint8_t) p_emoteId)) {
|
||||
AnimCache* cache = p_roi ? GetOrBuildAnimCache(p_roi, g_emoteAnims[p_emoteId][0]) : nullptr;
|
||||
AnimCache* cache = p_roi ? GetOrBuildAnimCache(p_roi, g_emoteEntries[p_emoteId].phases[0].anim) : nullptr;
|
||||
m_frozenEmoteId = p_emoteId;
|
||||
m_frozenAnimCache = cache;
|
||||
m_frozenAnimDuration = (cache && cache->anim) ? (float) cache->anim->GetDuration() : 0.0f;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#include "extensions/multiplayer/protocol.h"
|
||||
|
||||
#include "legogamestate.h"
|
||||
#include "legopathactor.h"
|
||||
#include "misc.h"
|
||||
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
@ -9,76 +8,6 @@
|
||||
namespace Multiplayer
|
||||
{
|
||||
|
||||
const char* const g_walkAnimNames[] = {
|
||||
"CNs001xx", // 0: Normal (default)
|
||||
"CNs002xx", // 1: Joyful
|
||||
"CNs003xx", // 2: Gloomy
|
||||
"CNs005xx", // 3: Leaning
|
||||
"CNs006xx", // 4: Scared
|
||||
"CNs007xx", // 5: Hyper
|
||||
};
|
||||
const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
|
||||
|
||||
const char* const g_idleAnimNames[] = {
|
||||
"CNs008xx", // 0: Sway (default)
|
||||
"CNs009xx", // 1: Groove
|
||||
"CNs010xx", // 2: Excited
|
||||
};
|
||||
const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
|
||||
|
||||
// Emote animation table. Each entry is {phase1, phase2}.
|
||||
// phase2 is nullptr for one-shot emotes; non-null makes it a multi-part stateful emote.
|
||||
const char* const g_emoteAnims[][2] = {
|
||||
{"CNs011xx", nullptr}, // 0: Wave (one-shot)
|
||||
{"CNs012xx", nullptr}, // 1: Hat Tip (one-shot)
|
||||
{"BNsDis01", "BNsAss01"}, // 2: Disassemble / Reassemble (multi-part)
|
||||
};
|
||||
const int g_emoteAnimCount = sizeof(g_emoteAnims) / sizeof(g_emoteAnims[0]);
|
||||
|
||||
// Vehicle model names (LOD names). The helicopter is a compound ROI ("copter")
|
||||
// with no standalone LOD; use its body part instead.
|
||||
const char* const g_vehicleROINames[VEHICLE_COUNT] =
|
||||
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
|
||||
|
||||
// Ride animation names for small vehicles (NULL = large vehicle, no ride anim)
|
||||
const char* const g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
|
||||
|
||||
// Vehicle variant ROI names used in ride animations
|
||||
const char* const g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
||||
|
||||
bool IsLargeVehicle(int8_t p_vehicleType)
|
||||
{
|
||||
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
||||
}
|
||||
|
||||
int8_t DetectVehicleType(LegoPathActor* p_actor)
|
||||
{
|
||||
static const struct {
|
||||
const char* className;
|
||||
int8_t vehicleType;
|
||||
} vehicleMap[] = {
|
||||
{"Helicopter", VEHICLE_HELICOPTER},
|
||||
{"Jetski", VEHICLE_JETSKI},
|
||||
{"DuneBuggy", VEHICLE_DUNEBUGGY},
|
||||
{"Bike", VEHICLE_BIKE},
|
||||
{"SkateBoard", VEHICLE_SKATEBOARD},
|
||||
{"Motorcycle", VEHICLE_MOTOCYCLE},
|
||||
{"TowTrack", VEHICLE_TOWTRACK},
|
||||
{"Ambulance", VEHICLE_AMBULANCE},
|
||||
};
|
||||
|
||||
if (!p_actor) {
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
for (const auto& entry : vehicleMap) {
|
||||
if (p_actor->IsA(entry.className)) {
|
||||
return entry.vehicleType;
|
||||
}
|
||||
}
|
||||
return VEHICLE_NONE;
|
||||
}
|
||||
|
||||
void EncodeUsername(char p_out[8])
|
||||
{
|
||||
SDL_memset(p_out, 0, 8);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user