#include "npc/NpcObject.h" #include "common/math/GameMath.h" #include namespace frostbite2D { namespace { constexpr float kNpcNameMarginY = 8.0f; constexpr float kNpcNameFallbackTopY = -96.0f; const Vec2 kNpcNameOutlineOffsets[] = { Vec2(-1.0f, -1.0f), Vec2(-1.0f, 0.0f), Vec2(-1.0f, 1.0f), Vec2(0.0f, -1.0f), Vec2(0.0f, 1.0f), Vec2(1.0f, -1.0f), Vec2(1.0f, 0.0f), Vec2(1.0f, 1.0f), }; void ConfigureNameTextLabel(RefPtr label, const char* name, const Color& color, int zOrder) { if (!label) { return; } label->SetName(name); label->SetFont("default"); label->SetAnchor(0.5f, 1.0f); label->SetTextColor(color); label->SetZOrder(zOrder); } const std::string& EmptyString() { static const std::string kEmpty; return kEmpty; } } // namespace bool NpcObject::Construction(int npcId) { RemoveAllChildren(); npcId_ = -1; direction_ = 1; worldPosition_ = CharacterWorldPosition(); config_.reset(); animation_ = nullptr; interactionHighlight_ = nullptr; nameOutlineLabels_.fill(nullptr); nameLabel_ = nullptr; interactable_ = true; interacting_ = false; interactionHighlighted_ = false; syncedHighlightCompositeVersion_ = 0; auto config = npc::loadNpcConfig(npcId); if (!config) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "NpcObject: failed to load npc config %d", npcId); return false; } auto animation = MakePtr(); if (!animation->Init(*config)) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "NpcObject: failed to init npc animation %d", npcId); return false; } SDL_Log("NpcObject: npc %d config loaded name=%s fieldName=%s ani=%s", npcId, config->name.c_str(), config->fieldName.c_str(), config->fieldAnimationPath.c_str()); config_ = *config; npcId_ = npcId; animation_ = animation; EnsureInteractionHighlight(); AddChild(animation_); EnsureNameLabel(); SetWorldPosition({}); SetDirection(1); SetInteractionHighlighted(false); RefreshNameLabel(); SDL_Log("NpcObject: npc %d construction complete", npcId_); return true; } void NpcObject::SetDirection(int direction) { direction_ = direction >= 0 ? 1 : -1; if (animation_) { animation_->SetDirection(direction_); } RefreshNameLabel(); SyncInteractionHighlight(); } void NpcObject::SetNpcPosition(const Vec2& pos) { worldPosition_.x = gameMath::RoundWorldCoordinate(pos.x); worldPosition_.y = gameMath::RoundWorldCoordinate(pos.y); SDL_Log("NpcObject: npc %d set ground position to (%d, %d, %d)", npcId_, worldPosition_.x, worldPosition_.y, worldPosition_.z); SyncActorPositionFromWorld(); } void NpcObject::SetWorldPosition(const CharacterWorldPosition& pos) { worldPosition_ = pos; SDL_Log("NpcObject: npc %d set world position to (%d, %d, %d)", npcId_, worldPosition_.x, worldPosition_.y, worldPosition_.z); SyncActorPositionFromWorld(); } void NpcObject::SetInteractable(bool interactable) { interactable_ = interactable; if (!interactable_) { interacting_ = false; } } void NpcObject::SetInteractionHighlighted(bool highlighted) { if (interactionHighlighted_ == highlighted) { SyncInteractionHighlight(); return; } interactionHighlighted_ = highlighted; if (!interactionHighlighted_) { syncedHighlightCompositeVersion_ = 0; } SyncInteractionHighlight(); } void NpcObject::BeginInteract() { if (CanInteract()) { interacting_ = true; } } void NpcObject::EndInteract() { interacting_ = false; } bool NpcObject::CanInteract() const { return interactable_ && config_.has_value(); } const std::string& NpcObject::GetName() const { return config_ ? config_->name : EmptyString(); } const std::string& NpcObject::GetFieldName() const { return config_ ? config_->fieldName : EmptyString(); } const std::string& NpcObject::GetDisplayName() const { const std::string& fieldName = GetFieldName(); if (!fieldName.empty()) { return fieldName; } return GetName(); } bool NpcObject::IsAnimationFinished() const { return animation_ ? animation_->IsAnimationFinished() : false; } void NpcObject::Update(float deltaTime) { Actor::Update(deltaTime); SyncInteractionHighlight(); } void NpcObject::EnsureInteractionHighlight() { if (interactionHighlight_) { return; } interactionHighlight_ = MakePtr(); interactionHighlight_->SetZOrder(-1); interactionHighlight_->SetPosition(0.0f, 0.0f); interactionHighlight_->SetVisible(false); AddChild(interactionHighlight_); } void NpcObject::EnsureNameLabel() { if (nameLabel_) { return; } for (size_t i = 0; i < nameOutlineLabels_.size(); ++i) { RefPtr outlineLabel = TextSprite::create(); ConfigureNameTextLabel(outlineLabel, "npcNameOutline", Color(0.0f, 0.0f, 0.0f, 1.0f), 1); nameOutlineLabels_[i] = outlineLabel; AddChild(outlineLabel); } nameLabel_ = TextSprite::create(); ConfigureNameTextLabel(nameLabel_, "npcName", Color(1.0f, 0.96f, 0.78f, 1.0f), 2); AddChild(nameLabel_); } void NpcObject::RefreshNameLabel() { if (!nameLabel_) { return; } const std::string& displayName = GetDisplayName(); bool hasDisplayName = !displayName.empty(); nameLabel_->SetVisible(hasDisplayName); for (auto& outlineLabel : nameOutlineLabels_) { if (outlineLabel) { outlineLabel->SetVisible(hasDisplayName); } } if (!hasDisplayName) { return; } nameLabel_->SetText(displayName); for (auto& outlineLabel : nameOutlineLabels_) { if (outlineLabel) { outlineLabel->SetText(displayName); } } Rect displayBounds; float labelBaseY = kNpcNameFallbackTopY; if (animation_ && animation_->GetDisplayLocalBounds(displayBounds)) { labelBaseY = displayBounds.top() - kNpcNameMarginY; } nameLabel_->SetPosition(0.0f, labelBaseY); for (size_t i = 0; i < nameOutlineLabels_.size(); ++i) { if (!nameOutlineLabels_[i]) { continue; } const Vec2& offset = kNpcNameOutlineOffsets[i]; nameOutlineLabels_[i]->SetPosition(offset.x, labelBaseY + offset.y); } } void NpcObject::SyncInteractionHighlight() { if (!interactionHighlight_) { return; } if (!interactionHighlighted_ || !animation_ || !animation_->EnsureCompositeTextureReady() || !animation_->HasCompositeTexture()) { interactionHighlight_->SetVisible(false); return; } uint64 compositeVersion = animation_->GetCompositeVersion(); if (syncedHighlightCompositeVersion_ != compositeVersion) { Ptr compositeTexture = animation_->GetCompositeTexture(); Vec2 compositeSize = animation_->GetCompositeTextureSize(); if (!compositeTexture || compositeSize.x <= 0.0f || compositeSize.y <= 0.0f) { interactionHighlight_->SetVisible(false); return; } interactionHighlight_->SetTexture(compositeTexture); interactionHighlight_->SetSize(compositeSize); interactionHighlight_->SetGroundAnchorInTexture( animation_->GetCompositeGroundAnchorInTexture()); interactionHighlight_->SetPosition(0.0f, 0.0f); syncedHighlightCompositeVersion_ = compositeVersion; } interactionHighlight_->SetVisible(true); } void NpcObject::SyncActorPositionFromWorld() { SetPosition(worldPosition_.ToScreenPosition()); SetZOrder(worldPosition_.y); } } // namespace frostbite2D