diff --git a/Content.Server/Advertise/AdvertiseComponent.cs b/Content.Server/Advertise/AdvertiseComponent.cs
index f36cc7ae1e6..144d4db975b 100644
--- a/Content.Server/Advertise/AdvertiseComponent.cs
+++ b/Content.Server/Advertise/AdvertiseComponent.cs
@@ -3,23 +3,21 @@
namespace Content.Server.Advertise
{
- [RegisterComponent, Access(typeof(AdvertiseSystem))]
+ [RegisterComponent]
public sealed partial class AdvertiseComponent : Component
{
///
/// Minimum time in seconds to wait before saying a new ad, in seconds. Has to be larger than or equal to 1.
///
- [ViewVariables(VVAccess.ReadWrite)]
[DataField("minWait")]
- public int MinimumWait { get; private set; } = 8 * 60;
+ public int MinimumWait = 8 * 60;
///
/// Maximum time in seconds to wait before saying a new ad, in seconds. Has to be larger than or equal
/// to
///
- [ViewVariables(VVAccess.ReadWrite)]
[DataField("maxWait")]
- public int MaximumWait { get; private set; } = 10 * 60;
+ public int MaximumWait = 10 * 60;
///
/// The identifier for the advertisements pack prototype.
diff --git a/Content.Server/Antag/Mimic/MobReplacementRuleComponent.cs b/Content.Server/Antag/Mimic/MobReplacementRuleComponent.cs
index d89d61606db..0824d48ae27 100644
--- a/Content.Server/Antag/Mimic/MobReplacementRuleComponent.cs
+++ b/Content.Server/Antag/Mimic/MobReplacementRuleComponent.cs
@@ -13,9 +13,33 @@ public sealed partial class MobReplacementRuleComponent : Component
[DataField]
public EntProtoId Proto = "MobMimic";
+ [DataField]
+ public int NumberToReplace { get; set; }
+
+ [DataField]
+ public string Announcement = "station-event-rampant-intelligence-announcement";
+
///
/// Chance per-entity.
///
[DataField]
public float Chance = 0.001f;
+
+ [DataField]
+ public bool DoAnnouncement = true;
+
+ [DataField]
+ public float MimicMeleeDamage = 20f;
+
+ [DataField]
+ public float MimicMoveSpeed = 1f;
+
+ [DataField]
+ public string MimicAIType = "SimpleHostileCompound";
+
+ [DataField]
+ public bool MimicSmashGlass = true;
+
+ [DataField]
+ public bool VendorModify = true;
}
diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs
index 2446b976e1a..dd65d36067e 100644
--- a/Content.Server/Antag/MobReplacementRuleSystem.cs
+++ b/Content.Server/Antag/MobReplacementRuleSystem.cs
@@ -1,15 +1,44 @@
+using System.Numerics;
using Content.Server.Antag.Mimic;
+using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
+using Content.Server.NPC.Systems;
+using Content.Server.Station.Systems;
+using Content.Server.GameTicking;
using Content.Shared.VendingMachines;
using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Server.GameObjects;
+using Robust.Shared.Physics.Systems;
+using System.Linq;
+using Robust.Shared.Physics;
+using Content.Shared.Movement.Components;
+using Content.Shared.Damage;
+using Content.Server.NPC.HTN;
+using Content.Server.NPC;
+using Content.Shared.Weapons.Melee;
+using Content.Server.Advertise;
+using Content.Server.Power.Components;
+using Content.Shared.CombatMode;
namespace Content.Server.Antag;
public sealed class MobReplacementRuleSystem : GameRuleSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly GameTicker _gameTicker = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+ [Dependency] private readonly NPCSystem _npc = default!;
+ [Dependency] private readonly TransformSystem _transform = default!;
+ [Dependency] private readonly AdvertiseSystem _advertise = default!;
+
protected override void Started(EntityUid uid, MobReplacementRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
@@ -17,21 +46,134 @@ protected override void Started(EntityUid uid, MobReplacementRuleComponent compo
var query = AllEntityQuery();
var spawns = new List<(EntityUid Entity, EntityCoordinates Coordinates)>();
+ var stations = _gameTicker.GetSpawnableStations();
while (query.MoveNext(out var vendingUid, out _, out var xform))
{
- if (!_random.Prob(component.Chance))
+ var ownerStation = _station.GetOwningStation(vendingUid);
+
+ if (ownerStation == null
+ || ownerStation != stations[0])
+ continue;
+
+ // Make sure that we aren't running this on something that is already a mimic
+ if (HasComp(vendingUid))
continue;
spawns.Add((vendingUid, xform.Coordinates));
}
- foreach (var entity in spawns)
+ if (spawns == null)
{
- var coordinates = entity.Coordinates;
- Del(entity.Entity);
+ //WTF THE STATION DOESN'T EXIST! WE MUST BE IN A TEST! QUICK, PUT A MIMIC AT 0,0!!!
+ Spawn(component.Proto, new EntityCoordinates(uid, new Vector2(0, 0)));
+ }
+ else
+ {
+ // This is intentionally not clamped. If a server host wants to replace every vending machine in the entire station with a mimic, who am I to stop them?
+ var k = MathF.MaxMagnitude(component.NumberToReplace, 1);
+ while (k > 0 && spawns != null && spawns.Count > 0)
+ {
+ if (k > 1)
+ {
+ var spawnLocation = _random.PickAndTake(spawns);
+ BuildAMimicWorkshop(spawnLocation.Entity, component);
+ }
+ else
+ {
+ BuildAMimicWorkshop(spawns[0].Entity, component);
+ }
+
+ if (k == MathF.MaxMagnitude(component.NumberToReplace, 1)
+ && component.DoAnnouncement)
+ _chat.DispatchStationAnnouncement(stations[0], Loc.GetString("station-event-rampant-intelligence-announcement"), playDefaultSound: true,
+ colorOverride: Color.Red, sender: "Central Command");
+
+ k--;
+ }
+ }
+ }
+
+ ///
+ /// It's like Build a Bear, but MURDER
+ ///
+ ///
+ public void BuildAMimicWorkshop(EntityUid uid, MobReplacementRuleComponent component)
+ {
+ var metaData = MetaData(uid);
+ var vendorPrototype = metaData.EntityPrototype;
+ var mimicProto = _prototype.Index(component.Proto);
+
+ var vendorComponents = vendorPrototype?.Components.Keys
+ .Where(n => n != "Transform" && n != "MetaData")
+ .Select(name => (name, _componentFactory.GetRegistration(name).Type))
+ .ToList() ?? new List<(string name, Type type)>();
+
+ var mimicComponents = mimicProto?.Components.Keys
+ .Where(n => n != "Transform" && n != "MetaData")
+ .Select(name => (name, _componentFactory.GetRegistration(name).Type))
+ .ToList() ?? new List<(string name, Type type)>();
- Spawn(component.Proto, coordinates);
+ foreach (var name in mimicComponents.Except(vendorComponents))
+ {
+ var newComponent = _componentFactory.GetComponent(name.name);
+ EntityManager.AddComponent(uid, newComponent);
}
+
+ var xform = Transform(uid);
+ if (xform.Anchored)
+ _transform.Unanchor(uid, xform);
+
+ SetupMimicNPC(uid, component);
+
+ if (TryComp(uid, out var vendor)
+ && component.VendorModify)
+ SetupMimicVendor(uid, component, vendor);
+ }
+ ///
+ /// This handles getting the entity ready to be a hostile NPC
+ ///
+ ///
+ ///
+ private void SetupMimicNPC(EntityUid uid, MobReplacementRuleComponent component)
+ {
+ _physics.SetBodyType(uid, BodyType.KinematicController);
+ _npcFaction.AddFaction(uid, "SimpleHostile");
+
+ var melee = EnsureComp(uid);
+ melee.Angle = 0;
+ DamageSpecifier dspec = new()
+ {
+ DamageDict = new()
+ {
+ { "Blunt", component.MimicMeleeDamage }
+ }
+ };
+ melee.Damage = dspec;
+
+ var movementSpeed = EnsureComp(uid);
+ (movementSpeed.BaseSprintSpeed, movementSpeed.BaseWalkSpeed) = (component.MimicMoveSpeed, component.MimicMoveSpeed);
+
+ var htn = EnsureComp(uid);
+ htn.RootTask = new HTNCompoundTask() { Task = component.MimicAIType };
+ htn.Blackboard.SetValue(NPCBlackboard.NavSmash, component.MimicSmashGlass);
+ _npc.WakeNPC(uid, htn);
+ }
+
+ ///
+ /// Handling specific interactions with vending machines
+ ///
+ ///
+ ///
+ ///
+ private void SetupMimicVendor(EntityUid uid, MobReplacementRuleComponent mimicComponent, AdvertiseComponent vendorComponent)
+ {
+ vendorComponent.MinimumWait = 5;
+ vendorComponent.MaximumWait = 15;
+ _advertise.SayAdvertisement(uid, vendorComponent);
+ _advertise.RefreshTimer(uid, vendorComponent);
+
+ if (TryComp(uid, out var aPC))
+ aPC.NeedsPower = false;
}
}
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index bf69b4b9094..8f8c787f3f3 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -43,7 +43,7 @@ public sealed partial class GameTicker
// Mainly to avoid allocations.
private readonly List _possiblePositions = new();
- private List GetSpawnableStations()
+ public List GetSpawnableStations()
{
var spawnableStations = new List();
var query = EntityQueryEnumerator();
diff --git a/Resources/Locale/en-US/station-events/events/rempant-brand-intelligence.ftl b/Resources/Locale/en-US/station-events/events/rempant-brand-intelligence.ftl
new file mode 100644
index 00000000000..8f7403dabd2
--- /dev/null
+++ b/Resources/Locale/en-US/station-events/events/rempant-brand-intelligence.ftl
@@ -0,0 +1 @@
+station-event-rampant-intelligence-announcement = Rampant brand intelligence has been detected board your station. Please inspect any vendors for aggressive marketing tactics, and reboot them if necessary.
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml
index 657ac466f84..1f153ff3141 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml
@@ -1,13 +1,17 @@
- type: entity
name: Mimic
id: MobMimic
- parent: [ SimpleMobBase, MobCombat ]
description: Surprise. # When this gets a proper write this should use the object's actual description >:)
components:
- type: Tag
tags:
- FootstepSound
+ - type: Clickable
+ - type: CombatMode
+ - type: InteractionOutline
- type: InputMover
+ - type: Input
+ context: "human"
- type: MobMover
- type: NpcFactionMember
factions:
@@ -20,6 +24,7 @@
- type: Icon
sprite: Structures/Machines/VendingMachines/cola.rsi
state: normal
+ - type: Physics
- type: Fixtures
fixtures:
fix1:
@@ -38,6 +43,3 @@
damage:
types:
Blunt: 20
- - type: MovementSpeedModifier
- baseWalkSpeed : 1
- baseSprintSpeed : 1
diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
index c90d42b42cc..c9d9cfbf2c2 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
@@ -29,7 +29,7 @@
- MachineMask
layer:
- MachineLayer
- density: 200
+ density: 1000
- type: Destructible
thresholds:
- trigger:
diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml
index f4936f51f50..e9c7e5fbad7 100644
--- a/Resources/Prototypes/GameRules/events.yml
+++ b/Resources/Prototypes/GameRules/events.yml
@@ -447,3 +447,4 @@
minimumPlayers: 20
weight: 5
- type: MobReplacementRule
+ numberToReplace: 1