Skip to content

Commit

Permalink
Cursed Mask (#29659)
Browse files Browse the repository at this point in the history
* Cursed Mask

* extra expressions

* block ingestion

* mind returning

* okay fix the removal shit
  • Loading branch information
EmoGarbage404 authored Aug 10, 2024
1 parent 53058df commit fc1446e
Show file tree
Hide file tree
Showing 20 changed files with 412 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Content.Client/Clothing/Systems/CursedMaskSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Content.Shared.Clothing;

namespace Content.Client.Clothing.Systems;

/// <inheritdoc/>
public sealed class CursedMaskSystem : SharedCursedMaskSystem;
92 changes: 92 additions & 0 deletions Content.Server/Clothing/Systems/CursedMaskSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Content.Server.Administration.Logs;
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Server.NPC;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.Popups;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Database;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Players;
using Content.Shared.Popups;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;

namespace Content.Server.Clothing.Systems;

/// <inheritdoc/>
public sealed class CursedMaskSystem : SharedCursedMaskSystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly HTNSystem _htn = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly PopupSystem _popup = default!;

// We can't store this info on the component easily
private static readonly ProtoId<HTNCompoundPrototype> TakeoverRootTask = "SimpleHostileCompound";

protected override void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer)
{
if (ent.Comp.CurrentState != CursedMaskExpression.Anger)
return;

if (TryComp<ActorComponent>(wearer, out var actor) && actor.PlayerSession.GetMind() is { } mind)
{
var session = actor.PlayerSession;
if (!_ticker.OnGhostAttempt(mind, false))
return;

ent.Comp.StolenMind = mind;

_popup.PopupEntity(Loc.GetString("cursed-mask-takeover-popup"), wearer, session, PopupType.LargeCaution);
_adminLog.Add(LogType.Action,
LogImpact.Extreme,
$"{ToPrettyString(wearer):player} had their body taken over and turned into an enemy through the cursed mask {ToPrettyString(ent):entity}");
}

var npcFaction = EnsureComp<NpcFactionMemberComponent>(wearer);
ent.Comp.OldFactions = npcFaction.Factions;
_npcFaction.ClearFactions((wearer, npcFaction), false);
_npcFaction.AddFaction((wearer, npcFaction), ent.Comp.CursedMaskFaction);

ent.Comp.HasNpc = !EnsureComp<HTNComponent>(wearer, out var htn);
htn.RootTask = new HTNCompoundTask { Task = TakeoverRootTask };
htn.Blackboard.SetValue(NPCBlackboard.Owner, wearer);
_npc.WakeNPC(wearer, htn);
_htn.Replan(htn);
}

protected override void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args)
{
// If we are taking off the cursed mask
if (ent.Comp.CurrentState == CursedMaskExpression.Anger)
{
if (ent.Comp.HasNpc)
RemComp<HTNComponent>(args.Wearer);

var npcFaction = EnsureComp<NpcFactionMemberComponent>(args.Wearer);
_npcFaction.RemoveFaction((args.Wearer, npcFaction), ent.Comp.CursedMaskFaction, false);
_npcFaction.AddFactions((args.Wearer, npcFaction), ent.Comp.OldFactions);

ent.Comp.HasNpc = false;
ent.Comp.OldFactions.Clear();

if (Exists(ent.Comp.StolenMind))
{
_mind.TransferTo(ent.Comp.StolenMind.Value, args.Wearer);
_adminLog.Add(LogType.Action,
LogImpact.Extreme,
$"{ToPrettyString(args.Wearer):player} was restored to their body after the removal of {ToPrettyString(ent):entity}.");
ent.Comp.StolenMind = null;
}
}

RandomizeCursedMask(ent, args.Wearer);
}
}
65 changes: 65 additions & 0 deletions Content.Shared/Clothing/Components/CursedMaskComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Content.Shared.Damage;
using Content.Shared.NPC.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;

namespace Content.Shared.Clothing.Components;

/// <summary>
/// This is used for a mask that takes over the host when worn.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCursedMaskSystem))]
public sealed partial class CursedMaskComponent : Component
{
/// <summary>
/// The current expression shown. Used to determine which effect is applied.
/// </summary>
[DataField]
public CursedMaskExpression CurrentState = CursedMaskExpression.Neutral;

/// <summary>
/// Speed modifier applied when the "Joy" expression is present.
/// </summary>
[DataField]
public float JoySpeedModifier = 1.15f;

/// <summary>
/// Damage modifier applied when the "Despair" expression is present.
/// </summary>
[DataField]
public DamageModifierSet DespairDamageModifier = new();

/// <summary>
/// Whether or not the mask is currently attached to an NPC.
/// </summary>
[DataField]
public bool HasNpc;

/// <summary>
/// The mind that was booted from the wearer when the mask took over.
/// </summary>
[DataField]
public EntityUid? StolenMind;

[DataField]
public ProtoId<NpcFactionPrototype> CursedMaskFaction = "SimpleHostile";

[DataField]
public HashSet<ProtoId<NpcFactionPrototype>> OldFactions = new();
}

[Serializable, NetSerializable]
public enum CursedMaskVisuals : byte
{
State
}

[Serializable, NetSerializable]
public enum CursedMaskExpression : byte
{
Neutral,
Joy,
Despair,
Anger
}
73 changes: 73 additions & 0 deletions Content.Shared/Clothing/SharedCursedMaskSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
using Robust.Shared.Random;
using Robust.Shared.Timing;

namespace Content.Shared.Clothing;

/// <summary>
/// This handles <see cref="CursedMaskComponent"/>
/// </summary>
public abstract class SharedCursedMaskSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;

/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<CursedMaskComponent, ClothingGotEquippedEvent>(OnClothingEquip);
SubscribeLocalEvent<CursedMaskComponent, ClothingGotUnequippedEvent>(OnClothingUnequip);
SubscribeLocalEvent<CursedMaskComponent, ExaminedEvent>(OnExamine);

SubscribeLocalEvent<CursedMaskComponent, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent>>(OnMovementSpeedModifier);
SubscribeLocalEvent<CursedMaskComponent, InventoryRelayedEvent<DamageModifyEvent>>(OnModifyDamage);
}

private void OnClothingEquip(Entity<CursedMaskComponent> ent, ref ClothingGotEquippedEvent args)
{
RandomizeCursedMask(ent, args.Wearer);
TryTakeover(ent, args.Wearer);
}

protected virtual void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args)
{
RandomizeCursedMask(ent, args.Wearer);
}

private void OnExamine(Entity<CursedMaskComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString($"cursed-mask-examine-{ent.Comp.CurrentState.ToString()}"));
}

private void OnMovementSpeedModifier(Entity<CursedMaskComponent> ent, ref InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args)
{
if (ent.Comp.CurrentState == CursedMaskExpression.Joy)
args.Args.ModifySpeed(ent.Comp.JoySpeedModifier);
}

private void OnModifyDamage(Entity<CursedMaskComponent> ent, ref InventoryRelayedEvent<DamageModifyEvent> args)
{
if (ent.Comp.CurrentState == CursedMaskExpression.Despair)
args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, ent.Comp.DespairDamageModifier);
}

protected void RandomizeCursedMask(Entity<CursedMaskComponent> ent, EntityUid wearer)
{
var random = new System.Random((int) _timing.CurTick.Value);
ent.Comp.CurrentState = random.Pick(Enum.GetValues<CursedMaskExpression>());
_appearance.SetData(ent, CursedMaskVisuals.State, ent.Comp.CurrentState);
_movementSpeedModifier.RefreshMovementSpeedModifiers(wearer);
}

protected virtual void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer)
{

}
}
6 changes: 6 additions & 0 deletions Content.Shared/Inventory/Events/UnequipAttemptEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs
/// </summary>
public readonly EntityUid Equipment;

/// <summary>
/// The slotFlags of the slot this item is being removed from.
/// </summary>
public readonly SlotFlags SlotFlags;

/// <summary>
/// The slot the entity is being unequipped from.
/// </summary>
Expand All @@ -33,6 +38,7 @@ public UnequipAttemptEventBase(EntityUid unequipee, EntityUid unEquipTarget, Ent
UnEquipTarget = unEquipTarget;
Equipment = equipment;
Unequipee = unequipee;
SlotFlags = slotDefinition.SlotFlags;
Slot = slotDefinition.Name;
}
}
Expand Down
16 changes: 16 additions & 0 deletions Content.Shared/Inventory/SelfEquipOnlyComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Robust.Shared.GameStates;

namespace Content.Shared.Inventory;

/// <summary>
/// This is used for an item that can only be equipped/unequipped by the user.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SelfEquipOnlySystem))]
public sealed partial class SelfEquipOnlyComponent : Component
{
/// <summary>
/// Whether or not the self-equip only condition requires the person to be conscious.
/// </summary>
[DataField]
public bool UnequipRequireConscious = true;
}
45 changes: 45 additions & 0 deletions Content.Shared/Inventory/SelfEquipOnlySystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Clothing.Components;
using Content.Shared.Inventory.Events;

namespace Content.Shared.Inventory;

public sealed class SelfEquipOnlySystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;

/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<SelfEquipOnlyComponent, BeingEquippedAttemptEvent>(OnBeingEquipped);
SubscribeLocalEvent<SelfEquipOnlyComponent, BeingUnequippedAttemptEvent>(OnBeingUnequipped);
}

private void OnBeingEquipped(Entity<SelfEquipOnlyComponent> ent, ref BeingEquippedAttemptEvent args)
{
if (args.Cancelled)
return;

if (TryComp<ClothingComponent>(ent, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE)
return;

if (args.Equipee != args.EquipTarget)
args.Cancel();
}

private void OnBeingUnequipped(Entity<SelfEquipOnlyComponent> ent, ref BeingUnequippedAttemptEvent args)
{
if (args.Cancelled)
return;

if (args.Unequipee == args.UnEquipTarget)
return;

if (TryComp<ClothingComponent>(ent, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE)
return;

if (ent.Comp.UnequipRequireConscious && !_actionBlocker.CanConsciouslyPerformAction(args.UnEquipTarget))
return;
args.Cancel();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,10 @@ public void ModifySpeed(float walk, float sprint)
WalkSpeedModifier *= walk;
SprintSpeedModifier *= sprint;
}

public void ModifySpeed(float mod)
{
ModifySpeed(mod, mod);
}
}
}
22 changes: 22 additions & 0 deletions Content.Shared/NPC/Systems/NpcFactionSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,28 @@ public void AddFaction(Entity<NpcFactionMemberComponent?> ent, string faction, b
RefreshFactions((ent, ent.Comp));
}

/// <summary>
/// Adds this entity to the particular faction.
/// </summary>
public void AddFactions(Entity<NpcFactionMemberComponent?> ent, HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
{
ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);

foreach (var faction in factions)
{
if (!_proto.HasIndex(faction))
{
Log.Error($"Unable to find faction {faction}");
continue;
}

ent.Comp.Factions.Add(faction);
}

if (dirty)
RefreshFactions((ent, ent.Comp));
}

/// <summary>
/// Removes this entity from the particular faction.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions Resources/Locale/en-US/clothing/components/cursed-mask.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cursed-mask-examine-Neutral = It depicts an entirely unremarkable visage.
cursed-mask-examine-Joy = It depicts a face basking in joy.
cursed-mask-examine-Despair = It depicts a face wraught with despair.
cursed-mask-examine-Anger = It depicts a furious expression locked in rage.
cursed-mask-takeover-popup = The mask seizes control over your body!
Loading

0 comments on commit fc1446e

Please sign in to comment.