Traitor activation fix for missing PDA (#30359)

* Implant the uplink if no PDA is found

* comments

* tidy up loose ends

* Whoops usually I start with the namespace, how did I forget it, shame shame

* Consistent data type for starting TC balance, misc changes

* Implant briefing, guidebook

* Update AutoTraitor, add uplink, codeword and briefing parameters to TraitorRuleComponent,  no pda for reinforcements

* engine 5c0ce43

* pass pda to AddUplink

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* nicer string handling

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* case typo 1

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* case typo 2

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* case typo 3

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* minor layout changes

* removed redundant implant check

* minor cleanup

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
Errant
2024-10-18 14:55:43 +02:00
committed by GitHub
parent 338781c243
commit 23e4f81b30
11 changed files with 247 additions and 127 deletions

View File

@@ -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<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
/// <summary>
/// Give this traitor an Uplink on spawn.
/// </summary>
[DataField]
public bool GiveUplink = true;
/// <summary>
/// Give this traitor the codewords.
/// </summary>
[DataField]
public bool GiveCodewords = true;
/// <summary>
/// Give this traitor a briefing in chat.
/// </summary>
[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.
/// </summary>
[DataField]
public int StartingBalance = 20;
public FixedPoint2 StartingBalance = 20;
}

View File

@@ -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<TraitorRuleComponent>
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);
// 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<RingerUplinkComponent>(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", "#"))));
{
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
startingBalance = 0;
else
startingBalance = startingBalance - prototype.AntagAdvantage;
}
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
// Choose and generate an Uplink, and return the uplink code if applicable
var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
code = uplinkParams.Item1;
briefing = uplinkParams.Item2;
}
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<TraitorRuleComponent>
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<RingerUplinkComponent>(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<TraitorRuleComponent>
}
// 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"))));
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();
}

View File

@@ -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
{
/// <summary>
/// Whether to give the traitor an uplink or not.
/// The traitor profile to use
/// </summary>
[DataField("giveUplink"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveUplink = true;
/// <summary>
/// Whether to give the traitor objectives or not.
/// </summary>
[DataField("giveObjectives"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveObjectives = true;
[DataField]
public EntProtoId Profile = "Traitor";
}

View File

@@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[ValidatePrototypeId<EntityPrototype>]
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<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, comp.Profile);
}
}

View File

@@ -1,23 +1,29 @@
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
{
[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<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
private const string FallbackUplinkImplant = "UplinkImplant";
private const string FallbackUplinkCatalog = "UplinkUplinkImplanter";
/// <summary>
/// Adds an uplink to the target
@@ -29,39 +35,73 @@ namespace Content.Server.Traitor.Uplink
/// <returns>Whether or not the uplink was added successfully</returns>
public bool AddUplink(
EntityUid user,
FixedPoint2? balance,
FixedPoint2 balance,
EntityUid? uplinkEntity = null,
bool giveDiscounts = false
)
bool giveDiscounts = false)
{
// Try to find target item if none passed
uplinkEntity ??= FindUplinkTarget(user);
if (uplinkEntity == null)
{
return false;
}
return ImplantUplink(user, balance, giveDiscounts);
EnsureComp<UplinkComponent>(uplinkEntity.Value);
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
store.AccountOwner = user;
store.Balance.Clear();
if (balance != null)
{
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store);
SetUplink(user, uplinkEntity.Value, balance, giveDiscounts);
// TODO add BUI. Currently can't be done outside of yaml -_-
// ^ What does this even mean?
return true;
}
/// <summary>
/// Configure TC for the uplink
/// </summary>
private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
{
var store = EnsureComp<StoreComponent>(uplink);
store.AccountOwner = user;
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
uplink,
store);
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplinkEntity.Value,
Store: uplink,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplinkEntity.Value, store)
.ToArray()
);
Listings: _store.GetAvailableListings(user, uplink, store)
.ToArray());
RaiseLocalEvent(ref uplinkInitializedEvent);
// TODO add BUI. Currently can't be done outside of yaml -_-
}
/// <summary>
/// Implant an uplink as a fallback measure if the traitor had no PDA
/// </summary>
private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
{
var implantProto = new string(FallbackUplinkImplant);
if (!_proto.TryIndex<ListingPrototype>(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<StoreComponent>(implant))
return false;
SetUplink(user, implant.Value, balance, giveDiscounts);
return true;
}
@@ -94,4 +134,3 @@ namespace Content.Server.Traitor.Uplink
return null;
}
}
}

View File

@@ -94,20 +94,36 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// </summary>
public void AddImplants(EntityUid uid, IEnumerable<String> implants)
{
var coords = Transform(uid).Coordinates;
foreach (var id in implants)
{
var ent = Spawn(id, coords);
AddImplant(uid, id);
}
}
/// <summary>
/// Adds a single implant to a person, and returns the implant.
/// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>.
/// </summary>
/// <returns>
/// The implant, if it was successfully created. Otherwise, null.
/// </returns>>
public EntityUid? AddImplant(EntityUid uid, String implantId)
{
var coords = Transform(uid).Coordinates;
var ent = Spawn(implantId, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}");
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
return null;
}
}
return ent;
}
/// <summary>

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.
<Box>
<GuideEntityEmbed Entity="PassengerPDA" Caption="PDA"/>
<GuideEntityEmbed Entity="Telecrystal" Caption="Telecrystals"/>
<GuideEntityEmbed Entity="BaseUplinkRadio" Caption="Uplink Implant"/>
</Box>
[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.
<Box>
<GuideEntityEmbed Entity="WeaponPistolCobra" Caption="Weaponry"/>