#include <Urho3D/Core/CoreEvents.h>
#include <Urho3D/Audio/Audio.h>
#include <Urho3D/Audio/Sound.h>
#include <Urho3D/Audio/SoundSource.h>
#include <Urho3D/Audio/SoundSource3D.h>
#include <Urho3D/Audio/SoundListener.h>
#include <Urho3D/Engine/Engine.h>
#include <Urho3D/Graphics/AnimatedModel.h>
#include <Urho3D/Graphics/Animation.h>
#include <Urho3D/Graphics/AnimationState.h>
#include <Urho3D/Graphics/Camera.h>
#include <Urho3D/Graphics/DebugRenderer.h>
#include <Urho3D/Graphics/Graphics.h>
#include <Urho3D/Graphics/Light.h>
#include <Urho3D/Graphics/Material.h>
#include <Urho3D/Graphics/Model.h>
#include <Urho3D/Graphics/Octree.h>
#include <Urho3D/Graphics/Renderer.h>
#include <Urho3D/Graphics/Skybox.h>
#include <Urho3D/Graphics/Zone.h>
#include <Urho3D/IO/File.h>
#include <Urho3D/IO/FileSystem.h>
#include <Urho3D/Input/Input.h>
#include <Urho3D/Math/Quaternion.h>
#include <Urho3D/Math/Random.h>
#include <Urho3D/Physics/CollisionShape.h>
#include <Urho3D/Physics/Constraint.h>
#include <Urho3D/Physics/PhysicsWorld.h>
#include <Urho3D/Physics/RigidBody.h>
#include <Urho3D/Resource/ResourceCache.h>
#include <Urho3D/Scene/Scene.h>
#include <Urho3D/Scene/ValueAnimation.h>
#include <Urho3D/UI/Font.h>
#include <Urho3D/UI/Text.h>
#include <Urho3D/UI/UI.h>

#include <cmath>

#include "Airborne.h"
#include "Common.h"
#include "Bunion.h"
#include "Character.h"
#include "Grower.h"
#include "Splitter.h"
#include "Remover.h"

#include <Urho3D/DebugNew.h>

#ifdef __EMSCRIPTEN__
    #include <emscripten/html5.h>
#endif

namespace {
    constexpr auto FOG_END = 500.0f;
    constexpr auto PLAYER_CAPSULE_DIAMETER = 1.0f;
    const auto TONGUE_SCALE = Vector3(0.5f, 0.01f, 0.3f);
    auto constexpr BUNION_Y = 5.0f;
    const auto BUNION_START_POSITION = Vector3(0.0f, BUNION_Y, 10.0f);
}

URHO3D_DEFINE_APPLICATION_MAIN(Bunion)

Bunion::Bunion(Context* context) : Sample(context), drawDebug_{false} {
    Airborne::RegisterObject(context);
    Character::RegisterObject(context);
    Grower::RegisterObject(context);
    Remover::RegisterObject(context);
    Splitter::RegisterObject(context);
}

void Bunion::Setup() {
    engineParameters_[EP_WINDOW_TITLE] = GetTypeName();
    engineParameters_[EP_FULL_SCREEN] = false;
    engineParameters_[EP_HEADLESS] = false;
    engineParameters_[EP_SOUND] = true;
    engineParameters_["TextureFilterMode"] = FILTER_NEAREST;

    engineParameters_[EP_LOG_NAME] =
        GetSubsystem<FileSystem>()->GetAppPreferencesDir("urho3d", "logs")
        + GetTypeName() + ".log";


#ifdef __EMSCRIPTEN__
    {
        // Resize graphics system if the canvas has changed size.

        double client_w, client_h;
        emscripten_get_element_css_size(nullptr, &client_w, &client_h);
        auto const width_new = static_cast<int>(client_w);
        auto const height_new = static_cast<int>(client_h);

        engineParameters_[EP_WINDOW_WIDTH] = width_new;
        engineParameters_[EP_WINDOW_HEIGHT] = height_new;
    }
#endif



    // Construct a search path to find the resource prefix with two entries:
    // The first entry is an empty path which will be substituted with
    // program/bin directory -- this entry is for binary when it is still in
    // build tree The second and third entries are possible relative paths from
    // the installed program/bin directory to the asset directory -- these
    // entries are for binary when it is in the Urho3D SDK installation
    // location
    if (!engineParameters_.Contains(EP_RESOURCE_PREFIX_PATHS)) {
        engineParameters_[EP_RESOURCE_PREFIX_PATHS] =
            ";../share/Resources;../share/Urho3D/Resources";
    }

}

void Bunion::Start() {
    // Execute base class startup
    Sample::Start();

    // Create the scene content
    CreateScene();

    // Setup the viewport for displaying the scene
    SetupViewport();

    // Hook up to the frame update and render post-update events
    SubscribeToEvents();

    // Set the mouse mode to use in the sample
    Sample::InitMouseMode(MM_RELATIVE);

    // Music
    {
        auto const source = scene_->CreateComponent<SoundSource>();
        source->SetSoundType(SOUND_MUSIC);

        auto const cache = GetSubsystem<ResourceCache>();
        auto const music = cache->GetResource<Sound>("fds/music.ogg");
        music->SetLooped(true);
        source->SetGain(0.50f);
        source->Play(music);
    }
}

void Bunion::CreateScene() {
    auto &cache = *GetSubsystem<ResourceCache>();

    scene_ = new Scene(context_);

    scene_ = new Scene(context_);

    auto sceneFile = cache.GetResource<XMLFile>("fds/BunionScene.xml");
    scene_->LoadXML(sceneFile->GetRoot());

    // Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000,
    // 1000) Create a physics simulation world with default parameters, which
    // will update at 60fps. Like the Octree must exist before creating
    // drawable components, the PhysicsWorld must exist before creating physics
    // components.  Finally, create a DebugRenderer component so that we can
    // draw physics debug geometry
    scene_->CreateComponent<DebugRenderer>();

    // Create a Zone component for ambient lighting & fog control
    Node* zoneNode = scene_->CreateChild("Zone");
    Zone* zone = zoneNode->CreateComponent<Zone>();
    zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.0f));
    zone->SetAmbientColor(Color(0.35f, 0.35f, 0.35f));
    zone->SetFogColor({0.890625f, 1.0f, 0.984375f});
    zone->SetFogStart(300.0f);
    zone->SetFogEnd(FOG_END);


    CreatePlayer();
    {
        bunion_ = scene_->CreateChild("Player");
        bunion_->SetPosition(BUNION_START_POSITION);
        auto playerObject = bunion_->CreateComponent<AnimatedModel>();
        auto model = cache.GetResource<Model>("fds/FrogSphere.mdl");
        playerObject->SetModel(model);
        playerObject->SetMaterial(cache.GetResource<Material>("fds/FrogSphereMaterial.xml"));
        playerObject->SetCastShadows(true);

        bunion_->CreateComponent<Grower>();
        bunion_->CreateComponent<Airborne>();

        RigidBody* body = bunion_->CreateComponent<RigidBody>();
        body->SetMass(1.0f);
        body->SetFriction(0.75f);
        CollisionShape* shape = bunion_->CreateComponent<CollisionShape>();
        shape->SetTriangleMesh(model);

        {
            auto mouthAnimation = cache.GetResource<Animation>("fds/FrogAnimation.Mouth.ani");
            auto state = playerObject->AddAnimationState(mouthAnimation);

            if (state) {
                state->SetWeight(0.0f);
            }
        }

        {
            auto animation = cache.GetResource<Animation>("fds/FrogBrowIdle.ani");
            auto state = playerObject->AddAnimationState(animation);

            if (state) {
                state->SetWeight(1.0f);
                state->SetTime(0.0f);
                state->SetStartBone(playerObject->GetSkeleton().GetBone("Centre.Brow"));
                state->SetLooped(true);
            }
        }

        {
            auto animation = cache.GetResource<Animation>("fds/FrogEyesHit.ani");
            animation->SetAnimationName("FrogEyesHit");
            auto state = playerObject->AddAnimationState(animation);

            if (state) {
                state->SetWeight(0.0f);
                state->SetTime(0.0f);
                state->SetStartBone(playerObject->GetSkeleton().GetBone("Centre.Eyes"));
            }
        }

        {
            auto leftArmAnimation = cache.GetResource<Animation>("fds/FrogLeftArmWiggle.ani");
            auto state = playerObject->AddAnimationState(leftArmAnimation);

            if (state) {
                state->SetWeight(1.0f);
                state->SetTime(0.0f);
                state->SetStartBone(playerObject->GetSkeleton().GetBone("Left.Arm"));
                state->SetLooped(true);
            }
        }

        {
            auto animation = cache.GetResource<Animation>("fds/FrogLegWiggle.Left.ani");
            auto state = playerObject->AddAnimationState(animation);

            if (state) {
                state->SetWeight(1.0f);
                state->SetTime(0.0f);
                state->SetStartBone(playerObject->GetSkeleton().GetBone("Left.Leg"));
                state->SetLooped(true);
            }
        }

        {
            auto animation = cache.GetResource<Animation>("fds/FrogLegWiggle.Right.ani");
            auto state = playerObject->AddAnimationState(animation);

            if (state) {
                state->SetWeight(1.0f);
                state->SetTime(0.0f);
                state->SetStartBone(playerObject->GetSkeleton().GetBone("Right.Leg"));
                state->SetLooped(true);
            }
        }


        tongue_ = scene_->CreateChild("Tongue");
        tongue_->SetPosition(Vector3(BUNION_START_POSITION.x_, BUNION_Y + 0.9f, BUNION_START_POSITION.z_ - 1.0f));
        tongue_->SetRotation( Quaternion(15.0f, 0.0f, -90.0f));

        auto tongueObject = tongue_->CreateComponent<StaticModel>();
        auto tongueBody = tongue_->CreateComponent<RigidBody>();
        auto tongueShape = tongue_->CreateComponent<CollisionShape>();
        auto tongueConstraint = tongue_->CreateComponent<Constraint>();

        tongueObject->SetModel(cache.GetResource<Model>("Models/Box.mdl"));
        tongueObject->SetMaterial(cache.GetResource<Material>("fds/FrogTongueMaterial.xml"));
        tongueObject->SetCastShadows(true);
        tongueShape->SetBox(Vector3::ONE);
        tongue_->SetScale(TONGUE_SCALE);
        tongueBody->SetMass(0.001f);
        tongueBody->SetFriction(0.05f);
        tongueConstraint->SetConstraintType(CONSTRAINT_HINGE);
        tongueConstraint->SetOtherBody(body);
        tongueConstraint->SetAxis(Vector3::RIGHT);
        tongueConstraint->SetOtherAxis(Vector3::LEFT);
        tongueConstraint->SetLowLimit(Vector2(-20.0f, 0.0f)); // Let the wheel rotate freely around the axis
        tongueConstraint->SetHighLimit(Vector2(20.0f, 0.0f));
        tongueConstraint->SetWorldPosition(Vector3(BUNION_START_POSITION.x_, BUNION_Y + 0.9f, BUNION_START_POSITION.z_ - 0.95f));
        tongueConstraint->SetDisableCollision(true);
    }

    CreateInstructions();
}

void Bunion::CreatePlayer() {
    constexpr auto HEIGHT = 5.0f;

    playerNode_ = scene_->CreateChild("Player");
    playerNode_->AddTag("player");
    playerNode_->SetPosition({0.0f, HEIGHT, -20.0f});
    character_ = playerNode_->CreateComponent<Character>();

    auto &body = *playerNode_->CreateComponent<RigidBody>();

    // A non-zero mass makes body dynamic.
    body.SetMass(1.0f);

    // Prevent physics from turning the player.
    body.SetAngularFactor(Vector3::ZERO);

    // Signal collision even when resting, so that we get ground collisions
    // properly.
    body.SetCollisionEventMode(COLLISION_ALWAYS);

    // Capsule collision shape.
    auto &shape = *playerNode_->CreateComponent<CollisionShape>();
    shape.SetCapsule(PLAYER_CAPSULE_DIAMETER, HEIGHT);

    // Camera
    auto const cameraNode = playerNode_->CreateChild("Camera");
    auto const camera = cameraNode->CreateComponent<Camera>();
    camera->SetFarClip(FOG_END);


    // 3D Audio
    auto const listener = playerNode_->CreateComponent<SoundListener>();
    GetSubsystem<Audio>()->SetListener(listener);
}

void Bunion::SetupViewport()
{
    Renderer* renderer = GetSubsystem<Renderer>();

    // Set up a viewport to the Renderer subsystem so that the 3D scene can be
    // seen.
    SharedPtr<Viewport> viewport(new Viewport(context_, scene_, playerNode_->GetChild("Camera")->GetComponent<Camera>()));
    renderer->SetViewport(0, viewport);


}

void Bunion::SubscribeToEvents()
{
    // Subscribe HandleUpdate() function for processing update events
    SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(Bunion, HandleUpdate));

    // Subscribe HandlePostRenderUpdate() function for processing the
    // post-render update event, during which we request debug geometry
    SubscribeToEvent(E_POSTRENDERUPDATE,
                     URHO3D_HANDLER(Bunion, HandlePostRenderUpdate));
}


void Bunion::MoveCamera(Input &input, float const timeStep) {
    auto &controls = character_->controls_;

    auto const set = [&input, &controls](unsigned const control, Urho3D::Key const key) {
        controls.Set(control, input.GetKeyDown(key));
    };

    set(CTRL_FORWARD, KEY_W);
    set(CTRL_BACK, KEY_S);
    set(CTRL_LEFT, KEY_A);
    set(CTRL_RIGHT, KEY_D);

    auto const scale = [&input](int const value) {
        return static_cast<float>(value) * YAW_SENSITIVITY;
    };

    auto const pitch = Clamp(
            controls.pitch_ + scale(input.GetMouseMoveY()),
            -60.0f, 60.0f
            );

    controls.yaw_ += scale(input.GetMouseMoveX());
    controls.pitch_ = pitch;

    playerNode_->SetRotation({0.0f, controls.yaw_, 0.0f});
    playerNode_->GetChild("Camera")->SetRotation({pitch, 0.0f, 0.0f});
}

void Bunion::CreateInstructions()
{
    ResourceCache* cache = GetSubsystem<ResourceCache>();
    UI* ui = GetSubsystem<UI>();

    Text* titleText = ui->GetRoot()->CreateChild<Text>();
    titleText->SetText("Bunion");
    titleText->SetFont(cache->GetResource<Font>("fds/0_CocogoosePro-Regular.ttf"), 32);
    titleText->SetTextAlignment(HA_CENTER);
    titleText->SetHorizontalAlignment(HA_CENTER);
    titleText->SetVerticalAlignment(VA_CENTER);
    titleText->SetPosition(0, -ui->GetRoot()->GetHeight() / 2.5);
    titleText->SetTextEffect(TE_SHADOW);
    {
        ValueAnimation* anim{ new ValueAnimation{ context_ } };
        anim->SetKeyFrame(0.0f, 0.0f);
        anim->SetKeyFrame(0.5f, 1.0f);
        anim->SetKeyFrame(10.0f, 1.0f);
        anim->SetKeyFrame(12.0f, 0.0f);
        titleText->SetAttributeAnimation("Opacity", anim, WM_ONCE);
    }

    Text* instructionText = ui->GetRoot()->CreateChild<Text>();
    instructionText->SetName("Instructions");
    instructionText->SetText(
        "Use WASD keys to move and mouse to look\n"
        "Left mouse button shoots tofu\n"
    );
    instructionText->SetFont(cache->GetResource<Font>("fds/0_CocogoosePro-Regular.ttf"), 15);
    instructionText->SetTextAlignment(HA_CENTER);

    instructionText->SetHorizontalAlignment(HA_CENTER);
    instructionText->SetVerticalAlignment(VA_CENTER);
    instructionText->SetPosition(0, ui->GetRoot()->GetHeight() / 4);
    instructionText->SetTextEffect(TE_SHADOW);
    {
        ValueAnimation* anim{ new ValueAnimation{ context_ } };
        anim->SetKeyFrame(0.0f, 0.0f);
        anim->SetKeyFrame(0.5f, 1.0f);
        anim->SetKeyFrame(10.0f, 1.0f);
        anim->SetKeyFrame(12.0f, 0.0f);
        instructionText->SetAttributeAnimation("Opacity", anim, WM_ONCE);
    }

    Text* creditsText = ui->GetRoot()->CreateChild<Text>();
    creditsText->SetName("Credits");
    creditsText->SetText("Game by Foxdog Studios\nLloyd Henning\nPeter Sutton\n\nMusic by Mattuliser\n\nFor Ludum Dare 40\nMade using Urho3D, Blender, nvim and Pepsi");
    creditsText->SetFont(cache->GetResource<Font>("fds/0_CocogoosePro-Regular.ttf"), 12);
    creditsText->SetTextAlignment(HA_RIGHT);
    creditsText->SetHorizontalAlignment(HA_CENTER);
    creditsText->SetVerticalAlignment(VA_CENTER);
    creditsText->SetPosition(ui->GetRoot()->GetHeight() / 4, -ui->GetRoot()->GetHeight() / 3.5);
    creditsText->SetTextEffect(TE_SHADOW);
    creditsText->SetOpacity(0.0f);
}

void Bunion::HandleMiscInput(Input const &input) {
    if (input.GetMouseButtonPress(MOUSEB_LEFT)) {
        SpawnObject();
    }
}

void Bunion::SpawnObject() {
    ResourceCache* cache = GetSubsystem<ResourceCache>();
    auto const boxNode = scene_->CreateChild("SmallBox");

    auto const rotation = Quaternion(
        playerNode_->GetChild("Camera")->GetRotation().PitchAngle(),
        playerNode_->GetRotation().YawAngle(),
        0.0f
    );

    boxNode->SetPosition(playerNode_->GetPosition() + (
        rotation * Vector3{
            0.0f,
            0.0f,
            (PLAYER_CAPSULE_DIAMETER + BULLET_SCALE) / 2.0f
        }
    ));

    auto const getRandRot = []() { return Random(-40.0f, 40.0f); };

    boxNode->SetRotation({
        rotation.PitchAngle() + getRandRot(),
        rotation.YawAngle() + getRandRot(),
        rotation.RollAngle() + getRandRot()
    });

    boxNode->CreateComponent<Splitter>();
    boxNode->CreateComponent<Remover>();

    boxNode->SetScale(BULLET_SCALE);
    boxNode->AddTag("animate-on-hit");
    boxNode->AddTag("eye-roll-on-hit");
    StaticModel* boxObject = boxNode->CreateComponent<StaticModel>();
    boxObject->SetModel(cache->GetResource<Model>("fds/Tofu.mdl"));
    boxObject->SetMaterial(cache->GetResource<Material>("fds/TofuMaterialHappy.xml"));
    boxObject->SetCastShadows(true);

    // Create physics components, use a smaller mass also
    RigidBody* body = boxNode->CreateComponent<RigidBody>();
    body->SetMass(0.1f);
    body->SetFriction(0.75f);
    CollisionShape* shape = boxNode->CreateComponent<CollisionShape>();
    shape->SetBox(Vector3::ONE * 2);

    const float OBJECT_VELOCITY = 15.0f;

    // Set initial velocity for the RigidBody based on camera forward vector.
    // Add also a slight up component to overcome gravity better.
    body->SetLinearVelocity(rotation * Vector3(0.0f, 0.05f, 1.0f) * OBJECT_VELOCITY);

    PlaySFX(*playerNode_, "throw", 0.10f, 0.05f);
}

void Bunion::HandleUpdate(StringHash eventType, VariantMap& eventData)
{
    using namespace Update;


    // Take the frame time step, which is stored as a float.
    auto const timeStep = eventData[P_TIMESTEP].GetFloat();

    // Move the camera, scale movement with time step, if the UI is not focused.
    if (!GetSubsystem<UI>()->GetFocusElement()) {
      auto &input = *GetSubsystem<Input>();
      MoveCamera(input, timeStep);
      HandleMiscInput(input);
    }

    AnimatedModel* model = bunion_->GetComponent<AnimatedModel>(true);
    for (auto state : model->GetAnimationStates()) {
        state->AddTime(timeStep);
    }

    auto const cache = GetSubsystem<ResourceCache>();
    auto bodies = PODVector<RigidBody*>{};
    tongue_->GetComponent<RigidBody>(true)->GetCollidingBodies(bodies);

    for (auto rb : bodies) {
        auto node = rb->GetNode();
        if (node->HasTag("animate-on-hit")) {
            tofuEaten_ += 1;
            node->RemoveTag("animate-on-hit");
            node->RemoveTag("eye-roll-on-hit");
            node->GetComponent<Splitter>()->Split();
            bunion_->GetComponent<Grower>()->Grow(tofuEaten_);
            if (model->GetNumAnimationStates() > 0)
            {
                AnimationState* state = model->GetAnimationStates()[0];
                state->SetWeight(1.0f);
                state->SetTime(0.0f);
            }

            PlaySFX(*bunion_, "crunch", 0.10, 0.05f);

            if (tofuEaten_ % 3 == 0) {
                {
                    auto const tongueScale = tongue_->GetScale();
                    SharedPtr<ValueAnimation> tongueScaleAnimation{new ValueAnimation(context_)};

                    tongueScaleAnimation->SetKeyFrame(
                            GROWTH_START_TIME,
                            Vector3{tongueScale.x_, tongueScale.y_, tongueScale.z_});

                    tongueScaleAnimation->SetKeyFrame(
                        GROWTH_END_TIME,
                        Vector3(
                            tongueScale.x_ + (PLAYER_SCALE_INC * tofuEaten_ * TONGUE_SCALE.x_),
                            tongueScale.y_ + (PLAYER_SCALE_INC * tofuEaten_ * TONGUE_SCALE.y_),
                            tongueScale.z_ + (PLAYER_SCALE_INC * tofuEaten_ * TONGUE_SCALE.z_)
                        )
                    );
                    tongue_->SetAttributeAnimation("Scale", tongueScaleAnimation, WM_ONCE);
                }
            }
        }
    }

    {
        auto bodies = PODVector<RigidBody*>{};
        bunion_->GetComponent<RigidBody>(true)->GetCollidingBodies(bodies);
        for (auto rb: bodies) {
            auto node = rb->GetNode();
            if (node->HasTag("eye-roll-on-hit")) {
                PlaySFX(*bunion_, "thunk", 0.05f, 0.10f);
                node->RemoveTag("eye-roll-on-hit");
                auto state = model->GetAnimationState(String{"FrogEyesHit"});
                state->SetWeight(1.0f);
                state->SetTime(0.0f);
            }
        }
    }

    {
        auto constexpr KILL_FLOOR = -100;
        if (bunion_->GetPosition().y_ < KILL_FLOOR) {
            bunion_->RemoveTag("said-goodbye");
            auto position = BUNION_START_POSITION;
            position.y_ += bunion_->GetScale().y_;
            bunion_->SetPosition(position);
            auto body = bunion_->GetComponent<RigidBody>();
            bunion_->SetScale(Vector3::ONE);
            tongue_->SetScale(TONGUE_SCALE);
            body->SetLinearVelocity(Vector3::ZERO);
            body->SetAngularVelocity(Vector3::ZERO);
            bunion_->SetRotation(Quaternion());
            tofuEaten_ = 0;
            UI* ui = GetSubsystem<UI>();
            auto textBox = ui->GetRoot()->GetChildDynamicCast<Text>(String{"Instructions"});
            textBox->SetText("Back for more.");
            textBox->RemoveAttributeAnimation("Opacity");
            {
                auto constexpr OFFSET = 0.4f;
                ValueAnimation* anim{ new ValueAnimation{ context_ } };
                anim->SetKeyFrame(0.0f, 0.0f);
                anim->SetKeyFrame(OFFSET + 0.4f, 0.0f);
                anim->SetKeyFrame(OFFSET + 0.5f, 1.0f);
                anim->SetKeyFrame(OFFSET + 3.0f, 1.0f);
                anim->SetKeyFrame(OFFSET + 4.0f, 0.0f);
                textBox->SetAttributeAnimation("Opacity", anim, WM_ONCE);
            }
        } else {
            auto constexpr GOODBYE_FLOOR = -5;
            if (bunion_->GetPosition().y_ < GOODBYE_FLOOR && !bunion_->HasTag("said-goodbye")) {
                bunion_->AddTag("said-goodbye");
                UI* ui = GetSubsystem<UI>();
                auto textBox = ui->GetRoot()->GetChildDynamicCast<Text>(String{"Instructions"});
                textBox->SetText("Oh. Farewell.");
                textBox->RemoveAttributeAnimation("Opacity");
                {
                    auto constexpr OFFSET = 0.0f;
                    ValueAnimation* anim{ new ValueAnimation{ context_ } };
                    anim->SetKeyFrame(0.0f, 0.0f);
                    anim->SetKeyFrame(OFFSET + 0.5f, 1.0f);
                    anim->SetKeyFrame(OFFSET + 4.0f, 0.0f);
                    textBox->SetAttributeAnimation("Opacity", anim, WM_ONCE);
                }
            }
        }
    }
}

void Bunion::HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
{
    // If draw debug mode is enabled, draw physics debug geometry. Use depth
    // test to make the result easier to interpret.
    if (drawDebug_) {
        scene_->GetComponent<PhysicsWorld>()->DrawDebugGeometry(true);
    }
}

void Bunion::PlaySFX(Node &sourceNode, String const &name, float const gain,
                     float const frequencyRange) {
    auto const path = String{"fds/"} + name + ".wav";
    auto const sound = GetSubsystem<ResourceCache>()->GetResource<Sound>(path);
    auto const source = sourceNode.CreateComponent<SoundSource3D>();
    source->SetAutoRemoveMode(REMOVE_COMPONENT);
    source->SetNearDistance(1.0f);
    source->SetFarDistance(50.0f);
    auto const halfFR = frequencyRange / 2.0f;
    source->SetFrequency(
            sound->GetFrequency() * Random(1.0f - halfFR, 1.0f + halfFR));
    source->SetGain(gain);
    source->SetSoundType(SOUND_EFFECT);
    source->Play(sound);
}
