private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
{
if (!component.CanMicrowave || !TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
- return;
+ return;
if (TryComp<AccessComponent>(uid, out var access))
{
}
// Give them a wonderful new access to compensate for everything
- var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
+ var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
+
+ if (ids.Length == 0)
+ return;
+
+ var random = _random.Pick(ids);
access.Tags.Add(random.ID);
Dirty(uid, access);
[ViewVariables(VVAccess.ReadWrite)]
public bool TargetInLOS = false;
+ /// <summary>
+ /// If true, only opaque objects will block line of sight.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite)]
+ // ReSharper disable once InconsistentNaming
+ public bool UseOpaqueForLOSChecks = false;
+
/// <summary>
/// Delay after target is in LOS before we start shooting.
/// </summary>
/// The base task to use for planning
/// </summary>
[ViewVariables(VVAccess.ReadWrite),
- DataField("rootTask", required: true)]
+ DataField("rootTask", required: true)]
public HTNCompoundTask RootTask = default!;
/// <summary>
/// Is this NPC currently planning?
/// </summary>
[ViewVariables] public bool Planning => PlanningJob != null;
+
+ /// <summary>
+ /// Determines whether plans should be made / updated for this entity
+ /// </summary>
+ [DataField]
+ public bool Enabled = true;
}
component.PlanningJob = null;
}
+ /// <summary>
+ /// Enable / disable the hierarchical task network of an entity
+ /// </summary>
+ /// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
+ /// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
+ /// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
+ // ReSharper disable once InconsistentNaming
+ [PublicAPI]
+ public void SetHTNEnabled(Entity<HTNComponent> ent, bool state, float planCooldown = 0f)
+ {
+ if (ent.Comp.Enabled == state)
+ return;
+
+ ent.Comp.Enabled = state;
+ ent.Comp.PlanAccumulator = planCooldown;
+
+ ent.Comp.PlanningToken?.Cancel();
+ ent.Comp.PlanningToken = null;
+
+ if (ent.Comp.Plan != null)
+ {
+ var currentOperator = ent.Comp.Plan.CurrentOperator;
+
+ ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
+ ShutdownPlan(ent.Comp);
+
+ ent.Comp.Plan = null;
+ }
+
+ if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
+ RequestPlan(ent.Comp);
+ }
+
/// <summary>
/// Forces the NPC to replan.
/// </summary>
_planQueue.Process();
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
- while(query.MoveNext(out var uid, out _, out var comp))
+ while (query.MoveNext(out var uid, out _, out var comp))
{
// If we're over our max count or it's not MapInit then ignore the NPC.
if (count >= maxUpdates)
break;
+ if (!comp.Enabled)
+ continue;
+
if (comp.PlanningJob != null)
{
if (comp.PlanningJob.Exception != null)
using Content.Server.Interaction;
+using Content.Shared.Physics;
namespace Content.Server.NPC.HTN.Preconditions;
[DataField("rangeKey")]
public string RangeKey = "RangeKey";
+ [DataField("opaqueKey")]
+ public bool UseOpaqueForLOSChecksKey = true;
+
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
return false;
var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
+ var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
- return _interaction.InRangeUnobstructed(owner, target, range);
+ return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
}
}
[DataField("requireLOS")]
public bool RequireLOS = false;
+ /// <summary>
+ /// If true, only opaque objects will block line of sight.
+ /// </summary>
+ [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<string, object>? Effects)> Plan(NPCBlackboard blackboard,
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
+
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
+ ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
{
--- /dev/null
+namespace Content.Server.NPC.Queries.Considerations;
+
+/// <summary>
+/// Returns 0f if the NPC has a <see cref="TurretTargetSettingsComponent"/> and the
+/// target entity is exempt from being targeted, otherwise it returns 1f.
+/// See <see cref="TurretTargetSettingsSystem.EntityIsTargetForTurret"/>
+/// for further details on turret target validation.
+/// </summary>
+public sealed partial class TurretTargetingCon : UtilityConsideration
+{
+
+}
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
+using Content.Shared.Physics;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Map;
if (comp.LOSAccumulator < 0f)
{
comp.LOSAccumulator += UnoccludedCooldown;
+
// For consistency with NPC steering.
- comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f);
+ var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
+ comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
}
if (!comp.TargetInLOS)
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.Systems;
+using Content.Shared.Turrets;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
+ [Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
private EntityQuery<PuddleComponent> _puddleQuery;
private EntityQuery<TransformComponent> _xformQuery;
return 1f;
return 0f;
}
+ case TurretTargetingCon:
+ {
+ if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
+ _turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
+ return 1f;
+
+ return 0f;
+ }
default:
throw new NotImplementedException();
}
/// <summary>
/// The player-visible name of the access level, in the ID card console and such.
/// </summary>
- [DataField("name")]
+ [DataField]
public string? Name { get; set; }
+ /// <summary>
+ /// Denotes whether this access level is intended to be assignable to a crew ID card.
+ /// </summary>
+ [DataField]
+ public bool CanAddToIdCard = true;
+
public string GetAccessLevelName()
{
if (Name is { } name)
--- /dev/null
+using Content.Shared.Access;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Turrets;
+
+/// <summary>
+/// Attached to entities to provide them with turret target selection data.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(TurretTargetSettingsSystem))]
+public sealed partial class TurretTargetSettingsComponent : Component
+{
+ /// <summary>
+ /// Crew with one or more access levels from this list are exempt from being targeted by turrets.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public HashSet<ProtoId<AccessLevelPrototype>> ExemptAccessLevels = new();
+}
--- /dev/null
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Turrets;
+
+/// <summary>
+/// This system is used for validating potential targets for NPCs with a <see cref="TurretTargetSettingsComponent"/> (i.e., turrets).
+/// A turret will consider an entity a valid target if the entity does not possess any access tags which appear on the
+/// turret's <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
+/// </summary>
+public sealed partial class TurretTargetSettingsSystem : EntitySystem
+{
+ [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+
+ private ProtoId<AccessLevelPrototype> _accessLevelBorg = "Borg";
+ private ProtoId<AccessLevelPrototype> _accessLevelBasicSilicon = "BasicSilicon";
+
+ /// <summary>
+ /// Adds or removes access levels from a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
+ /// </summary>
+ /// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="exemption">The proto ID for the access level</param>
+ /// <param name="enabled">Set 'true' to add the exemption, or 'false' to remove it</param>
+ [PublicAPI]
+ public void SetAccessLevelExemption(Entity<TurretTargetSettingsComponent> ent, ProtoId<AccessLevelPrototype> exemption, bool enabled)
+ {
+ if (enabled)
+ ent.Comp.ExemptAccessLevels.Add(exemption);
+ else
+ ent.Comp.ExemptAccessLevels.Remove(exemption);
+ }
+
+ /// <summary>
+ /// Adds or removes a collection of access levels from a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
+ /// </summary>
+ /// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="exemption">The collection of access level proto IDs to add or remove</param>
+ /// <param name="enabled">Set 'true' to add the collection as exemptions, or 'false' to remove them</param>
+ [PublicAPI]
+ public void SetAccessLevelExemptions(Entity<TurretTargetSettingsComponent> ent, ICollection<ProtoId<AccessLevelPrototype>> exemptions, bool enabled)
+ {
+ foreach (var exemption in exemptions)
+ SetAccessLevelExemption(ent, exemption, enabled);
+ }
+
+ /// <summary>
+ /// Sets a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list to contain only a supplied collection of access levels.
+ /// </summary>
+ /// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="exemptions">The supplied collection of access level proto IDs</param>
+ [PublicAPI]
+ public void SyncAccessLevelExemptions(Entity<TurretTargetSettingsComponent> ent, ICollection<ProtoId<AccessLevelPrototype>> exemptions)
+ {
+ ent.Comp.ExemptAccessLevels.Clear();
+ SetAccessLevelExemptions(ent, exemptions, true);
+ }
+
+ /// <summary>
+ /// Sets a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list to match that of another.
+ /// </summary>
+ /// <param name="target">The entity this is having its exemption list updated <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="source">The entity that is being used as a template for the target</param>
+ [PublicAPI]
+ public void SyncAccessLevelExemptions(Entity<TurretTargetSettingsComponent> target, Entity<TurretTargetSettingsComponent> source)
+ {
+ SyncAccessLevelExemptions(target, source.Comp.ExemptAccessLevels);
+ }
+
+ /// <summary>
+ /// Returns whether a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list contains a specific access level.
+ /// </summary>
+ /// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="exemption">The access level proto ID being checked</param>
+ [PublicAPI]
+ public bool HasAccessLevelExemption(Entity<TurretTargetSettingsComponent> ent, ProtoId<AccessLevelPrototype> exemption)
+ {
+ if (ent.Comp.ExemptAccessLevels.Count == 0)
+ return false;
+
+ return ent.Comp.ExemptAccessLevels.Contains(exemption);
+ }
+
+ /// <summary>
+ /// Returns whether a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list contains one or more access levels from another collection.
+ /// </summary>
+ /// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="exemptions"></param>
+ [PublicAPI]
+ public bool HasAnyAccessLevelExemption(Entity<TurretTargetSettingsComponent> ent, ICollection<ProtoId<AccessLevelPrototype>> exemptions)
+ {
+ if (ent.Comp.ExemptAccessLevels.Count == 0)
+ return false;
+
+ foreach (var exemption in exemptions)
+ {
+ if (HasAccessLevelExemption(ent, exemption))
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns whether an entity is a valid target for a turret.
+ /// </summary>
+ /// <remarks>
+ /// Returns false if the target possesses one or more access tags that are present on the entity's <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
+ /// </remarks>
+ /// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
+ /// <param name="target">The target entity</param>
+ [PublicAPI]
+ public bool EntityIsTargetForTurret(Entity<TurretTargetSettingsComponent> ent, EntityUid target)
+ {
+ var accessLevels = _accessReader.FindAccessTags(target);
+
+ if (accessLevels.Contains(_accessLevelBorg))
+ return !HasAccessLevelExemption(ent, _accessLevelBorg);
+
+ if (accessLevels.Contains(_accessLevelBasicSilicon))
+ return !HasAccessLevelExemption(ent, _accessLevelBasicSilicon);
+
+ return !HasAnyAccessLevelExemption(ent, accessLevels);
+ }
+}
id-card-access-level-central-command = Central Command
id-card-access-level-wizard = Wizard
+
+id-card-access-level-station-ai = Artifical Intelligence
+id-card-access-level-borg = Cyborg
+id-card-access-level-basic-silicon = Robot
\ No newline at end of file
- type: accessGroup
id: Command
tags:
- - Command
- Captain
- - HeadOfPersonnel
+ - Command
+ - ChiefEngineer
+ - ChiefMedicalOfficer
- Cryogenics
-
+ - HeadOfPersonnel
+ - HeadOfSecurity
+ - Quartermaster
+ - ResearchDirector
+
- type: accessLevel
id: EmergencyShuttleRepealAll
name: id-card-access-level-emergency-shuttle-repeal
+ canAddToIdCard: false
- type: accessLevel
id: Cryogenics
--- /dev/null
+- type: accessLevel
+ id: Borg
+ name: id-card-access-level-borg
+ canAddToIdCard: false
+
+- type: accessLevel
+ id: BasicSilicon
+ name: id-card-access-level-basic-silicon
+ canAddToIdCard: false
+
+- type: accessLevel
+ id: StationAi
+ name: id-card-access-level-station-ai
+ canAddToIdCard: false
+
+- type: accessGroup
+ id: Silicon
+ tags:
+ - StationAi
+ - Borg
+ - BasicSilicon
enabled: false
groups:
- AllAccess
+ tags:
+ - Borg
- type: AccessReader
access: [["Command"], ["Research"]]
- type: ShowJobIcons
- type: NpcFactionMember
factions:
- SimpleNeutral
+ - type: Access
+ tags:
+ - BasicSilicon
- type: IntrinsicRadioReceiver
- type: ActiveRadio
channels:
- type: Access
groups:
- AllAccess
+ - Silicon
tags:
- NuclearOperative
- SyndicateAgent
- type: Access
groups:
- AllAccess
+ - Silicon
- type: Eye
drawFov: false
- type: Examiner
- type: Access
groups:
- AllAccess
+ - Silicon
tags:
- CentralCommand
- NuclearOperative
- Armory
- Atmospherics
- Bar
+ - BasicSilicon
+ - Borg
- Brig
- Detective
- Captain
- Salvage
- Security
- Service
+ - StationAi
- Theatre
- CentralCommand
- NuclearOperative
- tasks:
- !type:HTNCompoundTask
task: IdleSpinCompound
+
+- type: htnCompound
+ id: EnergyTurretCompound
+ branches:
+ - tasks:
+ - !type:HTNPrimitiveTask
+ operator: !type:UtilityOperator
+ proto: NearbyGunTargets
+
+ - !type:HTNPrimitiveTask
+ preconditions:
+ - !type:KeyExistsPrecondition
+ key: Target
+ - !type:TargetInRangePrecondition
+ targetKey: Target
+ # TODO: Non-scuffed
+ rangeKey: RangedRange
+ - !type:TargetInLOSPrecondition
+ targetKey: Target
+ rangeKey: RangedRange
+ opaqueKey: true
+ operator: !type:GunOperator
+ targetKey: Target
+ opaqueKey: true
+ services:
+ - !type:UtilityService
+ id: RangedService
+ proto: NearbyGunTargets
+ key: Target
+
+ - tasks:
+ - !type:HTNCompoundTask
+ task: IdleSpinCompound
- type: htnCompound
id: SimpleRangedHostileCompound
considerations:
- !type:TargetIsAliveCon
curve: !type:BoolCurve
+ - !type:TargetIsCritCon
+ curve: !type:InverseBoolCurve
+ - !type:TurretTargetingCon
+ curve: !type:BoolCurve
- !type:TargetDistanceCon
curve: !type:PresetCurve
preset: TargetDistance