diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 5626f11e0e3..c66161d92e5 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -141,5 +141,6 @@ public enum WinCondition : byte NukiesAbandoned, AllNukiesDead, SomeNukiesAlive, - AllNukiesAlive + AllNukiesAlive, + NukiesKidnappedHeads, // DeltaV - Hostage ops } diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index e22626594f6..9ebbd9be8e8 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -1,3 +1,5 @@ +using Content.Server._DV.Objectives.Components; // DeltaV +using Content.Server._DV.Objectives.Systems; // DeltaV using Content.Server.Antag; using Content.Server.Communications; using Content.Server.GameTicking.Rules.Components; @@ -37,6 +39,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly KidnapHeadsConditionSystem _kidnap = default!; // DeltaV + [Dependency] private readonly SharedMapSystem _map = default!; // DeltaV [ValidatePrototypeId] private const string TelecrystalCurrencyPrototype = "Telecrystal"; @@ -49,6 +53,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnNukeExploded); + SubscribeLocalEvent(OnFTLCompleted); // DeltaV - Kidnap heads objective SubscribeLocalEvent(OnRunLevelChanged); SubscribeLocalEvent(OnNukeDisarm); @@ -156,6 +161,34 @@ private void OnNukeExploded(NukeExplodedEvent ev) } } + // DeltaV - Kidnap heads nukie objective + private void OnFTLCompleted(Entity ent, ref FTLCompletedEvent args) + { + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var nukeops, out _)) + { + // Get the nukie outpost map. + if (!TryComp(uid, out var ruleGridsComp) || ruleGridsComp.Map == null) + return; + + // Make sure your on the same map as the nukie outposts map. + if (args.MapUid == _map.GetMap(ruleGridsComp.Map.Value)) + { + // Now check of the kidnap heads objective is complete... (Yes this is suspect) + var objectives = EntityQueryEnumerator(); + if (!objectives.MoveNext(out var objUid, out var kidnapHeads)) // No kidnap head objectives + return; + + if (!_kidnap.IsCompleted((objUid, kidnapHeads))) + return; + + nukeops.WinConditions.Add(WinCondition.NukiesKidnappedHeads); + SetWinType((uid, nukeops), WinType.OpsMajor); + _roundEndSystem.EndRound(); + } + } + } + private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { if (ev.New is not GameRunLevel.PostRound) @@ -487,7 +520,7 @@ private void OnAfterAntagEntSelected(Entity ent, ref After private void OnGetBriefing(Entity role, ref GetBriefingEvent args) { // TODO Different character screen briefing for the 3 nukie types - args.Append(Loc.GetString("nukeops-briefing")); + // args.Append(Loc.GetString("nukeops-briefing")); Delta-V - Nukie operations take care of this. } /// diff --git a/Content.Server/Nuke/NukeCodePaperSystem.cs b/Content.Server/Nuke/NukeCodePaperSystem.cs index aac2d2361d0..c0961bf1672 100644 --- a/Content.Server/Nuke/NukeCodePaperSystem.cs +++ b/Content.Server/Nuke/NukeCodePaperSystem.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Server._DV.Antag; // DeltaV using Content.Server.Chat.Systems; using Content.Server.Fax; using Content.Shared.Fax.Components; @@ -35,6 +36,16 @@ private void SetupPaper(EntityUid uid, NukeCodePaperComponent? component = null, if (!Resolve(uid, ref component)) return; + // DeltaV - Not the best way of doing this + var evnt = new GetNukeCodePaperWriting(); + RaiseLocalEvent(ref evnt); + if (evnt.ToWrite != null) + { + if (TryComp(uid, out var deltavpaperComp)) + _paper.SetContent((uid, deltavpaperComp), evnt.ToWrite); + return; + } + // DeltaV - End if (TryGetRelativeNukeCode(uid, out var paperContent, station, onlyCurrentStation: component.AllNukesAvailable)) { if (TryComp(uid, out var paperComp)) diff --git a/Content.Server/_DV/Antag/NukieOperationComponent.cs b/Content.Server/_DV/Antag/NukieOperationComponent.cs new file mode 100644 index 00000000000..e3ce8a4a122 --- /dev/null +++ b/Content.Server/_DV/Antag/NukieOperationComponent.cs @@ -0,0 +1,30 @@ +using Content.Shared._DV.Antag; +using Content.Shared.Random; +using Robust.Shared.Prototypes; + +namespace Content.Server._DV.Antag; + +/// +/// Component holds what operations are possible and their weights. +/// +[RegisterComponent, Access(typeof(NukieOperationSystem))] +public sealed partial class NukieOperationComponent : Component +{ + /// + /// The different nukie operations. + /// + [DataField(required: true)] + public ProtoId Operations; + + /// + /// The chosen operation. Is set after the first nukie spawns. + /// + [DataField] + public ProtoId? ChosenOperation; +} + +/// +/// Event to get update the nuke code paper to not actually have the code anymore. +/// +[ByRefEvent] +public record struct GetNukeCodePaperWriting(string? ToWrite); diff --git a/Content.Server/_DV/Antag/NukieOperationSystem.cs b/Content.Server/_DV/Antag/NukieOperationSystem.cs new file mode 100644 index 00000000000..6c3a8c8c37f --- /dev/null +++ b/Content.Server/_DV/Antag/NukieOperationSystem.cs @@ -0,0 +1,70 @@ +using Content.Server.Antag; +using Content.Server.Objectives; +using Content.Shared._DV.FeedbackOverwatch; +using Content.Shared.Mind; +using Content.Shared.Random.Helpers; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._DV.Antag; + +public sealed class NukieOperationSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly ObjectivesSystem _objectives = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedFeedbackOverwatchSystem _feedback = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAntagSelected); + SubscribeLocalEvent(OnNukeCodePaperWritingEvent); + } + + private void OnAntagSelected(Entity ent, ref AfterAntagEntitySelectedEvent args) + { + // Yes this is bad, but I couldn't easily find an event that would work. + if (ent.Comp.ChosenOperation == null) + { + if (!_proto.TryIndex(ent.Comp.Operations, out var opProto)) + return; + + ent.Comp.ChosenOperation = _random.Pick(opProto.Weights); + } + + if (!_mind.TryGetMind(args.Session, out var mindId, out var mind)) + return; + + if (!_proto.TryIndex(ent.Comp.ChosenOperation, out var chosenOp)) + return; + + foreach (var objectiveProto in chosenOp.OperationObjectives) + { + if (!_objectives.TryCreateObjective((mindId, mind), objectiveProto, out var objective)) + { + Log.Error("Couldn't create objective for nukie: " + mindId); // This should never happen. + continue; + } + + _mind.AddObjective(mindId, mind, objective.Value); + + // TODO: Remove once enough feedback has been received! + if (objectiveProto.Id == "KidnapHeadsObjective") + _feedback.SendPopupMind(mindId, "NukieHostageRoundStartPopup"); + } + } + + private void OnNukeCodePaperWritingEvent(ref GetNukeCodePaperWriting ev) + { + // This is suspect AT BEST + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var nukieOperation)) // this should only loop once. + { + if (!_proto.TryIndex(nukieOperation.ChosenOperation, out var opProto) || opProto.NukeCodePaperOverride == null) + continue; + ev.ToWrite = Loc.GetString(opProto.NukeCodePaperOverride); + } + } +} diff --git a/Content.Server/_DV/FeedbackPopup/NukeHostageFeedbackPopupSystem.cs b/Content.Server/_DV/FeedbackPopup/NukeHostageFeedbackPopupSystem.cs new file mode 100644 index 00000000000..eff4da3673d --- /dev/null +++ b/Content.Server/_DV/FeedbackPopup/NukeHostageFeedbackPopupSystem.cs @@ -0,0 +1,60 @@ +using Content.Server._DV.Objectives.Components; +using Content.Shared._DV.FeedbackOverwatch; +using Content.Shared.GameTicking; +using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Roles; +using Content.Server.Roles; + +namespace Content.Server._DV.FeedbackPopup; + +/// +/// System to get feedback on the new objective! +/// +public sealed class NukeHostageFeedbackPopupSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedFeedbackOverwatchSystem _feedback = default!; + [Dependency] private readonly SharedRoleSystem _role = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnMobStateChanged); + } + + private void OnRoundEnd(RoundEndMessageEvent ev) + { + if (!IsHostageOps()) + return; + + var allMinds = _mind.GetAliveHumans(); + + foreach (var mind in allMinds) + { + if (mind.Comp.OwnedEntity != null && _role.MindHasRole(mind)) + _feedback.SendPopupMind(mind, "NukieHostageRoundEndPopup"); + else + _feedback.SendPopupMind(mind, "NukieHostageRoundEndCrewPopup"); + } + } + + private void OnMobStateChanged(MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Dead || !_mind.TryGetMind(args.Target, out var mindUid, out _) || !IsHostageOps()) + return; + + if (_role.MindHasRole(mindUid)) + _feedback.SendPopup(args.Target, "NukieHostageRoundEndPopup"); + } + + + /// + /// If even one person has the kidnap heads objective this will return true. + /// + private bool IsHostageOps() + { + return EntityQueryEnumerator().MoveNext(out _); + } +} diff --git a/Content.Server/_DV/Objectives/Components/KidnapHeadsConditionComponent.cs b/Content.Server/_DV/Objectives/Components/KidnapHeadsConditionComponent.cs new file mode 100644 index 00000000000..531644b8eff --- /dev/null +++ b/Content.Server/_DV/Objectives/Components/KidnapHeadsConditionComponent.cs @@ -0,0 +1,9 @@ +using Content.Server._DV.Objectives.Systems; + +namespace Content.Server._DV.Objectives.Components; + +/// +/// Kidnap some number of heads. Use the NumberObjective to set the exact number +/// +[RegisterComponent, Access(typeof(KidnapHeadsConditionSystem))] +public sealed partial class KidnapHeadsConditionComponent: Component; diff --git a/Content.Server/_DV/Objectives/Components/NukeStationConditionComponent.cs b/Content.Server/_DV/Objectives/Components/NukeStationConditionComponent.cs new file mode 100644 index 00000000000..55762fc9a23 --- /dev/null +++ b/Content.Server/_DV/Objectives/Components/NukeStationConditionComponent.cs @@ -0,0 +1,9 @@ +using Content.Server._DV.Objectives.Systems; + +namespace Content.Server._DV.Objectives.Components; + +/// +/// For nuclear operatives trying to nuke the station. Should only be completed if the correct station is exploded. +/// +[RegisterComponent, Access(typeof(NukeStationConditionSystem))] +public sealed partial class NukeStationConditionComponent : Component; diff --git a/Content.Server/_DV/Objectives/Systems/KidnapHeadsConditionSystem.cs b/Content.Server/_DV/Objectives/Systems/KidnapHeadsConditionSystem.cs new file mode 100644 index 00000000000..10502d7aa56 --- /dev/null +++ b/Content.Server/_DV/Objectives/Systems/KidnapHeadsConditionSystem.cs @@ -0,0 +1,68 @@ +using Content.Server._DV.Objectives.Components; +using Content.Server.Objectives.Systems; +using Content.Server.Revolutionary.Components; +using Content.Shared.Cuffs; +using Content.Shared.Cuffs.Components; +using Content.Shared.Mind; +using Content.Shared.Objectives.Components; + +namespace Content.Server._DV.Objectives.Systems; + +public sealed class KidnapHeadsConditionSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly NumberObjectiveSystem _number = default!; + [Dependency] private readonly SharedCuffableSystem _cuffable = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetProgress); + } + + private void OnGetProgress(Entity condition, ref ObjectiveGetProgressEvent args) + { + args.Progress = GetProgress(condition); + } + + public float GetProgress(Entity condition) + { + GetTotalAndCuffedHeads(out var totalHeads, out var cuffedHeads); + + if (totalHeads == 0) + return 1.0f; + + return (float) cuffedHeads / Math.Min(totalHeads, _number.GetTarget(condition)); + } + + public bool IsCompleted(Entity condition) + { + GetTotalAndCuffedHeads(out var totalHeads, out var cuffedHeads); + if (totalHeads == 0) + return false; + + return cuffedHeads == Math.Min(totalHeads, _number.GetTarget(condition)); + } + + private void GetTotalAndCuffedHeads(out int totalHeads, out int cuffedHeads) + { + var allHumanMinds = _mind.GetAliveHumans(); + totalHeads = 0; + cuffedHeads = 0; + foreach (var mind in allHumanMinds) + { + if (mind.Comp.OwnedEntity is not { } mob) + continue; + + if (!HasComp(mob)) + continue; + totalHeads++; + + if (!TryComp(mob, out var cuffable) || !_cuffable.IsCuffed((mob, cuffable))) + continue; + cuffedHeads++; + } + } +} + diff --git a/Content.Server/_DV/Objectives/Systems/NukeStationConditionSystem.cs b/Content.Server/_DV/Objectives/Systems/NukeStationConditionSystem.cs new file mode 100644 index 00000000000..b851a66a7aa --- /dev/null +++ b/Content.Server/_DV/Objectives/Systems/NukeStationConditionSystem.cs @@ -0,0 +1,43 @@ +using Content.Server._DV.Objectives.Components; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Nuke; +using Content.Server.Objectives.Systems; +using Content.Server.Station.Components; +using Content.Shared.Objectives.Components; + +namespace Content.Server._DV.Objectives.Systems; + +public sealed class NukeStationConditionSystem : EntitySystem +{ + [Dependency] private readonly CodeConditionSystem _codeCondition = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNukeExploded); + } + + private void OnNukeExploded(NukeExplodedEvent ev) + { + var nukeOpsQuery = EntityQueryEnumerator(); + while (nukeOpsQuery.MoveNext(out _, out var nukeopsRule)) // this should only loop once. + { + if (!TryComp(nukeopsRule.TargetStation, out var data)) + return; + + foreach (var grid in data.Grids) + { + if (grid != ev.OwningStation) // They nuked the target station! + continue; + + // Set all the objectives to true. + var nukeStationQuery = EntityQueryEnumerator(); + while (nukeStationQuery.MoveNext(out var uid, out _)) + { + _codeCondition.SetCompleted(uid); + } + } + } + } +} diff --git a/Content.Shared/_DV/Antag/NukieOperationPrototype.cs b/Content.Shared/_DV/Antag/NukieOperationPrototype.cs new file mode 100644 index 00000000000..c0dc4007fb5 --- /dev/null +++ b/Content.Shared/_DV/Antag/NukieOperationPrototype.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Antag; + +/// +/// This is for nukie operations. E.g nuke the station or kidnap x number of heads. +/// +[Prototype] +public sealed partial class NukieOperationPrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + [DataField(required: true)] + public List OperationObjectives = new(); + + [DataField] + public LocId? NukeCodePaperOverride; +} diff --git a/Content.Shared/_DV/FeedbackOverwatch/FeedbackPopupInformationComponent.cs b/Content.Shared/_DV/FeedbackOverwatch/FeedbackPopupInformationComponent.cs new file mode 100644 index 00000000000..46fe6b94736 --- /dev/null +++ b/Content.Shared/_DV/FeedbackOverwatch/FeedbackPopupInformationComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.FeedbackOverwatch; + +/// +/// Component that stores information about feedback popups on a players mind. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class FeedbackPopupInformationComponent : Component +{ + /// + /// List of popups that this mind has already seen. + /// + [DataField, AutoNetworkedField] + public HashSet> SeenPopups = new(); +} diff --git a/Content.Shared/_DV/FeedbackOverwatch/SharedFeedbackOverwatchSystem.cs b/Content.Shared/_DV/FeedbackOverwatch/SharedFeedbackOverwatchSystem.cs index f12bf691201..2add4971b8b 100644 --- a/Content.Shared/_DV/FeedbackOverwatch/SharedFeedbackOverwatchSystem.cs +++ b/Content.Shared/_DV/FeedbackOverwatch/SharedFeedbackOverwatchSystem.cs @@ -43,8 +43,9 @@ private void LoadPrototypes() /// /// UID of the entity the player is controlling. /// Popup to send them. + /// If true, if the popup is sent to the same mind again, it will not be displayed. /// Returns true if the popup message was sent to the client successfully. - public bool SendPopup(EntityUid? uid, ProtoId popupPrototype) + public bool SendPopup(EntityUid? uid, ProtoId popupPrototype, bool sendOnlyOnce = true) { if (uid == null) return false; @@ -52,7 +53,7 @@ public bool SendPopup(EntityUid? uid, ProtoId popupProto if (!_mind.TryGetMind(uid.Value, out var mindUid, out _)) return false; - return SendPopupMind(mindUid, popupPrototype); + return SendPopupMind(mindUid, popupPrototype, sendOnlyOnce); } /// @@ -60,12 +61,24 @@ public bool SendPopup(EntityUid? uid, ProtoId popupProto /// /// UID of the players mind. /// Popup to send them. + /// If true, if the popup is sent to the same mind again, it will not be displayed. /// Returns true if the popup message was sent to the client successfully. - public bool SendPopupMind(EntityUid? uid, ProtoId popupPrototype) + public bool SendPopupMind(EntityUid? uid, ProtoId popupPrototype, bool sendOnlyOnce = true) { if (uid == null) return false; + if (sendOnlyOnce) + { + EnsureComp(uid.Value, out var feedbackInfoComp); + + // If it's already been seen, don't resend it. + if (!feedbackInfoComp.SeenPopups.Add(popupPrototype)) + return false; + + Dirty(uid.Value, feedbackInfoComp); + } + if (!_mind.TryGetSession(uid, out var session)) return false; diff --git a/Content.Shared/_DV/Implants/AddComponentsImplant/AddComponentsImplantComponent.cs b/Content.Shared/_DV/Implants/AddComponentsImplant/AddComponentsImplantComponent.cs new file mode 100644 index 00000000000..5be9fc06e5a --- /dev/null +++ b/Content.Shared/_DV/Implants/AddComponentsImplant/AddComponentsImplantComponent.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Implants.AddComponentsImplant; + +/// +/// When added to an implanter will add the passed in components to the implanted entity. +/// +/// +/// Warning: Multiple implants with this component adding the same components will not properly remove components +/// unless removed in the inverse order of their injection (Last in, first out). +/// +[RegisterComponent] +public sealed partial class AddComponentsImplantComponent : Component +{ + /// + /// What components will be added to the entity. If the component already exists, it will be skipped. + /// + [DataField(required: true)] + public ComponentRegistry ComponentsToAdd; + + /// + /// What components were added to the entity after implanted. Is used to know what components to remove. + /// + [DataField] + public ComponentRegistry AddedComponents = new(); +} diff --git a/Content.Shared/_DV/Implants/AddComponentsImplant/AddComponentsImplantSystem.cs b/Content.Shared/_DV/Implants/AddComponentsImplant/AddComponentsImplantSystem.cs new file mode 100644 index 00000000000..2e5c19f88f3 --- /dev/null +++ b/Content.Shared/_DV/Implants/AddComponentsImplant/AddComponentsImplantSystem.cs @@ -0,0 +1,39 @@ +using Robust.Shared.Containers; +using Content.Shared.Implants; + +namespace Content.Shared._DV.Implants.AddComponentsImplant; + +public sealed class AddComponentsImplantSystem : EntitySystem +{ + [Dependency] private readonly IComponentFactory _factory = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnImplantImplantedEvent); + SubscribeLocalEvent(OnRemove); + } + + private void OnImplantImplantedEvent(Entity ent, ref ImplantImplantedEvent args) + { + if (args.Implanted is not {} target) + return; + + foreach (var component in ent.Comp.ComponentsToAdd) + { + // Don't add the component if it already exists + if (EntityManager.HasComponent(target, _factory.GetComponent(component.Key).GetType())) + continue; + + EntityManager.AddComponent(target, component.Value); + ent.Comp.AddedComponents.Add(component.Key, component.Value); + } + } + + private void OnRemove(Entity ent, ref EntGotRemovedFromContainerMessage args) + { + EntityManager.RemoveComponents(args.Container.Owner, ent.Comp.AddedComponents); + + // Clear the list so the implant can be reused. + ent.Comp.AddedComponents.Clear(); + } +} diff --git a/Content.Shared/_DV/Implants/AddFactions/AddFactionsImplantComponent.cs b/Content.Shared/_DV/Implants/AddFactions/AddFactionsImplantComponent.cs new file mode 100644 index 00000000000..af9db77d911 --- /dev/null +++ b/Content.Shared/_DV/Implants/AddFactions/AddFactionsImplantComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.NPC.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Implants.AddFactions; + +/// +/// Will add all the factions to the person being implanted. +/// +[RegisterComponent] +public sealed partial class AddFactionsImplantComponent : Component +{ + /// + /// These factions will be added when implanted. + /// + [DataField(required: true)] + public HashSet> Factions; + + /// + /// These are the factions that were actually added. Used know what factions to remove when the implant is removed. + /// + [DataField] + public HashSet> AddedFactions = new(); +} diff --git a/Content.Shared/_DV/Implants/AddFactions/AddFactionsImplantSystem.cs b/Content.Shared/_DV/Implants/AddFactions/AddFactionsImplantSystem.cs new file mode 100644 index 00000000000..7717a1e5a75 --- /dev/null +++ b/Content.Shared/_DV/Implants/AddFactions/AddFactionsImplantSystem.cs @@ -0,0 +1,39 @@ +using Content.Shared.Implants; +using Content.Shared.NPC.Systems; +using Robust.Shared.Containers; + +namespace Content.Shared._DV.Implants.AddFactions; + +public sealed class AddFactionsImplantSystem : EntitySystem +{ + [Dependency] private readonly NpcFactionSystem _npc = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnImplantImplantedEvent); + SubscribeLocalEvent(OnRemove); + } + + private void OnImplantImplantedEvent(Entity ent, ref ImplantImplantedEvent args) + { + if (args.Implanted is not {} target) + return; + + foreach (var faction in ent.Comp.Factions) + { + if (_npc.IsMember(target, faction)) // If it's already in that faction, skip this. + continue; + + _npc.AddFaction(target, faction); + ent.Comp.AddedFactions.Add(faction); + } + } + + private void OnRemove(Entity ent, ref EntGotRemovedFromContainerMessage args) + { + foreach (var faction in ent.Comp.AddedFactions) + _npc.RemoveFaction(args.Container.Owner, faction); + + ent.Comp.AddedFactions.Clear(); + } +} diff --git a/Resources/Locale/en-US/_DV/feedbackpopup/popups/nukeHostagepopup.ftl b/Resources/Locale/en-US/_DV/feedbackpopup/popups/nukeHostagepopup.ftl new file mode 100644 index 00000000000..25f4314ca62 --- /dev/null +++ b/Resources/Locale/en-US/_DV/feedbackpopup/popups/nukeHostagepopup.ftl @@ -0,0 +1,17 @@ +# Round start nukie +feedbackpopup-hostage-nukie-start-name = Nukie-Hostage-START +feedbackpopup-hostage-nukie-start-title = [bold]New Nuke-Ops objective[/bold] +feedbackpopup-hostage-nukie-start-description-0 = You rolled the new hostage nukie objective! This is a new objective we are testing for nuke ops. This is at the very early stages of development, so any feedback is good. Expect bugs (Please report them <3)! +feedbackpopup-hostage-nukie-start-description-1 = To complete the objective, cuff the required amount of heads (If there aren't that many on the station the current amount of heads will be all that is required) and FTL warp back to the outpost. +feedbackpopup-hostage-nukie-start-description-2 = At the [bold]end of the round[/bold] you will get a feedback form. + +# On death, or game end nukie +feedbackpopup-hostage-nukie-end-name = Nukie-Hostage-NUKIE +feedbackpopup-hostage-nukie-end-title = [bold]Hostage objective feedback![/bold] +feedbackpopup-hostage-nukie-end-description-0 = Thanks for play testing our new nuke ops objective! If you would like to share any feedback about this feature, please comment it below. We would like to hear what you liked or didn't like, and how you think this feature could be improved. +feedbackpopup-hostage-nukie-end-description-1 = Suggestions will be relayed to the DISCORD-CHANNEL-NAME-HERE channel. + +# Crew popup on round end. +feedbackpopup-hostage-crew-end-name = Nukie-Hostage-CREW +feedbackpopup-hostage-crew-end-title = [bold]Hostage objective feedback (Crew)![/bold] + diff --git a/Resources/Locale/en-US/_DV/nukie-operations/operations.ftl b/Resources/Locale/en-US/_DV/nukie-operations/operations.ftl new file mode 100644 index 00000000000..b264291679f --- /dev/null +++ b/Resources/Locale/en-US/_DV/nukie-operations/operations.ftl @@ -0,0 +1,6 @@ +# Kidnap heads objective +nukie-operations-kidnap-heads-nuke-codes-override = This is a hostage operation. Look at your objectives for more information. It is recommended to buy hostage implants and handcuffs from your uplink. [color=red]To complete your objective you must FTL back to your outpost with the required number of heads restrained[/color]. +nukie-operations-kidnap-heads-objective-title = Cut off the head. +nukie-operations-kidnap-heads-objective-descript = We need to you kidnap {$count} heads of staff from the station. We only care about the heads, any crew who dies is just collateral. + +nukeops-cond-nukieskidnappedheads = The nuclear operative kidnapped the heads of the station. diff --git a/Resources/Locale/en-US/_DV/store/uplink-catalog.ftl b/Resources/Locale/en-US/_DV/store/uplink-catalog.ftl index 4f9b354a857..817d3476130 100644 --- a/Resources/Locale/en-US/_DV/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/_DV/store/uplink-catalog.ftl @@ -15,6 +15,9 @@ uplink-syndicate-radio-implanter-desc = A cranial implant that lets you talk on uplink-syndicate-radio-implanter-bundle-name = Syndicate Radio Implanter Bundle uplink-syndicate-radio-implanter-bundle-desc = Two implanters for the price of one and a half! Share one with your Syndicate friend. +uplink-syndicate-hostage-implanter-bundle-name = Hostage implant bundle +uplink-syndicate-hostage-implanter-bundle-desc = These implants pacify when injected and also allow the hostages to enter your shuttle without being shot by turrets! + uplink-doorjack-name = Airlock Access Override uplink-doorjack-desc = A specialized cryptographic sequencer, designed solely to doorjack NanoTrasen's updated airlocks. Does not tamper with anything else. @@ -31,3 +34,7 @@ uplink-appraisal-tool-gun-name = Appraisal Tool Gun uplink-appraisal-tool-gun-desc = A modified Viper to appear as an appraisal tool, at the cost of slightly slower firerate uplink-storage-implanter-delta-desc = Hide goodies inside of yourself with new bluespace technology! Budget cuts have resulted in it NOT STORING High Value or storage items. + +# Objectives +uplink-syndicate-handcuff-bundle-name = Handcuff implant bundle +uplink-syndicate-handcuff-bundle-desc = Includes 10 metal handcuffs. We recommended you share with your friends! diff --git a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml index beffe8959a6..ee3913c4aa2 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml @@ -254,3 +254,15 @@ components: - type: Implanter implant: MindShieldImplant + +# Nukie implants + +- type: entity + id: HostageImplanter + suffix: hostage + parent: BaseImplantOnlyImplanterSyndi + components: + - type: Implanter + implant: HostageImplant + implantTime: 20 + drawTime: 15 diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index 9ae08dfa74a..b9ce4411b21 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -330,3 +330,22 @@ permanent: false # DeltaV - let peaceful revs etc remove mindshields addedComponents: # DeltaV - replaces mindshield tag logic - type: MindShield + +# Nukie implants + +- type: entity + parent: BaseSubdermalImplant + id: HostageImplant + name: hostage implant + description: Perfect for all your hostage ops needs! Allows the implanted entity to be ignored by syndicate turrets. + categories: [ HideSpawnMenu ] + components: + - type: SubdermalImplant + - type: AddComponentsImplant + componentsToAdd: + - type: Pacified + disallowDisarm: true + disallowAllCombat: true + - type: AddFactionsImplant + factions: + - Hostage diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 7982c1e2b78..777aa5da1d0 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -96,6 +96,8 @@ minPlayers: 20 - type: LoadMapRule mapPath: /Maps/Nonstations/nukieplanet.yml + - type: NukieOperation # DeltaV - Nukie operations! + operations: NukieOperations - type: AntagSelection selectionTime: PrePlayerSpawn definitions: diff --git a/Resources/Prototypes/_DV/Catalog/Fills/Boxes/general.yml b/Resources/Prototypes/_DV/Catalog/Fills/Boxes/general.yml index aaab0d11936..7ea998369c7 100644 --- a/Resources/Prototypes/_DV/Catalog/Fills/Boxes/general.yml +++ b/Resources/Prototypes/_DV/Catalog/Fills/Boxes/general.yml @@ -16,7 +16,7 @@ whitelist: components: - EncryptionKey - + - type: entity name: justice encryption key box parent: BoxEncryptionKeyPassenger @@ -42,3 +42,47 @@ contents: - id: SyndicateRadioImplanter amount: 2 + +- type: entity + name: syndicate hostage implanter box + parent: BoxCardboard + id: BoxSyndicateHostageImplanter + description: Contains hostage implants. + components: + - type: Sprite + layers: + - state: box_of_doom + - state: implant + - type: Storage + maxItemSize: Small + grid: + - 0,0,2,3 + whitelist: + components: + - Implanter + - type: StorageFill + contents: + - id: HostageImplanter + amount: 6 + +- type: entity + name: syndicate handcuff box + parent: BoxCardboard + id: BoxSyndicateHandcuffBundle + description: Contains a large amount of handcuffs + components: + - type: Sprite + layers: + - state: box_of_doom + - state: handcuff + - type: Storage + maxItemSize: Small + grid: + - 0,0,4,3 + whitelist: + components: + - Handcuff + - type: StorageFill + contents: + - id: Handcuffs + amount: 10 diff --git a/Resources/Prototypes/_DV/Catalog/uplink_catalog.yml b/Resources/Prototypes/_DV/Catalog/uplink_catalog.yml index 4f211e8aa7a..5b4f965d998 100644 --- a/Resources/Prototypes/_DV/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/_DV/Catalog/uplink_catalog.yml @@ -147,3 +147,46 @@ - !type:BuyerDepartmentCondition whitelist: - Logistics + +- type: listing + id: UplinkHostageImplanter + name: uplink-syndicate-hostage-implanter-bundle-name + description: uplink-syndicate-hostage-implanter-bundle-desc + icon: { sprite: Objects/Misc/handcuffs.rsi, state: handcuff } + productEntity: BoxSyndicateHostageImplanter + discountCategory: usualDiscounts + discountDownTo: + Telecrystal: 1 + cost: + Telecrystal: 2 + categories: + - UplinkImplants + conditions: + - !type:BuyerWhitelistCondition + blacklist: + components: + - SurplusBundle + - !type:StoreWhitelistCondition + whitelist: + tags: + - NukeOpsUplink + +- type: listing + id: UplinkHandcuffBundle + name: uplink-syndicate-handcuff-bundle-name + description: uplink-syndicate-handcuff-bundle-desc + icon: { sprite: Objects/Misc/handcuffs.rsi, state: handcuff } + productEntity: BoxSyndicateHandcuffBundle + cost: + Telecrystal: 1 + categories: + - UplinkObjectives + conditions: + - !type:BuyerWhitelistCondition + blacklist: + components: + - SurplusBundle + - !type:StoreWhitelistCondition + whitelist: + tags: + - NukeOpsUplink diff --git a/Resources/Prototypes/_DV/FeedbackPopup/feedbackpopupsNukeHostage.yml b/Resources/Prototypes/_DV/FeedbackPopup/feedbackpopupsNukeHostage.yml new file mode 100644 index 00000000000..afa397cec79 --- /dev/null +++ b/Resources/Prototypes/_DV/FeedbackPopup/feedbackpopupsNukeHostage.yml @@ -0,0 +1,28 @@ +# Round start nukie popup. +- type: feedbackPopup + id: NukieHostageRoundStartPopup + popupName: feedbackpopup-hostage-nukie-start-name + title: feedbackpopup-hostage-nukie-start-title + description: + - feedbackpopup-hostage-nukie-start-description-0 + - feedbackpopup-hostage-nukie-start-description-1 + - feedbackpopup-hostage-nukie-start-description-2 + feedbackField: false + +# On death, or game end nukie popup. +- type: feedbackPopup + id: NukieHostageRoundEndPopup + popupName: feedbackpopup-hostage-nukie-end-name + title: feedbackpopup-hostage-nukie-end-title + description: + - feedbackpopup-hostage-nukie-end-description-0 + - feedbackpopup-hostage-nukie-end-description-1 + +# Crew popup on round end. +- type: feedbackPopup + id: NukieHostageRoundEndCrewPopup + popupName: feedbackpopup-hostage-crew-end-name + title: feedbackpopup-hostage-crew-end-title + description: + - feedbackpopup-hostage-nukie-end-description-0 + - feedbackpopup-hostage-nukie-end-description-1 diff --git a/Resources/Prototypes/_DV/Objectives/nukies.yml b/Resources/Prototypes/_DV/Objectives/nukies.yml new file mode 100644 index 00000000000..8e613bfc9d9 --- /dev/null +++ b/Resources/Prototypes/_DV/Objectives/nukies.yml @@ -0,0 +1,66 @@ +# Nukies operations + +- type: weightedRandom + id: NukieOperations + weights: + NukieOperationDestroyStation: 1 + NukieOperationKidnapHeads: 1 + +- type: nukieOperation + id: NukieOperationDestroyStation + operationObjectives: + - NukeStationObjective + +- type: nukieOperation + id: NukieOperationKidnapHeads + operationObjectives: + - KidnapHeadsObjective + - NukieStealDiskObjective + nukeCodePaperOverride: nukie-operations-kidnap-heads-nuke-codes-override + +- type: entity + parent: BaseObjective + id: NukeStationObjective + name: Nuke the station. + description: Its your job. Get going! + components: + - type: Objective + difficulty: 4.0 + issuer: objective-issuer-syndicate + icon: + sprite: Objects/Devices/nuke.rsi + state: nuclearbomb_base + - type: NukeStationCondition + - type: CodeCondition + +- type: entity + parent: BaseObjective + id: KidnapHeadsObjective + components: + - type: Objective + difficulty: 4.0 + issuer: objective-issuer-syndicate + icon: + sprite: Objects/Misc/handcuffs.rsi + state: handcuff + - type: KidnapHeadsCondition + - type: NumberObjective + min: 4 # Min and max have to be the same. Otherwise, each operative will have a different number. + max: 4 + title: nukie-operations-kidnap-heads-objective-title + description: nukie-operations-kidnap-heads-objective-descript + +- type: entity + parent: BaseStealObjective + id: NukieStealDiskObjective + components: + - type: Objective + difficulty: 4.0 + issuer: objective-issuer-syndicate + icon: + sprite: Objects/Misc/nukedisk.rsi + state: icon + - type: StealCondition + stealGroup: NukeDisk + owner: objective-condition-steal-station + verifyMapExistence: false diff --git a/Resources/Prototypes/_DV/ai_factions.yml b/Resources/Prototypes/_DV/ai_factions.yml index 783fc9d9889..2307eef3bc3 100644 --- a/Resources/Prototypes/_DV/ai_factions.yml +++ b/Resources/Prototypes/_DV/ai_factions.yml @@ -3,3 +3,6 @@ hostile: - Mouse - SimpleHostile + +- type: npcFaction + id: Hostage diff --git a/Resources/Prototypes/ai_factions.yml b/Resources/Prototypes/ai_factions.yml index c2b62b5bdc6..d9e343534a4 100644 --- a/Resources/Prototypes/ai_factions.yml +++ b/Resources/Prototypes/ai_factions.yml @@ -62,6 +62,8 @@ - Zombie - Dragon - AllHostile + friendly: # DeltaV Nukie hostage ops + - Hostage - type: npcFaction id: Xeno