diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs index 62f92963aa..6f82aa042f 100644 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Dataset; +using Content.Shared.FixedPoint; using Content.Shared.NPC.Prototypes; using Content.Shared.Random; using Content.Shared.Roles; @@ -31,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component [DataField] public ProtoId ObjectiveIssuers = "TraitorCorporations"; + /// + /// Give this traitor an Uplink on spawn. + /// + [DataField] + public bool GiveUplink = true; + + /// + /// Give this traitor the codewords. + /// + [DataField] + public bool GiveCodewords = true; + + /// + /// Give this traitor a briefing in chat. + /// + [DataField] + public bool GiveBriefing = true; + public int TotalTraitors => TraitorMinds.Count; public string[] Codewords = new string[3]; @@ -68,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component /// The amount of TC traitors start with. /// [DataField] - public int StartingBalance = 20; + public FixedPoint2 StartingBalance = 20; } diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 44ad00ae17..1987613763 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -7,6 +7,7 @@ using Content.Server.PDA.Ringer; using Content.Server.Roles; using Content.Server.Traitor.Uplink; using Content.Shared.Database; +using Content.Shared.FixedPoint; using Content.Shared.GameTicking.Components; using Content.Shared.Mind; using Content.Shared.NPC.Systems; @@ -75,38 +76,46 @@ public sealed class TraitorRuleSystem : GameRuleSystem return codewords; } - public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true) + public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component) { //Grab the mind if it wasn't provided if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) return false; - var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); + var briefing = ""; + + if (component.GiveCodewords) + briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); + var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values); + // Uplink code will go here if applicable, but we still need the variable if there aren't any Note[]? code = null; - if (giveUplink) + + if (component.GiveUplink) { // Calculate the amount of currency on the uplink. var startingBalance = component.StartingBalance; if (_jobs.MindTryGetJob(mindId, out var prototype)) - startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0); + { + if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2 + startingBalance = 0; + else + startingBalance = startingBalance - prototype.AntagAdvantage; + } - // creadth: we need to create uplink for the antag. - // PDA should be in place already - var pda = _uplink.FindUplinkTarget(traitor); - if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true)) - return false; - - // Give traitors their codewords and uplink code to keep in their character info menu - code = EnsureComp(pda.Value).Code; - - // If giveUplink is false the uplink code part is omitted - briefing = string.Format("{0}\n{1}", briefing, - Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); + // Choose and generate an Uplink, and return the uplink code if applicable + var uplinkParams = RequestUplink(traitor, startingBalance, briefing); + code = uplinkParams.Item1; + briefing = uplinkParams.Item2; } - _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification); + string[]? codewords = null; + if (component.GiveCodewords) + codewords = component.Codewords; + + if (component.GiveBriefing) + _antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification); component.TraitorMinds.Add(mindId); @@ -134,6 +143,32 @@ public sealed class TraitorRuleSystem : GameRuleSystem return true; } + private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing) + { + var pda = _uplink.FindUplinkTarget(traitor); + Note[]? code = null; + + var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true); + + if (pda is not null && uplinked) + { + // Codes are only generated if the uplink is a PDA + code = EnsureComp(pda.Value).Code; + + // If giveUplink is false the uplink code part is omitted + briefing = string.Format("{0}\n{1}", + briefing, + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); + return (code, briefing); + } + else if (pda is null && uplinked) + { + briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short"); + } + + return (null, briefing); + } + // TODO: AntagCodewordsComponent private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args) { @@ -141,13 +176,17 @@ public sealed class TraitorRuleSystem : GameRuleSystem } // TODO: figure out how to handle this? add priority to briefing event? - private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null) + private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null) { var sb = new StringBuilder(); sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown")))); - sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords)))); + if (codewords != null) + sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords)))); if (uplinkCode != null) sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#")))); + else + sb.AppendLine(Loc.GetString("traitor-role-uplink-implant")); + return sb.ToString(); } diff --git a/Content.Server/Traitor/Components/AutoTraitorComponent.cs b/Content.Server/Traitor/Components/AutoTraitorComponent.cs index ab4bee2f26..a4710afd8e 100644 --- a/Content.Server/Traitor/Components/AutoTraitorComponent.cs +++ b/Content.Server/Traitor/Components/AutoTraitorComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Traitor.Systems; +using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Components; @@ -9,14 +10,8 @@ namespace Content.Server.Traitor.Components; public sealed partial class AutoTraitorComponent : Component { /// - /// Whether to give the traitor an uplink or not. + /// The traitor profile to use /// - [DataField("giveUplink"), ViewVariables(VVAccess.ReadWrite)] - public bool GiveUplink = true; - - /// - /// Whether to give the traitor objectives or not. - /// - [DataField("giveObjectives"), ViewVariables(VVAccess.ReadWrite)] - public bool GiveObjectives = true; + [DataField] + public EntProtoId Profile = "Traitor"; } diff --git a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs index e9307effbc..d5a4db591a 100644 --- a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs +++ b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs @@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem { [Dependency] private readonly AntagSelectionSystem _antag = default!; - [ValidatePrototypeId] - private const string DefaultTraitorRule = "Traitor"; - public override void Initialize() { base.Initialize(); @@ -24,6 +21,6 @@ public sealed class AutoTraitorSystem : EntitySystem private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args) { - _antag.ForceMakeAntag(args.Mind.Comp.Session, DefaultTraitorRule); + _antag.ForceMakeAntag(args.Mind.Comp.Session, comp.Profile); } } diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index ae809dc4d7..4c0a990b14 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -1,97 +1,136 @@ using System.Linq; using Content.Server.Store.Systems; using Content.Server.StoreDiscount.Systems; +using Content.Shared.FixedPoint; using Content.Shared.Hands.EntitySystems; +using Content.Shared.Implants; using Content.Shared.Inventory; using Content.Shared.PDA; -using Content.Shared.FixedPoint; using Content.Shared.Store; using Content.Shared.Store.Components; +using Robust.Shared.Prototypes; -namespace Content.Server.Traitor.Uplink +namespace Content.Server.Traitor.Uplink; + +public sealed class UplinkSystem : EntitySystem { - public sealed class UplinkSystem : EntitySystem + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly StoreSystem _store = default!; + [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!; + + [ValidatePrototypeId] + public const string TelecrystalCurrencyPrototype = "Telecrystal"; + private const string FallbackUplinkImplant = "UplinkImplant"; + private const string FallbackUplinkCatalog = "UplinkUplinkImplanter"; + + /// + /// Adds an uplink to the target + /// + /// The person who is getting the uplink + /// The amount of currency on the uplink. If null, will just use the amount specified in the preset. + /// The entity that will actually have the uplink functionality. Defaults to the PDA if null. + /// Marker that enables discounts for uplink items. + /// Whether or not the uplink was added successfully + public bool AddUplink( + EntityUid user, + FixedPoint2 balance, + EntityUid? uplinkEntity = null, + bool giveDiscounts = false) { - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly StoreSystem _store = default!; + // Try to find target item if none passed - [ValidatePrototypeId] - public const string TelecrystalCurrencyPrototype = "Telecrystal"; + uplinkEntity ??= FindUplinkTarget(user); - /// - /// Adds an uplink to the target - /// - /// The person who is getting the uplink - /// The amount of currency on the uplink. If null, will just use the amount specified in the preset. - /// The entity that will actually have the uplink functionality. Defaults to the PDA if null. - /// Marker that enables discounts for uplink items. - /// Whether or not the uplink was added successfully - public bool AddUplink( - EntityUid user, - FixedPoint2? balance, - EntityUid? uplinkEntity = null, - bool giveDiscounts = false - ) + if (uplinkEntity == null) + return ImplantUplink(user, balance, giveDiscounts); + + EnsureComp(uplinkEntity.Value); + + SetUplink(user, uplinkEntity.Value, balance, giveDiscounts); + + // TODO add BUI. Currently can't be done outside of yaml -_- + // ^ What does this even mean? + + return true; + } + + /// + /// Configure TC for the uplink + /// + private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts) + { + var store = EnsureComp(uplink); + store.AccountOwner = user; + + store.Balance.Clear(); + _store.TryAddCurrency(new Dictionary { { TelecrystalCurrencyPrototype, balance } }, + uplink, + store); + + var uplinkInitializedEvent = new StoreInitializedEvent( + TargetUser: user, + Store: uplink, + UseDiscounts: giveDiscounts, + Listings: _store.GetAvailableListings(user, uplink, store) + .ToArray()); + RaiseLocalEvent(ref uplinkInitializedEvent); + } + + /// + /// Implant an uplink as a fallback measure if the traitor had no PDA + /// + private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts) + { + var implantProto = new string(FallbackUplinkImplant); + + if (!_proto.TryIndex(FallbackUplinkCatalog, out var catalog)) + return false; + + if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost)) + return false; + + if (balance < cost) // Can't use Math functions on FixedPoint2 + balance = 0; + else + balance = balance - cost; + + var implant = _subdermalImplant.AddImplant(user, implantProto); + + if (!HasComp(implant)) + return false; + + SetUplink(user, implant.Value, balance, giveDiscounts); + return true; + } + + /// + /// Finds the entity that can hold an uplink for a user. + /// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.) + /// + public EntityUid? FindUplinkTarget(EntityUid user) + { + // Try to find PDA in inventory + if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) { - // Try to find target item if none passed - uplinkEntity ??= FindUplinkTarget(user); - if (uplinkEntity == null) + while (containerSlotEnumerator.MoveNext(out var pdaUid)) { - return false; + if (!pdaUid.ContainedEntity.HasValue) + continue; + + if (HasComp(pdaUid.ContainedEntity.Value) || HasComp(pdaUid.ContainedEntity.Value)) + return pdaUid.ContainedEntity.Value; } - - EnsureComp(uplinkEntity.Value); - var store = EnsureComp(uplinkEntity.Value); - - store.AccountOwner = user; - store.Balance.Clear(); - if (balance != null) - { - store.Balance.Clear(); - _store.TryAddCurrency(new Dictionary { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store); - } - - var uplinkInitializedEvent = new StoreInitializedEvent( - TargetUser: user, - Store: uplinkEntity.Value, - UseDiscounts: giveDiscounts, - Listings: _store.GetAvailableListings(user, uplinkEntity.Value, store) - .ToArray() - ); - RaiseLocalEvent(ref uplinkInitializedEvent); - // TODO add BUI. Currently can't be done outside of yaml -_- - - return true; } - /// - /// Finds the entity that can hold an uplink for a user. - /// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.) - /// - public EntityUid? FindUplinkTarget(EntityUid user) + // Also check hands + foreach (var item in _handsSystem.EnumerateHeld(user)) { - // Try to find PDA in inventory - if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator)) - { - while (containerSlotEnumerator.MoveNext(out var pdaUid)) - { - if (!pdaUid.ContainedEntity.HasValue) - continue; - - if (HasComp(pdaUid.ContainedEntity.Value) || HasComp(pdaUid.ContainedEntity.Value)) - return pdaUid.ContainedEntity.Value; - } - } - - // Also check hands - foreach (var item in _handsSystem.EnumerateHeld(user)) - { - if (HasComp(item) || HasComp(item)) - return item; - } - - return null; + if (HasComp(item) || HasComp(item)) + return item; } + + return null; } } diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 830d2270aa..94203de615 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -94,22 +94,38 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem /// public void AddImplants(EntityUid uid, IEnumerable implants) { - var coords = Transform(uid).Coordinates; foreach (var id in implants) { - var ent = Spawn(id, coords); - if (TryComp(ent, out var implant)) - { - ForceImplant(uid, ent, implant); - } - else - { - Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}"); - Del(ent); - } + AddImplant(uid, id); } } + /// + /// Adds a single implant to a person, and returns the implant. + /// Logs any implant ids that don't have . + /// + /// + /// The implant, if it was successfully created. Otherwise, null. + /// > + public EntityUid? AddImplant(EntityUid uid, String implantId) + { + var coords = Transform(uid).Coordinates; + var ent = Spawn(implantId, coords); + + if (TryComp(ent, out var implant)) + { + ForceImplant(uid, ent, implant); + } + else + { + Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}"); + Del(ent); + return null; + } + + return ent; + } + /// /// Forces an implant into a person /// Good for on spawn related code or admin additions diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl index fd3e6b82aa..cf2f2b1130 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl @@ -26,7 +26,7 @@ traitor-death-match-end-round-description-entry = {$originalName}'s PDA, with {$ traitor-role-greeting = You are an agent sent by {$corporation} on behalf of [color = darkred]The Syndicate.[/color] Your objectives and codewords are listed in the character menu. - Use the uplink loaded into your PDA to buy the tools you'll need for this mission. + Use your uplink to buy the tools you'll need for this mission. Death to Nanotrasen! traitor-role-codewords = The codewords are: [color = lightgray] @@ -36,9 +36,13 @@ traitor-role-codewords = traitor-role-uplink-code = Set your ringtone to the notes [color = lightgray]{$code}[/color] to lock or unlock your uplink. Remember to lock it after, or the stations crew will easily open it too! +traitor-role-uplink-implant = + Your uplink implant has been activated, access it from your hotbar. + The uplink is secure unless someone removes it from your body. # don't need all the flavour text for character menu traitor-role-codewords-short = The codewords are: {$codewords}. traitor-role-uplink-code-short = Your uplink code is {$code}. Set it as your PDA ringtone to access uplink. +traitor-role-uplink-implant-short = Your uplink was implanted. Access it from your hotbar. diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 6ae711a39d..e2dd9ac3f3 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1407,8 +1407,7 @@ components: # make the player a traitor once its taken - type: AutoTraitor - giveUplink: false - giveObjectives: false + profile: TraitorReinforcement - type: entity id: MobMonkeySyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink @@ -1569,8 +1568,7 @@ components: # make the player a traitor once its taken - type: AutoTraitor - giveUplink: false - giveObjectives: false + profile: TraitorReinforcement - type: entity id: MobKoboldSyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 4a7a48a0d5..7fc8bf7d6c 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -31,8 +31,7 @@ components: # make the player a traitor once its taken - type: AutoTraitor - giveUplink: false - giveObjectives: false + profile: TraitorReinforcement - type: entity parent: MobHumanSyndicateAgent diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 46d4366f68..cec5c9ee09 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -189,6 +189,15 @@ mindRoles: - MindRoleTraitor +- type: entity + id: TraitorReinforcement + parent: Traitor + components: + - type: TraitorRule + giveUplink: false + giveCodewords: false # It would actually give them a different set of codewords than the regular traitors, anyway + giveBriefing: false + - type: entity id: Revolutionary parent: BaseGameRule diff --git a/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml b/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml index 3e48200e88..1c7e74f444 100644 --- a/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml +++ b/Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml @@ -18,12 +18,17 @@ By pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color], you'll see your personal uplink code. [bold]Setting your PDA's ringtone as this code will open the uplink.[/bold] Pressing [color=yellow][bold][keybind="OpenCharacterMenu"][/bold][/color] also lets you view your objectives and the codewords. + If you do not have a PDA when you are activated, an [color=cyan]uplink implant[/color] is provided [bold]for the full [color=red]TC[/color] price of the implant.[/bold] + It can be accessed from your hotbar. + - [bold]Make sure to close your uplink to prevent anyone else from seeing it.[/bold] You don't want [color=#cb0000]Security[/color] to get their hands on this premium selection of contraband! + [bold]Make sure to close your PDA uplink to prevent anyone else from seeing it.[/bold] You don't want [color=#cb0000]Security[/color] to get their hands on this premium selection of contraband! + + Implanted uplinks are not normally accessible to other people, so they do not have any security measures. They can, however, be removed from you with an empty implanter.