isle-portable/extensions/src/common/charactercustomizer.cpp
Christian Semmler 3d7bbdf0ae
Add third person camera extension
Introduces a third person camera system with orbit camera, input handling
(mouse/keyboard/touch/gamepad), display actor cloning, and camera-relative
movement. Includes shared character utilities (animator, cloner, customizer)
and an IExtraAnimHandler interface for optional animation extensions.
Also includes generic base game fixes and extension system improvements.
2026-03-29 08:20:57 -07:00

371 lines
9.3 KiB
C++

#include "extensions/common/charactercustomizer.h"
#include "3dmanager/lego3dmanager.h"
#include "3dmanager/lego3dview.h"
#include "extensions/common/charactercloner.h"
#include "extensions/common/constants.h"
#include "extensions/common/customizestate.h"
#include "legoactor.h"
#include "legoactors.h"
#include "legocharactermanager.h"
#include "legogamestate.h"
#include "legovideomanager.h"
#include "misc.h"
#include "mxatom.h"
#include "mxdsaction.h"
#include "mxmisc.h"
#include "roi/legolod.h"
#include "roi/legoroi.h"
#include "viewmanager/viewlodlist.h"
#include "viewmanager/viewmanager.h"
#include <SDL3/SDL_stdinc.h>
using namespace Extensions::Common;
static const MxU32 g_characterSoundIdOffset = 50;
static const MxU32 g_characterSoundIdMoodOffset = 66;
static const MxU32 g_characterAnimationId = 10;
static const MxU32 g_maxSound = 9;
static const MxU32 g_maxMove = 4;
static constexpr int COLORABLE_PARTS_COUNT = 10;
static uint32_t s_variantCounter = 10000;
// MARK: Private helpers
LegoROI* CharacterCustomizer::FindChildROI(LegoROI* p_rootROI, const char* p_name)
{
const CompoundObject* comp = p_rootROI->GetComp();
for (CompoundObject::const_iterator it = comp->begin(); it != comp->end(); it++) {
LegoROI* roi = (LegoROI*) *it;
if (!SDL_strcasecmp(p_name, roi->GetName())) {
return roi;
}
}
return NULL;
}
// MARK: Public API
uint8_t CharacterCustomizer::ResolveActorInfoIndex(uint8_t p_displayActorIndex)
{
return p_displayActorIndex;
}
bool CharacterCustomizer::SwitchColor(
LegoROI* p_rootROI,
uint8_t p_actorInfoIndex,
CustomizeState& p_state,
int p_partIndex
)
{
if (p_partIndex < 0 || p_partIndex >= COLORABLE_PARTS_COUNT) {
return false;
}
// Remap derived parts to independent parts
if (p_partIndex == c_clawlftPart) {
p_partIndex = c_armlftPart;
}
else if (p_partIndex == c_clawrtPart) {
p_partIndex = c_armrtPart;
}
else if (p_partIndex == c_headPart) {
p_partIndex = c_infohatPart;
}
else if (p_partIndex == c_bodyPart) {
p_partIndex = c_infogronPart;
}
if (!(g_actorLODs[p_partIndex + 1].m_flags & LegoActorLOD::c_useColor)) {
return false;
}
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return false;
}
const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[p_partIndex];
p_state.colorIndices[p_partIndex]++;
if (part.m_nameIndices[p_state.colorIndices[p_partIndex]] == 0xff) {
p_state.colorIndices[p_partIndex] = 0;
}
if (!p_rootROI) {
return true;
}
LegoROI* targetROI = FindChildROI(p_rootROI, g_actorLODs[p_partIndex + 1].m_name);
if (!targetROI) {
return false;
}
LegoFloat red, green, blue, alpha;
LegoROI::GetRGBAColor(part.m_names[part.m_nameIndices[p_state.colorIndices[p_partIndex]]], red, green, blue, alpha);
targetROI->SetLodColor(red, green, blue, alpha);
return true;
}
bool CharacterCustomizer::SwitchVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, CustomizeState& p_state)
{
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return false;
}
const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[c_infohatPart];
p_state.hatVariantIndex++;
if (part.m_partNameIndices[p_state.hatVariantIndex] == 0xff) {
p_state.hatVariantIndex = 0;
}
if (!p_rootROI) {
return true;
}
ApplyHatVariant(p_rootROI, p_actorInfoIndex, p_state);
return true;
}
bool CharacterCustomizer::SwitchSound(CustomizeState& p_state)
{
p_state.sound++;
if (p_state.sound >= g_maxSound) {
p_state.sound = 0;
}
return true;
}
bool CharacterCustomizer::SwitchMove(CustomizeState& p_state)
{
p_state.move++;
if (p_state.move >= g_maxMove) {
p_state.move = 0;
}
return true;
}
bool CharacterCustomizer::SwitchMood(CustomizeState& p_state)
{
p_state.mood++;
if (p_state.mood > 3) {
p_state.mood = 0;
}
return true;
}
void CharacterCustomizer::ApplyChange(
LegoROI* p_rootROI,
uint8_t p_actorInfoIndex,
CustomizeState& p_state,
uint8_t p_changeType,
uint8_t p_partIndex
)
{
switch (p_changeType) {
case CHANGE_VARIANT:
SwitchVariant(p_rootROI, p_actorInfoIndex, p_state);
break;
case CHANGE_SOUND:
SwitchSound(p_state);
break;
case CHANGE_MOVE:
SwitchMove(p_state);
break;
case CHANGE_COLOR:
SwitchColor(p_rootROI, p_actorInfoIndex, p_state, p_partIndex);
break;
case CHANGE_MOOD:
SwitchMood(p_state);
break;
}
}
int CharacterCustomizer::MapClickedPartIndex(const char* p_partName)
{
for (int i = 0; i < COLORABLE_PARTS_COUNT; i++) {
if (!SDL_strcasecmp(p_partName, g_actorLODs[i + 1].m_name)) {
return i;
}
}
return -1;
}
void CharacterCustomizer::ApplyFullState(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state)
{
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return;
}
// Apply colors for the 6 independent colorable parts
static const int colorableParts[] =
{c_infohatPart, c_infogronPart, c_armlftPart, c_armrtPart, c_leglftPart, c_legrtPart};
for (int i = 0; i < (int) sizeOfArray(colorableParts); i++) {
int partIndex = colorableParts[i];
if (!(g_actorLODs[partIndex + 1].m_flags & LegoActorLOD::c_useColor)) {
continue;
}
LegoROI* childROI = FindChildROI(p_rootROI, g_actorLODs[partIndex + 1].m_name);
if (!childROI) {
continue;
}
const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[partIndex];
LegoFloat red, green, blue, alpha;
LegoROI::GetRGBAColor(
part.m_names[part.m_nameIndices[p_state.colorIndices[partIndex]]],
red,
green,
blue,
alpha
);
childROI->SetLodColor(red, green, blue, alpha);
}
// Apply hat variant if different from default
const LegoActorInfo::Part& hatPart = g_actorInfoInit[p_actorInfoIndex].m_parts[c_infohatPart];
if (p_state.hatVariantIndex != hatPart.m_partNameIndex) {
ApplyHatVariant(p_rootROI, p_actorInfoIndex, p_state);
}
}
void CharacterCustomizer::ApplyHatVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state)
{
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return;
}
const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[c_infohatPart];
MxU8 partNameIndex = part.m_partNameIndices[p_state.hatVariantIndex];
if (partNameIndex == 0xff) {
return;
}
LegoROI* childROI = FindChildROI(p_rootROI, g_actorLODs[c_infohatLOD].m_name);
if (childROI != NULL) {
char lodName[256];
ViewLODList* lodList = GetViewLODListManager()->Lookup(part.m_partName[partNameIndex]);
MxS32 lodSize = lodList->Size();
SDL_snprintf(lodName, sizeof(lodName), "%s_cv%u", p_rootROI->GetName(), s_variantCounter++);
ViewLODList* dupLodList = GetViewLODListManager()->Create(lodName, lodSize);
Tgl::Renderer* renderer = VideoManager()->GetRenderer();
LegoFloat red, green, blue, alpha;
LegoROI::GetRGBAColor(
part.m_names[part.m_nameIndices[p_state.colorIndices[c_infohatPart]]],
red,
green,
blue,
alpha
);
for (MxS32 i = 0; i < lodSize; i++) {
LegoLOD* lod = (LegoLOD*) (*lodList)[i];
LegoLOD* clone = lod->Clone(renderer);
clone->SetColor(red, green, blue, alpha);
dupLodList->PushBack(clone);
}
lodList->Release();
lodList = dupLodList;
if (childROI->GetLodLevel() >= 0) {
VideoManager()->Get3DManager()->GetLego3DView()->GetViewManager()->RemoveROIDetailFromScene(childROI);
}
childROI->SetLODList(lodList);
lodList->Release();
}
}
void CharacterCustomizer::PlayClickSound(LegoROI* p_roi, const CustomizeState& p_state, bool p_basedOnMood)
{
MxU32 objectId =
p_basedOnMood ? (p_state.mood + g_characterSoundIdMoodOffset) : (p_state.sound + g_characterSoundIdOffset);
if (objectId) {
MxDSAction action;
action.SetAtomId(MxAtomId(LegoCharacterManager::GetCustomizeAnimFile(), e_lowerCase2));
action.SetObjectId(objectId);
const char* name = p_roi->GetName();
action.AppendExtra(SDL_strlen(name) + 1, name);
Start(&action);
}
}
MxU32 CharacterCustomizer::PlayClickAnimation(LegoROI* p_roi, const CustomizeState& p_state)
{
MxU32 objectId = p_state.move + g_characterAnimationId;
MxDSAction action;
action.SetAtomId(MxAtomId(LegoCharacterManager::GetCustomizeAnimFile(), e_lowerCase2));
action.SetObjectId(objectId);
char extra[1024];
SDL_snprintf(extra, sizeof(extra), "SUBST:actor_01:%s", p_roi->GetName());
action.AppendExtra(SDL_strlen(extra) + 1, extra);
StartActionIfInitialized(action);
return objectId;
}
void CharacterCustomizer::StopClickAnimation(MxU32 p_objectId)
{
MxDSAction action;
action.SetAtomId(MxAtomId(LegoCharacterManager::GetCustomizeAnimFile(), e_lowerCase2));
action.SetObjectId(p_objectId);
DeleteObject(action);
}
bool CharacterCustomizer::ResolveClickChangeType(uint8_t& p_changeType, int& p_partIndex, LegoROI* p_clickedROI)
{
p_partIndex = -1;
switch (GameState()->GetActorId()) {
case LegoActor::c_pepper:
if (GameState()->GetCurrentAct() == LegoGameState::e_act2 ||
GameState()->GetCurrentAct() == LegoGameState::e_act3) {
return false;
}
p_changeType = CHANGE_VARIANT;
break;
case LegoActor::c_mama:
p_changeType = CHANGE_SOUND;
break;
case LegoActor::c_papa:
p_changeType = CHANGE_MOVE;
break;
case LegoActor::c_nick:
p_changeType = CHANGE_COLOR;
if (p_clickedROI) {
p_partIndex = MapClickedPartIndex(p_clickedROI->GetName());
}
if (p_partIndex < 0) {
return false;
}
break;
case LegoActor::c_laura:
p_changeType = CHANGE_MOOD;
break;
case LegoActor::c_brickster:
return false;
default:
return false;
}
return true;
}