diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b71481f5..c2253019c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - New `AHuman` INI and Lua (R/W) property `MaxWalkPathCrouchShift`, which determines how much the actor will automatically duck down to avoid low ceilings above them. This can be set to 0 to never duck. Defaults to 6. +- New `AHuman` INI and Lua (R/W) property `MaxCrouchRotation`, which determines how much the actor will rotate when ducking to avoid low ceilings above them. This can be set to 0 to never duck. Defaults to a quarter of Pi * 1.25 (roughly 56 degrees). + - New `MOPixel` INI and Lua (R/W) property `Staininess`, which defines how likely a pixel is to stain a surface when it collides with it. Staining a surface changes that surface's `Color` to that of this `MOPixel`, without changing the underlying material. Value can be between 0 and 1. Defaults to 0 (never stain). - New `Activity` INI and Lua (R/W) property `AllowsUserSaving`, which can be used to enable/disable manual user saving/loading. This defaults to true for all `GAScripted` with an `OnSave()` function, but false otherwise. Lua `ActivityMan::SaveGame()` function now forces a save even if `AllowsUserSaving` is disabled. This allows mods and scripted gamemodes to handle saving in their own way (for example, only allowing saving at set points). diff --git a/Entities/AHuman.cpp b/Entities/AHuman.cpp index f509e3ced..02339c8de 100644 --- a/Entities/AHuman.cpp +++ b/Entities/AHuman.cpp @@ -64,6 +64,7 @@ void AHuman::Clear() m_ProneState = NOTPRONE; m_ProneTimer.Reset(); m_MaxWalkPathCrouchShift = 6.0F; + m_MaxCrouchRotation = c_QuarterPI * 1.25F for (int i = 0; i < MOVEMENTSTATECOUNT; ++i) { m_Paths[FGROUND][i].Reset(); m_Paths[BGROUND][i].Reset(); @@ -86,7 +87,7 @@ void AHuman::Clear() m_BGArmFlailScalar = 0.7F; m_EquipHUDTimer.Reset(); m_WalkAngle.fill(Matrix()); - m_WalkPathYOffset = 0.0F; + m_WalkPathOffset.Reset(); m_ArmSwingRate = 1.0F; m_DeviceArmSwayRate = 0.5F; @@ -210,6 +211,7 @@ int AHuman::Create(const AHuman &reference) { m_BackupBGFootGroup->SetLimbPos(atomGroupToUseAsFootGroupBG->GetLimbPos()); m_MaxWalkPathCrouchShift = reference.m_MaxWalkPathCrouchShift; + m_MaxCrouchRotation = reference.m_MaxCrouchRotation; if (reference.m_StrideSound) { m_StrideSound = dynamic_cast(reference.m_StrideSound->Clone()); } @@ -287,9 +289,8 @@ int AHuman::ReadProperty(const std::string_view &propName, Reader &reader) { m_BackupBGFootGroup = new AtomGroup(*m_pBGFootGroup); m_BackupBGFootGroup->RemoveAllAtoms(); }); - MatchProperty("MaxWalkPathCrouchShift", { - reader >> m_MaxWalkPathCrouchShift; - }); + MatchProperty("MaxWalkPathCrouchShift", { reader >> m_MaxWalkPathCrouchShift; }); + MatchProperty("MaxCrouchRotation", { reader >> m_MaxCrouchRotation; }); MatchProperty("StrideSound", { m_StrideSound = new SoundContainer; reader >> m_StrideSound; @@ -355,6 +356,8 @@ int AHuman::Save(Writer &writer) const writer << m_pBGFootGroup; writer.NewProperty("MaxWalkPathCrouchShift"); writer << m_MaxWalkPathCrouchShift; + writer.NewProperty("MaxCrouchRotation"); + writer << m_MaxCrouchRotation; writer.NewProperty("StrideSound"); writer << m_StrideSound; @@ -1725,9 +1728,14 @@ void AHuman::UpdateWalkAngle(AHuman::Layer whichLayer) { g_SceneMan.CastStrengthRay(hitPosRight, Vector(0.0F, -desiredCrouchHeadRoom), 10.0F, hitPosRight, 0, g_MaterialGrass); float headroom = m_pHead->GetPos().m_Y - std::max(hitPosLeft.m_Y, hitPosRight.m_Y); float adjust = desiredCrouchHeadRoom - headroom; - m_WalkPathYOffset = std::clamp(LERP(0.0F, 1.0F, m_WalkPathYOffset, adjust, 0.3F), 0.0F, m_MaxWalkPathCrouchShift); + float walkPathYOffset = std::clamp(LERP(0.0F, 1.0F, -m_WalkPathOffset.m_Y, adjust, 0.3F), 0.0F, m_MaxWalkPathCrouchShift); + m_WalkPathOffset.m_Y = -walkPathYOffset; + + // Adjust our X offset to try to keep our legs under our centre-of-mass + float predictedPosition = ((m_pHead->GetPos().m_X - m_Pos.m_X) * 0.15F) + m_Vel.m_X; + m_WalkPathOffset.m_X = predictedPosition; } else { - m_WalkPathYOffset = 0.0f; + m_WalkPathOffset.Reset(); } } } @@ -2240,7 +2248,7 @@ void AHuman::PreControllerUpdate() // Reset the stride timer if the path is about to restart. if (m_Paths[FGROUND][WALK].PathEnded() || m_Paths[FGROUND][WALK].PathIsAtStart()) { m_StrideTimer.Reset(); } Vector jointPos = m_Pos + RotateOffset(m_pFGLeg->GetParentOffset()); - m_ArmClimbing[BGROUND] = !m_pFGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[FGROUND][WALK].GetLowestY()), Vector(0.0F, -m_WalkPathYOffset)); + m_ArmClimbing[BGROUND] = !m_pFGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[FGROUND][WALK].GetLowestY()), m_WalkPathOffset); } else { m_ArmClimbing[BGROUND] = false; } @@ -2249,7 +2257,7 @@ void AHuman::PreControllerUpdate() // Reset the stride timer if the path is about to restart. if (m_Paths[BGROUND][WALK].PathEnded() || m_Paths[BGROUND][WALK].PathIsAtStart()) { m_StrideTimer.Reset(); } Vector jointPos = m_Pos + RotateOffset(m_pBGLeg->GetParentOffset()); - m_ArmClimbing[FGROUND] = !m_pBGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[BGROUND][WALK].GetLowestY()), Vector(0.0F, -m_WalkPathYOffset)); + m_ArmClimbing[FGROUND] = !m_pBGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[BGROUND][WALK].GetLowestY()), m_WalkPathOffset); } else { if (m_pBGLeg) { m_pBGFootGroup->FlailAsLimb(m_Pos, RotateOffset(m_pBGLeg->GetParentOffset()), m_pBGLeg->GetMaxLength(), m_PrevVel, m_AngularVel, m_pBGLeg->GetMass(), deltaTime); } m_ArmClimbing[FGROUND] = false; @@ -2389,12 +2397,12 @@ void AHuman::PreControllerUpdate() if (m_pFGLeg) { Vector jointPos = m_Pos.GetFloored() + m_pFGLeg->GetParentOffset().GetXFlipped(m_HFlipped); - m_pFGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][STAND], deltaTime, nullptr, !m_pBGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY()), Vector(0.0F, -m_WalkPathYOffset)); + m_pFGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][STAND], deltaTime, nullptr, !m_pBGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY()), m_WalkPathOffset); } if (m_pBGLeg) { Vector jointPos = m_Pos.GetFloored() + m_pBGLeg->GetParentOffset().GetXFlipped(m_HFlipped); - m_pBGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][STAND], deltaTime, nullptr, !m_pFGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY()), Vector(0.0F, -m_WalkPathYOffset)); + m_pBGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][STAND], deltaTime, nullptr, !m_pFGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY()), m_WalkPathOffset); } } } @@ -2646,7 +2654,13 @@ void AHuman::Update() } } else { // Upright body posture - float rotDiff = rot - (GetRotAngleTarget(m_MoveState) * (m_AimAngle > 0 ? 1.0F - (m_AimAngle / c_HalfPI) : 1.0F) * GetFlipFactor()); + float rotTarget = (GetRotAngleTarget(m_MoveState) * (m_AimAngle > 0 ? 1.0F - (m_AimAngle / c_HalfPI) : 1.0F) * GetFlipFactor()); + + // Lean forwards when crouching + float crouchAngleAdjust = m_HFlipped ? m_MaxCrouchRotation : -m_MaxCrouchRotation; + rotTarget += LERP(0.0F, m_MaxWalkPathCrouchShift, 0.0F, crouchAngleAdjust, m_WalkPathOffset.m_Y * -1.0F); + + float rotDiff = rot - rotTarget; m_AngularVel = m_AngularVel * (0.98F - 0.06F * (m_Health / m_MaxHealth)) - (rotDiff * 0.5F); } } @@ -2755,10 +2769,10 @@ void AHuman::Draw(BITMAP *pTargetBitmap, const Vector &targetPos, DrawMode mode, } if (mode == g_DrawColor && !onlyPhysical && g_SettingsMan.DrawLimbPathVisualizations()) { - m_Paths[m_HFlipped][WALK].Draw(pTargetBitmap, targetPos, 122); - m_Paths[m_HFlipped][CRAWL].Draw(pTargetBitmap, targetPos, 122); - m_Paths[m_HFlipped][ARMCRAWL].Draw(pTargetBitmap, targetPos, 13); - m_Paths[m_HFlipped][CLIMB].Draw(pTargetBitmap, targetPos, 165); + m_Paths[m_HFlipped][WALK].Draw(pTargetBitmap, targetPos, 122); + m_Paths[m_HFlipped][CRAWL].Draw(pTargetBitmap, targetPos, 122); + m_Paths[m_HFlipped][ARMCRAWL].Draw(pTargetBitmap, targetPos, 13); + m_Paths[m_HFlipped][CLIMB].Draw(pTargetBitmap, targetPos, 165); } } diff --git a/Entities/AHuman.h b/Entities/AHuman.h index 04a70b725..88462f63c 100644 --- a/Entities/AHuman.h +++ b/Entities/AHuman.h @@ -875,6 +875,18 @@ DefaultPieMenuNameGetter("Default Human Pie Menu"); /// The new value for this AHuman's max walkpath adjustment. void SetMaxWalkPathCrouchShift(float newValue) { m_MaxWalkPathCrouchShift = newValue; } + /// + /// Gets this AHuman's max crouch rotation to duck below low ceilings. + /// + /// This AHuman's max crouch rotation adjustment. + float GetMaxCrouchRotation() const { return m_MaxCrouchRotation; } + + /// + /// Sets this AHuman's max crouch rotation to duck below low ceilings. + /// + /// The new value for this AHuman's max crouch rotation adjustment. + void SetMaxCrouchRotation(float newValue) { m_MaxCrouchRotation = newValue; } + /// /// Gets this AHuman's stride sound. Ownership is NOT transferred! /// @@ -949,6 +961,8 @@ DefaultPieMenuNameGetter("Default Human Pie Menu"); Timer m_ProneTimer; // The maximum amount our walkpath can be shifted upwards to crouch and avoid ceilings above us float m_MaxWalkPathCrouchShift; + // The maximum amount we will duck our head down to avoid obstacles above us. + float m_MaxCrouchRotation; // Limb paths for different movement states. // [0] is for the foreground limbs, and [1] is for BG. LimbPath m_Paths[2][MOVEMENTSTATECOUNT]; @@ -972,7 +986,7 @@ DefaultPieMenuNameGetter("Default Human Pie Menu"); float m_BGArmFlailScalar; //!< The rate at which this AHuman's BG Arm follows the the bodily rotation. Set to a negative value for a "counterweight" effect. Timer m_EquipHUDTimer; //!< Timer for showing the name of any newly equipped Device. std::array m_WalkAngle; //!< An array of rot angle targets for different movement states. - float m_WalkPathYOffset; + Vector m_WalkPathOffset; float m_ArmSwingRate; //!< Controls the rate at which this AHuman's Arms follow the movement of its Legs while they're not holding device(s). float m_DeviceArmSwayRate; //!< Controls the rate at which this AHuman's Arms follow the movement of its Legs while they're holding device(s). One-handed devices sway half as much as two-handed ones. Defaults to three quarters of Arm swing rate. diff --git a/Entities/LimbPath.cpp b/Entities/LimbPath.cpp index bdeac6a2a..ca98fddef 100644 --- a/Entities/LimbPath.cpp +++ b/Entities/LimbPath.cpp @@ -12,6 +12,7 @@ // Inclusions of header files #include "LimbPath.h" + #include "PresetMan.h" #include "SLTerrain.h" @@ -189,8 +190,8 @@ int LimbPath::ReadProperty(const std::string_view &propName, Reader &reader) Vector LimbPath::RotatePoint(const Vector &point) const { - Vector offset = (m_RotationOffset + m_PositionOffset).GetXFlipped(m_HFlipped); - return ((point - offset) * m_Rotation) + offset; + Vector offset = (m_RotationOffset).GetXFlipped(m_HFlipped); + return (((point - offset) * m_Rotation) + offset) + m_PositionOffset; } ////////////////////////////////////////////////////////////////////////////////////////// @@ -247,7 +248,7 @@ void LimbPath::Destroy(bool notInherited) Vector LimbPath::GetProgressPos() { - Vector returnVec(m_Start + m_PositionOffset); + Vector returnVec(m_Start); if (IsStaticPoint()) { return m_JointPos + RotatePoint(returnVec); } @@ -274,7 +275,7 @@ Vector LimbPath::GetProgressPos() Vector LimbPath::GetCurrentSegTarget() { - Vector returnVec(m_Start + m_PositionOffset); + Vector returnVec(m_Start); if (IsStaticPoint()) { return m_JointPos + RotatePoint(returnVec); } @@ -566,7 +567,7 @@ bool LimbPath::RestartFree(Vector &limbPos, MOID MOIDToIgnore, int ignoreTeam) if (IsStaticPoint()) { Vector notUsed; - Vector targetPos = m_JointPos + RotatePoint(m_Start + m_PositionOffset); + Vector targetPos = m_JointPos + RotatePoint(m_Start); Vector beginPos = targetPos; // TODO: don't hardcode the beginpos beginPos.m_Y -= 24; @@ -588,14 +589,15 @@ bool LimbPath::RestartFree(Vector &limbPos, MOID MOIDToIgnore, int ignoreTeam) int i = 0; for (; i < m_StartSegCount; ++i) { - result = g_SceneMan.CastObstacleRay(GetProgressPos(), RotatePoint(*m_CurrentSegment + m_PositionOffset), notUsed, limbPos, MOIDToIgnore, ignoreTeam, g_MaterialGrass); + Vector offsetSegment = (*m_CurrentSegment); + result = g_SceneMan.CastObstacleRay(GetProgressPos(), RotatePoint(offsetSegment), notUsed, limbPos, MOIDToIgnore, ignoreTeam, g_MaterialGrass); // If we found an obstacle after the first pixel, report the current segment as the starting one and that there is free space here if (result > 0) { // Set accurate segment progress // TODO: See if this is a good idea, or if we should just set it to 0 and set limbPos to the start of current segment - m_SegProgress = g_SceneMan.ShortestDistance(GetProgressPos(), limbPos).GetMagnitude() / (*m_CurrentSegment + m_PositionOffset).GetMagnitude(); + m_SegProgress = g_SceneMan.ShortestDistance(GetProgressPos(), limbPos).GetMagnitude() / offsetSegment.GetMagnitude(); limbPos = GetProgressPos(); // m_SegProgress = 0; m_Ended = false; @@ -685,7 +687,7 @@ void LimbPath::Draw(BITMAP *pTargetBitmap, const Vector &targetPos, unsigned char color) const { - Vector prevPoint = m_Start + m_PositionOffset; + Vector prevPoint = m_Start; Vector nextPoint = prevPoint; for (std::deque::const_iterator itr = m_Segments.begin(); itr != m_Segments.end(); ++itr) { diff --git a/Lua/LuaBindingsEntities.cpp b/Lua/LuaBindingsEntities.cpp index 2815e350f..7fbeb4028 100644 --- a/Lua/LuaBindingsEntities.cpp +++ b/Lua/LuaBindingsEntities.cpp @@ -461,6 +461,7 @@ namespace RTE { .property("FGFoot", &AHuman::GetFGFoot, &LuaAdaptersPropertyOwnershipSafetyFaker::AHumanSetFGFoot) .property("BGFoot", &AHuman::GetBGFoot, &LuaAdaptersPropertyOwnershipSafetyFaker::AHumanSetBGFoot) .property("MaxWalkPathCrouchShift", &AHuman::GetMaxWalkPathCrouchShift, &AHuman::SetMaxWalkPathCrouchShift) + .property("MaxCrouchRotation", &AHuman::GetMaxCrouchRotation, &AHuman::SetMaxCrouchRotation) .property("StrideSound", &AHuman::GetStrideSound, &LuaAdaptersPropertyOwnershipSafetyFaker::AHumanSetStrideSound) .property("UpperBodyState", &AHuman::GetUpperBodyState, &AHuman::SetUpperBodyState) .property("MovementState", &AHuman::GetMovementState, &AHuman::SetMovementState)