diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escDodge.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escDodge.asset new file mode 100644 index 000000000..a601e2f7b --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escDodge.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3} + m_Name: escDodge + m_EditorClassIdentifier: + targetType: + assemblyQualifiedName: EntityStates.Lancer.Dodge, Starstorm2, Version=1.0.0.0, + Culture=neutral, PublicKeyToken=null + serializedFieldsCollection: + serializedFields: + - fieldName: duration + fieldValue: + stringValue: 0.5 + objectValue: {fileID: 0} + - fieldName: initialSpeedCoefficient + fieldValue: + stringValue: 6.5 + objectValue: {fileID: 0} + - fieldName: finalSpeedCoefficient + fieldValue: + stringValue: 1.5 + objectValue: {fileID: 0} + - fieldName: dodgeFOV + fieldValue: + stringValue: -1 + objectValue: {fileID: 0} diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escIonDetonation.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escIonDetonation.asset new file mode 100644 index 000000000..abd38eba7 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escIonDetonation.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3} + m_Name: escIonDetonation + m_EditorClassIdentifier: + targetType: + assemblyQualifiedName: EntityStates.Lancer.IonDetonation, Starstorm2, Version=1.0.0.0, + Culture=neutral, PublicKeyToken=null + serializedFieldsCollection: + serializedFields: + - fieldName: baseDuration + fieldValue: + stringValue: 1 + objectValue: {fileID: 0} + - fieldName: novaDamageCoefficient + fieldValue: + stringValue: 8 + objectValue: {fileID: 0} + - fieldName: novaRadius + fieldValue: + stringValue: 12 + objectValue: {fileID: 0} + - fieldName: novaForce + fieldValue: + stringValue: 2000 + objectValue: {fileID: 0} + - fieldName: novaProcCoefficient + fieldValue: + stringValue: 1 + objectValue: {fileID: 0} + - fieldName: novaSoundString + fieldValue: + stringValue: + objectValue: {fileID: 0} diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escIonFistPunch.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escIonFistPunch.asset new file mode 100644 index 000000000..b15a4cb70 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escIonFistPunch.asset @@ -0,0 +1,39 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3} + m_Name: escIonFistPunch + m_EditorClassIdentifier: + targetType: + assemblyQualifiedName: EntityStates.Lancer.IonFistPunch, Starstorm2, Version=1.0.0.0, + Culture=neutral, PublicKeyToken=null + serializedFieldsCollection: + serializedFields: + - fieldName: baseDuration + fieldValue: + stringValue: 0.4 + objectValue: {fileID: 0} + - fieldName: damageCoefficient + fieldValue: + stringValue: 1.5 + objectValue: {fileID: 0} + - fieldName: force + fieldValue: + stringValue: 20 + objectValue: {fileID: 0} + - fieldName: procCoefficient + fieldValue: + stringValue: 1 + objectValue: {fileID: 0} + - fieldName: attackSoundString + fieldValue: + stringValue: + objectValue: {fileID: 0} diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escRecallSpear.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escRecallSpear.asset new file mode 100644 index 000000000..a3c690686 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escRecallSpear.asset @@ -0,0 +1,27 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3} + m_Name: escRecallSpear + m_EditorClassIdentifier: + targetType: + assemblyQualifiedName: EntityStates.Lancer.RecallSpear, Starstorm2, Version=1.0.0.0, + Culture=neutral, PublicKeyToken=null + serializedFieldsCollection: + serializedFields: + - fieldName: baseDuration + fieldValue: + stringValue: 0.3 + objectValue: {fileID: 0} + - fieldName: recallSoundString + fieldValue: + stringValue: + objectValue: {fileID: 0} diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escSpearThrust.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escSpearThrust.asset new file mode 100644 index 000000000..c63e0d3aa --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escSpearThrust.asset @@ -0,0 +1,84 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3} + m_Name: escSpearThrust + m_EditorClassIdentifier: + targetType: + assemblyQualifiedName: EntityStates.Lancer.SpearThrust, Starstorm2, Version=1.0.0.0, + Culture=neutral, PublicKeyToken=null + serializedFieldsCollection: + serializedFields: + - fieldName: hitboxGroupName + fieldValue: + stringValue: SpearHitbox + objectValue: {fileID: 0} + - fieldName: damageCoefficient + fieldValue: + stringValue: 3.5 + objectValue: {fileID: 0} + - fieldName: procCoefficient + fieldValue: + stringValue: 1 + objectValue: {fileID: 0} + - fieldName: baseDuration + fieldValue: + stringValue: 0.9 + objectValue: {fileID: 0} + - fieldName: attackStartTimeFraction + fieldValue: + stringValue: 0.25 + objectValue: {fileID: 0} + - fieldName: attackEndTimeFraction + fieldValue: + stringValue: 0.45 + objectValue: {fileID: 0} + - fieldName: earlyExitTimeFraction + fieldValue: + stringValue: 0.6 + objectValue: {fileID: 0} + - fieldName: pushForce + fieldValue: + stringValue: 300 + objectValue: {fileID: 0} + - fieldName: hitStopDuration + fieldValue: + stringValue: 0.08 + objectValue: {fileID: 0} + - fieldName: hitHopVelocity + fieldValue: + stringValue: 4 + objectValue: {fileID: 0} + - fieldName: attackRecoil + fieldValue: + stringValue: 0.75 + objectValue: {fileID: 0} + - fieldName: muzzleString + fieldValue: + stringValue: SpearMuzzle + objectValue: {fileID: 0} + - fieldName: playbackRateParam + fieldValue: + stringValue: Primary.playbackRate + objectValue: {fileID: 0} + - fieldName: damageType + fieldValue: + stringValue: '{"assemblyQualifiedName":"RoR2.DamageType, RoR2, Version=0.0.0.0, + Culture=neutral, PublicKeyToken=null","values":"Generic"}' + objectValue: {fileID: 0} + - fieldName: tipperDamageCoefficient + fieldValue: + stringValue: 2 + objectValue: {fileID: 0} + - fieldName: thrustAnimationState + fieldValue: + stringValue: SpearThrust + objectValue: {fileID: 0} diff --git a/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escThrowSpear.asset b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escThrowSpear.asset new file mode 100644 index 000000000..15832ab51 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/AssetStorm/InDev/Characters/Lancer/Skills/escThrowSpear.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 89795172, guid: 951ce57ad999ac1f040a4dceb5f8b763, type: 3} + m_Name: escThrowSpear + m_EditorClassIdentifier: + targetType: + assemblyQualifiedName: EntityStates.Lancer.ThrowSpear, Starstorm2, Version=1.0.0.0, + Culture=neutral, PublicKeyToken=null + serializedFieldsCollection: + serializedFields: + - fieldName: baseDuration + fieldValue: + stringValue: 0.5 + objectValue: {fileID: 0} + - fieldName: damageCoefficient + fieldValue: + stringValue: 4 + objectValue: {fileID: 0} + - fieldName: force + fieldValue: + stringValue: 40 + objectValue: {fileID: 0} + - fieldName: procCoefficient + fieldValue: + stringValue: 1 + objectValue: {fileID: 0} + - fieldName: throwSoundString + fieldValue: + stringValue: + objectValue: {fileID: 0} + - fieldName: muzzleString + fieldValue: + stringValue: SpearMuzzle + objectValue: {fileID: 0} diff --git a/SS2-Project/Assets/Starstorm2/Components/Duke/DukeController.cs b/SS2-Project/Assets/Starstorm2/Components/Duke/DukeController.cs new file mode 100644 index 000000000..f0446fbaa --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Components/Duke/DukeController.cs @@ -0,0 +1,18 @@ +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace SS2.Components +{ + [RequireComponent(typeof(CharacterBody))] + public class DukeController : NetworkBehaviour + { + public bool useAltCamera; + + private void Awake() + { + // TODO Make it configurable + useAltCamera = false; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Components/GupBall/GupBallKickReceiver.cs b/SS2-Project/Assets/Starstorm2/Components/GupBall/GupBallKickReceiver.cs new file mode 100644 index 000000000..b9999336d --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Components/GupBall/GupBallKickReceiver.cs @@ -0,0 +1,96 @@ +using UnityEngine; +using RoR2; + +namespace SS2.Components +{ + /// + /// Receives incoming damage on a GupBall body and converts it into a constant-velocity kick + /// in the direction away from the attacker. Damage is always rejected so the ball never dies. + /// + /// Implements IOnIncomingDamageServerReceiver (not IOnTakeDamageServerReceiver) because: + /// - It fires before damage is applied (HealthComponent.cs:1318) + /// - We can set damageInfo.rejected = true to prevent all damage + /// - godMode would block ALL hooks, so we use rejection instead + /// + /// Networking: This hook runs server-only inside HealthComponent.TakeDamage [Server]. + /// The ball body is server-authoritative (no player owner), so ApplyForceImpulse + /// applies directly. CharacterNetworkTransform syncs position to clients. + /// + public class GupBallKickReceiver : MonoBehaviour, IOnIncomingDamageServerReceiver + { + [Header("Kick Settings")] + public float kickSpeed = 30f; + public float maxSpeed = 60f; + [Tooltip("Multiplier for vertical kick component. Lower = flatter kicks.")] + public float verticalKickMultiplier = 0.6f; + [Tooltip("Upward bias added to every kick so ground-level hits arc slightly.")] + public float upwardBias = 0.1f; + + private CharacterBody body; + private CharacterMotor motor; + + private void Awake() + { + if (!TryGetComponent(out body)) + { + Debug.LogError("GupBallKickReceiver: Missing CharacterBody on " + gameObject.name); + } + if (!TryGetComponent(out motor)) + { + Debug.LogError("GupBallKickReceiver: Missing CharacterMotor on " + gameObject.name); + } + } + + public void OnIncomingDamageServer(DamageInfo damageInfo) + { + damageInfo.rejected = true; + + if (motor == null || body == null) + return; + + if (damageInfo.attacker == null) + return; + + CharacterBody attackerBody; + if (!damageInfo.attacker.TryGetComponent(out attackerBody)) + return; + + Vector3 ballPosition = body.corePosition; + Vector3 attackerPosition = attackerBody.corePosition; + Vector3 kickDir = ballPosition - attackerPosition; + + // Epsilon check: attacker overlapping the ball + if (kickDir.sqrMagnitude < 0.001f) + { + kickDir = Vector3.up; + } + else + { + kickDir.Normalize(); + } + + // Dampen vertical component + kickDir.y *= verticalKickMultiplier; + + // Add upward bias so ground-level hits arc + kickDir.y += upwardBias; + + kickDir.Normalize(); + + Vector3 kickForce = kickDir * kickSpeed; + + PhysForceInfo forceInfo = PhysForceInfo.Create(); + forceInfo.force = kickForce; + forceInfo.massIsOne = true; + forceInfo.ignoreGroundStick = true; + forceInfo.resetVelocity = true; + motor.ApplyForceImpulse(in forceInfo); + + // Safety clamp + if (motor.velocity.sqrMagnitude > maxSpeed * maxSpeed) + { + motor.velocity = motor.velocity.normalized * maxSpeed; + } + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Components/Lancer/IonFieldBehavior.cs b/SS2-Project/Assets/Starstorm2/Components/Lancer/IonFieldBehavior.cs new file mode 100644 index 000000000..8b44b13f3 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Components/Lancer/IonFieldBehavior.cs @@ -0,0 +1,69 @@ +using MSU; +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace SS2.Components +{ + public sealed class IonFieldBehavior : BaseBuffBehaviour + { + [BuffDefAssociation(useOnServer = true, useOnClient = false)] + private static BuffDef GetBuffDef() => SS2.Survivors.Lancer.bdIonField; + + public static float tickInterval = 0.5f; + public static float baseDamagePerTick = 1f; + public static float baseRadius = 5f; + public static float radiusPerStack = 0.5f; + public static GameObject tickEffectPrefab; + + private float tickTimer; + + private void OnEnable() + { + tickTimer = 0f; + } + + private void FixedUpdate() + { + if (!NetworkServer.active || !hasAnyStacks || !characterBody) + return; + + tickTimer += Time.fixedDeltaTime; + if (tickTimer < tickInterval) + return; + + tickTimer -= tickInterval; + + int stacks = characterBody.GetBuffCount(buffDef); + float damage = baseDamagePerTick * (1f + 0.2f * (characterBody.level - 1f)) * stacks; + float radius = baseRadius + radiusPerStack * (stacks - 1); + + if (tickEffectPrefab) + { + EffectManager.SpawnEffect(tickEffectPrefab, new EffectData + { + origin = characterBody.corePosition, + scale = radius + }, true); + } + + BlastAttack blastAttack = new BlastAttack(); + blastAttack.position = characterBody.corePosition; + blastAttack.baseDamage = damage; + blastAttack.baseForce = 0f; + blastAttack.radius = radius; + blastAttack.attacker = gameObject; + blastAttack.inflictor = null; + // Ion field is a debuff on enemies. We want the AoE to damage enemies (from the player perspective). + // Since the debuffed body is an enemy, using its teamIndex would PROTECT enemies from the blast. + // Use TeamIndex.Player so the blast damages everyone NOT on the player team + blastAttack.teamIndex = TeamIndex.Player; + blastAttack.crit = false; + blastAttack.procCoefficient = 0f; + blastAttack.damageColorIndex = DamageColorIndex.Item; + blastAttack.falloffModel = BlastAttack.FalloffModel.None; + blastAttack.damageType = DamageType.Generic; + blastAttack.Fire(); + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Components/Lancer/LancerController.cs b/SS2-Project/Assets/Starstorm2/Components/Lancer/LancerController.cs new file mode 100644 index 000000000..7583e0a82 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Components/Lancer/LancerController.cs @@ -0,0 +1,108 @@ +using RoR2; +using RoR2.Skills; +using UnityEngine; +using UnityEngine.Networking; + +namespace SS2.Components +{ + public class LancerController : NetworkBehaviour + { + public enum SpearState : byte + { + Held = 0, + Thrown = 1, + Returning = 2 + } + + [SerializeField] + public SkillDef unarmedPrimarySkillDef; + + [SerializeField] + public SkillDef recallSpearSkillDef; + + [SyncVar(hook = nameof(OnSpearStateChanged))] + private SpearState _spearState; + + private GameObject _spearProjectileInstance; + private GenericSkill primarySkill; + private GenericSkill secondarySkill; + + public SpearState spearState => _spearState; + + private void Awake() + { + if (TryGetComponent(out SkillLocator skillLocator)) + { + primarySkill = skillLocator.primary; + secondarySkill = skillLocator.secondary; + } + else + { + Debug.LogError("LancerController: Failed to get SkillLocator component."); + } + } + + [Server] + public void SetSpearState(SpearState newState) + { + OnSpearStateChanged(newState); + _spearState = newState; + } + + private void OnSpearStateChanged(SpearState newState) + { + _spearState = newState; + + switch (newState) + { + case SpearState.Thrown: + ApplyUnarmedOverrides(); + break; + case SpearState.Held: + RemoveUnarmedOverrides(); + break; + } + } + + public void SetSpearProjectile(GameObject go) + { + _spearProjectileInstance = go; + } + + public GameObject GetSpearProjectile() + { + return _spearProjectileInstance; + } + + public Vector3 GetSpearPosition() + { + if (_spearProjectileInstance) + return _spearProjectileInstance.transform.position; + + return gameObject.transform.position; + } + + private bool _overridesApplied; + + public void ApplyUnarmedOverrides() + { + if (_overridesApplied) return; + _overridesApplied = true; + primarySkill?.SetSkillOverride(this, unarmedPrimarySkillDef, GenericSkill.SkillOverridePriority.Contextual); + secondarySkill?.SetSkillOverride(this, recallSpearSkillDef, GenericSkill.SkillOverridePriority.Contextual); + } + + public void RemoveUnarmedOverrides() + { + if (!_overridesApplied) return; + _overridesApplied = false; + primarySkill?.UnsetSkillOverride(this, unarmedPrimarySkillDef, GenericSkill.SkillOverridePriority.Contextual); + secondarySkill?.UnsetSkillOverride(this, recallSpearSkillDef, GenericSkill.SkillOverridePriority.Contextual); + } + + private void OnDestroy() + { + RemoveUnarmedOverrides(); + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Components/Lancer/LancerSpearProjectile.cs b/SS2-Project/Assets/Starstorm2/Components/Lancer/LancerSpearProjectile.cs new file mode 100644 index 000000000..a9d835f1a --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Components/Lancer/LancerSpearProjectile.cs @@ -0,0 +1,277 @@ +using System.Collections.Generic; +using RoR2; +using RoR2.Projectile; +using UnityEngine; +using UnityEngine.Networking; + +namespace SS2.Components +{ + [RequireComponent(typeof(ProjectileController))] + public class LancerSpearProjectile : NetworkBehaviour, IProjectileImpactBehavior + { + public enum SpearProjectileState : byte + { + Flying = 0, + Stuck = 1, + Returning = 2 + } + + public float travelSpeed = 80f; + public float returnSpeed = 60f; + public float returnAcceleration = 5f; + public string stickSoundString = ""; + public float catchDistance = 3f; + + public static float ionFieldPassThroughCoefficient = 1f; + + private ProjectileController projectileController; + private Rigidbody rigidbody; + private ProjectileDamage projectileDamage; + private bool hasRegisteredWithOwner; + + // Stick data + private Transform stuckTransform; + private Vector3 localStuckPosition; + private Quaternion localStuckRotation; + private CharacterBody stuckBody; + + // Return pass-through tracking + private HashSet returnHitTargets = new HashSet(); + + [SyncVar] + private SpearProjectileState _state; + + private void Awake() + { + if (!TryGetComponent(out projectileController)) + Debug.LogError("LancerSpearProjectile: Failed to get ProjectileController."); + if (!TryGetComponent(out rigidbody)) + Debug.LogError("LancerSpearProjectile: Failed to get Rigidbody."); + if (!TryGetComponent(out projectileDamage)) + Debug.LogError("LancerSpearProjectile: Failed to get ProjectileDamage."); + } + + private void Start() + { + if (!projectileController.owner) + { + if (NetworkServer.active) + Object.Destroy(gameObject); + return; + } + + if (projectileController.owner.TryGetComponent(out LancerController lancerController)) + { + lancerController.SetSpearProjectile(gameObject); + hasRegisteredWithOwner = true; + } + else + { + Debug.LogError("LancerSpearProjectile: Owner missing LancerController."); + } + } + + public void OnProjectileImpact(ProjectileImpactInfo impactInfo) + { + if (_state != SpearProjectileState.Flying) + return; + + if (impactInfo.collider) + { + HurtBox hurtBox = impactInfo.collider.GetComponent(); + if (hurtBox && hurtBox.healthComponent && hurtBox.healthComponent.gameObject == projectileController.owner) + return; + } + + if (NetworkServer.active) + { + HurtBox hurtBox = impactInfo.collider?.GetComponent(); + + // Deal damage on hit (replaces removed ProjectileSingleTargetImpact) + if (hurtBox && hurtBox.healthComponent) + { + DamageInfo damageInfo = new DamageInfo(); + if (projectileDamage) + { + damageInfo.damage = projectileDamage.damage; + damageInfo.crit = projectileDamage.crit; + damageInfo.attacker = projectileController.owner; + damageInfo.inflictor = gameObject; + damageInfo.position = impactInfo.estimatedPointOfImpact; + damageInfo.force = projectileDamage.force * transform.forward; + damageInfo.procChainMask = projectileController.procChainMask; + damageInfo.procCoefficient = projectileController.procCoefficient; + damageInfo.damageColorIndex = projectileDamage.damageColorIndex; + damageInfo.damageType = projectileDamage.damageType; + } + hurtBox.healthComponent.TakeDamage(damageInfo); + GlobalEventManager.instance.OnHitEnemy(damageInfo, hurtBox.healthComponent.gameObject); + GlobalEventManager.instance.OnHitAll(damageInfo, hurtBox.healthComponent.gameObject); + + // Apply ion field to enemy hit + if (hurtBox.healthComponent.body) + { + hurtBox.healthComponent.body.AddTimedBuff(Survivors.Lancer.bdIonField, Survivors.Lancer.ionFieldDuration); + } + } + + Stick(impactInfo.collider, impactInfo.estimatedImpactNormal); + } + } + + private void Stick(Collider collider, Vector3 normal) + { + Transform hitTransform = collider.transform; + HurtBox hurtBox = collider.GetComponent(); + if (hurtBox && hurtBox.healthComponent) + { + stuckBody = hurtBox.healthComponent.body; + hitTransform = collider.transform; + } + + stuckTransform = hitTransform; + localStuckPosition = hitTransform.InverseTransformPoint(transform.position); + localStuckRotation = Quaternion.Inverse(hitTransform.rotation) * transform.rotation; + + rigidbody.velocity = Vector3.zero; + rigidbody.isKinematic = true; + rigidbody.detectCollisions = false; + + _state = SpearProjectileState.Stuck; + + if (!string.IsNullOrEmpty(stickSoundString)) + Util.PlaySound(stickSoundString, gameObject); + } + + public void BeginReturn() + { + if (!NetworkServer.active) + return; + + rigidbody.isKinematic = false; + rigidbody.detectCollisions = false; + stuckTransform = null; + stuckBody = null; + returnHitTargets.Clear(); + + _state = SpearProjectileState.Returning; + } + + private void FixedUpdate() + { + switch (_state) + { + case SpearProjectileState.Flying: + break; + + case SpearProjectileState.Stuck: + UpdateStuck(); + break; + + case SpearProjectileState.Returning: + UpdateReturn(); + break; + } + } + + private void UpdateStuck() + { + if (stuckTransform) + { + transform.SetPositionAndRotation( + stuckTransform.TransformPoint(localStuckPosition), + stuckTransform.rotation * localStuckRotation + ); + } + else if (_state == SpearProjectileState.Stuck) + { + // Stuck target was destroyed — begin returning to owner + if (NetworkServer.active) + { + BeginReturn(); + } + } + } + + private void UpdateReturn() + { + if (!projectileController.owner) + { + if (NetworkServer.active) + Object.Destroy(gameObject); + return; + } + + Vector3 direction = (projectileController.owner.transform.position - transform.position).normalized; + rigidbody.velocity = direction * returnSpeed; + + // Apply ion field to enemies along return path + if (NetworkServer.active) + { + Collider[] colliders = Physics.OverlapSphere(transform.position, 2f, LayerIndex.entityPrecise.mask); + foreach (Collider col in colliders) + { + HurtBox hurtBox = col.GetComponent(); + if (hurtBox && hurtBox.healthComponent && hurtBox.healthComponent.body) + { + if (returnHitTargets.Contains(hurtBox.healthComponent)) + continue; + + TeamComponent victimTeam = hurtBox.healthComponent.body.teamComponent; + TeamIndex ownerTeam = projectileController.teamFilter ? projectileController.teamFilter.teamIndex : TeamIndex.None; + if (victimTeam && victimTeam.teamIndex != ownerTeam) + { + hurtBox.healthComponent.body.AddTimedBuff(Survivors.Lancer.bdIonField, Survivors.Lancer.ionFieldDuration); + returnHitTargets.Add(hurtBox.healthComponent); + } + } + } + } + + float distToOwner = Vector3.Distance(transform.position, projectileController.owner.transform.position); + if (distToOwner <= catchDistance) + { + OnCaughtByOwner(); + } + } + + private void OnCaughtByOwner() + { + if (!projectileController.owner) + return; + + if (projectileController.owner.TryGetComponent(out LancerController lancerController)) + { + if (NetworkServer.active) + { + lancerController.SetSpearState(LancerController.SpearState.Held); + } + lancerController.SetSpearProjectile(null); + } + else + { + Debug.LogError("LancerSpearProjectile: Owner missing LancerController on catch."); + } + + if (NetworkServer.active) + { + Object.Destroy(gameObject); + } + } + + private void OnDestroy() + { + if (hasRegisteredWithOwner && projectileController && projectileController.owner) + { + if (projectileController.owner.TryGetComponent(out LancerController lancerController)) + { + if (lancerController.GetSpearProjectile() == gameObject) + { + lancerController.SetSpearProjectile(null); + } + } + } + } + + } +} diff --git a/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairController.cs b/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairController.cs index 477fdc3bf..4bc5279d6 100644 --- a/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairController.cs +++ b/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairController.cs @@ -2,7 +2,6 @@ using RoR2.HudOverlay; using RoR2.UI; using System.Collections.Generic; -using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.Networking; @@ -10,17 +9,17 @@ namespace SS2.Components { public class SelfRepairController : NetworkBehaviour { - public CharacterBody characterBody; public float repairMax = 10f; public float repairGainPerUpdate = 0.10f; - public GameObject repairOverlayPrefab = SS2.Survivors.Toolbot.RepairOverlayPrefab; - + public GameObject repairOverlayPrefab; public string repairOverlayChildLocatorEntry = "CrosshairExtras"; + + private CharacterBody characterBody; private OverlayController repairOverlayController; private List fillUiList = new List(); - [SyncVar(hook = "OnRepairModified")] + [SyncVar(hook = nameof(OnRepairModified))] private float _repair; public float repair { @@ -38,38 +37,26 @@ public bool isMaxRepair } } - public float Network_charge + public void OnEnable() { - get + if (!TryGetComponent(out characterBody)) { - return _repair; + Debug.LogError("SelfRepairController: Missing CharacterBody on " + gameObject.name); } - [param: In] - set + + if (repairOverlayPrefab != null) { - if (NetworkServer.localClientActive && syncVarHookGuard) + OverlayCreationParams repairOverlayCreationParams = new OverlayCreationParams { - syncVarHookGuard = true; - OnRepairModified(value); - syncVarHookGuard = false; - } - SetSyncVar(value, ref _repair, 1U); + prefab = repairOverlayPrefab, + childLocatorEntry = repairOverlayChildLocatorEntry + }; + repairOverlayController = HudOverlayManager.AddOverlay(gameObject, repairOverlayCreationParams); + repairOverlayController.onInstanceAdded += OnRepairOverlayInstanceAdded; + repairOverlayController.onInstanceRemove += OnRepairOverlayInstanceRemoved; } } - public void OnEnable() - { - OverlayCreationParams repairOverlayCreationParams = new OverlayCreationParams - { - prefab = repairOverlayPrefab, - childLocatorEntry = repairOverlayChildLocatorEntry - }; - repairOverlayController = HudOverlayManager.AddOverlay(gameObject, repairOverlayCreationParams); - repairOverlayController.onInstanceAdded += OnRepairOverlayInstanceAdded; - repairOverlayController.onInstanceRemove += OnRepairOverlayInstanceRemoved; - characterBody = GetComponent(); - } - private void OnDisable() { if (repairOverlayController != null) @@ -85,13 +72,13 @@ private void FixedUpdate() { if (NetworkServer.active) { - UpdateUI(); - if (!isMaxRepair) { AddRepair(repairGainPerUpdate); } } + + UpdateUI(); } private void UpdateUI() @@ -104,70 +91,41 @@ private void UpdateUI() private void OnRepairOverlayInstanceAdded(OverlayController controller, GameObject instance) { - fillUiList.Add(instance.GetComponent()); + if (instance.TryGetComponent(out ImageFillController ifc)) + { + fillUiList.Add(ifc); + } + else + { + Debug.LogError("SelfRepairController: Overlay instance missing ImageFillController"); + } } private void OnRepairOverlayInstanceRemoved(OverlayController controller, GameObject instance) { - fillUiList.Remove(instance.transform.GetComponent()); + if (instance.TryGetComponent(out ImageFillController ifc)) + { + fillUiList.Remove(ifc); + } } public void AddRepair(float amount) { if (!NetworkServer.active) { - SS2Log.Error("self repair controller add repair on client"); + SS2Log.Error("SelfRepairController: AddRepair called on client"); return; } if (isMaxRepair && amount > 0) amount = 0f; - Network_charge = Mathf.Clamp(repair + amount, 0f, repairMax); + _repair = Mathf.Clamp(repair + amount, 0f, repairMax); } private void OnRepairModified(float newRepair) { - Network_charge = newRepair; - } - - //let him cook - public override bool OnSerialize(NetworkWriter writer, bool forceAll) - { - if (forceAll) - { - writer.Write(_repair); - return true; - } - bool flag = false; - if ((syncVarDirtyBits & 1U) != 0U) - { - if (!flag) - { - writer.WritePackedUInt32(syncVarDirtyBits); - flag = true; - } - writer.Write(_repair); - } - if (!flag) - { - writer.WritePackedUInt32(syncVarDirtyBits); - } - return flag; - } - - public override void OnDeserialize(NetworkReader reader, bool initialState) - { - if (initialState) - { - _repair = reader.ReadSingle(); - return; - } - int num = (int)reader.ReadPackedUInt32(); - if ((num & 1) != 0) - { - OnRepairModified(reader.ReadSingle()); - } + _repair = newRepair; } } } \ No newline at end of file diff --git a/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairSkillDef.cs b/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairSkillDef.cs index bba169f29..c3c2b58cb 100644 --- a/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairSkillDef.cs +++ b/SS2-Project/Assets/Starstorm2/Components/Toolbot/SelfRepairSkillDef.cs @@ -13,14 +13,16 @@ public class SelfRepairSkillDef : SkillDef { public override BaseSkillInstanceData OnAssigned([NotNull] GenericSkill skillSlot) { - skillSlot.characterBody.gameObject.AddComponent(); + SelfRepairController selfRepairController = skillSlot.characterBody.gameObject.EnsureComponent(); + + if (SS2.Survivors.Toolbot.RepairOverlayPrefab != null) + { + selfRepairController.repairOverlayPrefab = SS2.Survivors.Toolbot.RepairOverlayPrefab; + } - SelfRepairController selfRepairMeter = skillSlot.characterBody.gameObject.EnsureComponent(); - //selfRepairMeter.repairOverlayPrefab = SS2.Survivors.Toolbot.RepairOverlayPrefab; - return new SelfRepairSkillDef.InstanceData { - selfRepairMeter = selfRepairMeter + selfRepairMeter = selfRepairController }; } diff --git a/SS2-Project/Assets/Starstorm2/Modules/Characters/Monsters/GupBall.cs b/SS2-Project/Assets/Starstorm2/Modules/Characters/Monsters/GupBall.cs new file mode 100644 index 000000000..9ab0c5590 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/Characters/Monsters/GupBall.cs @@ -0,0 +1,110 @@ +using RoR2; +using UnityEngine; +using MSU; +using MSU.Config; +using RoR2.ContentManagement; +using RiskOfOptions.OptionConfigs; +using SS2.Components; + +namespace SS2.Monsters +{ + public sealed class GupBall : SS2Monster + { + public override SS2AssetRequest AssetRequest => + SS2Assets.LoadAssetAsync("acGupBall", SS2Bundle.Indev); + + // Config values + private static ConfiguredFloat cfgKickSpeed; + private static ConfiguredFloat cfgMaxSpeed; + private static ConfiguredFloat cfgVerticalKickMultiplier; + private static ConfiguredFloat cfgUpwardBias; + private static ConfiguredFloat cfgBaseMoveSpeed; + + public override bool IsAvailable(ContentPack contentPack) + { + return SS2Config.enableBeta && base.IsAvailable(contentPack); + } + + public override void Initialize() + { + cfgKickSpeed = SS2Config.ConfigFactory.MakeConfiguredFloat(30f, b => + { + b.section = "GupBall"; + b.key = "Kick Speed"; + b.description = "Velocity applied to the ball per hit."; + b.configFile = SS2Config.ConfigMonster; + b.sliderConfig = new SliderConfig { min = 5f, max = 100f }; + }).DoConfigure(); + + cfgMaxSpeed = SS2Config.ConfigFactory.MakeConfiguredFloat(60f, b => + { + b.section = "GupBall"; + b.key = "Max Speed"; + b.description = "Maximum velocity the ball can reach."; + b.configFile = SS2Config.ConfigMonster; + b.sliderConfig = new SliderConfig { min = 10f, max = 200f }; + }).DoConfigure(); + + cfgVerticalKickMultiplier = SS2Config.ConfigFactory.MakeConfiguredFloat(0.6f, b => + { + b.section = "GupBall"; + b.key = "Vertical Kick Multiplier"; + b.description = "Multiplier for the vertical component of kicks. Lower values = flatter kicks."; + b.configFile = SS2Config.ConfigMonster; + b.sliderConfig = new SliderConfig { min = 0f, max = 1f }; + }).DoConfigure(); + + cfgUpwardBias = SS2Config.ConfigFactory.MakeConfiguredFloat(0.1f, b => + { + b.section = "GupBall"; + b.key = "Upward Bias"; + b.description = "Upward nudge added to each kick so ground-level hits arc slightly."; + b.configFile = SS2Config.ConfigMonster; + b.sliderConfig = new SliderConfig { min = 0f, max = 0.5f }; + }).DoConfigure(); + + cfgBaseMoveSpeed = SS2Config.ConfigFactory.MakeConfiguredFloat(7f, b => + { + b.section = "GupBall"; + b.key = "Friction (Base Move Speed)"; + b.description = "Controls how quickly the ball decelerates. Higher = more friction."; + b.configFile = SS2Config.ConfigMonster; + b.sliderConfig = new SliderConfig { min = 0f, max = 20f }; + }).DoConfigure(); + + if (CharacterPrefab == null) + { + SS2Log.Warning("GupBall: CharacterPrefab is null, skipping Initialize."); + return; + } + + // Set body flags + if (CharacterPrefab.TryGetComponent(out var characterBody)) + { + characterBody.baseMoveSpeed = cfgBaseMoveSpeed; + characterBody.bodyFlags |= CharacterBody.BodyFlags.IgnoreFallDamage; + } + else + { + Debug.LogError("GupBall: Missing CharacterBody on " + CharacterPrefab.name); + } + + // Hide health bar + if (CharacterPrefab.TryGetComponent(out var healthComponent)) + { + healthComponent.dontShowHealthbar = true; + } + else + { + Debug.LogError("GupBall: Missing HealthComponent on " + CharacterPrefab.name); + } + + // Add kick receiver + var kickReceiver = CharacterPrefab.AddComponent(); + kickReceiver.kickSpeed = cfgKickSpeed; + kickReceiver.maxSpeed = cfgMaxSpeed; + kickReceiver.verticalKickMultiplier = cfgVerticalKickMultiplier; + kickReceiver.upwardBias = cfgUpwardBias; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/Duke.cs b/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/Duke.cs new file mode 100644 index 000000000..a6d80184a --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/Duke.cs @@ -0,0 +1,183 @@ +using MSU; +using R2API; +using RoR2; +using RoR2.ContentManagement; +using RoR2.UI; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace SS2.Survivors +{ + public sealed class Duke : SS2Survivor, IContentPackModifier + { + public override SS2AssetRequest AssetRequest => SS2Assets.LoadAssetAsync("acDuke", SS2Bundle.Indev); + + public static ModdedProcType RicochetProc { get; private set; } + + public static BuffDef bdDukeStun; + + // Ricochet tuning + private static float ricochetDamageCoefficient = 1f; + private static float ricochetRange = 30f; + private static float ricochetProcCoefficient = 0.5f; + + // Ricochet VFX + public static GameObject ricochetTracerPrefab; + public static GameObject ricochetHitPrefab; + + public override bool IsAvailable(ContentPack contentPack) + { + return SS2Config.enableBeta && base.IsAvailable(contentPack); + } + + public override void Initialize() + { + RicochetProc = ProcTypeAPI.ReserveProcType(); + + bdDukeStun = AssetCollection.FindAsset("bdDukeStun"); + if (!bdDukeStun) + Debug.LogError("[Duke] Failed to find bdDukeStun BuffDef in asset collection. Stun and ricochet-on-stun will not work."); + + GlobalEventManager.onServerDamageDealt += OnServerDamageDealt; + + AssignEffects(); + ModifyPrefab(); + } + + public void ModifyContentPack(ContentPack contentPack) + { + } + + private void ModifyPrefab() + { + if (CharacterPrefab.TryGetComponent(out CharacterBody cb)) + { + cb.preferredPodPrefab = Addressables.LoadAssetAsync("RoR2/Base/SurvivorPod/SurvivorPod.prefab").WaitForCompletion(); + cb._defaultCrosshairPrefab = Addressables.LoadAssetAsync("RoR2/Base/Bandit2/Bandit2Crosshair.prefab").WaitForCompletion(); + } + else + { + Debug.LogError("[Duke] ModifyPrefab: CharacterPrefab missing CharacterBody component."); + } + } + + private void AssignEffects() + { + // M1 Deagle VFX + EntityStates.Duke.FireDeagle.muzzleEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/MuzzleflashFMJ.prefab").WaitForCompletion(); + EntityStates.Duke.FireDeagle.tracerPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/TracerCommandoDefault.prefab").WaitForCompletion(); + EntityStates.Duke.FireDeagle.hitPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/HitsparkCommando.prefab").WaitForCompletion(); + + // ADS Shot VFX + EntityStates.Duke.FireADSShot.muzzleEffectPrefab = Addressables.LoadAssetAsync("RoR2/DLC1/Railgunner/MuzzleflashRailgun.prefab").WaitForCompletion(); + EntityStates.Duke.FireADSShot.tracerPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/TracerCommandoDefault.prefab").WaitForCompletion(); + EntityStates.Duke.FireADSShot.hitPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/HitsparkCommando.prefab").WaitForCompletion(); + + // ADS weakpoint crosshair — clone StandardCrosshair and add weakpoint visualization (Pilot pattern) + var baseCrosshair = Addressables.LoadAssetAsync("RoR2/Base/UI/StandardCrosshair.prefab").WaitForCompletion(); + var dukeCrosshair = UnityEngine.Object.Instantiate(baseCrosshair); + dukeCrosshair.name = "DukeADSCrosshair"; + var visualizerPrefab = Addressables.LoadAssetAsync("RoR2/DLC1/Railgunner/RailgunnerSniperTargetVisualizerLight.prefab").WaitForCompletion(); + dukeCrosshair.AddComponent(); // required by SniperTargetViewer + var targetViewer = dukeCrosshair.AddComponent(); + targetViewer.visualizerPrefab = visualizerPrefab; + EntityStates.Duke.AimDownSights.crosshairOverridePrefab = dukeCrosshair; + + // Stun cone VFX + EntityStates.Duke.StunCone.effectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Common/VFX/OmniExplosionVFXQuick.prefab").WaitForCompletion(); + + // Ricochet VFX + ricochetTracerPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/TracerCommandoDefault.prefab").WaitForCompletion(); + ricochetHitPrefab = Addressables.LoadAssetAsync("RoR2/Base/Commando/HitsparkCommando.prefab").WaitForCompletion(); + } + + private void OnServerDamageDealt(DamageReport damageReport) + { + if (!NetworkServer.active) return; + + DamageInfo damageInfo = damageReport.damageInfo; + CharacterBody attackerBody = damageReport.attackerBody; + CharacterBody victimBody = damageReport.victimBody; + + if (!attackerBody || !victimBody) return; + + // Only Duke bodies trigger ricochet + if (!attackerBody.TryGetComponent(out Components.DukeController _)) return; + + // Prevent ricochet chain (n=1 only) + if (damageInfo.procChainMask.HasModdedProc(RicochetProc)) return; + + // Trigger conditions: crit OR victim has Duke stun debuff + bool shouldRicochet = damageInfo.crit || (bdDukeStun && victimBody.HasBuff(bdDukeStun)); + if (!shouldRicochet) return; + + // Find nearest enemy to the victim + HurtBox targetHurtBox = FindRicochetTarget(attackerBody, victimBody); + if (!targetHurtBox) return; + + FireRicochet(damageInfo, attackerBody, victimBody, targetHurtBox); + } + + private HurtBox FindRicochetTarget(CharacterBody attackerBody, CharacterBody victimBody) + { + var search = new BullseyeSearch(); + search.searchOrigin = victimBody.corePosition; + search.searchDirection = Vector3.zero; + search.teamMaskFilter = TeamMask.GetEnemyTeams(attackerBody.teamComponent.teamIndex); + search.filterByLoS = true; + search.sortMode = BullseyeSearch.SortMode.Distance; + search.maxDistanceFilter = ricochetRange; + search.filterByDistinctEntity = true; + search.RefreshCandidates(); + + // Exclude the victim itself + return search.GetResults() + .Where(h => h && h.healthComponent && h.healthComponent.alive && h.healthComponent != victimBody.healthComponent) + .FirstOrDefault(); + } + + private void FireRicochet(DamageInfo originalDamage, CharacterBody attackerBody, CharacterBody victimBody, HurtBox target) + { + Vector3 origin = victimBody.corePosition; + Vector3 direction = (target.transform.position - origin).normalized; + + // Offset origin slightly toward the target to avoid re-hitting the victim's own collider + origin += direction * (victimBody.radius + 0.5f); + + ProcChainMask procMask = originalDamage.procChainMask; + procMask.AddModdedProc(RicochetProc); + + DamageTypeCombo damageType = DamageType.Generic; + + var bulletAttack = new BulletAttack + { + aimVector = direction, + origin = origin, + damage = originalDamage.damage * ricochetDamageCoefficient, + damageType = damageType, + damageColorIndex = DamageColorIndex.Default, + minSpread = 0f, + maxSpread = 0f, + falloffModel = BulletAttack.FalloffModel.None, + maxDistance = ricochetRange, + force = 100f, + isCrit = originalDamage.crit, + owner = attackerBody.gameObject, + muzzleName = "", + smartCollision = true, + procChainMask = procMask, + procCoefficient = ricochetProcCoefficient, + radius = 0.5f, + weapon = attackerBody.gameObject, + tracerEffectPrefab = ricochetTracerPrefab, + hitEffectPrefab = ricochetHitPrefab + }; + bulletAttack.Fire(); + + Debug.Log("[Duke] Ricochet fired from " + victimBody.name + " toward " + target.healthComponent.body.name); + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/Lancer.cs b/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/Lancer.cs new file mode 100644 index 000000000..e7eea329c --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/Characters/Survivors/Lancer.cs @@ -0,0 +1,176 @@ +using MSU; +using R2API; +using RoR2; +using RoR2.ContentManagement; +using RoR2.Projectile; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Networking; + +namespace SS2.Survivors +{ + public sealed class Lancer : SS2Survivor + { + public override SS2AssetRequest AssetRequest => SS2Assets.LoadAssetAsync("acLancer", SS2Bundle.Indev); + + public static BuffDef bdIonField; + + // Ion field tuning + public static float ionFieldDuration = 5f; + public static int ionFieldMaxStacks = 5; + + public static DamageAPI.ModdedDamageType TipperIonFieldDamageType { get; set; } + + // Code-created projectile prefabs + private static GameObject lancerSpearProjectile; + private static GameObject lancerIonFistProjectile; + + public override bool IsAvailable(ContentPack contentPack) + { + return SS2Config.enableBeta && base.IsAvailable(contentPack); + } + + public override void Initialize() + { + bdIonField = AssetCollection.FindAsset("bdIonField"); + + if (bdIonField) + { + bdIonField.canStack = true; + bdIonField.maxStacks = ionFieldMaxStacks; + } + + TipperIonFieldDamageType = DamageAPI.ReserveDamageType(); + GlobalEventManager.onServerDamageDealt += OnServerDamageDealt; + + CreateProjectiles(); + AssignEffects(); + ModifyPrefab(); + } + + public override void ModifyContentPack(ContentPack contentPack) + { + base.ModifyContentPack(contentPack); + contentPack.projectilePrefabs.Add(new GameObject[] { lancerSpearProjectile, lancerIonFistProjectile }); + } + + private void CreateProjectiles() + { + CreateSpearProjectile(); + CreateIonFistProjectile(); + } + + private void CreateSpearProjectile() + { + var original = Addressables.LoadAssetAsync("RoR2/Base/Bandit2/Bandit2ShivProjectile.prefab").WaitForCompletion(); + lancerSpearProjectile = PrefabAPI.InstantiateClone(original, "LancerSpearProjectile", true); + + // Remove vanilla impact/stick behavior — LancerSpearProjectile handles everything + if (lancerSpearProjectile.TryGetComponent(out ProjectileStickOnImpact stick)) + Object.DestroyImmediate(stick); + else + Debug.LogError("[Lancer] Bandit2ShivProjectile clone missing ProjectileStickOnImpact"); + + if (lancerSpearProjectile.TryGetComponent(out ProjectileSingleTargetImpact singleTarget)) + Object.DestroyImmediate(singleTarget); + else + Debug.LogError("[Lancer] Bandit2ShivProjectile clone missing ProjectileSingleTargetImpact"); + + if (lancerSpearProjectile.TryGetComponent(out RoR2.EntityLogic.DelayedEvent delayedEvent)) + Object.DestroyImmediate(delayedEvent); + + if (lancerSpearProjectile.TryGetComponent(out RoR2.EventFunctions eventFunctions)) + Object.DestroyImmediate(eventFunctions); + + // Configure ProjectileSimple + if (lancerSpearProjectile.TryGetComponent(out ProjectileSimple ps)) + { + ps.desiredForwardSpeed = 80f; + ps.lifetime = 30f; + ps.updateAfterFiring = false; + } + else + { + Debug.LogError("[Lancer] Spear projectile missing ProjectileSimple"); + } + + // Disable gravity for linear travel + if (lancerSpearProjectile.TryGetComponent(out Rigidbody rb)) + { + rb.useGravity = false; + } + + // Add our custom spear behavior + lancerSpearProjectile.AddComponent(); + + EnsureNetworkIdentity(lancerSpearProjectile); + + Debug.Log("[Lancer] Created LancerSpearProjectile prefab"); + } + + private void CreateIonFistProjectile() + { + var original = Addressables.LoadAssetAsync("RoR2/DLC2/Seeker/SpiritPunchProjectile.prefab").WaitForCompletion(); + lancerIonFistProjectile = PrefabAPI.InstantiateClone(original, "LancerIonFistProjectile", true); + + // Keep all Seeker VFX (ghost, dissipate, impact) — reusing vanilla assets + // Keep ProjectileOverlapAttack, trigger collider, HitBox child, fireFrequency=60 + // Speed and lifetime already good (70 speed, 0.75s lifetime) + + EnsureNetworkIdentity(lancerIonFistProjectile); + + Debug.Log("[Lancer] Created LancerIonFistProjectile prefab (SpiritPunch clone)"); + } + + private void AssignEffects() + { + // Cache shared loads + var muzzleflashFMJ = Addressables.LoadAssetAsync("RoR2/Base/Commando/MuzzleflashFMJ.prefab").WaitForCompletion(); + var omniImpactVFXSlash = Addressables.LoadAssetAsync("RoR2/Base/Common/VFX/OmniImpactVFXSlash.prefab").WaitForCompletion(); + + // ThrowSpear + EntityStates.Lancer.ThrowSpear.projectilePrefab = lancerSpearProjectile; + EntityStates.Lancer.ThrowSpear.muzzleflashEffectPrefab = muzzleflashFMJ; + + // IonFistPunch + EntityStates.Lancer.IonFistPunch.projectilePrefab = lancerIonFistProjectile; + EntityStates.Lancer.IonFistPunch.muzzleflashEffectPrefab = Addressables.LoadAssetAsync("RoR2/DLC2/Seeker/SpiritPunchMuzzleFlashVFX.prefab").WaitForCompletion(); + + // IonDetonation + EntityStates.Lancer.IonDetonation.novaEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Vagrant/VagrantNovaExplosion.prefab").WaitForCompletion(); + EntityStates.Lancer.IonDetonation.novaImpactEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Loader/OmniImpactVFXLoader.prefab").WaitForCompletion(); + + // IonFieldBehavior tick effect + Components.IonFieldBehavior.tickEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Common/VFX/OmniExplosionVFXQuick.prefab").WaitForCompletion(); + + // SpearThrust (bridging static → instance fields in OnEnter) + EntityStates.Lancer.SpearThrust.spearSwingEffectPrefab = Addressables.LoadAssetAsync("RoR2/Base/Merc/MercSwordSlash.prefab").WaitForCompletion(); + EntityStates.Lancer.SpearThrust.spearHitEffectPrefab = omniImpactVFXSlash; + + Debug.Log("[Lancer] All VFX/projectile references assigned"); + } + + private static void EnsureNetworkIdentity(GameObject prefab) + { + if (!prefab.GetComponent()) + prefab.AddComponent(); + } + + private static void OnServerDamageDealt(DamageReport damageReport) + { + if (DamageAPI.HasModdedDamageType(damageReport.damageInfo, TipperIonFieldDamageType)) + { + if (damageReport.victimBody) + { + damageReport.victimBody.AddTimedBuff(bdIonField, ionFieldDuration); + } + } + } + + private void ModifyPrefab() + { + var cb = CharacterPrefab.GetComponent(); + cb.preferredPodPrefab = Addressables.LoadAssetAsync("RoR2/Base/SurvivorPod/SurvivorPod.prefab").WaitForCompletion(); + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/Characters/Variants/Toolbot.cs b/SS2-Project/Assets/Starstorm2/Modules/Characters/Variants/Toolbot.cs index 7a17b1eb4..933db2216 100644 --- a/SS2-Project/Assets/Starstorm2/Modules/Characters/Variants/Toolbot.cs +++ b/SS2-Project/Assets/Starstorm2/Modules/Characters/Variants/Toolbot.cs @@ -17,20 +17,17 @@ public class Toolbot : SS2VanillaSurvivor public override void Initialize() { - //GameObject toolbotBodyPrefab = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/ToolbotBody.prefab").WaitForCompletion(); + GameObject toolbotBodyPrefab = Addressables.LoadAssetAsync("RoR2/Base/Toolbot/ToolbotBody.prefab").WaitForCompletion(); - //SkillLocator skillLocator = toolbotBodyPrefab.GetComponent(); - //SkillFamily primarySkillFamily = skillLocator.primary.skillFamily; - //SkillFamily secondarySkillFamily = skillLocator.secondary.skillFamily; - //SkillFamily specialSkillFamily = skillLocator.special.skillFamily; + SkillLocator skillLocator = toolbotBodyPrefab.GetComponent(); + SkillFamily secondarySkillFamily = skillLocator.secondary.skillFamily; - //RepairOverlayPrefab = assetCollection.FindAsset("ToolbotRepairUI"); + RepairOverlayPrefab = assetCollection.FindAsset("ToolbotRepairUI"); - //SelfRepairSkillDef sdSelfRepair = assetCollection.FindAsset("sdSelfRepair"); - - //AddSkill(secondarySkillFamily, sdSelfRepair); - //AddEntityStateMachine(toolbotBodyPrefab, "Toolbox"); // Used by skills like self-repair that dont want to interrupt weapon + SelfRepairSkillDef sdSelfRepair = assetCollection.FindAsset("sdSelfRepair"); + AddSkill(secondarySkillFamily, sdSelfRepair); + AddEntityStateMachine(toolbotBodyPrefab, "Toolbox"); } public override void ModifyContentPack(ContentPack contentPack) @@ -44,4 +41,3 @@ public override bool IsAvailable(ContentPack contentPack) } } } - diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/AimDownSights.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/AimDownSights.cs new file mode 100644 index 000000000..144ea193e --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/AimDownSights.cs @@ -0,0 +1,160 @@ +using RoR2; +using RoR2.Skills; +using RoR2.UI; +using UnityEngine; + +namespace EntityStates.Duke +{ + public class AimDownSights : BaseSkillState + { + private static float camEntryDuration = 0.2f; + private static float camExitDuration = 0.4f; + private static float overrideDelay = 0.175f; + + private static string enterSoundString = "Play_bandit2_m2_charge"; + private static string exitSoundString = "Play_bandit2_m2_uncharge"; + + public static GameObject crosshairOverridePrefab; + + [SerializeField] + public SkillDef adsFireSkillDef; + + private float overrideStopwatch; + private bool overrideSet; + private CrosshairUtils.OverrideRequest crosshairOverrideRequest; + private CameraTargetParams.CameraParamsOverrideHandle camOverrideHandle; + private SS2.Components.DukeController dukeController; + + private CharacterCameraParamsData adsCameraParams = new CharacterCameraParamsData + { + pivotVerticalOffset = 1.37f, + idealLocalCameraPos = new Vector3(1.2f, -0.75f, -6.1f), + }; + + private CharacterCameraParamsData altCameraParams = new CharacterCameraParamsData + { + pivotVerticalOffset = 1.37f, + idealLocalCameraPos = new Vector3(-1.2f, -0.75f, -6.1f), + }; + + public override void OnEnter() + { + base.OnEnter(); + + Util.PlaySound(enterSoundString, gameObject); + PlayCrossfade("Gesture, Override", "AimDownSights", "Secondary.playbackRate", 0.5f, 0.3f); + + characterBody.SetAimTimer(2f); + + if (gameObject.TryGetComponent(out SS2.Components.DukeController controller)) + { + dukeController = controller; + } + else + { + Debug.LogError("[Duke] AimDownSights: Failed to get DukeController component."); + } + + ApplyCameraOverride(); + + if (skillLocator.secondary.stock > 0) + { + TrySetSkillOverride(); + } + + if (crosshairOverridePrefab) + { + crosshairOverrideRequest = CrosshairUtils.RequestOverrideForBody(characterBody, crosshairOverridePrefab, CrosshairUtils.OverridePriority.Skill); + } + } + + private void ApplyCameraOverride() + { + bool useAlt = dukeController && dukeController.useAltCamera; + + cameraTargetParams.RemoveParamsOverride(camOverrideHandle, camExitDuration); + + var request = new CameraTargetParams.CameraParamsOverrideRequest + { + cameraParamsData = useAlt ? altCameraParams : adsCameraParams, + priority = 0f + }; + camOverrideHandle = cameraTargetParams.AddParamsOverride(request, camEntryDuration); + } + + private void TrySetSkillOverride() + { + overrideStopwatch += Time.fixedDeltaTime; + if (!overrideSet && overrideStopwatch >= overrideDelay) + { + overrideSet = true; + skillLocator.primary.SetSkillOverride(this, adsFireSkillDef, GenericSkill.SkillOverridePriority.Replacement); + } + } + + private void UnsetSkillOverride() + { + overrideStopwatch = 0f; + if (overrideSet) + { + overrideSet = false; + skillLocator.primary.UnsetSkillOverride(this, adsFireSkillDef, GenericSkill.SkillOverridePriority.Replacement); + } + } + + public override void Update() + { + base.Update(); + if (Input.GetKeyDown(KeyCode.V) && isAuthority && dukeController) + { + dukeController.useAltCamera = !dukeController.useAltCamera; + ApplyCameraOverride(); + } + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + + characterBody.isSprinting = false; + characterBody.SetAimTimer(2f); + + if (skillLocator.secondary.stock > 0) + { + TrySetSkillOverride(); + } + else + { + UnsetSkillOverride(); + } + + if (isAuthority && !inputBank.skill2.down) + { + outer.SetNextStateToMain(); + } + } + + public override void OnExit() + { + base.OnExit(); + UnsetSkillOverride(); + PlayCrossfade("Gesture, Override", "BufferEmpty", "Secondary.playbackRate", 0.3f, 0.3f); + Util.PlaySound(exitSoundString, gameObject); + + if (cameraTargetParams) + { + cameraTargetParams.RemoveParamsOverride(camOverrideHandle, camExitDuration); + } + + if (crosshairOverrideRequest != null) + { + crosshairOverrideRequest.Dispose(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.PrioritySkill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/DiveRoll.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/DiveRoll.cs new file mode 100644 index 000000000..cb0543867 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/DiveRoll.cs @@ -0,0 +1,197 @@ +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Duke +{ + public class DiveRoll : BaseSkillState + { + // Roll phase + private static float rollDuration = 0.6f; + private static float rollInitialSpeedCoefficient = 6f; + private static float rollFinalSpeedCoefficient = 1.5f; + + // Slide phase + private static float maxSlideDuration = 1.2f; + private static float slideInitialSpeedCoefficient = 4f; + private static float slideFinalSpeedCoefficient = 1f; + + private static string rollSoundString = ""; + + private float rollSpeed; + private Vector3 forwardDirection; + private Vector3 previousPosition; + private Animator animator; + + private bool inSlidePhase; + private float slideAge; + + public override void OnEnter() + { + base.OnEnter(); + + animator = GetModelAnimator(); + + Util.PlaySound(Commando.DodgeState.dodgeSoundString, gameObject); + if (rollSoundString.Length > 0) + Util.PlaySound(rollSoundString, gameObject); + + PlayCrossfade("Body", "Utility", "Utility.rate", rollDuration * 1.25f, 0.05f); + PlayCrossfade("Gesture, Override", "BufferEmpty", 0.05f); + PlayCrossfade("FullBody, Override", "BufferEmpty", 0.05f); + + if (NetworkServer.active) + characterBody.AddTimedBuff(RoR2Content.Buffs.ArmorBoost, rollDuration); + + if (isAuthority && inputBank && characterDirection) + { + forwardDirection = ((inputBank.moveVector == Vector3.zero) ? characterDirection.forward : inputBank.moveVector).normalized; + } + + inSlidePhase = false; + slideAge = 0f; + RecalculateSpeed(); + + if (characterMotor && characterDirection) + { + characterMotor.velocity = forwardDirection * rollSpeed; + characterMotor.velocity.y = 0f; + } + + Vector3 velocity = characterMotor ? characterMotor.velocity : Vector3.zero; + previousPosition = transform.position - velocity; + } + + private void RecalculateSpeed() + { + if (inSlidePhase) + { + float t = Mathf.Clamp01(slideAge / maxSlideDuration); + rollSpeed = moveSpeedStat * Mathf.Lerp(slideInitialSpeedCoefficient, slideFinalSpeedCoefficient, t); + } + else + { + rollSpeed = moveSpeedStat * Mathf.Lerp(rollInitialSpeedCoefficient, rollFinalSpeedCoefficient, fixedAge / rollDuration); + } + } + + private void TransitionToSlide() + { + inSlidePhase = true; + slideAge = 0f; + + PlayCrossfade("Body", "Slide", 0.1f); + Util.PlaySound("Play_commando_shift", gameObject); + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + RecalculateSpeed(); + + characterBody.isSprinting = true; + + if (isAuthority) + { + if (inSlidePhase) + { + slideAge += Time.fixedDeltaTime; + + // Steer during slide + if (inputBank && characterDirection) + { + Vector3 moveInput = inputBank.moveVector; + if (moveInput != Vector3.zero) + { + forwardDirection = Vector3.Lerp(forwardDirection, moveInput.normalized, 3f * Time.fixedDeltaTime).normalized; + } + } + } + + Vector3 normalized = (transform.position - previousPosition).normalized; + if (characterMotor && characterDirection && normalized != Vector3.zero) + { + Vector3 vector = normalized * rollSpeed; + float y = vector.y; + vector.y = 0f; + float d = Mathf.Max(Vector3.Dot(vector, forwardDirection), 0f); + vector = forwardDirection * d; + vector.y += Mathf.Max(y, 0f); + characterMotor.velocity = vector; + } + + if (animator) + { + Vector3 rhs = inputBank ? characterDirection.forward : forwardDirection; + Vector3 rhs2 = Vector3.Cross(Vector3.up, rhs); + float num = Vector3.Dot(forwardDirection, rhs); + float num2 = Vector3.Dot(forwardDirection, rhs2); + animator.SetFloat("forwardSpeed", num); + animator.SetFloat("rightSpeed", num2); + } + + previousPosition = transform.position; + + // Phase transition and exit logic + if (!inSlidePhase && fixedAge >= rollDuration) + { + if (inputBank.skill3.down) + { + TransitionToSlide(); + } + else + { + outer.SetNextStateToMain(); + return; + } + } + + if (inSlidePhase) + { + if (!inputBank.skill3.down || slideAge >= maxSlideDuration) + { + outer.SetNextStateToMain(); + return; + } + } + } + } + + public override void OnExit() + { + // Reload M2 stocks + if (skillLocator && skillLocator.secondary) + { + skillLocator.secondary.Reset(); + Debug.Log("[Duke] DiveRoll: Restocked secondary (" + skillLocator.secondary.maxStock + " rounds)."); + } + + if (animator) + { + animator.SetBool("isRolling", false); + } + + if (cameraTargetParams) + cameraTargetParams.fovOverride = -1f; + + base.OnExit(); + } + + public override void OnSerialize(NetworkWriter writer) + { + base.OnSerialize(writer); + writer.Write(forwardDirection); + } + + public override void OnDeserialize(NetworkReader reader) + { + base.OnDeserialize(reader); + forwardDirection = reader.ReadVector3(); + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.Frozen; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/FireADSShot.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/FireADSShot.cs new file mode 100644 index 000000000..638f6daff --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/FireADSShot.cs @@ -0,0 +1,102 @@ +using RoR2; +using UnityEngine; + +namespace EntityStates.Duke +{ + public class FireADSShot : BaseSkillState + { + private static float damageCoefficient = 4f; + private static float procCoefficient = 1f; + private static float baseDuration = 0.4f; + private static float recoil = 4f; + private static float spreadBloom = 0.3f; + private static float force = 300f; + private static float bulletRadius = 0.4f; + private static float range = 256f; + private static string muzzleString = "Muzzle"; + + public static GameObject muzzleEffectPrefab; + public static GameObject tracerPrefab; + public static GameObject hitPrefab; + + private float duration; + private bool hasFired; + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + characterBody.SetAimTimer(2f); + hasFired = false; + PlayAnimation("Gesture, Override", "FireADS"); + Fire(); + } + + private void Fire() + { + if (hasFired) return; + hasFired = true; + + if (skillLocator.secondary.stock <= 0) return; + skillLocator.secondary.DeductStock(1); + characterBody.OnSkillActivated(skillLocator.secondary); + + bool isCrit = RollCrit(); + Util.PlayAttackSpeedSound("Play_railgunner_m1_fire", gameObject, attackSpeedStat); + AddRecoil(-0.6f * recoil, -1f * recoil, -0.3f * recoil, 0.3f * recoil); + + if (muzzleEffectPrefab) + EffectManager.SimpleMuzzleFlash(muzzleEffectPrefab, gameObject, muzzleString, false); + + if (isAuthority) + { + Ray aimRay = GetAimRay(); + + DamageTypeCombo damageType = DamageType.Generic; + damageType.damageSource = DamageSource.Secondary; + + var bulletAttack = new BulletAttack + { + aimVector = aimRay.direction, + origin = aimRay.origin, + damage = damageCoefficient * damageStat, + damageType = damageType, + damageColorIndex = DamageColorIndex.Default, + minSpread = 0f, + maxSpread = 0f, + falloffModel = BulletAttack.FalloffModel.None, + maxDistance = range, + force = force, + isCrit = isCrit, + owner = gameObject, + muzzleName = muzzleString, + smartCollision = true, + procChainMask = default, + procCoefficient = procCoefficient, + radius = bulletRadius, + weapon = gameObject, + tracerEffectPrefab = tracerPrefab, + hitEffectPrefab = hitPrefab, + sniper = true // BulletAttack auto-crits on sniper target hits (sets isCrit + DamageColorIndex.Sniper) + }; + + bulletAttack.Fire(); + } + characterBody.AddSpreadBloom(spreadBloom); + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.Skill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/FireDeagle.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/FireDeagle.cs new file mode 100644 index 000000000..12a0e4e2b --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/FireDeagle.cs @@ -0,0 +1,96 @@ +using RoR2; +using UnityEngine; + +namespace EntityStates.Duke +{ + public class FireDeagle : BaseSkillState + { + private static float damageCoefficient = 1.8f; + private static float procCoefficient = 1f; + private static float baseDuration = 0.5f; + private static float recoil = 3f; + private static float spreadBloom = 0.5f; + private static float force = 200f; + private static float bulletRadius = 1f; + private static float range = 256f; + private static string muzzleString = "Muzzle"; + + public static GameObject muzzleEffectPrefab; + public static GameObject tracerPrefab; + public static GameObject hitPrefab; + + private float duration; + private bool hasFired; + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + characterBody.SetAimTimer(2f); + hasFired = false; + PlayAnimation("Gesture, Override", "Primary"); + Fire(); + } + + private void Fire() + { + if (hasFired) return; + hasFired = true; + + bool isCrit = RollCrit(); + Util.PlayAttackSpeedSound("Play_bandit2_m1_rifle", gameObject, attackSpeedStat); + AddRecoil(-0.4f * recoil, -0.8f * recoil, -0.3f * recoil, 0.3f * recoil); + + if (muzzleEffectPrefab) + EffectManager.SimpleMuzzleFlash(muzzleEffectPrefab, gameObject, muzzleString, false); + + if (isAuthority) + { + Ray aimRay = GetAimRay(); + + DamageTypeCombo damageType = DamageType.Generic; + damageType.damageSource = DamageSource.Primary; + + var bulletAttack = new BulletAttack + { + aimVector = aimRay.direction, + origin = aimRay.origin, + damage = damageCoefficient * damageStat, + damageType = damageType, + damageColorIndex = DamageColorIndex.Default, + minSpread = 0f, + maxSpread = characterBody.spreadBloomAngle, + falloffModel = BulletAttack.FalloffModel.DefaultBullet, + maxDistance = range, + force = force, + isCrit = isCrit, + owner = gameObject, + muzzleName = muzzleString, + smartCollision = true, + procChainMask = default, + procCoefficient = procCoefficient, + radius = bulletRadius, + weapon = gameObject, + tracerEffectPrefab = tracerPrefab, + hitEffectPrefab = hitPrefab + }; + bulletAttack.Fire(); + } + characterBody.AddSpreadBloom(spreadBloom); + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.Skill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/StunCone.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/StunCone.cs new file mode 100644 index 000000000..4c5598951 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Duke/StunCone.cs @@ -0,0 +1,97 @@ +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Duke +{ + public class StunCone : BaseSkillState + { + private static float baseDuration = 0.8f; + private static float stunDuration = 2f; + private static float coneAngle = 60f; + private static float coneRange = 20f; + private static string soundString = "Play_loader_m1_swing"; + + public static GameObject effectPrefab; + + private float duration; + private bool hasFired; + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + hasFired = false; + + Util.PlayAttackSpeedSound(soundString, gameObject, attackSpeedStat); + PlayAnimation("Gesture, Override", "StunCone", "StunCone.playbackRate", duration); + characterBody.SetAimTimer(2f); + + FireCone(); + } + + private void FireCone() + { + if (hasFired) return; + hasFired = true; + + if (effectPrefab) + { + EffectManager.SimpleMuzzleFlash(effectPrefab, gameObject, "Muzzle", false); + } + + if (!NetworkServer.active) return; + + Ray aimRay = GetAimRay(); + + var search = new BullseyeSearch(); + search.teamMaskFilter = TeamMask.GetEnemyTeams(GetTeam()); + search.filterByLoS = true; + search.maxDistanceFilter = coneRange; + search.minAngleFilter = 0f; + search.maxAngleFilter = coneAngle; + search.sortMode = BullseyeSearch.SortMode.Distance; + search.filterByDistinctEntity = true; + search.searchOrigin = aimRay.origin; + search.searchDirection = aimRay.direction; + search.RefreshCandidates(); + + foreach (HurtBox hurtBox in search.GetResults()) + { + if (!hurtBox || !hurtBox.healthComponent || !hurtBox.healthComponent.alive) continue; + + CharacterBody victimBody = hurtBox.healthComponent.body; + if (!victimBody) continue; + + // Apply Duke stun buff (marker for ricochet) + if (SS2.Survivors.Duke.bdDukeStun) + victimBody.AddTimedBuff(SS2.Survivors.Duke.bdDukeStun, stunDuration); + + // Apply actual stun state via SetStateOnHurt + if (victimBody.TryGetComponent(out SetStateOnHurt setStateOnHurt)) + { + if (setStateOnHurt.canBeStunned) + { + setStateOnHurt.SetStun(stunDuration); + } + } + + Debug.Log("[Duke] StunCone: Stunned " + victimBody.name + " for " + stunDuration + "s."); + } + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.PrioritySkill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/Dodge.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/Dodge.cs new file mode 100644 index 000000000..447e0a982 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/Dodge.cs @@ -0,0 +1,122 @@ +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Lancer +{ + public class Dodge : BaseSkillState + { + private static float duration = 0.5f; + private static float initialSpeedCoefficient = 6.5f; + private static float finalSpeedCoefficient = 1.5f; + public static float dodgeFOV = -1f; + + private float rollSpeed; + private Vector3 forwardDirection; + private Vector3 previousPosition; + private Animator animator; + + public override void OnEnter() + { + base.OnEnter(); + + animator = GetModelAnimator(); + + Util.PlaySound(Commando.DodgeState.dodgeSoundString, gameObject); + + animator.SetBool("isRolling", true); + + PlayCrossfade("Body", "Utility", "Utility.rate", duration * 1.25f, 0.05f); + PlayCrossfade("Gesture, Override", "BufferEmpty", 0.05f); + PlayCrossfade("FullBody, Override", "BufferEmpty", 0.05f); + PlayCrossfade("Gesture, Override, LeftArm", "BufferEmpty", 0.05f); + PlayCrossfade("Gesture, Additive, LeftArm", "BufferEmpty", 0.05f); + + if (NetworkServer.active) characterBody.AddTimedBuff(RoR2Content.Buffs.ArmorBoost, 1.5f * duration); + + if (isAuthority && inputBank && characterDirection) + { + forwardDirection = ((inputBank.moveVector == Vector3.zero) ? characterDirection.forward : inputBank.moveVector).normalized; + } + + RecalculateRollSpeed(); + if (characterMotor && characterDirection) + { + characterMotor.velocity = forwardDirection * rollSpeed; + characterMotor.velocity.y = 0f; + } + + Vector3 velocity = characterMotor ? characterMotor.velocity : Vector3.zero; + previousPosition = transform.position - velocity; + } + + private void RecalculateRollSpeed() + { + rollSpeed = moveSpeedStat * Mathf.Lerp(initialSpeedCoefficient, finalSpeedCoefficient, fixedAge / duration); + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + RecalculateRollSpeed(); + + base.characterBody.isSprinting = true; + + if (isAuthority) + { + Vector3 normalized = (transform.position - previousPosition).normalized; + if (characterMotor && characterDirection && normalized != Vector3.zero) + { + Vector3 vector = normalized * rollSpeed; + float y = vector.y; + vector.y = 0f; + float d = Mathf.Max(Vector3.Dot(vector, forwardDirection), 0f); + vector = forwardDirection * d; + vector.y += Mathf.Max(y, 0f); + characterMotor.velocity = vector; + + Vector3 rhs = inputBank ? characterDirection.forward : forwardDirection; + Vector3 rhs2 = Vector3.Cross(Vector3.up, rhs); + float num = Vector3.Dot(forwardDirection, rhs); + float num2 = Vector3.Dot(forwardDirection, rhs2); + animator.SetFloat("forwardSpeed", num); + animator.SetFloat("rightSpeed", num2); + } + + previousPosition = transform.position; + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + return; + } + } + } + + public override void OnExit() + { + animator.SetBool("isRolling", false); + animator.SetBool("shouldExit", true); + + if (cameraTargetParams) + cameraTargetParams.fovOverride = -1f; + base.OnExit(); + } + + public override void OnSerialize(NetworkWriter writer) + { + base.OnSerialize(writer); + writer.Write(forwardDirection); + } + + public override void OnDeserialize(NetworkReader reader) + { + base.OnDeserialize(reader); + forwardDirection = reader.ReadVector3(); + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.Frozen; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/IonDetonation.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/IonDetonation.cs new file mode 100644 index 000000000..79530d7e3 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/IonDetonation.cs @@ -0,0 +1,109 @@ +using RoR2; +using SS2.Survivors; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Lancer +{ + public class IonDetonation : BaseSkillState + { + public static float baseDuration = 1f; + public static float novaDamageCoefficient = 8f; + public static float novaRadius = 12f; + public static float novaForce = 2000f; + public static float novaProcCoefficient = 1f; + public static GameObject novaEffectPrefab; + public static GameObject novaImpactEffectPrefab; + public static string novaSoundString = ""; + + private float duration; + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + PlayAnimation("Gesture, Override", "IonDetonation", "IonDetonation.playbackRate", duration); + Detonate(); + } + + private void Detonate() + { + Vector3 detonationPosition; + if (gameObject.TryGetComponent(out SS2.Components.LancerController lancerController)) + { + detonationPosition = lancerController.GetSpearPosition(); + } + else + { + Debug.LogError("IonDetonation: Failed to get LancerController component."); + detonationPosition = transform.position; + } + + Util.PlaySound(novaSoundString, gameObject); + + if (novaEffectPrefab) + { + EffectManager.SpawnEffect(novaEffectPrefab, new EffectData + { + origin = detonationPosition, + scale = novaRadius + }, false); + } + + if (NetworkServer.active) + { + BlastAttack blastAttack = new BlastAttack(); + blastAttack.attacker = gameObject; + blastAttack.baseDamage = damageStat * novaDamageCoefficient; + blastAttack.baseForce = novaForce; + blastAttack.bonusForce = Vector3.zero; + blastAttack.attackerFiltering = AttackerFiltering.NeverHitSelf; + blastAttack.crit = characterBody.RollCrit(); + blastAttack.damageColorIndex = DamageColorIndex.Default; + blastAttack.damageType = DamageType.Generic; + blastAttack.damageType.damageSource = DamageSource.Special; + blastAttack.falloffModel = BlastAttack.FalloffModel.None; + blastAttack.inflictor = gameObject; + blastAttack.position = detonationPosition; + blastAttack.procCoefficient = novaProcCoefficient; + blastAttack.radius = novaRadius; + blastAttack.teamIndex = teamComponent.teamIndex; + if (novaImpactEffectPrefab) + { + blastAttack.impactEffect = EffectCatalog.FindEffectIndexFromPrefab(novaImpactEffectPrefab); + } + blastAttack.Fire(); + + SphereSearch sphereSearch = new SphereSearch(); + sphereSearch.origin = detonationPosition; + sphereSearch.radius = novaRadius; + sphereSearch.mask = LayerIndex.entityPrecise.mask; + sphereSearch.RefreshCandidates(); + sphereSearch.FilterCandidatesByHurtBoxTeam(TeamMask.GetEnemyTeams(teamComponent.teamIndex)); + sphereSearch.FilterCandidatesByDistinctHurtBoxEntities(); + HurtBox[] hurtBoxes = sphereSearch.GetHurtBoxes(); + foreach (HurtBox hurtBox in hurtBoxes) + { + if (hurtBox.healthComponent && hurtBox.healthComponent.body) + { + hurtBox.healthComponent.body.AddTimedBuff(Lancer.bdIonField, Lancer.ionFieldDuration); + } + } + } + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.PrioritySkill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/IonFistPunch.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/IonFistPunch.cs new file mode 100644 index 000000000..15e529543 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/IonFistPunch.cs @@ -0,0 +1,103 @@ +using RoR2; +using RoR2.Projectile; +using RoR2.Skills; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Lancer +{ + public class IonFistPunch : BaseState, SteppedSkillDef.IStepSetter + { + public enum Gauntlet + { + Left, + Right + } + + public static GameObject projectilePrefab; + public static GameObject muzzleflashEffectPrefab; + public static float baseDuration = 0.4f; + public static float damageCoefficient = 1.5f; + public static float force = 20f; + public static float procCoefficient = 1f; + public static string attackSoundString = ""; + + private float duration; + private string muzzleString; + private Gauntlet gauntlet; + + public void SetStep(int i) + { + gauntlet = (Gauntlet)i; + } + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + StartAimMode(2f); + + Util.PlaySound(attackSoundString, gameObject); + + switch (gauntlet) + { + case Gauntlet.Left: + muzzleString = "FistLeft"; + PlayCrossfade("Gesture, Override", "PunchLeft", "Punch.playbackRate", duration, 0.1f); + break; + case Gauntlet.Right: + muzzleString = "FistRight"; + PlayCrossfade("Gesture, Override", "PunchRight", "Punch.playbackRate", duration, 0.1f); + break; + } + + if (isAuthority && projectilePrefab) + { + Ray aimRay = GetAimRay(); + FireProjectileInfo fireProjectileInfo = new FireProjectileInfo + { + projectilePrefab = projectilePrefab, + position = FindModelChild(muzzleString)?.position ?? aimRay.origin, + rotation = Util.QuaternionSafeLookRotation(aimRay.direction), + owner = gameObject, + damage = damageStat * damageCoefficient, + force = force, + crit = RollCrit(), + damageTypeOverride = new DamageTypeCombo(DamageType.Generic, DamageTypeExtended.Generic, DamageSource.Primary) + }; + ProjectileManager.instance.FireProjectile(fireProjectileInfo); + } + + if (muzzleflashEffectPrefab) + { + EffectManager.SimpleMuzzleFlash(muzzleflashEffectPrefab, gameObject, muzzleString, transmit: false); + } + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.Skill; + } + + public override void OnSerialize(NetworkWriter writer) + { + base.OnSerialize(writer); + writer.Write((byte)gauntlet); + } + + public override void OnDeserialize(NetworkReader reader) + { + base.OnDeserialize(reader); + gauntlet = (Gauntlet)reader.ReadByte(); + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/RecallSpear.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/RecallSpear.cs new file mode 100644 index 000000000..971d0de29 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/RecallSpear.cs @@ -0,0 +1,62 @@ +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Lancer +{ + public class RecallSpear : BaseSkillState + { + public static float baseDuration = 0.3f; + public static string recallSoundString = ""; + + private float duration; + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + + PlayCrossfade("Gesture, Override", "RecallSpear", "RecallSpear.playbackRate", duration, 0.1f); + Util.PlaySound(recallSoundString, gameObject); + + if (gameObject.TryGetComponent(out SS2.Components.LancerController lancerController)) + { + if (NetworkServer.active) + { + GameObject spearProjectile = lancerController.GetSpearProjectile(); + if (spearProjectile) + { + if (spearProjectile.TryGetComponent(out SS2.Components.LancerSpearProjectile spearComponent)) + { + spearComponent.BeginReturn(); + } + else + { + Debug.LogError("RecallSpear: Spear projectile missing LancerSpearProjectile component."); + } + } + + lancerController.SetSpearState(SS2.Components.LancerController.SpearState.Returning); + } + } + else + { + Debug.LogError("RecallSpear: Failed to get LancerController component."); + } + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.PrioritySkill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/SpearThrust.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/SpearThrust.cs new file mode 100644 index 000000000..19bb405f4 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/SpearThrust.cs @@ -0,0 +1,79 @@ +using EntityStates.Knight; +using R2API; +using RoR2; +using UnityEngine; + +namespace EntityStates.Lancer +{ + public class SpearThrust : BaseKnightMeleeAttack + { + public static float tipperDamageCoefficient = 2f; + public static string thrustAnimationState = "SpearThrust"; + + // Static VFX fields assigned from Lancer.cs Initialize() + // BaseKnightMeleeAttack's swingEffectPrefab/hitEffectPrefab are [SerializeField] instance fields, + // so we bridge them via static fields set in OnEnter before base.OnEnter(). + public static GameObject spearSwingEffectPrefab; + public static GameObject spearHitEffectPrefab; + + private OverlapAttack tipperAttack; + + public override void OnEnter() + { + hitboxGroupName = "SpearHitbox"; + muzzleString = "SpearMuzzle"; + swingEffectPrefab = spearSwingEffectPrefab; + hitEffectPrefab = spearHitEffectPrefab; + + base.OnEnter(); + + this.attack.damageType.damageSource = DamageSource.Primary; + + HitBoxGroup tipperHitboxGroup = FindHitBoxGroup("TipperHitbox"); + if (tipperHitboxGroup) + { + tipperAttack = new OverlapAttack(); + tipperAttack.damageType = attack.damageType; + tipperAttack.attacker = gameObject; + tipperAttack.inflictor = gameObject; + tipperAttack.teamIndex = GetTeam(); + tipperAttack.damage = tipperDamageCoefficient * damageStat; + tipperAttack.procCoefficient = procCoefficient; + tipperAttack.hitEffectPrefab = hitEffectPrefab; + tipperAttack.forceVector = bonusForce; + tipperAttack.pushAwayForce = pushForce; + tipperAttack.hitBoxGroup = tipperHitboxGroup; + tipperAttack.isCrit = attack.isCrit; + tipperAttack.maximumOverlapTargets = 1000; + tipperAttack.AddModdedDamageType(SS2.Survivors.Lancer.TipperIonFieldDamageType); + if (impactSound != null) + { + tipperAttack.impactSound = impactSound.index; + } + } + else + { + Debug.LogError("SpearThrust: TipperHitbox HitBoxGroup not found on model."); + } + } + + public override void PlayAttackAnimation() + { + if (isGrounded && !animator.GetBool("isMoving")) + { + PlayCrossfade("FullBody, Override", thrustAnimationState, playbackRateParam, duration, 0.08f); + } + PlayCrossfade("Gesture, Override", thrustAnimationState, playbackRateParam, duration, 0.08f); + } + + protected override void FireAttack() + { + base.FireAttack(); + + if (isAuthority && tipperAttack != null) + { + tipperAttack.Fire(); + } + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/ThrowSpear.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/ThrowSpear.cs new file mode 100644 index 000000000..f3f9521b2 --- /dev/null +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Lancer/ThrowSpear.cs @@ -0,0 +1,86 @@ +using RoR2; +using RoR2.Projectile; +using UnityEngine; +using UnityEngine.Networking; + +namespace EntityStates.Lancer +{ + public class ThrowSpear : BaseSkillState + { + public static float baseDuration = 0.5f; + public static float damageCoefficient = 4f; + public static float force = 40f; + public static float procCoefficient = 1f; + public static GameObject projectilePrefab; + public static GameObject muzzleflashEffectPrefab; + public static string throwSoundString = ""; + public static string muzzleString = "SpearMuzzle"; + + private float duration; + private bool hasFired; + + public override void OnEnter() + { + base.OnEnter(); + duration = baseDuration / attackSpeedStat; + StartAimMode(2f); + PlayCrossfade("Gesture, Override", "ThrowSpear", "ThrowSpear.playbackRate", duration, 0.1f); + Util.PlayAttackSpeedSound(throwSoundString, gameObject, attackSpeedStat); + + if (muzzleflashEffectPrefab) + EffectManager.SimpleMuzzleFlash(muzzleflashEffectPrefab, gameObject, muzzleString, false); + + FireSpear(); + } + + private void FireSpear() + { + if (hasFired) + return; + hasFired = true; + + if (isAuthority && projectilePrefab) + { + Ray aimRay = GetAimRay(); + FireProjectileInfo fireProjectileInfo = new FireProjectileInfo + { + projectilePrefab = projectilePrefab, + position = FindModelChild(muzzleString)?.position ?? aimRay.origin, + rotation = Util.QuaternionSafeLookRotation(aimRay.direction), + owner = gameObject, + damage = damageStat * damageCoefficient, + force = force, + crit = RollCrit(), + damageTypeOverride = new DamageTypeCombo(DamageType.SlowOnHit, DamageTypeExtended.Generic, DamageSource.Secondary) + }; + ProjectileManager.instance.FireProjectile(fireProjectileInfo); + } + + if (gameObject.TryGetComponent(out SS2.Components.LancerController lancerController)) + { + if (NetworkServer.active) + { + lancerController.SetSpearState(SS2.Components.LancerController.SpearState.Thrown); + } + } + else + { + Debug.LogError("ThrowSpear: Failed to get LancerController component."); + } + } + + public override void FixedUpdate() + { + base.FixedUpdate(); + if (fixedAge >= duration && isAuthority) + { + outer.SetNextStateToMain(); + } + } + + public override InterruptPriority GetMinimumInterruptPriority() + { + return InterruptPriority.PrioritySkill; + } + } +} diff --git a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Toolbot/SelfRepair.cs b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Toolbot/SelfRepair.cs index 9fce33925..7394afeeb 100644 --- a/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Toolbot/SelfRepair.cs +++ b/SS2-Project/Assets/Starstorm2/Modules/EntityStates/Toolbot/SelfRepair.cs @@ -1,35 +1,49 @@ using SS2.Components; +using UnityEngine; +using UnityEngine.Networking; namespace EntityStates.Toolbot { public class SelfRepair : BaseSkillState { - public float baseDuration = 1f; - private float duration = 3f; - private float healthGainPerRepair = 0.05f; - private float repairLossPerTick = 1f; + public static float baseDuration = 3f; + public static float healthFractionPerTick = 0.05f; + public static float repairCostPerTick = 1f; + public static float tickInterval = 0.5f; + private float duration; + private float tickTimer; private SelfRepairController selfRepairController; public override void OnEnter() { base.OnEnter(); - selfRepairController = GetComponent(); + duration = baseDuration / attackSpeedStat; + + if (!gameObject.TryGetComponent(out selfRepairController)) + { + Debug.LogError("SelfRepair: Missing SelfRepairController on " + gameObject.name); + } } public override void FixedUpdate() { base.FixedUpdate(); - if (base.fixedAge >= duration && selfRepairController.repair <= repairLossPerTick) + if (NetworkServer.active && selfRepairController != null) { - outer.SetNextStateToMain(); + tickTimer -= Time.fixedDeltaTime; + if (tickTimer <= 0f && selfRepairController.repair >= repairCostPerTick) + { + tickTimer = tickInterval; + characterBody.healthComponent.HealFraction(healthFractionPerTick, default); + selfRepairController.AddRepair(-repairCostPerTick); + } } - if (selfRepairController && base.isAuthority && selfRepairController.repair >= repairLossPerTick) + if (isAuthority && (fixedAge >= duration || selfRepairController == null || selfRepairController.repair < repairCostPerTick)) { - this.characterBody.healthComponent.HealFraction(healthGainPerRepair, default); - selfRepairController.AddRepair(-repairLossPerTick); + outer.SetNextStateToMain(); } }