using System; using System.Runtime.CompilerServices; using Animancer; using RebootKit.Engine.AI; using RebootKit.Engine.Extensions; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; using RebootReality.jelycho.Beacons; using RebootReality.jelycho.Player; using TriInspector; using Unity.Mathematics; using UnityEngine; using UnityEngine.AI; using Logger = RebootKit.Engine.Foundation.Logger; using UnityEvent = UnityEngine.Events.UnityEvent; namespace RebootReality.jelycho.Enemies { public class ZombieActorData : IActorData { public void Serialize(NetworkBufferWriter writer) { } public void Deserialize(NetworkBufferReader reader) { } public int GetMaxBytes() { return 0; } } public enum ZombieBodyPartType { Body, Head, LeftArm, RightArm, LeftLeg, RightLeg } // @TODO: weird naming [Serializable] public class ZombieBodyPart { public Transform root; public Transform[] wearables; public Collider[] colliders; public ParticleSystem bloodStreamParticles; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsAlive() { return root.gameObject.activeSelf; } public void HideParts() { root.gameObject.SetActive(false); foreach (Transform wearable in wearables) { wearable.gameObject.SetActive(false); } foreach (Collider collider in colliders) { collider.enabled = false; } } } class ZombiePickVictim : IStrategy { public BehaviourNode.Status Process(Actor target, float dt) { if (target is not ZombieActor zombie) { return BehaviourNode.Status.Failure; } Actor victim = zombie.FindNewVictim(); if (victim == null) { return BehaviourNode.Status.Failure; } return BehaviourNode.Status.Success; } } class ZombieGoToPlayer : IStrategy { public BehaviourNode.Status Process(Actor target, float dt) { if (target is not ZombieActor zombie) { return BehaviourNode.Status.Failure; } if (!zombie.HasTravelDestination) { float3 victimPos = zombie.Victim.transform.position; float dstToVictimSq = math.distancesq(victimPos, zombie.transform.position); if (dstToVictimSq < 1.0f) { return BehaviourNode.Status.Success; } zombie.GoTo(victimPos); return BehaviourNode.Status.Running; } return BehaviourNode.Status.Running; } } [DeclareBoxGroup("Body parts")] [DeclareBoxGroup("Animations")] public class ZombieActor : Actor, IKillable { static readonly Logger s_Logger = new Logger(nameof(ZombieActor)); [SerializeField] AnimancerComponent m_Animancer; [SerializeField] NavMeshAgent m_NavAgent; public NavMeshAgent NavAgent { get { return m_NavAgent; } } [SerializeField] Collider m_RootCollider; [SerializeField] Rigidbody[] m_RagdollRigidbodies; [SerializeField] float m_MaxAttackDistance = 2.0f; [SerializeField] float m_LoseInterestMinDistance = 10.0f; [SerializeField] ulong m_BaseDamage = 10; [SerializeField] float m_AttackDelay = 1.0f; [SerializeField, Group("Body parts")] ZombieBodyPart m_Head; [SerializeField, Group("Body parts")] ZombieBodyPart m_LeftArm; [SerializeField, Group("Body parts")] ZombieBodyPart m_RightArm; [SerializeField, Group("Body parts")] ZombieBodyPart m_LeftLeg; [SerializeField, Group("Body parts")] ZombieBodyPart m_RightLeg; [SerializeField, Group("Animations")] TransitionAsset m_GroundLocomotion; [SerializeField, Group("Animations")] StringAsset m_GroundLocomotionPropertyRight; [SerializeField, Group("Animations")] StringAsset m_GroundLocomotionPropertyForward; SmoothedVector2Parameter m_SmoothLocomotionDirection; [SerializeField, Group("Animations")] AnimationClip[] m_AttackClips; BehaviourTree m_BehaviourTree; public enum MindState { Normal, RunAway, Berserk } public bool IsRagdoll { get; private set; } = false; public MindState Mind { get; private set; } = MindState.Normal; public Actor Victim { get; private set; } public bool HasTravelDestination { get; private set; } public float3 TravelDestination { get; private set; } public UnityEvent died = new UnityEvent(); // // @MARK: Unity callbacks // void Awake() { SetRagdollLocal(IsRagdoll); m_SmoothLocomotionDirection = new SmoothedVector2Parameter(m_Animancer, m_GroundLocomotionPropertyRight, m_GroundLocomotionPropertyForward, 0.1f); m_Animancer.Play(m_GroundLocomotion); m_BehaviourTree = new BehaviourTree("Zombie Behaviour"); var rootSelector = new Selector("Root"); m_BehaviourTree.AddChild(rootSelector); rootSelector.AddChild(CreateNormalSequence()); } BehaviourNode CreateNormalSequence() { var normalSequence = new Sequence("Normal", () => Mind == MindState.Normal); normalSequence.AddChild(new Leaf("Pick Victim", new ZombiePickVictim())); var attackPlayerSequence = new Sequence("Attack Player", IsVictimPlayer); normalSequence.AddChild(attackPlayerSequence); attackPlayerSequence.AddChild(new Leaf("Go to Player", new ZombieGoToPlayer())); var attackMotherSequence = new Sequence("Attack Mother", IsVictimMother); normalSequence.AddChild(attackMotherSequence); return normalSequence; } // // @MARK: Actor // public override void OnClientTick(float deltaTime) { base.OnClientTick(deltaTime); if (!IsAlive()) { return; } float3 vel = m_NavAgent.velocity; float forwardVelocity = math.dot(m_NavAgent.transform.forward, vel); float rightVelocity = math.dot(m_NavAgent.transform.right, vel); m_SmoothLocomotionDirection.TargetValue = new Vector2(rightVelocity, forwardVelocity); } public override void OnServerTick(float deltaTime) { base.OnServerTick(deltaTime); if (RR.World.Context is not WorldContext world) { s_Logger.Error("Invalid world context"); return; } if (HasTravelDestination) { float3 pos = transform.position; if (math.distancesq(pos, TravelDestination) <= 1.0f) { HasTravelDestination = false; m_NavAgent.isStopped = true; } } m_BehaviourTree.Process(this, deltaTime); } // // @MARK: Zombie // public bool GoTo(float3 pos) { if (!RR.IsServer()) { s_Logger.Error("Only server can call GoTo"); return false; } TravelDestination = pos; HasTravelDestination = true; m_NavAgent.isStopped = false; return m_NavAgent.SetDestination(TravelDestination); } public bool IsVictimPlayer() { return Victim is PlayerActor; } public bool IsVictimMother() { return Victim is MotherActor; } public Actor FindNewVictim() { if (!RR.IsServer()) { s_Logger.Error("Only server can call FindNewVictim"); return null; } Victim = null; (PlayerActor playerActor, float distSqToPlayer) = FindClosestPlayerActor(transform.position); if (playerActor != null && distSqToPlayer < m_LoseInterestMinDistance * m_LoseInterestMinDistance) { Victim = playerActor; return Victim; } if (RR.World.Context is WorldContext ctx) { Victim = ctx.BaseManager.Mother; return Victim; } return null; } public void PerformAttack() { m_Animancer.Play(m_AttackClips.Random()); } void Die() { EnableRagdoll(); m_NavAgent.enabled = false; died.Invoke(); } (PlayerActor, float) FindClosestPlayerActor(float3 origin) { if (RR.World.Context is not WorldContext context) { return (null, -1.0f); } PlayerActor res = null; float closestDistanceSq = float.MaxValue; foreach (Actor actor in RR.Actors()) { if (actor is not PlayerActor playerActor) { continue; } float distSq = math.distancesq(actor.transform.position, origin); if (distSq < closestDistanceSq) { res = playerActor; closestDistanceSq = distSq; } } return (res, closestDistanceSq); } // // @MARK: Actor // protected override IActorData CreateActorData() { return new ZombieActorData(); } protected override void OnActorEventClient(ActorEvent actorEvent) { var zombieEvent = (ZombieActorEvents) actorEvent.EventID; switch (zombieEvent) { case ZombieActorEvents.EnableRagdoll: { SetRagdollLocal(true); break; } } } // // @MARK: Ragdoll // void EnableRagdoll() { SendActorEvent((byte)ZombieActorEvents.EnableRagdoll); } void SetRagdollLocal(bool active) { m_RootCollider.enabled = !active; foreach (Rigidbody ragdollRigidbody in m_RagdollRigidbodies) { ragdollRigidbody.isKinematic = !active; } m_Animancer.enabled = !active; } // // @MARK: IKillable // public ulong Health { get; private set; } = 100; public bool IsAlive() { return Health > 0; } public ulong OnHit(Actor attacker, ulong damage) { if (!RR.IsServer()) { s_Logger.Error("OnHit can only be called on the server."); return 0; } if (!IsAlive()) { return 0; } s_Logger.Info($"Hit: {damage}"); damage = math.min(damage, Health); Health -= damage; if (Health <= 0) { Die(); return damage; } return damage; } // // @MARK: damage? // public void HitFeedback(float3 hitPosition) { if (RR.World.Context is WorldContext worldContext) { worldContext.FeedbacksManager.SpawnBloodSplash(hitPosition); } } public void ReceiveDamage(ulong damage, ZombieBodyPartType bodyPartType) { if (!IsAlive()) { return; } ZombieBodyPart bodyPart = GetBodyParty(bodyPartType); if (!bodyPart.IsAlive()) { return; } bodyPart.HideParts(); bodyPart.bloodStreamParticles.Play(); Die(); } public bool HasBodyPart(ZombieBodyPartType bodyPart) { if (Health <= 0) { return false; } if (bodyPart == ZombieBodyPartType.Body) { return true; } ZombieBodyPart part = GetBodyParty(bodyPart); if (part == null) { return false; } return part.IsAlive(); } ZombieBodyPart GetBodyParty(ZombieBodyPartType bodyPart) { return bodyPart switch { ZombieBodyPartType.Body => null, ZombieBodyPartType.Head => m_Head, ZombieBodyPartType.LeftArm => m_LeftArm, ZombieBodyPartType.RightArm => m_RightArm, ZombieBodyPartType.LeftLeg => m_LeftLeg, ZombieBodyPartType.RightLeg => m_RightLeg, _ => throw new ArgumentOutOfRangeException(nameof(bodyPart), bodyPart, null) }; } /// /// @MARK: Editor Utility /// #if UNITY_EDITOR [Button] void SetupHurtboxes() { foreach (ZombieHurtbox hurtbox in GetComponentsInChildren()) { hurtbox.owner = this; UnityEditor.EditorUtility.SetDirty(hurtbox); } } #endif } enum ZombieActorEvents { None = 0x00, EnableRagdoll = 0x01 } }