using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures; using NitroxModel.Packets; using UnityEngine; using static NitroxModel.Packets.RangedAttackLastTargetUpdate; namespace NitroxClient.GameLogic; public class AI { private readonly IPacketSender packetSender; private readonly Dictionary actions = []; private readonly Dictionary cachedCreatureActionTypeByFullName; /// /// Contains the types derived from CreatureAction which should be synced. /// Actions concerning the creature movement should be ignored as it's already done through SplineFollowing /// private readonly HashSet creatureActionWhitelist = [ typeof(AttackLastTarget), typeof(RangedAttackLastTarget), typeof(AttackCyclops), typeof(Poop) ]; /// /// In the future, ensure all creatures are synced. We want each of them to be individually /// checked (that all their actions are synced) before marking them as synced. /// private readonly HashSet syncedCreatureWhitelist = [ typeof(ReaperLeviathan), typeof(SeaDragon), typeof(SeaTreader), typeof(GhostLeviathan) ]; public AI(IPacketSender packetSender) { this.packetSender = packetSender; Assembly assembly = Assembly.GetAssembly(typeof(CreatureAction)); cachedCreatureActionTypeByFullName = assembly.GetTypes() .Where(type => typeof(CreatureAction).IsAssignableFrom(type)) .ToDictionary(t => t.FullName, t => t); } public void BroadcastNewAction(NitroxId creatureId, Creature creature, CreatureAction newAction) { if (!syncedCreatureWhitelist.Contains(creature.GetType())) { return; } packetSender.Send(new CreatureActionChanged(creatureId, newAction.GetType().FullName)); } public void CreatureActionChanged(NitroxId id, string creatureActionTypeName) { if (!NitroxEntity.TryGetComponentFrom(id, out Creature creature)) { return; } if (cachedCreatureActionTypeByFullName.TryGetValue(creatureActionTypeName, out Type creatureActionType) && creature.TryGetComponent(creatureActionType, out Component component) && component is CreatureAction creatureAction) { actions[creature] = creatureAction; } } public static void AggressiveWhenSeeTargetChanged(NitroxId creatureId, NitroxId targetId, bool locked, float aggressionAmount) { if (!NitroxEntity.TryGetComponentFrom(creatureId, out AggressiveWhenSeeTarget aggressiveWhenSeeTarget) || !NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject)) { return; } Creature creature = aggressiveWhenSeeTarget.creature; // Code from AggressiveWhenSeeTarget.ScanForAggressionTarget creature.Aggression.Value = aggressionAmount; LastTarget lastTarget = aggressiveWhenSeeTarget.lastTarget; lastTarget.SetTargetInternal(targetObject); lastTarget.targetLocked = locked; if (aggressiveWhenSeeTarget.sightedSound && !aggressiveWhenSeeTarget.sightedSound.GetIsPlaying()) { // This call doesn't broadcast a sound packet aggressiveWhenSeeTarget.sightedSound.StartEvent(); } if (creature.TryGetComponent(out AttackLastTarget attackLastTarget)) { attackLastTarget.currentTarget = targetObject; } } public static void AttackCyclopsTargetChanged(NitroxId creatureId, NitroxId targetId, float aggressiveToNoiseAmount) { if (!NitroxEntity.TryGetComponentFrom(creatureId, out AttackCyclops attackCyclops) || !NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject)) { return; } // Kinda stuff from AttackCyclops.UpdateAggression attackCyclops.aggressiveToNoise.Value = aggressiveToNoiseAmount; // Force currentTarget to null to ensure SetCurrentTarget detects a change attackCyclops.currentTarget = null; attackCyclops.SetCurrentTarget(targetObject, targetObject.GetComponent()); } public static void RangedAttackLastTargetUpdate(NitroxId creatureId, NitroxId targetId, int attackTypeIndex, ActionState state) { if (!NitroxEntity.TryGetComponentFrom(creatureId, out RangedAttackLastTarget rangedAttackLastTarget) || !NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject)) { return; } RangedAttackLastTarget.RangedAttackType attackType = rangedAttackLastTarget.attackTypes[attackTypeIndex]; rangedAttackLastTarget.currentAttack = attackType; rangedAttackLastTarget.currentTarget = targetObject; switch (state) { case ActionState.CHARGING: rangedAttackLastTarget.StartCharging(attackType); break; case ActionState.CASTING: rangedAttackLastTarget.StartCasting(attackType); break; } } public static void CreaturePoopPerformed(NitroxId creatureId) { if (NitroxEntity.TryGetComponentFrom(creatureId, out Poop poop)) { // Code from Poop.Perform SafeAnimator.SetBool(poop.creature.GetAnimator(), poop.animationParameterName, false); poop.recourceSpawned = true; } } public bool TryGetActionForCreature(Creature creature, out CreatureAction action) { return actions.TryGetValue(creature, out action); } public bool IsCreatureActionWhitelisted(CreatureAction creatureAction) { return creatureActionWhitelist.Contains(creatureAction.GetType()); } public bool IsCreatureWhitelisted(Creature creature) { return syncedCreatureWhitelist.Contains(creature.GetType()); } }