From 542a7085c4f300412c2a1f80898f27fe5aa6d0a2 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Thu, 18 Jul 2024 15:30:00 -0400 Subject: [PATCH] Rework Mimic as Rampant Brand Intelligence (#395) # Description This PR was originally just going to be me fixing the stupid MobMimic bug. Over the course of 5 hours, and in a state of sleep deprived delirium, I somehow expanded this into something resembling the Rampant Brand Intelligence midround event. You're welcome I guess? I'm going to go get some sleep. # TODO - [x] Re-examine this after at least 9 hours of sleep, and vomit after I read my own shitcode - [x] Make it not shitcode # Changelog :cl: - add: Rampant Brand Intelligence has been spotted infesting NT Stations, be on the lookout for highly aggressive vending machines. --------- Signed-off-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Advertise/AdvertiseComponent.cs | 8 +- .../Mimic/MobReplacementRuleComponent.cs | 24 +++ .../Antag/MobReplacementRuleSystem.cs | 152 +++++++++++++++++- .../GameTicking/GameTicker.Spawning.cs | 2 +- .../events/rempant-brand-intelligence.ftl | 1 + .../Prototypes/Entities/Mobs/NPCs/mimic.yml | 10 +- .../Structures/Machines/vending_machines.yml | 2 +- Resources/Prototypes/GameRules/events.yml | 1 + 8 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 Resources/Locale/en-US/station-events/events/rempant-brand-intelligence.ftl 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