Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Content.Server/_ES/Masks/Masquerades/ESMasqueradeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Content.Shared._ES.Core.Timer;
using Content.Shared._ES.Masks;
using Content.Shared._ES.Masks.Components;
using Content.Shared._ES.Masks.Masquerades;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Random.Helpers;
Expand Down Expand Up @@ -317,4 +318,17 @@ public HashSet<ProtoId<ESTroupePrototype>> GetTroupesFromMasquerade(ESMasquerade

return troupes;
}

public bool TryGetMasqueradeData([NotNullWhen(true)] out MasqueradeRoleSet? set)
{
set = null;
var rule = EntityQuery<ESMasqueradeRuleComponent>().SingleOrDefault();

if (rule?.Masquerade is null)
return false;

set = rule.Masquerade.Masquerade;

return true;
}
}
13 changes: 13 additions & 0 deletions Content.Server/_ES/Masks/Superfan/ESSuperfanComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Content.Server._ES.Masks.Superfan;

/// <summary>
/// This is used for the syndie superfan and their conversion on traitor loss.
/// </summary>
/// <remarks>
/// Deliberately not generalized, as writing general code here is a bunch of extra tests and work that may never
/// be used. I like it when my language can do the typechecking for me instead of needing to test for it.
///
/// If we ever need equivalents for like, nihlings, this should not be hard to rewrite.
/// </remarks>
[RegisterComponent]
public sealed partial class ESSuperfanComponent : Component;
59 changes: 59 additions & 0 deletions Content.Server/_ES/Masks/Superfan/ESSuperfanSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Linq;
using Content.Server._ES.Masks.Masquerades;
using Content.Server.KillTracking;
using Content.Server.Mind;
using Content.Shared._ES.Masks;
using Content.Shared.Mind;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;

namespace Content.Server._ES.Masks.Superfan;

/// <seealso cref="ESSuperfanComponent"/>
public sealed class ESSuperfanSystem : EntitySystem
{
[Dependency] private readonly ESMaskSystem _mask = default!;
[Dependency] private readonly ESMasqueradeSystem _masquerade = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;

private static readonly ProtoId<ESTroupePrototype> TraitorsTroupe = "ESTraitor";

/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
}

private void OnKillReported(ref KillReportedEvent ev)
{
// TODO: This feels fishy. I'll leave the kill reporting rewrite nerds to
// figure out having a kill report for entire troupes down the line.
foreach (var member in _mask.GetTroupeMembers(TraitorsTroupe))
{
if (!_mind.IsCharacterDeadIc(Comp<MindComponent>(member)))
return; // Well the troupe ain't dead.
}

if (!_masquerade.TryGetMasqueradeData(out var set))
return; // Well, no masquerade means no conversion target.

if (set.SuperfanTarget is not { } entry)
{
Log.Error("Superfan has no target despite being in a masquerade!");
return;
}

var fanQuery = EntityQueryEnumerator<ESSuperfanComponent, MindComponent>();

while (fanQuery.MoveNext(out var ent, out var fan, out var mind))
{
if (_mind.IsCharacterDeadIc(mind))
continue; // Don't assign the dead to tot masks.

_mask.ChangeMask((ent, mind), entry.PickMasks(_random, _proto).Single());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;

namespace Content.Shared._ES.Masks.Components;

/// <summary>
/// This is used for blacklisting certain masks from targeted objectives.
/// </summary>
[RegisterComponent]
public sealed partial class ESTargetMaskBlacklistComponent : Component
{
/// <summary>
/// A blacklist of masks that cannot be targeted.
/// </summary>
[DataField]
public HashSet<ProtoId<ESMaskPrototype>> MaskBlacklist;
}
35 changes: 3 additions & 32 deletions Content.Shared/_ES/Masks/ESMasqueradePrototype.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared._ES.Masks.Masquerades;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;

namespace Content.Shared._ES.Masks;
Expand Down Expand Up @@ -53,7 +51,7 @@ public string LocDescription(ILocalizationManager loc)
/// <summary>
/// Setter for serialization because we're manually inlining some fields from MasqueradeKind.
/// </summary>
/// <seealso cref="MasqueradeKind.MinPlayers"/>
/// <seealso cref="MasqueradeRoleSet.MinPlayers"/>
[DataField(priority: 0, required: true, readOnly: true)]
private int MinPlayers
{
Expand All @@ -64,7 +62,7 @@ private int MinPlayers
/// <summary>
/// Setter for serialization because we're manually inlining some fields from MasqueradeKind.
/// </summary>
/// <seealso cref="MasqueradeKind.MaxPlayers"/>
/// <seealso cref="MasqueradeRoleSet.MaxPlayers"/>
[DataField(priority: 0, readOnly: true)]
private int? MaxPlayers
{
Expand Down Expand Up @@ -115,37 +113,10 @@ private int? MaxPlayers
public IReadOnlyList<EntProtoId> GameRules { get; private set; } = [];

[DataField(required: true, priority: 1)]
public MasqueradeKind Masquerade { get; private set; } = default!;
public MasqueradeRoleSet Masquerade { get; private set; } = default!;

void ISerializationHooks.AfterDeserialization()
{
Masquerade.Init();
}
}

/// <summary>
/// Base class for any masquerades. To introduce new ones, make sure you update the custom serializer too.
/// </summary>
[DataDefinition]
public abstract partial class MasqueradeKind
{
public virtual int MinPlayers { get; set; }

public virtual int? MaxPlayers { get; set; }

/// <summary>
/// The default mask used for post-start latejoiners.
/// </summary>
[DataField(readOnly: true, required: true)]
public MasqueradeEntry DefaultMask { get; set; } = default!;

internal virtual void Init() {}

/// <summary>
/// Attempts to get a mask list for the current player count.
/// </summary>
/// <remarks>
/// While the masks are random, the order in the output list is not.
/// </remarks>
public abstract bool TryGetMasks(int playerCount, IRobustRandom rng, IPrototypeManager proto, [NotNullWhen(true)] out List<ProtoId<ESMaskPrototype>>? masks);
};
21 changes: 21 additions & 0 deletions Content.Shared/_ES/Masks/ESTargetMaskSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Content.Shared._ES.Masks.Components;
using Content.Shared._ES.Objectives.Target.Components;

namespace Content.Shared._ES.Masks;

public sealed class ESTargetMaskSystem : EntitySystem
{
[Dependency] private readonly ESSharedMaskSystem _mask = default!;

/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ESTargetMaskBlacklistComponent, ESValidateObjectiveTargetCandidates>(Handler);
}

private void Handler(Entity<ESTargetMaskBlacklistComponent> ent, ref ESValidateObjectiveTargetCandidates args)
{
if (_mask.GetMaskOrNull(args.Candidate) is {} mask && ent.Comp.MaskBlacklist.Contains(mask))
args.Invalidate();
}
}
13 changes: 13 additions & 0 deletions Content.Shared/_ES/Masks/MaskCycle/ESActionChangeMaskEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;

namespace Content.Shared._ES.Masks.MaskCycle;

/// <summary>
/// An action event for changing to another mask.
/// </summary>
public sealed partial class ESActionChangeMaskEvent : InstantActionEvent
{
[DataField(required: true)]
public ProtoId<ESMaskPrototype> Mask;
}
32 changes: 32 additions & 0 deletions Content.Shared/_ES/Masks/MaskCycle/ESActionChangeMaskSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Content.Shared.Mind;

namespace Content.Shared._ES.Masks.MaskCycle;

/// <summary>
/// This handles the mask change action.
/// </summary>
public sealed class ESActionChangeMaskSystem : EntitySystem
{
[Dependency] private readonly ESSharedMaskSystem _mask = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;

/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ESActionChangeMaskEvent>(Handler);
}

private void Handler(ESActionChangeMaskEvent args)
{
if (args.Handled)
return;

if (!_mind.TryGetMind(args.Performer, out var mind, out var mindComp))
return;

_mask.RemoveMask((mind, mindComp));
_mask.ApplyMask((mind, mindComp), args.Mask);

args.Handled = true;
}
}
23 changes: 20 additions & 3 deletions Content.Shared/_ES/Masks/Masquerades/MasqueradeRoleSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,21 @@ namespace Content.Shared._ES.Masks.Masquerades;
/// A full set of roles for a masquerade, at different player counts.
/// </summary>
[DataDefinition]
public sealed partial class MasqueradeRoleSet : MasqueradeKind
public sealed partial class MasqueradeRoleSet
{
public int MinPlayers { get; set; }

public int? MaxPlayers { get; set; }

/// <summary>
/// The default mask used for post-start latejoiners.
/// </summary>
[DataField(readOnly: true, required: true)]
public MasqueradeEntry DefaultMask { get; set; } = default!;

[DataField(readOnly: true)]
public MasqueradeEntry? SuperfanTarget { get; set; } = default!;

/// <summary>
/// All the roles in this masquerade at given population levels, baked into something easy to use by the game.
/// </summary>
Expand All @@ -27,7 +40,7 @@ public sealed partial class MasqueradeRoleSet : MasqueradeKind
/// <remarks>
/// While the masks are random, the order in the output list is not.
/// </remarks>
public override bool TryGetMasks(int playerCount, IRobustRandom rng, IPrototypeManager proto, [NotNullWhen(true)] out List<ProtoId<ESMaskPrototype>>? masks)
public bool TryGetMasks(int playerCount, IRobustRandom rng, IPrototypeManager proto, [NotNullWhen(true)] out List<ProtoId<ESMaskPrototype>>? masks)
{
if (!TryGetEntriesForPop(playerCount, out var entries))
{
Expand Down Expand Up @@ -145,7 +158,7 @@ private sealed class MqKeySet(ProtoId<ESMaskSetPrototype> maskSet) : MqKey
public ProtoId<ESMaskSetPrototype> MaskSet { get; } = maskSet;
}

internal override void Init()
internal void Init()
{
// Validation wise, this would have better UX if all this happened at parse time so ValidationNodes could be made.
// But doing all of this at parse time would have Consequences I don't want to deal with and would significantly
Expand All @@ -158,6 +171,10 @@ internal override void Init()
DebugTools.Assert(minPlayers > 0, "You can't have any roles without players, minPlayers must be at least 1.");
DebugTools.Assert(minPlayers == MinPlayers, $"Minimum players should match the first specified set of entries (expected {MinPlayers}, found {minPlayers})");

DebugTools.AssertEqual(DefaultMask.Count, 1);
if (SuperfanTarget is not null)
DebugTools.AssertEqual(SuperfanTarget.Count, 1);

var lastAt = minPlayers;

foreach (var popCount in _roles.Keys.Order())
Expand Down
4 changes: 4 additions & 0 deletions Resources/Locale/en-US/_ES/masks/masks.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ es-mask-subverter-desc = As a Subverter, you have received two brain-altering ch
es-mask-demolitionist-name = Demolitionist
es-mask-demolitionist-desc = As a Demolitionist, you have been trained by the Syndicate for both controlled and uncontrolled "demolitions"--including in the use of explosive implants, should things turn out that way.

# Oddballs
es-mask-syndie-superfan-name = Syndie Superfan
es-mask-syndie-superfan-desc = As the Syndicate's biggest fan, you're a member of crew and win with them, unless the entire traitors team dies, upon which you switch sides and gain a new mask.

# Meta
es-objective-issuer-mask = Mask

Expand Down
9 changes: 9 additions & 0 deletions Resources/Prototypes/_ES/Masks/masks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,12 @@
- type: ESMaskCacheSpawner
cacheProto:
id: ESCrateCacheTraitorDemolitionist

# Neutral/oddball masks
- type: esMask
id: ESSyndieSuperfan
name: es-mask-syndie-superfan-name
troupe: ESCrew
description: es-mask-syndie-superfan-desc
mindComponents:
- type: ESSuperfan
3 changes: 2 additions & 1 deletion Resources/Prototypes/_ES/Masquerades/random.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
weight: 1
minPlayers: 1
gameRules: []
masquerade: !type:MasqueradeRoleSet
masquerade:
defaultMask: "#AllCrew" # Everyone besides the traitors gets random crew masks.
superfanTarget: "#AllTraitors"
roles:
1: [] # Greenshift.
4: ["#AllTraitors"]
Expand Down
7 changes: 4 additions & 3 deletions Resources/Prototypes/_ES/Masquerades/redcarpet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
weight: 1
minPlayers: 6
gameRules: []
masquerade: !type:MasqueradeRoleSet
masquerade:
defaultMask: "ESCrewmember/ESVIP"
superfanTarget: "ESSubverter"
roles:
6: ["ESMarauder", "ESInfiltrator", "ESInsider", "ESVIP", "ESVandal", "ESMercenary"] # 0 generic crew, 2 traitors
7: ["ESMartyr"]
8: ["ESVIP"]
9: ["ESVeteran"]
10: ["ESPhantom"]
10: ["ESSyndieSuperfan"]
11: ["ESVIP"]
13: ["ESAssassin"] # 0 generic crew, 3 traitors.
14: ["ESInsider"]
16: ["ESAvenger"] # 1 generic crew, 3 traitors.
17: ["#Reaped"]
17: ["#Softsafes"]
18: ["ESArmsDealer"]
19: ["ESMarauder"] # 1 generic crew, 4 traitors.
20: ["ESVIP"]
Expand Down
2 changes: 1 addition & 1 deletion Resources/Prototypes/_ES/Masquerades/showdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
weight: 1 # Not that common
minPlayers: 8
gameRules: []
masquerade: !type:MasqueradeRoleSet
masquerade:
defaultMask: "ESCrewmember"
roles:
8: ["ESMarauder(2)", "ESVandal", "ESMercenary", "ESVIP/ESFruitVendor", "ESArmsDealer", "ESInsider", "#Softsafes"] # 0 generic crew, 2 traitors
Expand Down
7 changes: 4 additions & 3 deletions Resources/Prototypes/_ES/Masquerades/traitors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
weight: 10
minPlayers: 6
gameRules: []
masquerade: !type:MasqueradeRoleSet
masquerade:
defaultMask: "ESCrewmember"
superfanTarget: "ESSubverter"
roles:
6: ["ESMarauder", "ESInfiltrator", "ESInsider", "#Softsafes", "ESVandal", "ESMercenary"] # 0 generic crew, 2 traitors
7: ["ESMartyr"]
8: ["#Softsafes"]
9: ["ESVeteran"]
10: ["ESPhantom"]
10: ["ESSyndieSuperfan"]
11: ["ESAvenger"]
13: ["ESAssassin"] # 0 generic crew, 3 traitors.
14: ["ESInsider"]
16: ["#Firmsafes"] # 1 generic crew, 3 traitors.
17: ["#Reaped"]
17: ["#Softsafes"]
18: ["ESArmsDealer"]
19: ["ESMarauder"] # 1 generic crew, 4 traitors.
20: ["#Softsafes"]
Expand Down
Loading
Loading