using System.Threading; using System.Threading.Tasks; using Content.Server.NPC.Components; using Content.Shared.CombatMode; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Robust.Shared.Audio; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged; public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown { [Dependency] private readonly IEntityManager _entManager = default!; [DataField("shutdownState")] public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished; /// /// Key that contains the target entity. /// [DataField("targetKey", required: true)] public string TargetKey = default!; /// /// Minimum damage state that the target has to be in for us to consider attacking. /// [DataField("targetState")] public MobState TargetState = MobState.Alive; /// /// Do we require line of sight of the target before failing. /// [DataField("requireLOS")] public bool RequireLOS = false; /// /// If true, only opaque objects will block line of sight. /// [DataField("opaqueKey")] public bool UseOpaqueForLOSChecks = false; // Like movement we add a component and pass it off to the dedicated system. public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard, CancellationToken cancelToken) { // Don't attack if they're already as wounded as we want them. if (!blackboard.TryGetValue(TargetKey, out var target, _entManager)) { return (false, null); } if (_entManager.TryGetComponent(target, out var mobState) && mobState.CurrentState > TargetState) { return (false, null); } return (true, null); } public override void Startup(NPCBlackboard blackboard) { base.Startup(blackboard); var ranged = _entManager.EnsureComponent(blackboard.GetValue(NPCBlackboard.Owner)); ranged.Target = blackboard.GetValue(TargetKey); ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks; if (blackboard.TryGetValue(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager)) { ranged.RotationSpeed = new Angle(rotSpeed); } if (blackboard.TryGetValue("SoundTargetInLOS", out var losSound, _entManager)) { ranged.SoundTargetInLOS = losSound; } } public void ConditionalShutdown(NPCBlackboard blackboard) { var owner = blackboard.GetValue(NPCBlackboard.Owner); _entManager.System().SetInCombatMode(owner, false); _entManager.RemoveComponent(owner); blackboard.Remove(TargetKey); } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) { base.Update(blackboard, frameTime); var owner = blackboard.GetValue(NPCBlackboard.Owner); HTNOperatorStatus status; if (_entManager.TryGetComponent(owner, out var combat) && blackboard.TryGetValue(TargetKey, out var target, _entManager)) { combat.Target = target; // Success if (_entManager.TryGetComponent(combat.Target, out var mobState) && mobState.CurrentState > TargetState) { status = HTNOperatorStatus.Finished; } else { switch (combat.Status) { case CombatStatus.TargetUnreachable: status = HTNOperatorStatus.Failed; break; case CombatStatus.NotInSight: if (RequireLOS) status = HTNOperatorStatus.Failed; else status = HTNOperatorStatus.Continuing; break; case CombatStatus.Normal: status = HTNOperatorStatus.Continuing; break; default: status = HTNOperatorStatus.Failed; break; } } } else { status = HTNOperatorStatus.Failed; } // Mark it as finished to continue the plan. if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished) { status = HTNOperatorStatus.Finished; } return status; } }