diff --git a/assets/opensb/interface/optionsmenu/optionsmenu.config.patch b/assets/opensb/interface/optionsmenu/optionsmenu.config.patch index a169bef..4755b45 100644 --- a/assets/opensb/interface/optionsmenu/optionsmenu.config.patch +++ b/assets/opensb/interface/optionsmenu/optionsmenu.config.patch @@ -54,6 +54,24 @@ "instrumentSlider" : { "type" : "slider", "position" : [62, 158], "gridImage" : "/interface/optionsmenu/largeselection.png" }, "instrumentLabel" : { "type" : "label", "position" : [32, 158], "value" : "Tunes" }, - "instrumentValueLabel" : { "type" : "label", "position" : [192, 158], "hAnchor" : "mid", "value" : "Replace Me" } + "instrumentValueLabel" : { "type" : "label", "position" : [192, 158], "hAnchor" : "mid", "value" : "Replace Me" }, + + "headRotationLabel" : { + "type" : "label", + "position" : [25, 51], + "hAnchor" : "left", + "value" : "HEAD ROTATION" + }, + "headRotationCheckbox" : { + "type" : "button", + "pressedOffset" : [0, 0], + "position" : [104, 51], + "base" : "/interface/optionsmenu/checkboxnocheck.png", + "hover" : "/interface/optionsmenu/checkboxnocheckhover.png", + "baseImageChecked" : "/interface/optionsmenu/checkboxcheck.png", + "hoverImageChecked" : "/interface/optionsmenu/checkboxcheckhover.png", + "checkable" : true, + "checked" : true + } } } \ No newline at end of file diff --git a/source/frontend/StarOptionsMenu.cpp b/source/frontend/StarOptionsMenu.cpp index 8253f1f..28d44f6 100644 --- a/source/frontend/StarOptionsMenu.cpp +++ b/source/frontend/StarOptionsMenu.cpp @@ -10,6 +10,7 @@ #include "StarVoiceSettingsMenu.hpp" #include "StarBindingsMenu.hpp" #include "StarGraphicsMenu.hpp" +#include "StarHumanoid.hpp" namespace Star { @@ -47,6 +48,9 @@ OptionsMenu::OptionsMenu(PaneManager* manager, UniverseClientPtr client) reader.registerCallback("allowAssetsMismatchCheckbox", [=](Widget*) { updateAllowAssetsMismatch(); }); + reader.registerCallback("headRotationCheckbox", [=](Widget*) { + updateHeadRotation(); + }); reader.registerCallback("backButton", [=](Widget*) { dismiss(); }); @@ -77,6 +81,7 @@ OptionsMenu::OptionsMenu(PaneManager* manager, UniverseClientPtr client) m_clientIPJoinableButton = fetchChild("clientIPJoinableCheckbox"); m_clientP2PJoinableButton = fetchChild("clientP2PJoinableCheckbox"); m_allowAssetsMismatchButton = fetchChild("allowAssetsMismatchCheckbox"); + m_headRotationButton = fetchChild("headRotationCheckbox"); m_instrumentLabel = fetchChild("instrumentValueLabel"); m_sfxLabel = fetchChild("sfxValueLabel"); @@ -115,7 +120,8 @@ StringList const OptionsMenu::ConfigKeys = { "tutorialMessages", "clientIPJoinable", "clientP2PJoinable", - "allowAssetsMismatch" + "allowAssetsMismatch", + "humanoidHeadRotation" }; void OptionsMenu::initConfig() { @@ -166,6 +172,12 @@ void OptionsMenu::updateAllowAssetsMismatch() { Root::singleton().configuration()->set("allowAssetsMismatch", m_allowAssetsMismatchButton->isChecked()); } +void OptionsMenu::updateHeadRotation() { + m_localChanges.set("humanoidHeadRotation", m_headRotationButton->isChecked()); + Root::singleton().configuration()->set("humanoidHeadRotation", m_headRotationButton->isChecked()); + Humanoid::globalHeadRotation() = m_headRotationButton->isChecked(); +} + void OptionsMenu::syncGuiToConf() { m_instrumentSlider->setVal(m_localChanges.get("instrumentVol").toInt(), false); m_instrumentLabel->setText(toString(m_instrumentSlider->val())); @@ -180,6 +192,7 @@ void OptionsMenu::syncGuiToConf() { m_clientIPJoinableButton->setChecked(m_localChanges.get("clientIPJoinable").toBool()); m_clientP2PJoinableButton->setChecked(m_localChanges.get("clientP2PJoinable").toBool()); m_allowAssetsMismatchButton->setChecked(m_localChanges.get("allowAssetsMismatch").toBool()); + m_headRotationButton->setChecked(m_localChanges.get("humanoidHeadRotation").optBool().value(true)); auto appController = GuiContext::singleton().applicationController(); if (!appController->p2pNetworkingService()) { diff --git a/source/frontend/StarOptionsMenu.hpp b/source/frontend/StarOptionsMenu.hpp index 9fde1ac..4a28fd0 100644 --- a/source/frontend/StarOptionsMenu.hpp +++ b/source/frontend/StarOptionsMenu.hpp @@ -36,6 +36,7 @@ private: void updateClientIPJoinable(); void updateClientP2PJoinable(); void updateAllowAssetsMismatch(); + void updateHeadRotation(); void syncGuiToConf(); @@ -52,6 +53,7 @@ private: ButtonWidgetPtr m_clientIPJoinableButton; ButtonWidgetPtr m_clientP2PJoinableButton; ButtonWidgetPtr m_allowAssetsMismatchButton; + ButtonWidgetPtr m_headRotationButton; LabelWidgetPtr m_instrumentLabel; LabelWidgetPtr m_sfxLabel; diff --git a/source/game/StarHumanoid.cpp b/source/game/StarHumanoid.cpp index c71e161..8f39abe 100644 --- a/source/game/StarHumanoid.cpp +++ b/source/game/StarHumanoid.cpp @@ -242,6 +242,14 @@ EnumMap const Humanoid::StateNames{ {Humanoid::State::Lay, "lay"}, }; +// gross, but I don't want to make config calls more than I need to +bool& Humanoid::globalHeadRotation(Maybe default) { + static Maybe s_headRotation; + if (!s_headRotation) + s_headRotation = Root::singleton().configuration()->get("humanoidHeadRotation").optBool().value(true); + return *s_headRotation; +}; + Humanoid::Humanoid(Json const& config) { loadConfig(config); @@ -252,7 +260,7 @@ Humanoid::Humanoid(Json const& config) { m_movingBackwards = false; m_altHand.angle = 0; m_facingDirection = Direction::Left; - m_rotation = 0; + m_headRotationTarget = m_headRotation = m_rotation = 0; m_scale = Vec2F::filled(1.f); m_drawVaporTrail = false; m_state = State::Idle; @@ -412,6 +420,10 @@ void Humanoid::setMovingBackwards(bool movingBackwards) { m_movingBackwards = movingBackwards; } +void Humanoid::setHeadRotation(float headRotation) { + m_headRotationTarget = headRotation; +} + void Humanoid::setRotation(float rotation) { m_rotation = rotation; } @@ -476,6 +488,10 @@ void Humanoid::setPrimaryHandNonRotatedDrawables(List drawables) { m_primaryHand.nonRotatedDrawables = std::move(drawables); } +bool Humanoid::primaryHandHoldingItem() const { + return m_primaryHand.holdingItem; +} + void Humanoid::setAltHandParameters(bool holdingItem, float angle, float itemAngle, bool recoil, bool outsideOfHand) { m_altHand.holdingItem = holdingItem; @@ -498,16 +514,24 @@ void Humanoid::setAltHandNonRotatedDrawables(List drawables) { m_altHand.nonRotatedDrawables = std::move(drawables); } +bool Humanoid::altHandHoldingItem() const { + return m_altHand.holdingItem; +} + void Humanoid::animate(float dt) { m_animationTimer += dt; m_emoteAnimationTimer += dt; m_danceTimer += dt; + float headRotationTarget = globalHeadRotation() ? m_headRotationTarget : 0.f; + float diff = angleDiff(m_headRotation, headRotationTarget); + m_headRotation = (headRotationTarget - (headRotationTarget - m_headRotation) * powf(.333333f, dt * 60.f)); } void Humanoid::resetAnimation() { m_animationTimer = 0.0f; m_emoteAnimationTimer = 0.0f; m_danceTimer = 0.0f; + m_headRotation = globalHeadRotation() ? 0.f : m_headRotationTarget; } List Humanoid::render(bool withItems, bool withRotationAndScale) { @@ -641,11 +665,28 @@ List Humanoid::render(bool withItems, bool withRotationAndScale) { else if (m_state == Lay) headPosition += m_headLayOffset; + auto addHeadDrawable = [&](Drawable drawable, bool forceFullbright = false) { + if (m_facingDirection == Direction::Left) + drawable.scale(Vec2F(-1, 1)); + drawable.fullbright |= forceFullbright; + if (m_headRotation != 0.f) { + float dir = numericalDirection(m_facingDirection); + Vec2F rotationPoint = headPosition; + rotationPoint[0] *= dir; + rotationPoint[1] -= .25f; + float headX = (m_headRotation / ((float)Constants::pi * 2.f)); + drawable.rotate(m_headRotation, rotationPoint); + drawable.position[0] -= state() == State::Run ? (fmaxf(headX * dir, 0.f) * 2.f) * dir : headX; + drawable.position[1] -= fabsf(m_headRotation / ((float)Constants::pi * 4.f)); + } + drawables.append(std::move(drawable)); + }; + if (!m_headFrameset.empty() && !m_bodyHidden) { String image = strf("{}:normal", m_headFrameset); auto drawable = Drawable::makeImage(std::move(image), 1.0f / TilePixels, true, headPosition); drawable.imagePart().addDirectives(getBodyDirectives(), true); - addDrawable(std::move(drawable), m_bodyFullbright); + addHeadDrawable(std::move(drawable), m_bodyFullbright); } if (!m_emoteFrameset.empty() && !m_bodyHidden) { @@ -653,14 +694,14 @@ List Humanoid::render(bool withItems, bool withRotationAndScale) { String image = strf("{}:{}.{}{}", m_emoteFrameset, emoteFrameBase(m_emoteState), emoteStateSeq, emoteDirectives.prefix()); auto drawable = Drawable::makeImage(std::move(image), 1.0f / TilePixels, true, headPosition); drawable.imagePart().addDirectives(emoteDirectives, true); - addDrawable(std::move(drawable), m_bodyFullbright); + addHeadDrawable(std::move(drawable), m_bodyFullbright); } if (!m_hairFrameset.empty() && !m_bodyHidden) { String image = strf("{}:normal", m_hairFrameset); auto drawable = Drawable::makeImage(std::move(image), 1.0f / TilePixels, true, headPosition); drawable.imagePart().addDirectives(getHairDirectives(), true).addDirectives(getHelmetMaskDirectives(), true); - addDrawable(std::move(drawable), m_bodyFullbright); + addHeadDrawable(std::move(drawable), m_bodyFullbright); } if (!m_bodyFrameset.empty() && !m_bodyHidden) { @@ -719,21 +760,21 @@ List Humanoid::render(bool withItems, bool withRotationAndScale) { String image = strf("{}:normal", m_facialHairFrameset); auto drawable = Drawable::makeImage(std::move(image), 1.0f / TilePixels, true, headPosition); drawable.imagePart().addDirectives(getFacialHairDirectives(), true).addDirectives(getHelmetMaskDirectives(), true); - addDrawable(std::move(drawable), m_bodyFullbright); + addHeadDrawable(std::move(drawable), m_bodyFullbright); } if (!m_facialMaskFrameset.empty() && !m_bodyHidden) { String image = strf("{}:normal", m_facialMaskFrameset); auto drawable = Drawable::makeImage(std::move(image), 1.0f / TilePixels, true, headPosition); drawable.imagePart().addDirectives(getFacialMaskDirectives(), true).addDirectives(getHelmetMaskDirectives(), true); - addDrawable(std::move(drawable)); + addHeadDrawable(std::move(drawable)); } if (!m_headArmorFrameset.empty()) { String image = strf("{}:normal{}", m_headArmorFrameset, m_headArmorDirectives.prefix()); auto drawable = Drawable::makeImage(std::move(image), 1.0f / TilePixels, true, headPosition); drawable.imagePart().addDirectives(getHeadDirectives(), true); - addDrawable(std::move(drawable)); + addHeadDrawable(std::move(drawable)); } auto frontArmDrawable = [&](String const& frameSet, Directives const& directives) -> Drawable { diff --git a/source/game/StarHumanoid.hpp b/source/game/StarHumanoid.hpp index c83ddb5..6fd1cbe 100644 --- a/source/game/StarHumanoid.hpp +++ b/source/game/StarHumanoid.hpp @@ -100,6 +100,8 @@ public: }; static EnumMap const StateNames; + static bool& globalHeadRotation(Maybe default = {}); + Humanoid(Json const& config); Humanoid(HumanoidIdentity const& identity); Humanoid(Humanoid const&) = default; @@ -163,6 +165,7 @@ public: void setDance(Maybe const& dance); void setFacingDirection(Direction facingDirection); void setMovingBackwards(bool movingBackwards); + void setHeadRotation(float headRotation); void setRotation(float rotation); void setScale(Vec2F scale); @@ -183,6 +186,7 @@ public: void setPrimaryHandFrameOverrides(String backFrameOverride, String frontFrameOverride); void setPrimaryHandDrawables(List drawables); void setPrimaryHandNonRotatedDrawables(List drawables); + bool primaryHandHoldingItem() const; // Same as primary hand. void setAltHandParameters(bool holdingItem, float angle, float itemAngle, bool recoil, @@ -190,6 +194,7 @@ public: void setAltHandFrameOverrides(String backFrameOverride, String frontFrameOverride); void setAltHandDrawables(List drawables); void setAltHandNonRotatedDrawables(List drawables); + bool altHandHoldingItem() const; // Updates the animation based on whatever the current animation state is, // wrapping or clamping animation time as appropriate. @@ -355,6 +360,8 @@ private: Maybe m_dance; Direction m_facingDirection; bool m_movingBackwards; + float m_headRotation; + float m_headRotationTarget; float m_rotation; Vec2F m_scale; bool m_drawVaporTrail; diff --git a/source/game/StarPlayer.cpp b/source/game/StarPlayer.cpp index 2f89ff3..24a3c16 100644 --- a/source/game/StarPlayer.cpp +++ b/source/game/StarPlayer.cpp @@ -1021,16 +1021,16 @@ void Player::update(float dt, uint64_t) { || m_humanoid->danceCyclicOrEnded() || m_movementController->running()) m_humanoid->setDance({}); + m_tools->suppressItems(suppressedItems); + m_tools->tick(dt, m_shifting, m_pendingMoves); + + if (auto overrideFacingDirection = m_tools->setupHumanoidHandItems(*m_humanoid, position(), aimPosition())) + m_movementController->controlFace(*overrideFacingDirection); + bool isClient = world()->isClient(); if (isClient) m_armor->setupHumanoidClothingDrawables(*m_humanoid, forceNude()); - m_tools->suppressItems(suppressedItems); - m_tools->tick(dt, m_shifting, m_pendingMoves); - - if (auto overrideFacingDirection = m_tools->setupHumanoidHandItems(*m_humanoid, position(), aimPosition())) - m_movementController->controlFace(*overrideFacingDirection); - m_effectsAnimator->resetTransformationGroup("flip"); if (m_movementController->facingDirection() == Direction::Left) m_effectsAnimator->scaleTransformationGroup("flip", Vec2F(-1, 1)); @@ -1075,6 +1075,35 @@ void Player::update(float dt, uint64_t) { m_effectEmitter->tick(dt, *entityMode()); + if (isClient) { + bool calculateHeadRotation = isMaster(); + if (!calculateHeadRotation) { + auto headRotationProperty = getSecretProperty("humanoid.headRotation"); + if (headRotationProperty.isType(Json::Type::Float)) { + m_humanoid->setHeadRotation(headRotationProperty.toFloat()); + } else + calculateHeadRotation = true; + } + if (calculateHeadRotation) { // master or not an OpenStarbound player + float headRotation = 0.f; + if (m_humanoid->primaryHandHoldingItem() || m_humanoid->altHandHoldingItem()) { + auto primary = m_tools->primaryHandItem(); + auto alt = m_tools->altHandItem(); + String const disableFlag = "disableHeadRotation"; + auto statusFlag = m_statusController->statusProperty(disableFlag); + if (!(statusFlag.isType(Json::Type::Bool) && statusFlag.toBool()) + && !(primary && primary->instanceValue(disableFlag)) + && !(alt && alt->instanceValue(disableFlag))) { + auto diff = world()->geometry().diff(aimPosition(), mouthPosition()); + diff.setX(fabsf(diff.x())); + headRotation = diff.angle() * .25f * numericalDirection(m_humanoid->facingDirection()); + } + } + m_humanoid->setHeadRotation(headRotation); + setSecretProperty("humanoid.headRotation", headRotation); + } + } + m_humanoid->setFacingDirection(m_movementController->facingDirection()); m_humanoid->setMovingBackwards(m_movementController->facingDirection() != m_movementController->movingDirection()); @@ -2610,6 +2639,7 @@ Maybe Player::getSecretPropertyView(String const& name) const { return {}; } + Json Player::getSecretProperty(String const& name, Json defaultValue) const { if (auto tag = m_effectsAnimator->globalTagPtr(secretProprefix + name)) { DataStreamExternalBuffer buffer(tag->utf8Ptr(), tag->utf8Size());