Cursed Mask (#29659)
* Cursed Mask * extra expressions * block ingestion * mind returning * okay fix the removal shit
6
Content.Client/Clothing/Systems/CursedMaskSystem.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Clothing;
|
||||
|
||||
namespace Content.Client.Clothing.Systems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class CursedMaskSystem : SharedCursedMaskSystem;
|
||||
92
Content.Server/Clothing/Systems/CursedMaskSystem.cs
Normal file
@@ -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
Content.Shared/Clothing/Components/CursedMaskComponent.cs
Normal file
@@ -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
Content.Shared/Clothing/SharedCursedMaskSystem.cs
Normal file
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -33,6 +38,7 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs
|
||||
UnEquipTarget = unEquipTarget;
|
||||
Equipment = equipment;
|
||||
Unequipee = unequipee;
|
||||
SlotFlags = slotDefinition.SlotFlags;
|
||||
Slot = slotDefinition.Name;
|
||||
}
|
||||
}
|
||||
|
||||
16
Content.Shared/Inventory/SelfEquipOnlyComponent.cs
Normal file
@@ -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
Content.Shared/Inventory/SelfEquipOnlySystem.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -69,5 +69,10 @@ namespace Content.Shared.Movement.Systems
|
||||
WalkSpeedModifier *= walk;
|
||||
SprintSpeedModifier *= sprint;
|
||||
}
|
||||
|
||||
public void ModifySpeed(float mod)
|
||||
{
|
||||
ModifySpeed(mod, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,28 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
||||
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>
|
||||
|
||||
@@ -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!
|
||||
@@ -62,3 +62,41 @@
|
||||
- type: HideLayerClothing
|
||||
slots:
|
||||
- Snout
|
||||
|
||||
- type: entity
|
||||
parent: ClothingMaskBase
|
||||
id: ClothingMaskGoldenCursed
|
||||
name: golden mask
|
||||
description: Previously used in strange pantomimes, after one of the actors went mad on stage these masks have avoided use. You swear its face contorts when you're not looking.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Mask/goldenmask.rsi
|
||||
layers:
|
||||
- state: icon
|
||||
map: [ "mask" ]
|
||||
- type: Clothing
|
||||
sprite: Clothing/Mask/goldenmask.rsi
|
||||
- type: Appearance
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.CursedMaskVisuals.State:
|
||||
mask:
|
||||
Neutral: { state: icon }
|
||||
Despair: { state: icon-despair }
|
||||
Joy: { state: icon-joy }
|
||||
Anger: { state: icon-anger }
|
||||
- type: Tag
|
||||
tags: [] # ignore "WhitelistChameleon" tag
|
||||
- type: SelfEquipOnly
|
||||
- type: CursedMask
|
||||
despairDamageModifier:
|
||||
coefficients:
|
||||
Blunt: 0.6
|
||||
Slash: 0.6
|
||||
Piercing: 0.4
|
||||
- type: HideLayerClothing
|
||||
slots:
|
||||
- Snout
|
||||
- type: IngestionBlocker
|
||||
- type: StaticPrice
|
||||
price: 5000
|
||||
|
||||
|
After Width: | Height: | Size: 453 B |
|
After Width: | Height: | Size: 401 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png
Normal file
|
After Width: | Height: | Size: 422 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png
Normal file
|
After Width: | Height: | Size: 360 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png
Normal file
|
After Width: | Height: | Size: 353 B |
39
Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at commit https://github.com/vgstation-coders/vgstation13/blob/HEAD/icons/obj/clothing/masks.dmi. Vox and Reptilian edits by EmoGarbage404 (Github)",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"name": "icon-joy"
|
||||
},
|
||||
{
|
||||
"name": "icon-despair"
|
||||
},
|
||||
{
|
||||
"name": "icon-anger"
|
||||
},
|
||||
{
|
||||
"name": "equipped-MASK",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "equipped-MASK-vox",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "inhand-left",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "inhand-right",
|
||||
"directions": 4
|
||||
}
|
||||
]
|
||||
}
|
||||