diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs index 7bfe4329986..df19d8054c6 100644 --- a/Content.Server/NPC/HTN/HTNSystem.cs +++ b/Content.Server/NPC/HTN/HTNSystem.cs @@ -387,11 +387,18 @@ private void Update(HTNComponent component, float frameTime) break; case HTNOperatorStatus.Failed: ShutdownTask(currentOperator, blackboard, status); - ShutdownPlan(component); +// ES START + if (component.Plan != null) + ShutdownPlan(component); +// ES END break; // Operator completed so go to the next one. case HTNOperatorStatus.Finished: ShutdownTask(currentOperator, blackboard, status); +// ES START + if (component.Plan == null) + break; +// ES END component.Plan.Index++; // Plan finished! diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 0683bbf42e6..f656757c436 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -28,9 +28,12 @@ using Robust.Shared.Utility; using Content.Shared.Atmos.Components; using System.Linq; +using Content.Server._ES.NPCs.Queries.Considerations; using Content.Shared.Damage.Components; using Content.Shared.Temperature.Components; -using Content.Shared._Offbrand.Wounds; // Offbrand +using Content.Shared._Offbrand.Wounds; +using Content.Shared.ActionBlocker; +using Content.Shared.Interaction; // Offbrand namespace Content.Server.NPC.Systems; @@ -39,6 +42,9 @@ namespace Content.Server.NPC.Systems; /// public sealed class NPCUtilitySystem : EntitySystem { +// ES START + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; +// ES END [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; @@ -170,6 +176,27 @@ private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityCon var owner = blackboard.GetValue(NPCBlackboard.Owner); switch (consideration) { +// ES START + case ESTargetBesideCon: + { + if (!TryComp(targetUid, out TransformComponent? targetXform) || + !TryComp(owner, out TransformComponent? xform)) + { + return 0f; + } + + if (!targetXform.Coordinates.TryDistance(EntityManager, _transform, xform.Coordinates, out var distance)) + { + return 0f; + } + + return Math.Clamp(distance / 0.5f, 0f, 1f); + } + case ESCanInteractCon: + { + return _actionBlocker.CanInteract(owner, targetUid) ? 1 : 0; + } +// ES END case FoodValueCon: { // do we have a mouth available? Is the food item opened? diff --git a/Content.Server/_ES/NPCs/Operators/ESSnipCableOperator.cs b/Content.Server/_ES/NPCs/Operators/ESSnipCableOperator.cs new file mode 100644 index 00000000000..3c21eba01ac --- /dev/null +++ b/Content.Server/_ES/NPCs/Operators/ESSnipCableOperator.cs @@ -0,0 +1,42 @@ +using Content.Server.Electrocution; +using Content.Server.NPC; +using Content.Server.NPC.HTN; +using Content.Server.NPC.HTN.PrimitiveTasks; +using Content.Server.Power.Components; + +namespace Content.Server._ES.NPCs.Operators; + +public sealed partial class ESSnipCableOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + private ElectrocutionSystem _electrocution; + + /// + /// Key that contains the target entity. + /// + [DataField] + public string TargetKey = "Target"; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _electrocution = sysManager.GetEntitySystem(); + } + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!blackboard.TryGetValue(TargetKey, out var target, _entityManager)) + return HTNOperatorStatus.Failed; + + if (!_entityManager.TryGetComponent(target, out var cableComponent)) + return HTNOperatorStatus.Failed; + + _electrocution.TryDoElectrifiedAct(target, owner); + _entityManager.SpawnNextToOrDrop(cableComponent.CableDroppedOnCutPrototype, target); + _entityManager.QueueDeleteEntity(target); + + return HTNOperatorStatus.Finished; + } +} diff --git a/Content.Server/_ES/NPCs/Queries/Considerations/ESCanInteractCon.cs b/Content.Server/_ES/NPCs/Queries/Considerations/ESCanInteractCon.cs new file mode 100644 index 00000000000..4fc1f1d0dbd --- /dev/null +++ b/Content.Server/_ES/NPCs/Queries/Considerations/ESCanInteractCon.cs @@ -0,0 +1,5 @@ +using Content.Server.NPC.Queries.Considerations; + +namespace Content.Server._ES.NPCs.Queries.Considerations; + +public sealed partial class ESCanInteractCon : UtilityConsideration; diff --git a/Content.Server/_ES/NPCs/Queries/Considerations/ESTargetBesideCon.cs b/Content.Server/_ES/NPCs/Queries/Considerations/ESTargetBesideCon.cs new file mode 100644 index 00000000000..3efc14d83a1 --- /dev/null +++ b/Content.Server/_ES/NPCs/Queries/Considerations/ESTargetBesideCon.cs @@ -0,0 +1,5 @@ +using Content.Server.NPC.Queries.Considerations; + +namespace Content.Server._ES.NPCs.Queries.Considerations; + +public sealed partial class ESTargetBesideCon : UtilityConsideration; diff --git a/Content.Server/_ES/StationEvents/VentSwarm/Components/ESVentSwarmRuleComponent.cs b/Content.Server/_ES/StationEvents/VentSwarm/Components/ESVentSwarmRuleComponent.cs new file mode 100644 index 00000000000..6dd95de9640 --- /dev/null +++ b/Content.Server/_ES/StationEvents/VentSwarm/Components/ESVentSwarmRuleComponent.cs @@ -0,0 +1,33 @@ +using Content.Shared.EntityTable.EntitySelectors; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server._ES.StationEvents.VentSwarm.Components; + +[RegisterComponent, AutoGenerateComponentPause] +[Access(typeof(ESVentSwarmRule))] +public sealed partial class ESVentSwarmRuleComponent : Component +{ + [DataField] + public EntityUid? Vent; + + [DataField] + public EntityTableSelector SpawnTable; + + [DataField] + public int MinSwarmCount = 6; + + [DataField] + public int MaxSwarmCount = 12; + + [DataField] + public int SwarmCount; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan NextSwarmTime; + + [DataField] + public TimeSpan MinSwarmDelay = TimeSpan.FromSeconds(1); + + [DataField] + public TimeSpan MaxSwarmDelay = TimeSpan.FromSeconds(3); +} diff --git a/Content.Server/_ES/StationEvents/VentSwarm/ESVentSwarmRule.cs b/Content.Server/_ES/StationEvents/VentSwarm/ESVentSwarmRule.cs new file mode 100644 index 00000000000..445deebbfdb --- /dev/null +++ b/Content.Server/_ES/StationEvents/VentSwarm/ESVentSwarmRule.cs @@ -0,0 +1,78 @@ +using Content.Server._ES.StationEvents.VentSwarm.Components; +using Content.Server.Pinpointer; +using Content.Server.Popups; +using Content.Server.StationEvents.Components; +using Content.Server.StationEvents.Events; +using Content.Shared._ES.Voting.Components; +using Content.Shared._ES.Voting.Results; +using Content.Shared.EntityTable; +using Content.Shared.GameTicking.Components; +using Robust.Shared.Utility; + +namespace Content.Server._ES.StationEvents.VentSwarm; + +public sealed class ESVentSwarmRule : StationEventSystem +{ + [Dependency] private readonly EntityTableSystem _entityTable = default!; + [Dependency] private readonly NavMapSystem _navMap = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnVotesCompleted); + } + + private void OnVotesCompleted(Entity ent, ref ESSynchronizedVotesCompletedEvent args) + { + if (!args.TryGetResult(0, out var ventOption) || + !TryGetEntity(ventOption.Entity, out var vent)) + { + ForceEndSelf(ent); + return; + } + + ent.Comp.Vent = vent.Value; + + if (TryComp(ent, out var station)) + { + station.StartAnnouncement = Loc.GetString("es-station-event-vent-swarm-start-announcement", + ("location", FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString(ent.Comp.Vent.Value)))); + } + } + + protected override void Started(EntityUid uid, ESVentSwarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + component.NextSwarmTime = Timing.CurTime; + component.SwarmCount = RobustRandom.Next(component.MinSwarmCount, component.MaxSwarmCount + 1); + } + + protected override void ActiveTick(EntityUid uid, ESVentSwarmRuleComponent component, GameRuleComponent gameRule, float frameTime) + { + base.ActiveTick(uid, component, gameRule, frameTime); + + if (!component.Vent.HasValue || TerminatingOrDeleted(component.Vent)) + { + ForceEndSelf(uid, gameRule); + return; + } + + if (Timing.CurTime < component.NextSwarmTime) + return; + component.NextSwarmTime += RobustRandom.Next(component.MinSwarmDelay, component.MaxSwarmDelay); + + foreach (var spawn in _entityTable.GetSpawns(component.SpawnTable)) + { + var ent = SpawnNextToOrDrop(spawn, component.Vent.Value); + _popup.PopupEntity(Loc.GetString("es-vent-swarm-popup", ("spawn", ent)), ent); + } + + component.SwarmCount--; + if (component.SwarmCount <= 0) + ForceEndSelf(uid, gameRule); + } +} diff --git a/Resources/Audio/_ES/Announcements/attributions.yml b/Resources/Audio/_ES/Announcements/attributions.yml index 0c186785728..04f2bd0499a 100644 --- a/Resources/Audio/_ES/Announcements/attributions.yml +++ b/Resources/Audio/_ES/Announcements/attributions.yml @@ -3,7 +3,7 @@ copyright: "Taken from tgstation, modified by pigeonbeans" source: "https://github.com/tgstation/tgstation/blob/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a/sound/ai/default/shuttlerecalled.ogg" -- files: ["attention_low.ogg", "attention_medium.ogg", "attention_high.ogg", "breaker_flip.ogg", "electrical_fire.ogg", "gas_leak.ogg", "greytide.ogg", "meteors.ogg", "power_off.ogg", "power_on.ogg", "solar_flare.ogg", "space_dust.ogg", "vent_foam.ogg", "radstorm_start.ogg", "radstorm_5mins.ogg", "radstorm_15mins.ogg", "radstorm_30mins.ogg"] +- files: ["attention_low.ogg", "attention_medium.ogg", "attention_high.ogg", "breaker_flip.ogg", "critterswarm.ogg", "electrical_fire.ogg", "gas_leak.ogg", "greytide.ogg", "meteors.ogg", "power_off.ogg", "power_on.ogg", "solar_flare.ogg", "space_dust.ogg", "vent_foam.ogg", "radstorm_start.ogg", "radstorm_5mins.ogg", "radstorm_15mins.ogg", "radstorm_30mins.ogg"] license: "CC-BY-SA-3.0" copyright: "Made by pigeonbeans using sounds from tgstation" source: "https://github.com/tgstation/tgstation/tree/master/sound/announcer/vox_fem" diff --git a/Resources/Audio/_ES/Announcements/critterswarm.ogg b/Resources/Audio/_ES/Announcements/critterswarm.ogg new file mode 100644 index 00000000000..2391d25ab0d Binary files /dev/null and b/Resources/Audio/_ES/Announcements/critterswarm.ogg differ diff --git a/Resources/Locale/en-US/_ES/station-events/vent-swarm.ftl b/Resources/Locale/en-US/_ES/station-events/vent-swarm.ftl new file mode 100644 index 00000000000..f81c642f359 --- /dev/null +++ b/Resources/Locale/en-US/_ES/station-events/vent-swarm.ftl @@ -0,0 +1,3 @@ +es-station-event-vent-swarm-start-announcement = An influx of unknown life forms have been detected on the station. Sensors indicate they've entered through ventilation {$location}. +es-vent-swarm-popup = {CAPITALIZE(THE($spawn))} crawls out of the vent! +es-voter-query-string-vent-swarm-location = The swarm will enter: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 2f2320c25ca..26bc3280ff4 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -369,7 +369,9 @@ - type: HTN constantlyReplan: false rootTask: - task: MouseCompound +# ES START + task: RuminantCompound +# ES END - type: Physics - type: Fixtures fixtures: @@ -1815,8 +1817,7 @@ - type: MobThresholds thresholds: 0: Alive - 10: Critical - 20: Dead + 5: Dead - type: MovementSpeedModifier baseWalkSpeed : 2.5 baseSprintSpeed : 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 4f5d17cc9ac..8081cd82657 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -411,7 +411,9 @@ - type: HTN constantlyReplan: false rootTask: - task: MouseCompound +# ES START + task: RuminantCompound +# ES END - type: Physics - type: Fixtures fixtures: diff --git a/Resources/Prototypes/NPCs/mob.yml b/Resources/Prototypes/NPCs/mob.yml index 77969ab92d7..37cb4afbdb2 100644 --- a/Resources/Prototypes/NPCs/mob.yml +++ b/Resources/Prototypes/NPCs/mob.yml @@ -25,13 +25,18 @@ - type: htnCompound id: MouseCompound branches: +# ES START + - tasks: + - !type:HTNCompoundTask + task: ESCutCablesCompound +# ES END - tasks: - !type:HTNCompoundTask task: FoodCompound - tasks: - !type:HTNCompoundTask task: IdleCompound - + - type: htnCompound id: MoproachCompound branches: diff --git a/Resources/Prototypes/_ES/GameRules/degradation_events.yml b/Resources/Prototypes/_ES/GameRules/degradation_events.yml index 1aee5d5f956..d801a604fb2 100644 --- a/Resources/Prototypes/_ES/GameRules/degradation_events.yml +++ b/Resources/Prototypes/_ES/GameRules/degradation_events.yml @@ -15,6 +15,7 @@ table: all: - id: ESDegradationEventAirlocks + - id: ESDegradationEventMouse - id: ESDegradationEventCommunicationsConsole - id: ESDegradationEventCriminalRecords - id: ESDegradationEventTelecoms @@ -33,6 +34,25 @@ - type: StationEvent weight: 50 +- type: entity + parent: ESBaseDegradationEvent + id: ESDegradationEventMouse + name: Mouse Swarm + description: Sends mice into maintenance to nibble wires. + components: + - type: ESSpawnRandomRule + spawnRegion: ESMaintenance + table: + group: + - id: MobMouse + - id: MobMouse1 + - id: MobMouse2 + rolls: 5, 8 + - type: ESTimedDespawn + lifetime: 1.0 + - type: StationEvent + weight: 40 + - type: entity parent: ESBaseDegradationEvent id: ESDegradationEventCommunicationsConsole diff --git a/Resources/Prototypes/_ES/GameRules/station_events.yml b/Resources/Prototypes/_ES/GameRules/station_events.yml index 8af6f325e00..64ffbca3a93 100644 --- a/Resources/Prototypes/_ES/GameRules/station_events.yml +++ b/Resources/Prototypes/_ES/GameRules/station_events.yml @@ -46,6 +46,7 @@ - id: ESStationEventVentClog - id: ESStationEventMeteorSwarm - id: ESStationEventElectricalFire + - id: ESStationEventVentSwarm # Event Prototypes @@ -214,6 +215,39 @@ startAnnouncement: station-event-vent-clog-start-announcement - type: VentClogRule +## Vent Swarm +- type: entity + parent: ESBaseStationEvent + id: ESStationEventVentSwarm + name: Critter Swarm + description: Causes a swarm of critters to enter through a vent + components: + - type: StationEvent + startAudio: + path: /Audio/_ES/Announcements/critterswarm.ogg + params: + volume: -4 + duration: 120 + - type: ESVentSwarmRule + spawnTable: + group: + - id: MobMouse + - id: MobMouse1 + - id: MobMouse2 + - type: ESSynchronizedVoteManager + votes: + - ESVoteVentSwarmLocation + +- type: entity + id: ESVoteVentSwarmLocation + name: Where should the swarm enter? + components: + - type: ESVote + queryString: es-voter-query-string-vent-swarm-location + duration: 30 + - type: ESGasVentVote + count: 4 + ## Meteor Swarm - type: entity parent: ESBaseStationEventDelayed diff --git a/Resources/Prototypes/_ES/NPCs/mouse.yml b/Resources/Prototypes/_ES/NPCs/mouse.yml new file mode 100644 index 00000000000..64872a67964 --- /dev/null +++ b/Resources/Prototypes/_ES/NPCs/mouse.yml @@ -0,0 +1,32 @@ +- type: htnCompound + id: ESCutCablesCompound + branches: + - tasks: + - !type:HTNPrimitiveTask + operator: !type:UtilityOperator + proto: ESOverCable + + - !type:HTNPrimitiveTask + preconditions: + - !type:TargetInLOSPrecondition + targetKey: Target + rangeKey: InteractRange + operator: !type:ESSnipCableOperator + targetKey: Target + +- type: utilityQuery + id: ESOverCable + query: + - !type:ComponentQuery + components: + - type: Cable + considerations: + - !type:ESTargetBesideCon + curve: !type:PresetCurve + preset: TargetDistance + - !type:ESCanInteractCon + curve: !type:BoolCurve + - !type:TargetAccessibleCon + curve: !type:BoolCurve + - !type:TargetInLOSOrCurrentCon + curve: !type:BoolCurve