#include "character/CharacterAnimation.h" #include "character/CharacterObject.h" #include #include #include #include #include #include namespace frostbite2D { namespace { const std::array kAvatarParts = { "weapon_avatar", "aurora_avatar", "hair_avatar", "hat_avatar", "face_avatar", "breast_avatar", "coat_avatar", "skin_avatar", "waist_avatar", "pants_avatar", "shoes_avatar", "weapon"}; std::string truncatePath(const std::string& path) { size_t slashPos = path.find_last_of('/'); if (slashPos == std::string::npos) { return {}; } return path.substr(0, slashPos); } std::string actionTail(const std::string& path) { size_t slashPos = path.find_last_of('/'); if (slashPos == std::string::npos) { return path; } return path.substr(slashPos); } } // namespace bool CharacterAnimation::Init(CharacterObject* parent, const character::CharacterConfig& config, const CharacterEquipmentManager& equipmentManager) { parent_ = parent; actionAnimations_.clear(); currentActionTag_.clear(); direction_ = 1; actionFrameFlagCallback_ = nullptr; actionEndCallback_ = nullptr; for (const auto& [actionName, actionPath] : config.animationPath) { for (const char* slotName : kAvatarParts) { CreateAnimationBySlot(actionName, slotName, actionPath, config, equipmentManager); } } if (actionAnimations_.empty()) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "CharacterAnimation: no usable action animations for job %d", config.jobId); return false; } return true; } std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) { size_t pos = path.find("%04d"); if (pos != std::string::npos) { path.replace(pos, 4, "%02d%02d"); } char buffer[512] = {}; std::snprintf(buffer, sizeof(buffer), path.c_str(), data.param1, data.param2); return std::string(buffer); } void CharacterAnimation::CreateAnimationBySlot( const std::string& actionName, const std::string& slotName, const std::string& actionPath, const character::CharacterConfig& config, const CharacterEquipmentManager& equipmentManager) { if (slotName == std::string("skin_avatar")) { Animation::ReplaceData replaceData(0, 0); if (const auto* equip = equipmentManager.GetEquip(slotName)) { auto it = equip->jobAnimations.find(config.jobId); if (it != equip->jobAnimations.end() && !it->second.empty()) { replaceData.param1 = it->second.front().imgFormat[0]; replaceData.param2 = it->second.front().imgFormat[1]; } } auto animation = MakePtr(actionPath, FormatImgPath, replaceData); if (!animation || !animation->IsUsable()) { return; } animation->SetVisible(false); AddChild(animation); actionAnimations_[actionName].push_back(animation); return; } const auto* equip = equipmentManager.GetEquip(slotName); if (!equip) { return; } auto jobAniIt = equip->jobAnimations.find(config.jobId); if (jobAniIt == equip->jobAnimations.end()) { return; } std::string equipDir = truncatePath(equip->path); std::string actionPathTail = actionTail(actionPath); for (const auto& variation : jobAniIt->second) { if (variation.animationGroup.empty()) { continue; } std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail; auto animation = MakePtr( aniPath, FormatImgPath, Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1])); if (!animation || !animation->IsUsable()) { continue; } animation->SetVisible(false); animation->SetZOrder(variation.layer); AddChild(animation); actionAnimations_[actionName].push_back(animation); } } std::string CharacterAnimation::DescribeAvailableActions() const { if (actionAnimations_.empty()) { return ""; } std::ostringstream stream; bool first = true; for (const auto& [actionName, animations] : actionAnimations_) { (void)animations; if (!first) { stream << ", "; } stream << actionName; first = false; } return stream.str(); } Animation* CharacterAnimation::GetPrimaryAnimation(const std::string& actionName) const { auto it = actionAnimations_.find(actionName); if (it == actionAnimations_.end()) { return nullptr; } for (const auto& animation : it->second) { if (animation && animation->IsUsable()) { return animation.Get(); } } for (const auto& animation : it->second) { if (animation) { return animation.Get(); } } return nullptr; } Animation* CharacterAnimation::GetCurrentPrimaryAnimation() const { if (currentActionTag_.empty()) { return nullptr; } return GetPrimaryAnimation(currentActionTag_); } void CharacterAnimation::RefreshRuntimeCallbacks() { auto currentIt = actionAnimations_.find(currentActionTag_); if (currentIt == actionAnimations_.end()) { return; } Animation* primaryAnimation = GetPrimaryAnimation(currentActionTag_); for (auto& animation : currentIt->second) { if (!animation) { continue; } if (animation.Get() == primaryAnimation) { animation->changeFrameCallback_ = actionFrameFlagCallback_; animation->endCallback_ = actionEndCallback_; } else { animation->changeFrameCallback_ = nullptr; animation->endCallback_ = nullptr; } } } bool CharacterAnimation::SetAction(const std::string& actionName) { auto nextIt = actionAnimations_.find(actionName); if (nextIt == actionAnimations_.end()) { if (parent_) { parent_->ReportFatalCharacterError("CharacterAnimation::SetAction", "requested animation tag is not loaded", std::string(), actionName); } else { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "CharacterAnimation: action %s missing and parent is null", actionName.c_str()); } return false; } if (!currentActionTag_.empty()) { auto currentIt = actionAnimations_.find(currentActionTag_); if (currentIt != actionAnimations_.end()) { for (auto& animation : currentIt->second) { animation->Reset(); animation->SetVisible(false); } } } for (auto& animation : nextIt->second) { animation->Reset(); animation->SetVisible(true); } currentActionTag_ = actionName; RefreshRuntimeCallbacks(); SetDirection(direction_); return true; } void CharacterAnimation::SetDirection(int direction) { direction_ = direction >= 0 ? 1 : -1; auto currentIt = actionAnimations_.find(currentActionTag_); if (currentIt == actionAnimations_.end()) { return; } for (auto& animation : currentIt->second) { if (animation) { animation->SetDirection(direction_); } } } bool CharacterAnimation::IsCurrentActionFinished() const { const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); if (!primaryAnimation) { return false; } return !primaryAnimation->IsUsable(); } int CharacterAnimation::GetCurrentFrameIndex() const { const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); return primaryAnimation ? primaryAnimation->GetCurrentFrameIndex() : 0; } int CharacterAnimation::GetCurrentFrameCount() const { const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); return primaryAnimation ? primaryAnimation->GetTotalFrameCount() : 0; } float CharacterAnimation::GetCurrentNormalizedProgress() const { int totalFrames = GetCurrentFrameCount(); if (totalFrames <= 1) { return IsCurrentActionFinished() ? 1.0f : 0.0f; } float progress = static_cast(GetCurrentFrameIndex()) / static_cast(totalFrames - 1); return std::clamp(progress, 0.0f, 1.0f); } animation::AniFrame CharacterAnimation::GetCurrentFrameInfo() const { const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); if (!primaryAnimation) { return animation::AniFrame(); } return primaryAnimation->GetCurrentFrameInfo(); } void CharacterAnimation::SetActionFrameFlagCallback(ActionFrameFlagCallback callback) { actionFrameFlagCallback_ = std::move(callback); RefreshRuntimeCallbacks(); } void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) { actionEndCallback_ = std::move(callback); RefreshRuntimeCallbacks(); } } // namespace frostbite2D