using System; using System.Linq; using EntityStates.VoidRaidCrab.Joint; using EntityStates.VoidRaidCrab.Leg; using HG; using UnityEngine; using UnityEngine.Networking; namespace RoR2.VoidRaidCrab; public class LegController : MonoBehaviour { private const string jointDeathStateMachineName = "Body"; public EntityStateMachine stateMachine; public Animator animator; public string primaryLayerName; [Tooltip("The transform that should be considered the origin of this leg, points outward from the base, and provides a transform for consistent local space conversions. This field must always be set, and is available for the case that the object this component is attached to is not a bone meeting the metioned criteria.")] public Transform originTransform; [Header("Regeneration")] public float jointRegenDuration = 15f; [Header("Footstep Concussive Blast")] public float footstepBlastRadius = 20f; public BlastAttack.FalloffModel footstepFalloffModel = BlastAttack.FalloffModel.SweetSpot; public float footstepBlastForce = 500f; public Vector3 footstepBonusForce; public GameObject footstepBlastEffectPrefab; [Header("Stomp")] public Transform stompRangeNearMarker; public Transform stompRangeFarMarker; public Transform stompRangeLeftMarker; public Transform stompRangeRightMarker; public float stompAimSpeed = 15f; public string stompXParameter; public string stompYParameter; public string stompPlaybackRateParam; private int stompXParameterHash; private int stompYParameterHash; private int stompPlaybackRateHash; [Header("Retraction")] public AnimationCurve retractionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); public string retractionLayerName; private float currentRetractionWeight; private float desiredRetractionWeight; public float retractionBlendTransitionRate = 1f; public bool shouldRetract; [NonSerialized] public bool isBreakSuppressed; private bool wasJointBodyDead; private bool isJointBodyCurrentlyDying; private float jointRegenStopwatchServer; private Vector3? stompTargetWorldPosition; private Vector2 currentLocalStompPosition = Vector2.zero; public CharacterBody mainBody { get; private set; } public GameObject mainBodyGameObject { get; private set; } public bool mainBodyHasEffectiveAuthority => mainBody?.hasEffectiveAuthority ?? false; public ChildLocator legChildLocator { get; private set; } public ChildLocator childLocator { get; private set; } public Transform toeTransform { get; private set; } public Transform toeTipTransform { get; private set; } public Transform footTranform { get; private set; } public CharacterMaster jointMaster { get; private set; } public CharacterBody jointBody { get { if (!jointMaster) { return null; } return jointMaster.GetBody(); } } public bool IsBusy() { return !(stateMachine.state is Idle); } public bool IsStomping() { return stateMachine.state is BaseStompState; } private void OnEnable() { CharacterModel componentInParent = GetComponentInParent(); mainBody = (componentInParent ? componentInParent.body : null); mainBodyGameObject = (mainBody ? mainBody.gameObject : null); childLocator = GetComponent(); footTranform = childLocator.FindChild("Foot"); toeTransform = childLocator.FindChild("Toe"); toeTipTransform = childLocator.FindChild("ToeTip"); stompXParameterHash = Animator.StringToHash(stompXParameter); stompYParameterHash = Animator.StringToHash(stompYParameter); stompPlaybackRateHash = Animator.StringToHash(stompPlaybackRateParam); } private void FixedUpdate() { desiredRetractionWeight = (shouldRetract ? 1f : 0f); currentRetractionWeight = Mathf.Clamp01(Mathf.MoveTowards(currentRetractionWeight, desiredRetractionWeight, Time.fixedDeltaTime * retractionBlendTransitionRate)); int layerIndex = animator.GetLayerIndex(retractionLayerName); float weight = retractionCurve.Evaluate(currentRetractionWeight); animator.SetLayerWeight(layerIndex, weight); if (!wasJointBodyDead && IsBreakPending()) { wasJointBodyDead = true; isJointBodyCurrentlyDying = true; } if (mainBodyHasEffectiveAuthority) { UpdateStompTargetPositionAuthority(Time.fixedDeltaTime); } if (NetworkServer.active && !jointBody) { jointRegenStopwatchServer += Time.fixedDeltaTime; if (jointRegenStopwatchServer >= jointRegenDuration) { RegenerateServer(); } } } public bool RequestStomp(GameObject target) { if (!IsBusy()) { stateMachine.SetNextState(new PreStompLegRaise { target = target }); return true; } return false; } public void SetStompTargetWorldPosition(Vector3? newStompTargetWorldPosition) { stompTargetWorldPosition = newStompTargetWorldPosition; } public bool SetJointMaster(CharacterMaster master, ChildLocator legChildLocator) { this.legChildLocator = legChildLocator; if (!jointMaster) { jointMaster = master; if ((bool)jointMaster) { jointMaster.onBodyDestroyed += OnJointBodyDestroyed; jointMaster.onBodyStart += OnJointBodyStart; MirrorLegJoints(); } return true; } Debug.LogError("LegController on " + base.gameObject.name + " already has a jointMaster set!"); return false; } private void OnJointBodyStart(CharacterBody body) { wasJointBodyDead = false; isJointBodyCurrentlyDying = false; MirrorLegJoints(); } private void OnJointBodyDestroyed(CharacterBody body) { isJointBodyCurrentlyDying = false; } public bool IsSupportingWeight() { if (!IsBroken() && !IsBreakPending()) { return !IsBusy(); } return false; } public bool CanBreak() { return jointBody; } public bool IsBreakPending() { if ((bool)jointBody) { HealthComponent healthComponent = jointBody.healthComponent; if ((bool)healthComponent) { return !healthComponent.alive; } return false; } return false; } public bool IsBroken() { if (!isJointBodyCurrentlyDying) { return !jointBody; } return true; } public bool DoesJointExist() { return jointBody; } public void CompleteBreakAuthority() { if (!jointMaster) { return; } CharacterBody characterBody = jointBody; if (!characterBody) { return; } HealthComponent healthComponent = characterBody.healthComponent; if ((bool)healthComponent) { if (NetworkServer.active) { DamageInfo damageInfo = new DamageInfo(); damageInfo.crit = false; damageInfo.damage = healthComponent.fullCombinedHealth; damageInfo.procCoefficient = 0f; mainBody.healthComponent.TakeDamage(damageInfo); GlobalEventManager.instance.OnHitEnemy(damageInfo, healthComponent.gameObject); GlobalEventManager.instance.OnHitAll(damageInfo, healthComponent.gameObject); } mainBody.healthComponent.UpdateLastHitTime(0f, Vector3.zero, damageIsSilent: true, healthComponent.lastHitAttacker); } if (EntityStateMachine.FindByCustomName(characterBody.gameObject, "Body").state is PreDeathState preDeathState) { preDeathState.canProceed = true; } } public void RegenerateServer() { if ((bool)jointMaster) { jointRegenStopwatchServer = 0f; if (!jointMaster.GetBody()) { jointMaster.Respawn(mainBody.transform.position, mainBody.transform.rotation); } } } private void MirrorLegJoints() { GameObject bodyObject = jointMaster.GetBodyObject(); if ((bool)bodyObject && (bool)legChildLocator) { ChildLocatorMirrorController component = bodyObject.GetComponent(); if ((bool)component) { component.referenceLocator = legChildLocator; } } } private Vector2 WorldPointToLocalStompPoint(Vector3 worldPoint) { Vector3 vector = originTransform.InverseTransformVector(worldPoint); return new Vector2(vector.x, vector.z); } private Vector2 LocalStompPointToStompParams(Vector2 stompPoint) { float x = originTransform.InverseTransformVector(stompRangeLeftMarker.position).x; float x2 = originTransform.InverseTransformVector(stompRangeRightMarker.position).x; float z = originTransform.InverseTransformVector(stompRangeNearMarker.position).z; float z2 = originTransform.InverseTransformVector(stompRangeFarMarker.position).z; float x3 = Util.Remap(currentLocalStompPosition.x, x, x2, -1f, 1f); float y = Util.Remap(currentLocalStompPosition.y, z, z2, -1f, 1f); return new Vector2(x3, y); } private void UpdateStompTargetPositionAuthority(float deltaTime) { Vector3 worldPoint = toeTipTransform.position; if (stompTargetWorldPosition.HasValue) { worldPoint = stompTargetWorldPosition.Value; } Vector2 target = WorldPointToLocalStompPoint(worldPoint); currentLocalStompPosition = Vector2.MoveTowards(currentLocalStompPosition, target, stompAimSpeed * deltaTime); Vector2 vector = LocalStompPointToStompParams(currentLocalStompPosition); animator.SetFloat(stompXParameterHash, vector.x); animator.SetFloat(stompYParameterHash, vector.y); } private bool GetKneeToToeTipRaycast(out Vector3 hitPosition, out Vector3 hitNormal, out Vector3 rayNormal) { Vector3 position = footTranform.position; Vector3 position2 = toeTipTransform.position; Vector3 vector = position2 - position; float magnitude = vector.magnitude; if (magnitude <= Mathf.Epsilon) { hitPosition = position2; hitNormal = Vector3.up; rayNormal = Vector3.down; return false; } Vector3 vector2 = vector / magnitude; RaycastHit[] array = Physics.RaycastAll(new Ray(position, vector2), magnitude, LayerIndex.world.mask, QueryTriggerInteraction.Ignore); float num = float.PositiveInfinity; int num2 = -1; for (int i = 0; i < array.Length; i++) { ref RaycastHit reference = ref array[i]; if (reference.distance < num && !(reference.collider.transform.root == base.transform.root)) { num2 = i; num = reference.distance; } } rayNormal = vector2; if (num2 != -1) { ref RaycastHit reference2 = ref array[num2]; hitPosition = reference2.point; hitNormal = reference2.normal; return true; } hitPosition = toeTipTransform.position; hitNormal = -rayNormal; return false; } public void DoToeConcussionBlastAuthority(Vector3? positionOverride = null, bool useEffect = true) { if (!mainBodyHasEffectiveAuthority) { throw new Exception("Caller does not have authority."); } Vector3 hitPosition; if (positionOverride.HasValue) { hitPosition = positionOverride.Value; } else { GetKneeToToeTipRaycast(out hitPosition, out var _, out var _); } if (useEffect) { EffectData effectData = new EffectData(); effectData.origin = hitPosition; effectData.scale = footstepBlastRadius; effectData.rotation = Quaternion.identity; EffectManager.SpawnEffect(footstepBlastEffectPrefab, effectData, transmit: true); } BlastAttack blastAttack = new BlastAttack(); blastAttack.attacker = mainBodyGameObject; blastAttack.teamIndex = (mainBody ? mainBody.teamComponent.teamIndex : TeamIndex.None); blastAttack.attackerFiltering = AttackerFiltering.NeverHitSelf; blastAttack.inflictor = mainBodyGameObject; blastAttack.radius = footstepBlastRadius; blastAttack.position = hitPosition; blastAttack.losType = BlastAttack.LoSType.None; blastAttack.procCoefficient = 0f; blastAttack.procChainMask = default(ProcChainMask); blastAttack.baseDamage = 0f; blastAttack.baseForce = footstepBlastForce; blastAttack.bonusForce = footstepBonusForce; blastAttack.canRejectForce = false; blastAttack.crit = false; blastAttack.damageColorIndex = DamageColorIndex.Default; blastAttack.damageType = DamageType.Silent; blastAttack.impactEffect = EffectIndex.Invalid; blastAttack.falloffModel = footstepFalloffModel; blastAttack.Fire(); } public GameObject CheckForStompTarget() { Vector3 a2 = stompRangeLeftMarker.position; Vector3 b2 = stompRangeRightMarker.position; Vector3 c2 = stompRangeNearMarker.position; Vector3 d2 = stompRangeFarMarker.position; Vector3 vector = Average(in a2, in b2, in c2, in d2); float a3 = Vector3.Distance(vector, a2); float b3 = Vector3.Distance(vector, b2); float c3 = Vector3.Distance(vector, c2); float d3 = Vector3.Distance(vector, d2); float num = Min(a3, b3, c3, d3); float num2 = Mathf.Sqrt(2f); float num3 = 1f / num2; float maxDistanceFilter = num * num3; BullseyeSearch bullseyeSearch = new BullseyeSearch(); bullseyeSearch.searchOrigin = vector; bullseyeSearch.minDistanceFilter = 0f; bullseyeSearch.maxDistanceFilter = maxDistanceFilter; bullseyeSearch.sortMode = BullseyeSearch.SortMode.Distance; bullseyeSearch.viewer = mainBody; bullseyeSearch.teamMaskFilter = TeamMask.AllExcept(mainBody.teamComponent.teamIndex); bullseyeSearch.filterByDistinctEntity = true; bullseyeSearch.filterByLoS = false; bullseyeSearch.RefreshCandidates(); return bullseyeSearch.GetResults().FirstOrDefault()?.healthComponent?.gameObject; static Vector3 Average(in Vector3 a, in Vector3 b, in Vector3 c, in Vector3 d) { Vector3 a4 = Vector3Utils.Average(in a, in b); Vector3 b4 = Vector3Utils.Average(in c, in d); return Vector3Utils.Average(in a4, in b4); } static float Min(float a, float b, float c, float d) { return Mathf.Min(Mathf.Min(a, b), Mathf.Min(c, d)); } } }