Ports Devils from Goobstation (#4639)

* or your debil

* Woop

* Whoops, Missed this

* Whoops, Suicide System

* Hush... Extended tests... Hush

* AAAAAA

* No-longer jaunt whilst dead

* :)

* Update Content.Shared/_Lavaland/Chasm/PreventChasmFallingSystem.cs

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: William Lemon <William.Lemon2@gmail.com>

* Update Content.Server/_Goobstation/Possession/PossessionSystem.cs

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: William Lemon <William.Lemon2@gmail.com>

* Please tobysenpai forgive me

---------

Signed-off-by: William Lemon <William.Lemon2@gmail.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
This commit is contained in:
William Lemon 2025-11-20 20:11:07 +11:00 committed by GitHub
parent 496cb0f7cc
commit f563f2c566
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
171 changed files with 5872 additions and 14 deletions

View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil.UI;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client._Goobstation.Devil.UI;
[UsedImplicitly]
public sealed class RevivalContractBoundUserInterface : BoundUserInterface
{
private RevivalContractMenu? _menu;
public RevivalContractBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<RevivalContractMenu>();
_menu.SetEntity(Owner);
_menu.Accepted += OnAccepted;
_menu.Rejected += OnRejected;
_menu.OnClose += OnClosed;
_menu.OpenCentered();
}
private void OnAccepted()
{
SendPredictedMessage(new RevivalContractMessage(true));
Close();
}
private void OnRejected()
{
SendPredictedMessage(new RevivalContractMessage(false));
Close();
}
private void OnClosed()
{
SendPredictedMessage(new RevivalContractMessage(false));
Close();
}
}

View File

@ -0,0 +1,17 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'revival-contract-menu-title'}"
MinWidth="300">
<BoxContainer Orientation="Vertical" Margin="4">
<Label Name="RevivalPrompt"
Text="{Loc 'revival-contract-menu-prompt'}"
Align="Left"
ClipText="True"/>
<Button Name="AcceptButton"
Text="{Loc 'revival-contract-prompt-accept'}"
ClipText="True"/>
<Button Name="RejectButton"
Text="{Loc 'revival-contract-prompt-reject'}"
ClipText="True"/>
</BoxContainer>
</DefaultWindow>

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.Devil.UI;
[GenerateTypedNameReferences]
public sealed partial class RevivalContractMenu : DefaultWindow
{
public event Action? Accepted;
public event Action? Rejected;
public EntityUid? Entity { get; private set; }
public RevivalContractMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
AcceptButton.OnPressed += OnAcceptPressed;
RejectButton.OnPressed += OnRejectPressed;
}
private void OnAcceptPressed(BaseButton.ButtonEventArgs args)
{
Accepted?.Invoke();
Close();
}
private void OnRejectPressed(BaseButton.ButtonEventArgs args)
{
Rejected?.Invoke();
Close();
}
public void SetEntity(EntityUid ent)
{
Entity = ent;
}
}

View File

@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Shared._Goobstation.Atmos;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
@ -144,7 +145,7 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float GetFeltLowPressure(EntityUid uid, BarotraumaComponent barotrauma, float environmentPressure)
{
if (barotrauma.HasImmunity)
if (barotrauma.HasImmunity || HasComp<SpecialPressureImmunityComponent>(uid)) // Goobstation - Special Immunity
{
return Atmospherics.OneAtmosphere;
}
@ -158,7 +159,7 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float GetFeltHighPressure(EntityUid uid, BarotraumaComponent barotrauma, float environmentPressure)
{
if (barotrauma.HasImmunity)
if (barotrauma.HasImmunity || HasComp<SpecialPressureImmunityComponent>(uid)) // Goobstation - Special Immunity
{
return Atmospherics.OneAtmosphere;
}

View File

@ -1,4 +1,5 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint; // Goobstation
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@ -39,6 +40,18 @@ namespace Content.Server.Bible.Components
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier DamageOnUntrainedUse = default!;
/// <summary>
/// How much damage to deal to the entity being smitten - Goob
/// </summary>
[DataField]
public DamageSpecifier SmiteDamage = new() { DamageDict = new Dictionary<string, FixedPoint2>() { { "Holy", 25 } } }; // Ungodly
/// <summary>
/// How long to stun the entity being smitten - Goob
/// </summary>
[DataField]
public TimeSpan SmiteStunDuration = TimeSpan.FromSeconds(8);
/// <summary>
/// Chance the bible will fail to heal someone with no helmet
/// </summary>

View File

@ -87,7 +87,7 @@ public sealed class BodySystem : SharedBodySystem
}
}
protected override void RemovePart(
public override void RemovePart( // DeltaV - Made public
Entity<BodyComponent?> bodyEnt,
Entity<BodyPartComponent> partEnt,
string slotId)

View File

@ -24,6 +24,7 @@ using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared._DV.CosmicCult.Components; // DeltaV
using Content.Shared._Goobstation.Body.Components; // Goobstation
namespace Content.Server.Body.Systems;
@ -80,7 +81,7 @@ public sealed class RespiratorSystem : EntitySystem
respirator.NextUpdate += respirator.AdjustedUpdateInterval;
if (_mobState.IsDead(uid) || HasComp<BreathingImmunityComponent>(uid)) // Shitmed: BreathingImmunity
if (_mobState.IsDead(uid) || HasComp<BreathingImmunityComponent>(uid) || HasComp<SpecialBreathingImmunityComponent>(uid)) // Shitmed: BreathingImmunity Goob immunity too
continue;
// Begin DeltaV Additions

View File

@ -31,6 +31,7 @@ public sealed class SuicideSystem : EntitySystem
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
private static readonly ProtoId<TagPrototype> CannotSuicideAny = "CannotSuicideAny"; // DeltaV / Goob, ProtoID Tag
public override void Initialize()
{
@ -49,7 +50,7 @@ public sealed class SuicideSystem : EntitySystem
public bool Suicide(EntityUid victim)
{
// Can't suicide if we're already dead
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState) || _tagSystem.HasTag(victim, CannotSuicideAny)) // Goobstation - DeltaV Use ProtoId
return false;
_adminLogger.Add(LogType.Mind, $"{ToPrettyString(victim):player} is attempting to suicide");

View File

@ -2,6 +2,7 @@ using Content.Server.Ghost;
using Content.Server.Morgue.Components;
using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared._Goobstation.CrematorImmune;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
@ -133,6 +134,8 @@ public sealed class CrematoriumSystem : EntitySystem
for (var i = storage.Contents.ContainedEntities.Count - 1; i >= 0; i--)
{
var item = storage.Contents.ContainedEntities[i];
if (HasComp<CrematoriumImmuneComponent>(item)) // GOOBCODE ALERT //
continue;
_containers.Remove(item, storage.Contents);
Del(item);
}

View File

@ -13,8 +13,9 @@ using Content.Shared.Rejuvenate;
using Content.Shared.Temperature;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Physics.Events;
using Content.Shared.Projectiles;
using Content.Shared._Goobstation.Temperature.Components;
using Content.Shared._Goobstation.Temperature;
namespace Content.Server.Temperature.Systems;
@ -52,6 +53,9 @@ public sealed class TemperatureSystem : EntitySystem
SubscribeLocalEvent<ChangeTemperatureOnCollideComponent, ProjectileHitEvent>(ChangeTemperatureOnCollide);
SubscribeLocalEvent<SpecialLowTempImmunityComponent, TemperatureImmunityEvent>(OnCheckLowTemperatureImmunity); // Goob edit
SubscribeLocalEvent<SpecialHighTempImmunityComponent, TemperatureImmunityEvent>(OnCheckHighTemperatureImmunity); // Goob edit
// Allows overriding thresholds based on the parent's thresholds.
SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
@ -116,14 +120,36 @@ public sealed class TemperatureSystem : EntitySystem
ShouldUpdateDamage.Clear();
}
// Goob start
private void OnCheckLowTemperatureImmunity(Entity<SpecialLowTempImmunityComponent> ent, ref TemperatureImmunityEvent args)
{
if (args.CurrentTemperature < args.IdealTemperature)
args.CurrentTemperature = args.IdealTemperature;
}
private void OnCheckHighTemperatureImmunity(Entity<SpecialHighTempImmunityComponent> ent, ref TemperatureImmunityEvent args)
{
if (args.CurrentTemperature > args.IdealTemperature)
args.CurrentTemperature = args.IdealTemperature;
}
// Goob end
public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null)
{
if (!Resolve(uid, ref temperature))
return;
float lastTemp = temperature.CurrentTemperature;
float delta = temperature.CurrentTemperature - temp;
temperature.CurrentTemperature = temp;
// Goob start
var tempEv = new TemperatureImmunityEvent(temperature.CurrentTemperature);
RaiseLocalEvent(uid, tempEv);
temperature.CurrentTemperature = tempEv.CurrentTemperature;
float delta = temperature.CurrentTemperature - temp;
// Goob end
RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta),
true);
}

View File

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Diagnostics.CodeAnalysis;
using Content.Server._Goobstation.Devil.GameTicking.Rules;
using Content.Server.Administration.Managers;
using Content.Server.Antag;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Mind.Components;
using Content.Shared.Verbs;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server._Goobstation.Administration.Systems;
public sealed partial class GoobAdminVerbSystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly IAdminManager _admin = default!;
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
{
if (!AntagVerbAllowed(args, out var targetPlayer))
return;
// Devil
Verb devilAntag = new()
{
Text = Loc.GetString("admin-verb-text-make-devil"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("_Goobstation/Actions/devil.rsi"), "summon-contract"),
Act = () =>
{
_antag.ForceMakeAntag<DevilRuleComponent>(targetPlayer, "Devil");
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-devil"),
};
args.Verbs.Add(devilAntag);
}
public bool AntagVerbAllowed(GetVerbsEvent<Verb> args, [NotNullWhen(true)] out ICommonSession? target)
{
target = null;
if (!TryComp<ActorComponent>(args.User, out var actor))
return false;
var player = actor.PlayerSession;
if (!_admin.HasAdminFlag(player, AdminFlags.Fun))
return false;
if (!HasComp<MindContainerComponent>(args.Target) || !TryComp<ActorComponent>(args.Target, out var targetActor))
return false;
target = targetActor.PlayerSession;
return true;
}
}

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Conchelle <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Verbs;
namespace Content.Server._Goobstation.Administration.Systems;
public sealed partial class GoobAdminVerbSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GetVerbsEvent<Verb>>(GetVerbs);
}
private void GetVerbs(GetVerbsEvent<Verb> args)
{
AddAntagVerbs(args);
}
}

View File

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Religion;
using Content.Server.Bible.Components;
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Exorcism;
using Content.Shared._Goobstation.Religion;
using Content.Shared._Shitmed.Targeting;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Content.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Server._Goobstation.Bible;
public sealed partial class GoobBibleSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
public override void Initialize()
{
base.Initialize();
}
public bool TryDoSmite(EntityUid bible, EntityUid performer, EntityUid target, UseDelayComponent? useDelay = null, BibleComponent? bibleComp = null)
{
if (!Resolve(bible, ref useDelay, ref bibleComp))
return false;
if (!TryComp<WeakToHolyComponent>(target, out var weakToHoly)
|| weakToHoly is { AlwaysTakeHoly: false }
|| !HasComp<BibleUserComponent>(performer)
|| !_timing.IsFirstTimePredicted
|| _delay.IsDelayed(bible)
|| !_netManager.IsServer)
return false;
var multiplier = 1f;
var isDevil = false;
if (TryComp<DevilComponent>(target, out var devil))
{
isDevil = true;
multiplier = devil.BibleUserDamageMultiplier;
}
if (!_mobStateSystem.IsIncapacitated(target))
{
var popup = Loc.GetString("weaktoholy-component-bible-sizzle", ("target", target), ("item", bible));
_popupSystem.PopupPredicted(popup, target, performer, PopupType.LargeCaution);
_audio.PlayPvs(bibleComp.SizzleSoundPath, target);
_damageableSystem.TryChangeDamage(target, bibleComp.SmiteDamage * multiplier, true, origin: bible, targetPart: TargetBodyPart.All);
_stun.TryParalyze(target, bibleComp.SmiteStunDuration * multiplier, false);
_delay.TryResetDelay((bible, useDelay));
}
else if (isDevil && HasComp<BibleUserComponent>(performer))
{
var doAfterArgs = new DoAfterArgs(
EntityManager,
performer,
TimeSpan.FromSeconds(10f),
new ExorcismDoAfterEvent(),
eventTarget: target,
target: target)
{
BreakOnMove = true,
NeedHand = true,
BlockDuplicate = true,
BreakOnDropItem = true,
};
_doAfter.TryStartDoAfter(doAfterArgs);
var popup = Loc.GetString("devil-banish-begin", ("target", target), ("user", performer));
_popupSystem.PopupEntity(popup, target, PopupType.LargeCaution);
}
return true;
}
}

View File

@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.DelayedDeath;
using Content.Shared._Goobstation.CheatDeath;
using Content.Shared._Goobstation.Devour.Events;
using Content.Server._Shitmed.DelayedDeath;
using Content.Server.Actions;
using Content.Server.Administration.Systems;
using Content.Server.Jittering;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Traits.Assorted;
using Robust.Shared.Network;
namespace Content.Server._Goobstation.Devil.CheatDeath;
public sealed partial class CheatDeathSystem : EntitySystem
{
[Dependency] private readonly RejuvenateSystem _rejuvenateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly JitteringSystem _jitter = default!;
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CheatDeathComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<CheatDeathComponent, ComponentRemove>(OnRemoval);
SubscribeLocalEvent<CheatDeathComponent, CheatDeathEvent>(OnDeathCheatAttempt);
SubscribeLocalEvent<CheatDeathComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CheatDeathComponent, DelayedDeathEvent>(OnDelayedDeath);
}
private void OnInit(Entity<CheatDeathComponent> ent, ref MapInitEvent args) =>
_actionsSystem.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.ActionCheatDeath);
private void OnRemoval(Entity<CheatDeathComponent> ent, ref ComponentRemove args) =>
_actionsSystem.RemoveAction(ent.Owner, ent.Comp.ActionEntity);
private void OnExamined(Entity<CheatDeathComponent> ent, ref ExaminedEvent args)
{
if (args.Examined != args.Examiner)
return;
if (ent.Comp.InfiniteRevives)
{
var unlimited = Loc.GetString("cheat-death-component-remaining-revives-unlimited");
args.PushMarkup(unlimited);
return;
}
var remaining = Loc.GetString("cheat-death-component-remaining-revives", ("amount", ent.Comp.ReviveAmount));
args.PushMarkup(remaining);
}
private void OnDelayedDeath(Entity<CheatDeathComponent> ent, ref DelayedDeathEvent args)
{
if (args.PreventRevive)
RemCompDeferred(ent.Owner, ent.Comp);
}
private void OnDeathCheatAttempt(Entity<CheatDeathComponent> ent, ref CheatDeathEvent args)
{
if (args.Handled)
return;
if (!_mobStateSystem.IsDead(ent) && !ent.Comp.CanCheatStanding)
{
var failPopup = Loc.GetString("action-cheat-death-fail-not-dead");
_popupSystem.PopupEntity(failPopup, ent, ent, PopupType.LargeCaution);
return;
}
// check if we're allowed to revive
var reviveEv = new BeforeSelfRevivalEvent(ent, "self-revive-fail");
RaiseLocalEvent(ent, ref reviveEv);
if (reviveEv.Cancelled)
return;
// If the entity is out of revives, or if they are unrevivable, return.
if (ent.Comp.ReviveAmount <= 0 || HasComp<UnrevivableComponent>(ent))
{
var failPopup = Loc.GetString("action-cheat-death-fail-no-lives");
_popupSystem.PopupEntity(failPopup, ent, ent, PopupType.LargeCaution);
return;
}
// If the holy damage exceeds the crit state, do not allow revives.
if (!TryComp<DamageableComponent>(ent, out var damageable)
|| !_thresholdSystem.TryGetIncapThreshold(ent, out var incapThreshold)
|| damageable.Damage.DamageDict["Holy"] >= incapThreshold)
{
var failPopup = Loc.GetString("action-cheat-death-holy-damage");
_popupSystem.PopupEntity(failPopup, ent, ent, PopupType.LargeCaution);
return;
}
// Show popup
if (_mobStateSystem.IsDead(ent) && !ent.Comp.CanCheatStanding)
{
var popup = Loc.GetString("action-cheated-death-dead", ("name", Name(ent)));
_popupSystem.PopupEntity(popup, ent, PopupType.LargeCaution);
}
else
{
var popup = Loc.GetString("action-cheated-death-alive", ("name", Name(ent)));
_popupSystem.PopupEntity(popup, ent, PopupType.LargeCaution);
}
// Revive entity
_rejuvenateSystem.PerformRejuvenate(ent);
_jitter.DoJitter(ent, TimeSpan.FromSeconds(5), true);
// Decrement remaining revives.
if (!ent.Comp.InfiniteRevives)
ent.Comp.ReviveAmount--;
// remove comp if at zero
if (ent.Comp.ReviveAmount <= 0 && !ent.Comp.InfiniteRevives)
RemComp(ent.Owner, ent.Comp);
args.Handled = true;
}
}

View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.CheatDeath;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared.Mobs;
namespace Content.Server._Goobstation.Devil.Condemned;
public sealed partial class CondemnedSystem
{
public void InitializeOnDeath()
{
SubscribeLocalEvent<CondemnedComponent, MobStateChangedEvent>(OnMobStateChanged);
}
private void OnMobStateChanged(EntityUid uid, CondemnedComponent comp, MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead
|| comp.SoulOwnedNotDevil
|| !comp.CondemnOnDeath)
return;
if (TryComp<CheatDeathComponent>(uid, out var cheatDeath)
&& cheatDeath.ReviveAmount > 0 || cheatDeath is { InfiniteRevives: true })
return;
StartCondemnation(uid, behavior: CondemnedBehavior.Delete);
}
}

View File

@ -0,0 +1,187 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared._Goobstation.Religion;
using Content.Server.IdentityManagement;
using Content.Server.Polymorph.Systems;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Polymorph;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Spawners;
using Content.Server.Forensics;
namespace Content.Server._Goobstation.Devil.Condemned;
public sealed partial class CondemnedSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PolymorphSystem _poly = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!; // DeltaV - Forensics DNA
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CondemnedComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CondemnedComponent, MapInitEvent>(OnStartup);
SubscribeLocalEvent<CondemnedComponent, ComponentRemove>(OnRemoved);
SubscribeLocalEvent<CondemnedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
InitializeOnDeath();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<CondemnedComponent>();
while (query.MoveNext(out var uid, out var comp))
{
switch (comp.CurrentPhase)
{
case CondemnedPhase.PentagramActive:
UpdatePentagramPhase(uid, frameTime, comp);
break;
case CondemnedPhase.HandActive:
UpdateHandPhase(uid, frameTime, comp);
break;
}
}
}
private void OnStartup(Entity<CondemnedComponent> condemned, ref MapInitEvent args)
{
if (condemned.Comp.SoulOwnedNotDevil)
return;
if (HasComp<WeakToHolyComponent>(condemned))
condemned.Comp.WasWeakToHoly = true;
else
EnsureComp<WeakToHolyComponent>(condemned).AlwaysTakeHoly = true;
}
private void OnRemoved(Entity<CondemnedComponent> condemned, ref ComponentRemove args)
{
if (condemned.Comp.SoulOwnedNotDevil)
return;
if (!condemned.Comp.WasWeakToHoly)
RemComp<WeakToHolyComponent>(condemned);
}
private void OnMoveAttempt(Entity<CondemnedComponent> condemned, ref UpdateCanMoveEvent args)
{
if (!condemned.Comp.FreezeDuringCondemnation
|| condemned.Comp.CurrentPhase != CondemnedPhase.Waiting)
return;
args.Cancel();
}
public void StartCondemnation(
EntityUid uid,
bool freezeEntity = true,
bool doFlavor = true,
CondemnedBehavior behavior = CondemnedBehavior.Delete)
{
var comp = EnsureComp<CondemnedComponent>(uid);
comp.CondemnOnDeath = false;
if (freezeEntity)
comp.FreezeDuringCondemnation = true;
var coords = Transform(uid).Coordinates;
Spawn(comp.PentagramProto, coords);
_audio.PlayPvs(comp.SoundEffect, coords);
if (comp.CondemnedBehavior == CondemnedBehavior.Delete && doFlavor)
_popup.PopupCoordinates(Loc.GetString("condemned-start", ("target", uid)), coords, PopupType.LargeCaution);
comp.CurrentPhase = CondemnedPhase.PentagramActive;
comp.PhaseTimer = 0f;
comp.CondemnedBehavior = behavior;
}
private void UpdatePentagramPhase(EntityUid uid, float frameTime, CondemnedComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.PhaseTimer += frameTime;
if (comp.PhaseTimer < 3f)
return;
var coords = Transform(uid).Coordinates;
var handEntity = Spawn(comp.HandProto, coords);
comp.HandDuration = TryComp<TimedDespawnComponent>(handEntity, out var timedDespawn)
? timedDespawn.Lifetime
: 1f;
comp.CurrentPhase = CondemnedPhase.HandActive;
comp.PhaseTimer = 0f;
}
private void UpdateHandPhase(EntityUid uid, float frameTime, CondemnedComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.PhaseTimer += frameTime;
if (comp.PhaseTimer < comp.HandDuration)
return;
DoCondemnedBehavior(uid, comp.ScrambleAfterBanish);
comp.CurrentPhase = CondemnedPhase.Complete;
}
private void DoCondemnedBehavior(EntityUid uid, bool scramble = true, CondemnedComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
switch (comp)
{
case { CondemnedBehavior: CondemnedBehavior.Delete }:
QueueDel(uid);
break;
case { CondemnedBehavior: CondemnedBehavior.Banish }:
if (scramble)
{
_forensics.RandomizeDNA(uid); // DeltaV - was ScrambleSystem, now Forensics.
_forensics.RandomizeFingerprint(uid);
}
_poly.PolymorphEntity(uid, comp.BanishProto);
break;
}
RemCompDeferred(uid, comp);
}
private void OnExamined(Entity<CondemnedComponent> condemned, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange
|| condemned.Comp.SoulOwnedNotDevil)
return;
var ev = new IsEyesCoveredCheckEvent();
RaiseLocalEvent(condemned, ev);
if (ev.IsEyesProtected)
return;
args.PushMarkup(Loc.GetString("condemned-component-examined", ("target", Identity.Entity(condemned, EntityManager) )));
}
}

View File

@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <linebarrelerenthusiast@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Linq;
using Content.Shared._Goobstation.Devil;
using Content.Server.Body.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Robust.Shared.Random;
namespace Content.Server._Goobstation.Devil.Contract;
public sealed partial class DevilContractSystem
{
private void InitializeSpecialActions()
{
SubscribeLocalEvent<DevilContractSoulOwnershipEvent>(OnSoulOwnership);
SubscribeLocalEvent<DevilContractLoseHandEvent>(OnLoseHand);
SubscribeLocalEvent<DevilContractLoseLegEvent>(OnLoseLeg);
SubscribeLocalEvent<DevilContractLoseOrganEvent>(OnLoseOrgan);
SubscribeLocalEvent<DevilContractChanceEvent>(OnChance);
}
private void OnSoulOwnership(DevilContractSoulOwnershipEvent args)
{
if (args.Contract?.ContractOwner is not { } contractOwner)
return;
TryTransferSouls(contractOwner, args.Target, 1);
}
private void OnLoseHand(DevilContractLoseHandEvent args)
{
if (!TryComp<BodyComponent>(args.Target, out var body))
return;
var hands = _bodySystem.GetBodyChildrenOfType(args.Target, BodyPartType.Hand, body).ToList();
if (hands.Count <= 0)
return;
var pick = _random.Pick(hands);
if (!TryComp<BodyPartComponent>(pick.Id, out var woundable)
|| !woundable.CanSever)
return;
_bodySystem.RemovePart(new(args.Target, body), pick, _bodySystem.GetSlotFromBodyPart(pick.Component));
QueueDel(pick.Id);
Dirty(args.Target, body);
Log.Debug($"Removed part {ToPrettyString(pick.Id)} from {ToPrettyString(args.Target)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
QueueDel(pick.Id);
}
private void OnLoseLeg(DevilContractLoseLegEvent args)
{
if (!TryComp<BodyComponent>(args.Target, out var body))
return;
var legs = _bodySystem.GetBodyChildrenOfType(args.Target, BodyPartType.Leg, body).ToList();
if (legs.Count <= 0)
return;
var pick = _random.Pick(legs);
if (!TryComp<BodyPartComponent>(pick.Id, out var woundable)
|| !woundable.CanSever)
return;
_bodySystem.RemovePart(new(args.Target, body), pick, _bodySystem.GetSlotFromBodyPart(pick.Component));
Dirty(args.Target, body);
Log.Debug($"Removed part {ToPrettyString(pick.Id)} from {ToPrettyString(args.Target)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
QueueDel(pick.Id);
}
private void OnLoseOrgan(DevilContractLoseOrganEvent args)
{
// don't remove the brain, as funny as that is.
var eligibleOrgans = _bodySystem.GetBodyOrgans(args.Target)
.Where(o => !HasComp<BrainComponent>(o.Id))
.ToList();
if (eligibleOrgans.Count <= 0)
return;
var pick = _random.Pick(eligibleOrgans);
_bodySystem.RemoveOrgan(pick.Id, pick.Component);
Log.Debug($"Removed part {ToPrettyString(pick.Id)} from {ToPrettyString(args.Target)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
QueueDel(pick.Id);
}
// LETS GO GAMBLING!!!!!
private void OnChance(DevilContractChanceEvent args)
{
AddRandomClause(args.Target);
}
}

View File

@ -0,0 +1,499 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <linebarrelerenthusiast@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Linq;
using System.Text.RegularExpressions;
using Content.Shared._Goobstation.Paper;
using Content.Server._Goobstation.Devil.Objectives.Components;
using Content.Server._Goobstation.Possession;
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared._Goobstation.Devil.Contract;
using Content.Server.Body.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems;
using Content.Server.Implants;
using Content.Server.Mind;
using Content.Server.Polymorph.Systems;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.Mindshield.Components;
using Content.Shared.Nutrition;
using Content.Shared.Paper;
using Content.Shared.Popups;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Content.Shared._EE.Silicon.Components;
namespace Content.Server._Goobstation.Devil.Contract;
public sealed partial class DevilContractSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = null!;
[Dependency] private readonly DamageableSystem _damageable = null!;
[Dependency] private readonly HandsSystem _hands = null!;
[Dependency] private readonly SharedAudioSystem _audio = null!;
[Dependency] private readonly IPrototypeManager _prototypeManager = null!;
[Dependency] private readonly BodySystem _bodySystem = null!;
[Dependency] private readonly IRobustRandom _random = null!;
[Dependency] private readonly SubdermalImplantSystem _implant = null!;
[Dependency] private readonly PolymorphSystem _polymorph = null!;
[Dependency] private readonly ExplosionSystem _explosion = null!;
[Dependency] private readonly MindSystem _mind = null!;
public override void Initialize()
{
base.Initialize();
InitializeRegex();
InitializeSpecialActions();
SubscribeLocalEvent<DevilContractComponent, BeingSignedAttemptEvent>(OnContractSignAttempt);
SubscribeLocalEvent<DevilContractComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DevilContractComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
SubscribeLocalEvent<DevilContractComponent, SignSuccessfulEvent>(OnSignStep);
SubscribeLocalEvent<DevilContractComponent, AfterFullyEatenEvent>(OnEaten);
}
private readonly Dictionary<LocId, Func<DevilContractComponent, EntityUid?>> _targetResolvers = new()
{
// The contractee is who is making the deal.
["devil-contract-contractee"] = comp => comp.Signer,
// The contractor is the entity offering the deal.
["devil-contract-contractor"] = comp => comp.ContractOwner,
};
private Regex _clauseRegex = null!;
private void InitializeRegex()
{
var escapedPatterns = _targetResolvers.Keys.Select(locId => Loc.GetString(locId)).ToList(); // malicious linq and regex
var targetPattern = string.Join("|", escapedPatterns);
_clauseRegex = new Regex($@"^\s*(?<target>{targetPattern})\s*:\s*(?<clause>.+?)\s*$",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline);
}
private void OnGetVerbs(Entity<DevilContractComponent> contract, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract
|| !args.CanAccess
|| !TryComp<DevilComponent>(args.User, out var devilComp))
return;
var user = args.User;
AlternativeVerb burnVerb = new()
{
Act = () => TryBurnContract(contract, (user, devilComp)),
Text = Loc.GetString("burn-contract-prompt"),
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Effects/fire.rsi"), "fire"),
};
args.Verbs.Add(burnVerb);
}
private void TryBurnContract(Entity<DevilContractComponent> contract, Entity<DevilComponent> devil)
{
var coordinates = Transform(contract).Coordinates;
if (contract.Comp is not { IsContractFullySigned: true})
{
Spawn(devil.Comp.FireEffectProto, coordinates);
_audio.PlayPvs(devil.Comp.FwooshPath, coordinates, new AudioParams(-2f, 1f, SharedAudioSystem.DefaultSoundRange, 1f, false, 0f));
QueueDel(contract);
}
else
_popupSystem.PopupCoordinates(Loc.GetString("burn-contract-popup-fail"), coordinates, devil, PopupType.MediumCaution);
}
private void OnExamined(Entity<DevilContractComponent> contract, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
UpdateContractWeight(contract);
args.PushMarkup(Loc.GetString("devil-contract-examined", ("weight", contract.Comp.ContractWeight)));
}
private void OnEaten(Entity<DevilContractComponent> contract, ref AfterFullyEatenEvent args)
{
_explosion.QueueExplosion(
args.User,
typeId: "Default",
totalIntensity: 1, // contract explosions should not cause any kind of major structural damage. you should at worst need to weld a window or repair a table.
slope: 1,
maxTileIntensity: 1,
maxTileBreak: 0,
addLog: false); // DeltaV - Use EntitySystem Logger intead of _sawmill
}
#region Signing Steps
private void OnContractSignAttempt(Entity<DevilContractComponent> contract, ref BeingSignedAttemptEvent args)
{
// Make sure that weight is set properly!
UpdateContractWeight(contract);
// Don't allow mortals to sign contracts for other people.
if (contract.Comp.IsVictimSigned && args.Signer != contract.Comp.ContractOwner)
{
var invalidUserPopup = Loc.GetString("devil-sign-invalid-user");
_popupSystem.PopupEntity(invalidUserPopup, contract, args.Signer);
args.Cancelled = true;
return;
}
// Only handle unsigned contracts.
if (contract.Comp.IsVictimSigned || contract.Comp.IsDevilSigned)
return;
if (!IsUserValid(args.Signer, out var failReason))
{
_popupSystem.PopupEntity(failReason, contract, args.Signer, PopupType.MediumCaution);
args.Cancelled = true;
return;
}
// Check if the weight is too low
if (!contract.Comp.IsContractSignable)
{
var difference = Math.Abs(contract.Comp.ContractWeight);
var unevenOddsPopup = Loc.GetString("contract-uneven-odds", ("number", difference));
_popupSystem.PopupEntity(unevenOddsPopup, contract, args.Signer, PopupType.MediumCaution);
args.Cancelled = true;
return;
}
// Check if devil is trying to sign first
if (args.Signer == contract.Comp.ContractOwner)
{
var tooEarlyPopup = Loc.GetString("devil-contract-early-sign-failed");
_popupSystem.PopupEntity(tooEarlyPopup, args.Signer, args.Signer, PopupType.MediumCaution);
args.Cancelled = true;
}
}
private void OnSignStep(Entity<DevilContractComponent> contract, ref SignSuccessfulEvent args)
{
// Determine signing phase
if (!contract.Comp.IsVictimSigned)
HandleVictimSign(contract, args.Paper, args.User);
else if (!contract.Comp.IsDevilSigned)
HandleDevilSign(contract, args.Paper, args.User);
// Final activation check
if (contract.Comp.IsContractFullySigned)
HandleBothPartiesSigned(contract);
}
private void HandleVictimSign(Entity<DevilContractComponent> contract, EntityUid signed, EntityUid signer)
{
contract.Comp.Signer = signer;
contract.Comp.IsVictimSigned = true;
if (TryComp<PaperComponent>(contract, out var paper))
paper.EditingDisabled = true;
_popupSystem.PopupEntity(Loc.GetString("contract-victim-signed"), signed, signer);
}
private void HandleDevilSign(Entity<DevilContractComponent> contract, EntityUid signed, EntityUid signer)
{
contract.Comp.IsDevilSigned = true;
_popupSystem.PopupEntity(Loc.GetString("contract-devil-signed"), signed, signer);
if (_mind.TryGetMind(signer, out var mindId, out var mind) &&
_mind.TryGetObjectiveComp<MeetContractWeightConditionComponent>(mindId, out var objectiveComp, mind))
objectiveComp.ContractWeight += contract.Comp.ContractWeight;
}
private void HandleBothPartiesSigned(Entity<DevilContractComponent> contract)
{
UpdateContractWeight(contract);
DoContractEffects(contract);
}
#endregion
#region Helper Events
public bool IsUserValid(EntityUid user, out string failReason)
{
if (HasComp<CondemnedComponent>(user)
|| HasComp<SiliconComponent>(user)
|| HasComp<BorgChassisComponent>(user))
{
failReason = Loc.GetString("devil-contract-no-soul-sign-failed");
return false;
}
if (HasComp<MindShieldComponent>(user)
&& !HasComp<DevilComponent>(user))
{
failReason = Loc.GetString("devil-contract-mind-shielded-failed");
return false;
}
if (HasComp<PossessedComponent>(user))
{
failReason = Loc.GetString("devil-contract-early-sign-failed");
return false;
}
failReason = string.Empty;
return true;
}
public bool TryTransferSouls(EntityUid devil, EntityUid contractee, int added)
{
// Can't sell what doesn't exist.
if (HasComp<CondemnedComponent>(contractee)
|| devil == contractee)
return false;
var ev = new SoulAmountChangedEvent(devil, contractee, added);
RaiseLocalEvent(devil, ref ev);
var condemned = EnsureComp<CondemnedComponent>(contractee);
condemned.SoulOwner = devil;
condemned.CondemnOnDeath = true;
return true;
}
private void UpdateContractWeight(Entity<DevilContractComponent> contract, PaperComponent? paper = null)
{
if (!Resolve(contract, ref paper))
return;
contract.Comp.CurrentClauses.Clear();
var newWeight = 0;
var matches = _clauseRegex.Matches(paper.Content);
foreach (Match match in matches)
{
if (!match.Success)
continue;
var clauseKey = match.Groups["clause"].Value.Trim().ToLowerInvariant().Replace(" ", "");
if (!_prototypeManager.TryIndex(clauseKey, out DevilClausePrototype? clauseProto)
|| !contract.Comp.CurrentClauses.Add(clauseProto))
continue;
newWeight += clauseProto.ClauseWeight;
}
contract.Comp.ContractWeight = newWeight;
}
private void DoContractEffects(Entity<DevilContractComponent> contract, PaperComponent? paper = null)
{
if (!Resolve(contract, ref paper))
return;
UpdateContractWeight(contract);
var matches = _clauseRegex.Matches(paper.Content);
var processedClauses = new HashSet<string>();
foreach (Match match in matches)
{
if (!match.Success)
continue;
var targetKey = match.Groups["target"].Value.Trim().ToLowerInvariant().Replace(" ", "");
var clauseKey = match.Groups["clause"].Value.Trim().ToLowerInvariant().Replace(" ", "");
var locId = _targetResolvers.Keys.FirstOrDefault(id => Loc.GetString(id).Equals(targetKey, StringComparison.OrdinalIgnoreCase));
var resolver = _targetResolvers[locId];
if (resolver(contract.Comp) == null)
{
Log.Warning($"Unknown resolver: {resolver(contract.Comp)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
continue;
}
if (!_prototypeManager.TryIndex(clauseKey, out DevilClausePrototype? clause))
{
Log.Warning($"Unknown contract clause: {clauseKey}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
continue;
}
// no duplicates
if (!processedClauses.Add(clauseKey))
{
Log.Warning($"Attempted to apply duplicate clause: {clauseKey} on contract {ToPrettyString(contract)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
continue;
}
var targetEntity = resolver(contract.Comp);
if (targetEntity is not null)
ApplyEffectToTarget(targetEntity.Value, clause, contract);
else
Log.Warning($"Invalid target entity from resolver for clause {clauseKey} in contract {ToPrettyString(contract)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
}
}
private void ApplyEffectToTarget(EntityUid target, DevilClausePrototype clause, Entity<DevilContractComponent>? contract)
{
DoPolymorphs(target, clause);
RemoveComponents(target, clause);
AddComponents(target, clause);
OverrideComponents(target, clause); // DeltaV - Fix component modifications
ChangeDamageModifier(target, clause);
AddImplants(target, clause);
SpawnItems(target, clause);
DoSpecialActions(target, contract, clause);
}
private void ChangeDamageModifier(EntityUid target, DevilClausePrototype clause)
{
if (clause.DamageModifierSet == null)
return;
_damageable.SetDamageModifierSetId(target, clause.DamageModifierSet);
}
private void RemoveComponents(EntityUid target, DevilClausePrototype clause)
{
if (clause.RemovedComponents == null)
return;
EntityManager.RemoveComponents(target, clause.RemovedComponents);
}
private void AddImplants(EntityUid target, DevilClausePrototype clause)
{
if (clause.Implants == null)
return;
_implant.AddImplants(target, clause.Implants);
}
private void AddComponents(EntityUid target, DevilClausePrototype clause)
{
if (clause.AddedComponents == null)
return;
EntityManager.AddComponents(target, clause.AddedComponents, false);
}
// Begin DeltaV Addition - Fix component modifications
private void OverrideComponents(EntityUid target, DevilClausePrototype clause)
{
if (clause.OverriddenComponents == null)
return;
EntityManager.AddComponents(target, clause.OverriddenComponents, true);
}
// End DeltaV Addition
private void SpawnItems(EntityUid target, DevilClausePrototype clause)
{
if (clause.SpawnedItems == null)
return;
foreach (var item in clause.SpawnedItems)
{
if (!_prototypeManager.TryIndex(item, out _))
continue;
var spawnedItem = SpawnNextToOrDrop(item, target);
_hands.TryPickupAnyHand(target, spawnedItem, false, false, false);
}
}
private void DoPolymorphs(EntityUid target, DevilClausePrototype clause)
{
if (clause.Polymorph == null)
return;
_polymorph.PolymorphEntity(target, clause.Polymorph.Value);
}
private void DoSpecialActions(EntityUid target, Entity<DevilContractComponent>? contract, DevilClausePrototype clause)
{
if (clause.Event == null)
return;
var ev = clause.Event;
ev.Target = target;
if (contract is not null)
ev.Contract = contract;
// you gotta cast this shit to object, don't ask me vro idk either
RaiseLocalEvent(target, (object)ev, true);
}
public void AddRandomNegativeClause(EntityUid target)
{
var negativeClauses = _prototypeManager.EnumeratePrototypes<DevilClausePrototype>()
.Where(c => c.ClauseWeight >= 0)
.ToList();
if (negativeClauses.Count == 0)
return;
var selectedClause = _random.Pick(negativeClauses);
ApplyEffectToTarget(target, selectedClause, null);
Log.Debug($"Selected {selectedClause.ID} effect for {ToPrettyString(target)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
}
public void AddRandomPositiveClause(EntityUid target)
{
var positiveClauses = _prototypeManager.EnumeratePrototypes<DevilClausePrototype>()
.Where(c => c.ClauseWeight <= 0)
.ToList();
if (positiveClauses.Count == 0)
return;
var selectedClause = _random.Pick(positiveClauses);
ApplyEffectToTarget(target, selectedClause, null);
Log.Debug($"Selected {selectedClause.ID} effect for {ToPrettyString(target)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
}
public void AddRandomClause(EntityUid target)
{
var clauses = _prototypeManager.EnumeratePrototypes<DevilClausePrototype>().ToList();
if (clauses.Count == 0)
return;
var selectedClause = _random.Pick(clauses);
ApplyEffectToTarget(target, selectedClause, null);
Log.Debug($"Selected {selectedClause.ID} effect for {ToPrettyString(target)}"); // DeltaV - Use EntitySystem Logger intead of _sawmill
}
#endregion
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Server._Goobstation.Devil.Contract.Revival;
[RegisterComponent]
public sealed partial class PendingRevivalContractComponent : Component
{
/// <summary>
/// The entity being revived.
/// </summary>
[ViewVariables]
public EntityUid? Contractee;
/// <summary>
/// The entity offering revival
/// </summary>
[ViewVariables]
public EntityUid? Offerer;
/// <summary>
/// The contract attached to this player.
/// </summary>
[ViewVariables]
public EntityUid? Contract;
/// <summary>
/// The MindId of the player.
/// </summary>
[ViewVariables]
public EntityUid MindId;
}

View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Server._Goobstation.Devil.Contract.Revival;
[RegisterComponent]
public sealed partial class RevivalContractComponent : Component
{
/// <summary>
/// The entity who signed the paper, AKA, the entity who has the effects applied.
/// </summary>
[DataField]
public EntityUid? Signer;
/// <summary>
/// The entity who created the contract, AKA, the entity who gains the soul.
/// </summary>
[DataField]
public EntityUid? ContractOwner;
}

View File

@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared._Goobstation.Devil.UI;
using Content.Server.Administration.Systems;
using Content.Server.Mind;
using Content.Shared.Interaction;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server._Goobstation.Devil.Contract.Revival;
public sealed partial class PendingRevivalContractSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
[Dependency] private readonly DevilContractSystem _contract = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RevivalContractComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<PendingRevivalContractComponent, RevivalContractMessage>(OnMessage);
}
private void AfterInteract(Entity<RevivalContractComponent> ent, ref AfterInteractEvent args)
{
if (args.Target is not { } target
|| !TryComp<MobStateComponent>(target, out var mobState)
|| mobState.CurrentState != MobState.Dead)
return;
// Non-devils can't offer deals silly.
if (!HasComp<DevilComponent>(args.User))
{
_popupSystem.PopupEntity(Loc.GetString("devil-sign-invalid-user"), args.User, PopupType.MediumCaution);
return;
}
// Make sure the mind actually exists
if (!_mind.TryGetMind(target, out var mindId, out var mindComp) || mindComp.CurrentEntity is not { } ghost)
{
_popupSystem.PopupEntity(Loc.GetString("revival-contract-no-mind"), args.User, args.User);
return;
}
// You can't offer two deals at once.
if (HasComp<PendingRevivalContractComponent>(ghost) || HasComp<CondemnedComponent>(target))
{
var failedPopup = Loc.GetString("revival-contract-use-failed", ("target", target)); // DeltaV - Added target param
_popupSystem.PopupEntity(failedPopup, args.User, args.User);
return;
}
// Create pending contract
var pending = EnsureComp<PendingRevivalContractComponent>(ghost);
pending.Contractee = target;
pending.Offerer = args.User;
pending.Contract = ent;
pending.MindId = mindId;
// Show confirmation
var successPopup = Loc.GetString("revival-contract-use-success", ("target", target));
_popupSystem.PopupEntity(successPopup, args.User, args.User);
ent.Comp.Signer = target;
ent.Comp.ContractOwner = args.User;
TryOpenUi(ghost);
}
private bool TryOpenUi(EntityUid target)
{
if (!_userInterface.HasUi(target, RevivalContractUiKey.Key))
return false;
if (_mind.TryGetMind(target, out _, out var mindComp) &&
_player.TryGetSessionById(mindComp.UserId, out var session) &&
session is { } insession)
_userInterface.OpenUi(target, RevivalContractUiKey.Key, insession);
return true;
}
private void OnMessage(Entity<PendingRevivalContractComponent> ent, ref RevivalContractMessage args)
{
if (args.Accepted && ent.Comp.Contractee is { } contractee)
{
TryReviveAndTransferSoul(contractee, ent.Comp);
_mind.UnVisit(ent.Comp.MindId);
}
RemComp<PendingRevivalContractComponent>(args.Actor);
}
private bool TryReviveAndTransferSoul(EntityUid target, PendingRevivalContractComponent pending)
{
if (TerminatingOrDeleted(target))
return false;
if (TryComp<RevivalContractComponent>(pending.Contract, out var contract) && contract is { ContractOwner: { Valid: true } contractOwner, Signer: { } signer })
{
_rejuvenate.PerformRejuvenate(target);
_popupSystem.PopupEntity(Loc.GetString("revival-contract-accepted"), target, target);
_contract.TryTransferSouls(contractOwner, signer, 1);
}
RemComp(target, pending);
return true;
}
}

View File

@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server._Goobstation.Devil.Contract;
using Content.Server._Goobstation.Devil.Contract.Revival;
using Content.Server._Goobstation.Devil.Grip;
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Devil.Actions;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared._Goobstation.Devil.Contract;
using Content.Shared.Cuffs.Components;
using Content.Shared.IdentityManagement;
namespace Content.Server._Goobstation.Devil;
public sealed partial class DevilSystem
{
private void SubscribeAbilities()
{
SubscribeLocalEvent<DevilComponent, CreateContractEvent>(OnContractCreated);
SubscribeLocalEvent<DevilComponent, CreateRevivalContractEvent>(OnRevivalContractCreated);
SubscribeLocalEvent<DevilComponent, ShadowJauntEvent>(OnShadowJaunt);
SubscribeLocalEvent<DevilComponent, DevilGripEvent>(OnDevilGrip);
SubscribeLocalEvent<DevilComponent, DevilPossessionEvent>(OnPossess);
}
private void OnContractCreated(Entity<DevilComponent> devil, ref CreateContractEvent args)
{
if (!TryUseAbility(args))
return;
var contract = Spawn(devil.Comp.ContractPrototype, Transform(devil).Coordinates);
_hands.TryPickupAnyHand(devil, contract);
if (!TryComp<DevilContractComponent>(contract, out var contractComponent))
return;
contractComponent.ContractOwner = args.Performer;
PlayFwooshSound(devil);
DoContractFlavor(devil, Identity.Name(devil, EntityManager));
}
private void OnRevivalContractCreated(Entity<DevilComponent> devil, ref CreateRevivalContractEvent args)
{
if (!TryUseAbility(args))
return;
var contract = Spawn(devil.Comp.RevivalContractPrototype, Transform(devil).Coordinates);
_hands.TryPickupAnyHand(devil, contract);
if (!TryComp<RevivalContractComponent>(contract, out var contractComponent))
return;
contractComponent.ContractOwner = args.Performer;
PlayFwooshSound(devil);
DoContractFlavor(devil, Identity.Name(devil, EntityManager));
}
private void OnShadowJaunt(Entity<DevilComponent> devil, ref ShadowJauntEvent args)
{
if (!TryUseAbility(args))
return;
Spawn(devil.Comp.JauntAnimationProto, Transform(devil).Coordinates);
Spawn(devil.Comp.PentagramEffectProto, Transform(devil).Coordinates);
if (TryComp<CuffableComponent>(devil, out var cuffableComponent))
_container.EmptyContainer(cuffableComponent.Container, true);
_poly.PolymorphEntity(devil, devil.Comp.JauntEntityProto);
}
private void OnDevilGrip(Entity<DevilComponent> devil, ref DevilGripEvent args)
{
if (!TryUseAbility(args))
return;
if (devil.Comp.DevilGrip != null)
{
foreach (var item in _hands.EnumerateHeld(devil.Owner))
{
if (!HasComp<DevilGripComponent>(item))
continue;
QueueDel(item);
return;
}
}
var grasp = Spawn(devil.Comp.GripPrototype, Transform(devil).Coordinates);
if (!_hands.TryPickupAnyHand(devil, grasp))
QueueDel(grasp);
devil.Comp.DevilGrip = args.Action.Owner;
}
private void OnPossess(Entity<DevilComponent> devil, ref DevilPossessionEvent args)
{
if (!TryComp<CondemnedComponent>(args.Target, out var condemned) || condemned.SoulOwnedNotDevil)
{
var message = Loc.GetString("invalid-possession-target");
_popup.PopupEntity(message, devil, devil);
return;
}
if (!TryUseAbility(args))
return;
if (devil.Comp.PowerLevel != DevilPowerLevel.None)
devil.Comp.PossessionDuration *= (int)devil.Comp.PowerLevel;
if (_possession.TryPossessTarget(args.Target, args.Performer, devil.Comp.PossessionDuration, true, polymorphPossessor: true))
{
Spawn(devil.Comp.JauntAnimationProto, Transform(args.Target).Coordinates);
Spawn(devil.Comp.PentagramEffectProto, Transform(args.Target).Coordinates);
}
}
}

View File

@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.CheatDeath;
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared.Body.Part;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Server._Goobstation.Devil;
public sealed partial class DevilSystem
{
private void InitializeHandshakeSystem()
{
SubscribeLocalEvent<DevilComponent, GetVerbsEvent<InnateVerb>>(OnGetVerbs);
SubscribeLocalEvent<PendingHandshakeComponent, GetVerbsEvent<InnateVerb>>(OnGetVerbsPending);
}
private void OnGetVerbs(EntityUid uid, DevilComponent comp, GetVerbsEvent<InnateVerb> args)
{
// Can't shake your own hand, and you can't shake from a distance
if (!args.CanAccess
|| !args.CanInteract
|| _state.IsIncapacitated(args.Target)
|| !HasComp<MobStateComponent>(args.Target)
|| HasComp<CondemnedComponent>(args.Target)
|| args.Target == args.User
|| !_body.BodyHasPartType(uid, BodyPartType.Hand) // cant shake if you have no hands
|| !_body.BodyHasPartType(args.Target, BodyPartType.Hand) // or if they have none
|| !_contract.IsUserValid(args.Target, out _))
return;
InnateVerb handshakeVerb = new()
{
Act = () => OfferHandshake(args.User, args.Target),
Text = Loc.GetString("hand-shake-prompt-verb", ("target", args.Target)),
Icon = new SpriteSpecifier.Rsi(new("_Goobstation/Actions/devil.rsi"), "summon-contract"),
Priority = 1 // Higher priority than default verbs
};
args.Verbs.Add(handshakeVerb);
}
private void OnGetVerbsPending(EntityUid uid, PendingHandshakeComponent comp, GetVerbsEvent<InnateVerb> args)
{
if (!args.CanAccess
|| !args.CanInteract
|| _state.IsIncapacitated(args.Target)
|| !HasComp<MobStateComponent>(args.Target)
|| args.Target != comp.Offerer)
return;
InnateVerb handshakeVerb = new()
{
Act = () => HandleHandshake(args.Target, args.User),
Text = Loc.GetString("hand-shake-accept-verb", ("target", args.Target)),
Icon = new SpriteSpecifier.Rsi(new("_Goobstation/Actions/devil.rsi"), "summon-contract"),
Priority = 1 // Higher priority than default verbs
};
args.Verbs.Add(handshakeVerb);
}
private void OfferHandshake(EntityUid user, EntityUid target)
{
if (HasComp<DevilComponent>(target)
|| HasComp<PendingHandshakeComponent>(target)
|| !_contract.IsUserValid(target, out _))
return;
var pending = AddComp<PendingHandshakeComponent>(target);
pending.Offerer = user;
pending.ExpiryTime = _timing.CurTime + TimeSpan.FromSeconds(15);
// Notify target
var popupMessage = Loc.GetString("handshake-offer-popup", ("user", user));
_popup.PopupEntity(popupMessage, target, target);
// Notify self
var selfPopup = Loc.GetString("handshake-offer-popup-self", ("target", target));
_popup.PopupEntity(selfPopup, user, user);
}
private void HandleHandshake(EntityUid user, EntityUid target)
{
if (!_contract.TryTransferSouls(user, target, 1))
{
var handshakeFail = Loc.GetString("handshake-fail", ("user", user));
_popup.PopupEntity(handshakeFail, user, user);
return;
}
var handshakeSucess = Loc.GetString("handshake-success", ("user", user));
_popup.PopupEntity(handshakeSucess, target, target);
_rejuvenate.PerformRejuvenate(target);
var cheatdeath = EnsureComp<CheatDeathComponent>(target);
cheatdeath.ReviveAmount = 1;
_contract.AddRandomNegativeClause(target);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<PendingHandshakeComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.ExpiryTime > _timing.CurTime)
continue;
RemCompDeferred(uid, comp);
}
}
}

View File

@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Text.RegularExpressions;
using Content.Shared._Goobstation.Religion;
using Content.Server._Goobstation.Devil.Condemned;
using Content.Server._Goobstation.Devil.Contract;
using Content.Server._Goobstation.Devil.Objectives.Components;
using Content.Server._Goobstation.Possession;
using Content.Shared._Goobstation.CheatDeath;
using Content.Shared._Goobstation.CrematorImmune;
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Devil.Condemned;
using Content.Shared._Goobstation.Exorcism;
using Content.Server.Actions;
using Content.Server.Administration.Systems;
using Content.Server.Antag.Components;
using Content.Server.Atmos.Components;
using Content.Server.Body.Systems;
using Content.Server.Destructible;
using Content.Server.Hands.Systems;
using Content.Server.Jittering;
using Content.Server.Mind;
using Content.Server.Polymorph.Systems;
using Content.Server.Popups;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Server.Stunnable;
using Content.Server.Temperature.Components;
using Content.Server.Zombies;
using Content.Shared._Lavaland.Chasm;
using Content.Shared._Shitmed.Body.Components;
using Content.Shared.Actions;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Shuttles.Components;
using Content.Shared.Temperature.Components;
using Robust.Server.Containers;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Shared.Body.Part;
using Content.Server.Bible.Components;
using Content.Shared._EE.Silicon.Components;
namespace Content.Server._Goobstation.Devil;
public sealed partial class DevilSystem : EntitySystem
{
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly PolymorphSystem _poly = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
[Dependency] private readonly DevilContractSystem _contract = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PossessionSystem _possession = default!;
[Dependency] private readonly CondemnedSystem _condemned = default!;
[Dependency] private readonly MobStateSystem _state = default!;
[Dependency] private readonly JitteringSystem _jittering = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly ContainerSystem _container = default!;
private static readonly Regex WhitespaceAndNonWordRegex = new(@"[\s\W]+", RegexOptions.Compiled);
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DevilComponent, MapInitEvent>(OnStartup);
SubscribeLocalEvent<DevilComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DevilComponent, ListenEvent>(OnListen);
SubscribeLocalEvent<DevilComponent, SoulAmountChangedEvent>(OnSoulAmountChanged);
SubscribeLocalEvent<DevilComponent, PowerLevelChangedEvent>(OnPowerLevelChanged);
SubscribeLocalEvent<DevilComponent, ExorcismDoAfterEvent>(OnExorcismDoAfter);
SubscribeLocalEvent<IdentityBlockerComponent, InventoryRelayedEvent<IsEyesCoveredCheckEvent>>(OnEyesCoveredCheckEvent);
InitializeHandshakeSystem();
SubscribeAbilities();
}
#region Startup & Remove
private void OnStartup(Entity<DevilComponent> devil, ref MapInitEvent args)
{
// Remove human components.
RemComp<CombatModeComponent>(devil);
RemComp<HungerComponent>(devil);
RemComp<ThirstComponent>(devil);
RemComp<TemperatureComponent>(devil);
RemComp<TemperatureSpeedComponent>(devil);
RemComp<CondemnedComponent>(devil);
RemComp<DestructibleComponent>(devil);
// Adjust stats
EnsureComp<ZombieImmuneComponent>(devil);
EnsureComp<BreathingImmunityComponent>(devil);
EnsureComp<PressureImmunityComponent>(devil);
EnsureComp<ActiveListenerComponent>(devil);
EnsureComp<WeakToHolyComponent>(devil).AlwaysTakeHoly = true;
EnsureComp<CrematoriumImmuneComponent>(devil);
EnsureComp<AntagImmuneComponent>(devil);
EnsureComp<PreventChasmFallingComponent>(devil).DeleteOnUse = false;
EnsureComp<FTLSmashImmuneComponent>(devil);
// Allow infinite revival
var revival = EnsureComp<CheatDeathComponent>(devil);
revival.InfiniteRevives = true;
revival.CanCheatStanding = true;
// Change damage modifier
if (TryComp<DamageableComponent>(devil, out var damageableComp))
_damageable.SetDamageModifierSetId(devil, devil.Comp.DevilDamageModifierSet, damageableComp);
// No decapitating the devil
foreach (var part in _body.GetBodyChildren(devil))
{
if (!TryComp(part.Id, out BodyPartComponent? woundable)) // DeltaV - Use Bodypart instead of woundable.
continue;
woundable.CanSever = false; // DeltaV - Use bodypart instead of Woundable
Dirty(part.Id, woundable);
}
// Add base actions
foreach (var actionId in devil.Comp.BaseDevilActions)
_actions.AddAction(devil, actionId);
// Self Explanatory
GenerateTrueName(devil);
}
#endregion
#region Event Listeners
private void OnSoulAmountChanged(Entity<DevilComponent> devil, ref SoulAmountChangedEvent args)
{
if (!_mind.TryGetMind(args.User, out var mindId, out var mind))
return;
devil.Comp.Souls += args.Amount;
_popup.PopupEntity(Loc.GetString("contract-soul-added"), args.User, args.User, PopupType.MediumCaution);
if (devil.Comp.Souls is > 1 and < 7 && devil.Comp.Souls % 2 == 0)
{
devil.Comp.PowerLevel = (DevilPowerLevel)(devil.Comp.Souls / 2); // malicious casting to enum
// Raise event
var ev = new PowerLevelChangedEvent(args.User, devil.Comp.PowerLevel);
RaiseLocalEvent(args.User, ref ev);
}
if (_mind.TryGetObjectiveComp<SignContractConditionComponent>(mindId, out var objectiveComp, mind))
objectiveComp.ContractsSigned += args.Amount;
}
private void OnPowerLevelChanged(Entity<DevilComponent> devil, ref PowerLevelChangedEvent args)
{
var popup = Loc.GetString($"devil-power-level-increase-{args.NewLevel.ToString().ToLowerInvariant()}");
_popup.PopupEntity(popup, args.User, args.User, PopupType.Large);
if (!_prototype.TryIndex(devil.Comp.DevilBranchPrototype, out var proto))
return;
foreach (var ability in proto.PowerActions)
{
if (args.NewLevel < ability.Key) // DeltaV - Just incase of admin shenanigans
continue;
foreach (var actionId in ability.Value)
_actions.AddAction(devil, actionId);
}
}
private void OnExamined(Entity<DevilComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange || ent.Comp.PowerLevel < DevilPowerLevel.Weak)
return;
var ev = new IsEyesCoveredCheckEvent();
RaiseLocalEvent(ent, ev);
if (ev.IsEyesProtected)
return;
args.PushMarkup(Loc.GetString("devil-component-examined", ("target", Identity.Entity(ent, EntityManager))));
}
private void OnEyesCoveredCheckEvent(Entity<IdentityBlockerComponent> ent, ref InventoryRelayedEvent<IsEyesCoveredCheckEvent> args)
{
if (ent.Comp.Enabled)
args.Args.IsEyesProtected = true;
}
private void OnListen(Entity<DevilComponent> devil, ref ListenEvent args)
{
// Other Devils and entities without souls have no authority over you.
if (HasComp<DevilComponent>(args.Source)
|| HasComp<CondemnedComponent>(args.Source)
|| HasComp<SiliconComponent>(args.Source)
|| args.Source == devil.Owner)
return;
var message = WhitespaceAndNonWordRegex.Replace(args.Message.ToLowerInvariant(), "");
var trueName = WhitespaceAndNonWordRegex.Replace(devil.Comp.TrueName.ToLowerInvariant(), "");
if (!message.Contains(trueName))
return;
// hardcoded, but this is just flavor so who cares :godo:
_jittering.DoJitter(devil, TimeSpan.FromSeconds(4), true);
if (_timing.CurTime < devil.Comp.LastTriggeredTime + devil.Comp.CooldownDuration)
return;
devil.Comp.LastTriggeredTime = _timing.CurTime;
if (HasComp<BibleUserComponent>(args.Source))
{
_damageable.TryChangeDamage(devil, devil.Comp.DamageOnTrueName * devil.Comp.BibleUserDamageMultiplier, true);
_stun.TryParalyze(devil, devil.Comp.ParalyzeDurationOnTrueName * devil.Comp.BibleUserDamageMultiplier, false);
var popup = Loc.GetString("devil-true-name-heard-chaplain", ("speaker", args.Source), ("target", devil));
_popup.PopupEntity(popup, devil, PopupType.LargeCaution);
}
else
{
_stun.TryParalyze(devil, devil.Comp.ParalyzeDurationOnTrueName, false);
_damageable.TryChangeDamage(devil, devil.Comp.DamageOnTrueName, true);
var popup = Loc.GetString("devil-true-name-heard", ("speaker", args.Source), ("target", devil));
_popup.PopupEntity(popup, devil, PopupType.LargeCaution);
}
}
private void OnExorcismDoAfter(Entity<DevilComponent> devil, ref ExorcismDoAfterEvent args)
{
if (args.Target is not { } target
|| args.Cancelled
|| args.Handled)
return;
_popup.PopupEntity(Loc.GetString("devil-exorcised", ("target", Name(devil))), devil, PopupType.LargeCaution);
_condemned.StartCondemnation(target, behavior: CondemnedBehavior.Banish, doFlavor: false);
}
#endregion
#region Helper Methods
private static bool TryUseAbility(BaseActionEvent action)
{
if (action.Handled)
return false;
action.Handled = true;
return true;
}
private void PlayFwooshSound(EntityUid uid, DevilComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
_audio.PlayPvs(comp.FwooshPath, uid, new AudioParams(-2f, 1f, SharedAudioSystem.DefaultSoundRange, 1f, false, 0f));
}
private void DoContractFlavor(EntityUid devil, string name)
{
var flavor = Loc.GetString("contract-summon-flavor", ("name", name));
_popup.PopupEntity(flavor, devil, PopupType.Medium);
}
private void GenerateTrueName(DevilComponent comp)
{
// Generate true name.
var firstNameOptions = _prototype.Index(comp.FirstNameTrue);
var lastNameOptions = _prototype.Index(comp.LastNameTrue);
comp.TrueName = string.Concat(_random.Pick(firstNameOptions.Values), " ", _random.Pick(lastNameOptions.Values));
}
#endregion
}

View File

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.NPC.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Devil.GameTicking.Rules;
[RegisterComponent, Access(typeof(DevilRuleSystem))]
public sealed partial class DevilRuleComponent : Component
{
[DataField]
public SoundPathSpecifier BriefingSound = new("/Audio/_Goobstation/Ambience/Antag/devil_start.ogg");
[DataField]
public ProtoId<NpcFactionPrototype> DevilFaction = "DevilFaction"; // DeltaV - Don't use literals.
[DataField]
public ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen"; // DeltaV - Don't use literals.
[DataField]
public EntProtoId DevilMindRole = "DevilMindRole";
}

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Text;
using Content.Server._Goobstation.Devil.Roles;
using Content.Shared._Goobstation.Devil;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Devil.GameTicking.Rules;
public sealed class DevilRuleSystem : GameRuleSystem<DevilRuleComponent>
{
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly ObjectivesSystem _objective = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DevilRuleComponent, AfterAntagEntitySelectedEvent>(OnSelectAntag);
SubscribeLocalEvent<DevilRuleComponent, ObjectivesTextPrependEvent>(OnTextPrepend);
SubscribeLocalEvent<DevilRoleComponent, GetBriefingEvent>(OnGetBrief);
}
private void OnSelectAntag(EntityUid uid, DevilRuleComponent comp, ref AfterAntagEntitySelectedEvent args)
{
MakeDevil(args.EntityUid, comp);
}
private bool MakeDevil(EntityUid target, DevilRuleComponent rule)
{
var devilComp = EnsureComp<DevilComponent>(target);
var briefing = Loc.GetString("devil-role-greeting", ("trueName", devilComp.TrueName), ("playerName", Name(target)));
_antag.SendBriefing(target, briefing, Color.DarkRed, rule.BriefingSound);
_npcFaction.RemoveFaction(target, rule.NanotrasenFaction);
_npcFaction.AddFaction(target, rule.DevilFaction);
return true;
}
private void OnGetBrief(Entity<DevilRoleComponent> role, ref GetBriefingEvent args)
{
var ent = args.Mind.Comp.OwnedEntity;
if (ent is null)
return;
args.Append(MakeBriefing(ent.Value));
}
private string MakeBriefing(EntityUid ent)
{
return !TryComp<DevilComponent>(ent, out var devilComp)
? null!
: Loc.GetString("devil-role-greeting", ("trueName", devilComp.TrueName), ("playerName", Name(ent)));
}
private void OnTextPrepend(EntityUid uid, DevilRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
var mostContractsName = string.Empty;
var mostContracts = 0f;
var query = EntityQueryEnumerator<DevilComponent>();
while (query.MoveNext(out var devil, out var devilComp))
{
if (!_mind.TryGetMind(devil, out var mindId, out var mind))
continue;
var metaData = MetaData(devil);
if (devilComp.Souls < mostContracts)
continue;
mostContracts = devilComp.Souls;
mostContractsName = _objective.GetTitle((mindId, mind), metaData.EntityName);
}
var sb = new StringBuilder();
sb.AppendLine(Loc.GetString($"roundend-prepend-devil-contracts{(!string.IsNullOrWhiteSpace(mostContractsName) ? "-named" : "")}", ("name", mostContractsName), ("number", mostContracts)));
args.Text = sb.ToString();
}
}

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
namespace Content.Server._Goobstation.Devil.Grip;
[RegisterComponent]
public sealed partial class DevilGripComponent : Component
{
[DataField]
public TimeSpan CooldownAfterUse = TimeSpan.FromSeconds(20);
/// <summary>
/// A blacklist of entities that cannot be gripped by the devil.
/// </summary>
[DataField]
public EntityWhitelist Blacklist = new();
[DataField]
public TimeSpan KnockdownTime = TimeSpan.FromSeconds(5f);
[DataField]
public float StaminaDamage = 80f;
[DataField]
public TimeSpan SpeechTime = TimeSpan.FromSeconds(10f);
[DataField]
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/_Goobstation/Effects/bone_crack.ogg");
[DataField]
public LocId Invocation = "devil-speech-grip";
}

View File

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Tim <timfalken@hotmail.com>
// SPDX-FileCopyrightText: 2025 Timfa <timfalken@hotmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Religion;
using Content.Server.Chat.Systems;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Actions;
using Content.Shared.Chat;
using Content.Shared.Damage.Systems;
using Content.Shared.Interaction;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
namespace Content.Server._Goobstation.Devil.Grip;
public sealed class DevilGripSystem : EntitySystem
{
[Dependency] private readonly SharedStaminaSystem _stamina = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RatvarianLanguageSystem _language = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DevilGripComponent, AfterInteractEvent>(OnAfterInteract);
}
private void OnAfterInteract(Entity<DevilGripComponent> ent, ref AfterInteractEvent args)
{
if (!args.CanReach
|| args.Target is not { } target
|| args.Target == args.User
|| _whitelist.IsBlacklistPass(ent.Comp.Blacklist, target)
|| !TryComp<DevilComponent>(args.User, out var devilComp))
return;
// DeltaV - No Recall or Intervention can work in this place, there is no escape.
// if (_divineIntervention.ShouldDeny(target))
// {
// _actions.SetCooldown(devilComp.DevilGrip, ent.Comp.CooldownAfterUse);
// devilComp.DevilGrip = null;
// InvokeGrasp(args.User, ent);
// QueueDel(ent);
// args.Handled = true;
// return;
// }
if (TryComp(target, out StatusEffectsComponent? status))
{
_ = _stun.TryKnockdown(target, ent.Comp.KnockdownTime, true, status) || _stun.TryStun(target, ent.Comp.KnockdownTime, true, status);
_stamina.TakeStaminaDamage(target, ent.Comp.StaminaDamage);
_language.DoRatvarian(target, ent.Comp.SpeechTime, true, status);
}
_actions.SetCooldown(devilComp.DevilGrip, ent.Comp.CooldownAfterUse);
devilComp.DevilGrip = null;
InvokeGrasp(args.User, ent);
QueueDel(ent);
args.Handled = true;
}
public void InvokeGrasp(EntityUid user, Entity<DevilGripComponent> ent)
{
_audio.PlayPvs(ent.Comp.Sound, user);
_chat.TrySendInGameICMessage(user, Loc.GetString(ent.Comp.Invocation), InGameICChatType.Speak, false);
}
}

View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server._Goobstation.Devil.Contract;
using Content.Server._Goobstation.Devil.Objectives.Systems;
namespace Content.Server._Goobstation.Devil.Objectives.Components;
[RegisterComponent, Access(typeof(DevilContractSystem), typeof(DevilObjectiveSystem))]
public sealed partial class MeetContractWeightConditionComponent : Component
{
[DataField]
public int ContractWeight;
}

View File

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server._Goobstation.Devil.Objectives.Systems;
namespace Content.Server._Goobstation.Devil.Objectives.Components;
[RegisterComponent, Access(typeof(DevilSystem), typeof(DevilObjectiveSystem))]
public sealed partial class SignContractConditionComponent : Component
{
[DataField]
public int ContractsSigned;
}

View File

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server._Goobstation.Devil.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Shared.Objectives.Components;
namespace Content.Server._Goobstation.Devil.Objectives.Systems;
public sealed partial class DevilObjectiveSystem : EntitySystem
{
[Dependency] private readonly NumberObjectiveSystem _number = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SignContractConditionComponent, ObjectiveGetProgressEvent>(OnContractGetProgress);
SubscribeLocalEvent<MeetContractWeightConditionComponent, ObjectiveGetProgressEvent>(OnWeightGetProgress);
}
private void OnContractGetProgress(EntityUid uid, SignContractConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
var target = _number.GetTarget(uid);
args.Progress = target != 0 ? MathF.Min((float)comp.ContractsSigned / target, 1f) : 1f;
}
private void OnWeightGetProgress(EntityUid uid, MeetContractWeightConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
var target = _number.GetTarget(uid);
args.Progress = target != 0 ? MathF.Min((float)comp.ContractWeight / target, 1f) : 1f;
}
}

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Roles;
namespace Content.Server._Goobstation.Devil.Roles;
[RegisterComponent]
public sealed partial class DevilRoleComponent : BaseMindRoleComponent;

View File

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Mind;
using Content.Shared.Polymorph;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Possession;
[RegisterComponent]
public sealed partial class PossessedComponent : Component
{
[ViewVariables]
public EntityUid OriginalMindId;
[ViewVariables]
public EntityUid OriginalEntity;
[ViewVariables]
public EntityUid PossessorMindId;
[ViewVariables]
public EntityUid PossessorOriginalEntity;
[ViewVariables]
public TimeSpan PossessionEndTime;
[ViewVariables]
public TimeSpan PossessionTimeRemaining;
[ViewVariables]
public bool WasPacified;
[ViewVariables]
public bool WasWeakToHoly;
[ViewVariables]
public Container PossessedContainer;
[DataField]
public EntProtoId<ActionComponent> EndPossessionAction = "ActionEndPossession";
[DataField]
public bool HideActions = true;
[ViewVariables]
public EntityUid? ActionEntity = null;
[ViewVariables]
public EntityUid[] HiddenActions;
[DataField]
public bool PolymorphEntity = true;
[DataField]
public ProtoId<PolymorphPrototype> Polymorph = new ("ShadowJauntPermanent");
[ViewVariables]
public readonly SoundPathSpecifier PossessionSoundPath = new ("/Audio/_Goobstation/Effects/bone_crack.ogg");
}

View File

@ -0,0 +1,281 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Possession;
using Content.Server.Actions;
using Content.Server.Polymorph.Components;
using Content.Server.Polymorph.Systems;
using Content.Server.Stunnable;
using Content.Shared.Administration.Logs;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Coordinates;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Mindshield.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Content.Shared.Zombies;
using Robust.Server.Containers;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Spawners;
using Robust.Shared.Timing;
using Content.Server.Bible.Components;
using Robust.Shared.Prototypes;
using Content.Shared._Goobstation.Religion;
namespace Content.Server._Goobstation.Possession;
public sealed partial class PossessionSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly ISharedAdminLogManager _admin = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly PolymorphSystem _polymorph = default!;
[Dependency] private readonly TagSystem _tag = default!;
private static readonly ProtoId<TagPrototype> CannotSuicideAnyTag = "CannotSuicideAny"; // DeltaV - Don't use literals.
private static readonly EntProtoId LolipopProto = "FoodLollipop"; // DeltaV - Don't use literals.
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PossessedComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<PossessedComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<PossessedComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<PossessedComponent, EndPossessionEarlyEvent>(OnEarlyEnd);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<PossessedComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (_timing.CurTime >= comp.PossessionEndTime)
RemComp<PossessedComponent>(uid);
comp.PossessionTimeRemaining = comp.PossessionEndTime - _timing.CurTime;
}
}
private void OnInit(Entity<PossessedComponent> possessed, ref MapInitEvent args)
{
if (!HasComp<WeakToHolyComponent>(possessed))
AddComp<WeakToHolyComponent>(possessed).AlwaysTakeHoly = true;
else
possessed.Comp.WasWeakToHoly = true;
if (possessed.Comp.HideActions)
possessed.Comp.HiddenActions = _action.HideActions(possessed);
_action.AddAction(possessed, ref possessed.Comp.ActionEntity, possessed.Comp.EndPossessionAction);
_tag.AddTag(possessed, CannotSuicideAnyTag); // DeltaV - Use ProtoId.
possessed.Comp.PossessedContainer = _container.EnsureContainer<Container>(possessed, "PossessedContainer");
}
private void OnEarlyEnd(EntityUid uid, PossessedComponent comp, ref EndPossessionEarlyEvent args)
{
if (args.Handled)
return;
// if polymorphed, undo
_polymorph.Revert(uid);
RemCompDeferred(uid, comp);
args.Handled = true;
}
private void OnComponentRemoved(Entity<PossessedComponent> possessed, ref ComponentRemove args)
{
MapCoordinates? coordinates = null;
_action.RemoveAction(possessed.Owner, possessed.Comp.ActionEntity);
if (possessed.Comp.HideActions)
_action.UnHideActions(possessed, possessed.Comp.HiddenActions);
if (possessed.Comp.PolymorphEntity && HasComp<PolymorphedEntityComponent>(possessed))
_polymorph.Revert(possessed.Owner);
_tag.RemoveTag(possessed, CannotSuicideAnyTag); // DeltaV - Use ProtoId.
// Remove associated components.
if (!possessed.Comp.WasPacified)
RemComp<PacifiedComponent>(possessed.Comp.OriginalEntity);
if (!possessed.Comp.WasWeakToHoly)
RemComp<WeakToHolyComponent>(possessed.Comp.OriginalEntity);
// Return the possessors mind to their body, and the target to theirs.
if (!TerminatingOrDeleted(possessed.Comp.PossessorMindId))
_mind.TransferTo(possessed.Comp.PossessorMindId, possessed.Comp.PossessorOriginalEntity);
if (!TerminatingOrDeleted(possessed.Comp.OriginalMindId))
_mind.TransferTo(possessed.Comp.OriginalMindId, possessed.Comp.OriginalEntity);
if (!TerminatingOrDeleted(possessed.Comp.OriginalEntity))
coordinates = _transform.ToMapCoordinates(possessed.Comp.OriginalEntity.ToCoordinates());
// Paralyze, so you can't just magdump them.
_stun.TryParalyze(possessed, TimeSpan.FromSeconds(10), false);
_popup.PopupEntity(Loc.GetString("possession-end-popup", ("target", possessed)), possessed, PopupType.LargeCaution);
// Teleport to the entity, kinda like you're popping out of their head!
if (!TerminatingOrDeleted(possessed.Comp.PossessorOriginalEntity) && coordinates is not null)
_transform.SetMapCoordinates(possessed.Comp.PossessorOriginalEntity, coordinates.Value);
_container.CleanContainer(possessed.Comp.PossessedContainer);
}
private void OnExamined(Entity<PossessedComponent> possessed, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange
|| args.Examined != args.Examiner)
return;
var timeRemaining = Math.Floor(possessed.Comp.PossessionTimeRemaining.TotalSeconds);
args.PushMarkup(Loc.GetString("possessed-component-examined", ("timeremaining", timeRemaining)));
}
/// <summary>
/// Attempts to temporarily possess a target.
/// </summary>
/// <param name="possessed">The entity being possessed.</param>
/// <param name="possessor">The entity possessing the previous entity.</param>
/// <param name="possessionDuration">How long does the possession last in seconds.</param>
/// <param name="pacifyPossessed">Should the possessor be pacified while inside the possessed body?</param>
/// <param name="doesMindshieldBlock">Does having a mindshield block being possessed?</param>
/// <param name="doesChaplainBlock">Is the chaplain immune to this possession?</param>
/// <param name="HideActions">Should all actions be hidden during?</param>
public bool TryPossessTarget(EntityUid possessed, EntityUid possessor, TimeSpan possessionDuration, bool pacifyPossessed, bool doesMindshieldBlock = false, bool doesChaplainBlock = true, bool hideActions = true, bool polymorphPossessor = true)
{
// Possessing a dead guy? What.
if (_mobState.IsIncapacitated(possessed) || HasComp<ZombieComponent>(possessed))
{
_popup.PopupClient(Loc.GetString("possession-fail-target-dead"), possessor, possessor);
return false;
}
// if you ever wanted to prevent this
if (doesMindshieldBlock && HasComp<MindShieldComponent>(possessed))
{
_popup.PopupClient(Loc.GetString("possession-fail-target-shielded"), possessor, possessor);
return false;
}
if (doesChaplainBlock && HasComp<BibleUserComponent>(possessed))
{
_popup.PopupClient(Loc.GetString("possession-fail-target-chaplain"), possessor, possessor);
return false;
}
if (HasComp<PossessedComponent>(possessed))
{
_popup.PopupClient(Loc.GetString("possession-fail-target-already-possessed"), possessor, possessor);
return false;
}
List<(Type, string)> blockers =
[
(typeof(DevilComponent), "devil"),
(typeof(GhostComponent), "ghost"),
(typeof(SpectralComponent), "ghost"),
(typeof(TimedDespawnComponent), "temporary"),
];
foreach (var (item1, item2) in blockers)
{
if (CheckMindswapBlocker(item1, item2, possessed, possessor))
return false;
}
if (!_mind.TryGetMind(possessor, out var possessorMind, out _))
return false;
DoPossess(possessed, possessor, possessionDuration, possessorMind, pacifyPossessed, hideActions, polymorphPossessor);
return true;
}
private void DoPossess(EntityUid? possessedNullable, EntityUid possessor, TimeSpan possessionDuration, EntityUid possessorMind, bool pacifyPossessed, bool hideActions, bool polymorphPossessor)
{
if (possessedNullable is not { } possessed)
return;
var possessedComp = EnsureComp<PossessedComponent>(possessed);
possessedComp.HideActions = hideActions;
if (pacifyPossessed)
{
if (!HasComp<PacifiedComponent>(possessed))
EnsureComp<PacifiedComponent>(possessed);
else
possessedComp.WasPacified = true;
}
possessedComp.PolymorphEntity = polymorphPossessor;
if (polymorphPossessor)
_polymorph.PolymorphEntity(possessor, possessedComp.Polymorph);
// Get the possession time.
possessedComp.PossessionEndTime = _timing.CurTime + possessionDuration;
// Store possessors original information.
possessedComp.PossessorOriginalEntity = possessor;
possessedComp.PossessorMindId = possessorMind;
// Store possessed original info
possessedComp.OriginalEntity = possessed;
if (_mind.TryGetMind(possessed, out var possessedMind, out _))
{
possessedComp.OriginalMindId = possessedMind;
// Nobodies gonna know.
var dummy = Spawn(LolipopProto, MapCoordinates.Nullspace);
_container.Insert(dummy, possessedComp.PossessedContainer);
_mind.TransferTo(possessedMind, dummy);
}
// Transfer into target
_mind.TransferTo(possessorMind, possessed);
// SFX
_popup.PopupEntity(Loc.GetString("possession-popup-self"), possessedMind, possessedMind, PopupType.LargeCaution);
_popup.PopupEntity(Loc.GetString("possession-popup-others", ("target", possessed)), possessed, PopupType.MediumCaution);
_audio.PlayPvs(possessedComp.PossessionSoundPath, possessed);
Log.Info($"{ToPrettyString(possessor)} possessed {ToPrettyString(possessed)}");
_admin.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(possessor)} possessed {ToPrettyString(possessed)}");
}
private bool CheckMindswapBlocker(Type type, string message, EntityUid possessed, EntityUid possessor)
{
if (!HasComp(possessed, type))
return false;
_popup.PopupClient(Loc.GetString($"possession-fail-{message}"), possessor, possessor);
return true;
}
}

View File

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <linebarrelerenthusiast@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Linq;
using Content.Shared._Goobstation.Religion;
using Content.Server._Goobstation.Bible;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
using Content.Shared._Shitmed.Targeting;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Timing;
using Content.Server.Bible.Components;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Prototypes; // Shitmed Change
namespace Content.Server._Goobstation.Religion;
public sealed class WeakToHolySystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly GoobBibleSystem _goobBible = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WeakToHolyComponent, InteractUsingEvent>(AfterBibleUse);
SubscribeLocalEvent<WeakToHolyComponent, MapInitEvent>(OnInit); // DeltaV - Add damage set
SubscribeLocalEvent<WeakToHolyComponent, ComponentRemove>(OnRemove); // DeltaV - Clean up on removal
}
// Begin DeltaV Additions - Holy Weakness
private void OnInit(Entity<WeakToHolyComponent> ent, ref MapInitEvent args)
{
if (!TryComp<DamageableComponent>(ent, out var damageable))
return;
var dmg = damageable.Damage;
if (dmg.DamageDict.ContainsKey("Holy"))
{
ent.Comp.HadHolyWeakness = true;
return;
}
dmg.DamageDict["Holy"] = 0;
}
private void OnRemove(Entity<WeakToHolyComponent> ent, ref ComponentRemove args)
{
if (!TryComp<DamageableComponent>(ent, out var damageable))
return;
if (!ent.Comp.HadHolyWeakness)
{
var dmg = damageable.Damage;
dmg.DamageDict.Remove("Holy");
}
}
// End DeltaV additions
private void AfterBibleUse(Entity<WeakToHolyComponent> ent, ref InteractUsingEvent args)
{
if (!TryComp<BibleComponent>(args.Used, out var bibleComp))
return;
if (!TryComp(args.Used, out UseDelayComponent? useDelay)
|| _useDelay.IsDelayed((args.Used, useDelay))
|| !HasComp<BibleUserComponent>(args.User))
return;
_goobBible.TryDoSmite(args.Used, args.User, args.Target, useDelay);
}
#region Holy Healing
public override void Update(float frameTime)
{
base.Update(frameTime);
// Holy damage healing.
var query = EntityQueryEnumerator<WeakToHolyComponent, BodyComponent>();
while (query.MoveNext(out var uid, out var weakToHoly, out var body))
{
if (weakToHoly.NextPassiveHealTick > _timing.CurTime)
continue;
weakToHoly.NextPassiveHealTick = _timing.CurTime + weakToHoly.HealTickDelay;
if (!TryComp<DamageableComponent>(uid, out var damageable))
continue;
if (TerminatingOrDeleted(uid)
|| _body.GetRootPartOrNull(uid, body: body) is not { }
|| !damageable.Damage.DamageDict.TryGetValue("Holy", out _))
continue;
// Rune healing.
if (weakToHoly.IsColliding)
_damageableSystem.TryChangeDamage(uid, weakToHoly.HealAmount, ignoreResistances: true, targetPart: TargetBodyPart.All);
// Passive healing.
_damageableSystem.TryChangeDamage(uid, weakToHoly.PassiveAmount, ignoreResistances: true, targetPart: TargetBodyPart.All);
}
}
#endregion
}

View File

@ -13,4 +13,25 @@ public sealed partial class DelayedDeathComponent : Component
/// How long it has been since the delayed death timer started.
/// </summary>
public float DeathTimer;
// Goobstation additions below
/// <summary>
/// If true, will prevent *almost* all types of revival.
/// Right now, this just means it won't allow devils to revive.
/// </summary>
[DataField]
public bool PreventAllRevives;
/// <summary>
/// What message is displayed when the time runs out - Goobstation
/// </summary>
[DataField]
public LocId DeathMessageId;
/// <summary>
/// What the defib displays when attempting to revive this entity. - Goobstation
/// </summary>
[DataField]
public LocId DefibFailMessageId = "defibrillator-missing-organs";
// End Goobstation additions
}

View File

@ -1,13 +1,18 @@
using Content.Server.Chat.Systems;
using Content.Shared._Goobstation.DelayedDeath;
using Content.Shared.Medical;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
namespace Content.Server._Shitmed.DelayedDeath;
public partial class DelayedDeathSystem : EntitySystem
{
[Dependency] private readonly ChatSystem _chat = default!; // Goobstation
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!; // Goobstation
public override void Initialize()
{
@ -30,6 +35,19 @@ public partial class DelayedDeathSystem : EntitySystem
// go crit then dead so deathgasp can happen
_mobState.ChangeMobState(ent, MobState.Critical, mob);
_mobState.ChangeMobState(ent, MobState.Dead, mob);
// goob code
var ev = new DelayedDeathEvent(ent, PreventRevive: comp.PreventAllRevives);
RaiseLocalEvent(ent, ref ev);
if (ev.Cancelled)
{
RemCompDeferred<DelayedDeathComponent>(ent);
continue;
}
if (!string.IsNullOrWhiteSpace(comp.DeathMessageId)) // Goobstation
_popupSystem.PopupEntity(Loc.GetString(comp.DeathMessageId), ent, ent, PopupType.LargeCaution);
}
}
}
@ -38,5 +56,8 @@ public partial class DelayedDeathSystem : EntitySystem
{
// can't defib someone without a heart or brain pal
args.Cancel();
var failPopup = Loc.GetString(ent.Comp.DefibFailMessageId); // Goobstation
_chat.TrySendInGameICMessage(args.Defib, failPopup, InGameICChatType.Speak, true);
}
}

View File

@ -1037,4 +1037,27 @@ public abstract class SharedActionsSystem : EntitySystem
ent.Comp.Temporary = temporary;
Dirty(ent);
}
// Shitmed Change Start - Starlight Abductors
public EntityUid[] HideActions(EntityUid performer, ActionsComponent? comp = null)
{
if (!Resolve(performer, ref comp, false))
return [];
var actions = comp.Actions.ToArray();
comp.Actions.Clear();
Dirty(performer, comp);
return actions;
}
public void UnHideActions(EntityUid performer, EntityUid[] actions, ActionsComponent? comp = null)
{
if (!Resolve(performer, ref comp, false))
return;
foreach (var action in actions)
comp.Actions.Add(action);
Dirty(performer, comp);
}
// Shitmed Change End
}

View File

@ -340,7 +340,7 @@ public partial class SharedBodySystem
AddLeg(partEnt, bodyEnt);
}
protected virtual void RemovePart(
public virtual void RemovePart( // DeltaV - Made public
Entity<BodyComponent?> bodyEnt,
Entity<BodyPartComponent> partEnt,
string slotId)

View File

@ -43,6 +43,17 @@ public sealed class ChasmSystem : EntitySystem
if (_timing.CurTime < chasm.NextDeletionTime)
continue;
// Lavaland Change start: Jaunter
var ev = new _Lavaland.Chasm.BeforeChasmFallingEvent(uid);
RaiseLocalEvent(uid, ref ev);
if (ev.Cancelled)
{
RemComp<ChasmFallingComponent>(uid);
_blocker.UpdateCanMove(uid);
continue;
}
// Lavaland Change end: Jaunter
QueueDel(uid);
}
}

View File

@ -1,3 +1,4 @@
using Content.Shared._Goobstation.Devour;
using Content.Shared.Actions;
using Content.Shared.Body.Events;
using Content.Shared.Body.Systems;
@ -115,6 +116,13 @@ public sealed class DevourSystem : EntitySystem
if (args.Args.Target != null && _whitelistSystem.IsWhitelistPass(ent.Comp.StomachStorageWhitelist, (EntityUid)args.Args.Target))
{
_containerSystem.Insert(args.Args.Target.Value, ent.Comp.Stomach);
// Goobstation start
if (HasComp<MobStateComponent>(args.Args.Target.Value)) // can be cases where objects are also whitelisted, which wont need this
EnsureComp<PreventSelfRevivalComponent>(args.Args.Target.Value);
// Goobstation end
}
//TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow.
//If it's not alive, it must be a structure.

View File

@ -6,6 +6,8 @@ using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Paper;
namespace Content.Shared._DV.Paper;
@ -61,6 +63,11 @@ public sealed class SignatureSystem : EntitySystem
if (ev.Cancelled)
return false;
var paperEvent = new BeingSignedAttemptEvent(paper, signer); // Goobstation
RaiseLocalEvent(paper.Owner, ref paperEvent);
if (paperEvent.Cancelled)
return false;
var signatureName = DetermineEntitySignature(signer);
var stampInfo = new StampDisplayInfo()
@ -78,20 +85,30 @@ public sealed class SignatureSystem : EntitySystem
return false;
}
// Show popups and play a paper writing sound
var signedOtherMessage = Loc.GetString("paper-signed-other", ("user", signer), ("target", paper.Owner));
_popup.PopupEntity(signedOtherMessage, signer, Filter.PvsExcept(signer, entityManager: EntityManager), true);
if (!HasComp<DevilComponent>(signer)) // Goobstation - Don't display popups for devils, it covers the others.
{
// Show popups and play a paper writing sound
var signedOtherMessage = Loc.GetString("paper-signed-other", ("user", signer), ("target", paper.Owner));
_popup.PopupEntity(signedOtherMessage, signer, Filter.PvsExcept(signer, entityManager: EntityManager), true);
var signedSelfMessage = Loc.GetString("paper-signed-self", ("target", paper.Owner));
_popup.PopupClient(signedSelfMessage, signer, signer);
var signedSelfMessage = Loc.GetString("paper-signed-self", ("target", paper.Owner));
_popup.PopupClient(signedSelfMessage, signer, signer);
}
_audio.PlayPredicted(comp.Sound, signer, signer);
var evSignSucessfulEvent = new SignSuccessfulEvent(paper, signer); // Goobstation - Devil Antagonist
RaiseLocalEvent(paper, ref evSignSucessfulEvent); // Goobstation - Devil Antagonist
return true;
}
private string DetermineEntitySignature(EntityUid uid)
{
// Goobstation - Allow devils to sign their true name.
if (TryComp<DevilComponent>(uid, out var devilComp) && !string.IsNullOrWhiteSpace(devilComp.TrueName))
return devilComp.TrueName;
// If the entity has an ID, use the name on it.
if (_idCard.TryFindIdCard(uid, out var id) && !string.IsNullOrWhiteSpace(id.Comp.FullName))
{

View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Marcus F <marcus2008stoke@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Atmos;
/// <summary>
/// Used to ensure that PressureImmunityComponent is not overriden.
/// </summary>
[RegisterComponent]
public sealed partial class SpecialPressureImmunityComponent : Component;

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Marcus F <199992874+thebiggestbruh@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Marcus F <marcus2008stoke@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Body.Components;
/// <summary>
/// Used to ensure that BreathingImmunityComponent is not overriden.
/// </summary>
[RegisterComponent]
public sealed partial class SpecialBreathingImmunityComponent : Component;

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Actions;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.CheatDeath;
[RegisterComponent, NetworkedComponent]
public sealed partial class CheatDeathComponent : Component
{
/// <summary>
/// How many revives does this entity have remaining.
/// </summary>
[DataField]
public int ReviveAmount = 1;
/// <summary>
/// Self-explanatory.
/// </summary>
[DataField]
public bool InfiniteRevives;
/// <summary>
/// Can this entity heal themselves while not being dead?
/// </summary>
[DataField]
public bool CanCheatStanding;
[DataField]
public EntProtoId ActionCheatDeath = "ActionCheatDeath";
[DataField]
public EntityUid? ActionEntity;
}
public sealed partial class CheatDeathEvent : InstantActionEvent { }

View File

@ -0,0 +1,6 @@
namespace Content.Shared._Goobstation.CrematorImmune;
[RegisterComponent]
public sealed partial class CrematoriumImmuneComponent : Component
{
}

View File

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.DelayedDeath;
/// <summary>
/// Raised on a user when delayed death is triggered on them.
/// (E.G, they die to it.)
/// </summary>
[ByRefEvent]
public record struct DelayedDeathEvent(EntityUid User, bool Cancelled = false, bool PreventRevive = true);

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Actions;
namespace Content.Shared._Goobstation.Devil.Actions;
public sealed partial class CreateContractEvent : InstantActionEvent;
public sealed partial class CreateRevivalContractEvent : InstantActionEvent;
public sealed partial class ShadowJauntEvent : InstantActionEvent;
public sealed partial class DevilGripEvent : InstantActionEvent;
public sealed partial class DevilPossessionEvent : EntityTargetActionEvent;

View File

@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Polymorph;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Devil.Condemned;
/// <summary>
/// Marks an entity as having sold their soul.
/// When you die, do NOT collect 200, do NOT pass go. Go directly to hell
/// </summary>
/// <remarks>
/// This should *really* be two components.
/// </remarks>
[RegisterComponent]
public sealed partial class CondemnedComponent : Component
{
/// <summary>
/// The current phase of the condemnation animation.
/// </summary>
[DataField]
public CondemnedPhase CurrentPhase = CondemnedPhase.Waiting;
/// <summary>
/// Who owns this entities soul
/// </summary>
[DataField]
public EntityUid? SoulOwner;
/// <summary>
/// The elapsed time of the phase.
/// </summary>
[DataField]
public float PhaseTimer;
/// <summary>
/// How long the hand effect will last
/// </summary>
[DataField]
public float HandDuration;
/// <summary>
/// Should the examine message show when examining someone with this component?
/// </summary>
[DataField("showExamine")]
public bool ShowExamineMessage = true;
/// <summary>
/// Is this entities soul owned, but not by a devil?
/// </summary>
[DataField]
public bool SoulOwnedNotDevil;
/// <summary>
/// Should this entity be sent to hell on death?
/// </summary>
[DataField]
public bool CondemnOnDeath;
/// <summary>
/// Was this target already weak to holy before becoming condemned?
/// </summary>
[DataField]
public bool WasWeakToHoly;
/// <summary>
/// Should movement be locked during the animation?
/// </summary>
[DataField]
public bool FreezeDuringCondemnation;
/// <summary>
/// If true, scrambles the targets DNA after banishing them.
/// </summary>
[DataField]
public bool ScrambleAfterBanish = true;
/// <summary>
/// Should this entity be banished (sent to limbo for several minutes) or should they just be deleted?
/// </summary>
[DataField]
public CondemnedBehavior CondemnedBehavior = CondemnedBehavior.Delete;
[DataField]
public EntProtoId PentagramProto = "Pentagram";
[DataField]
public EntProtoId HandProto = "HellHand";
[DataField]
public SoundPathSpecifier SoundEffect = new("/Audio/_Goobstation/Effects/earth_quake.ogg");
[DataField]
public ProtoId<PolymorphPrototype> BanishProto = "ShadowJaunt180";
}
public enum CondemnedPhase : byte
{
Waiting,
PentagramActive,
HandActive,
Complete
}
public enum CondemnedBehavior : byte
{
Delete,
Banish,
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Devil.Contract;
[RegisterComponent]
public sealed partial class ContractSignerComponent : Component
{
/// <summary>
/// The contract entity itself.
/// </summary>
[DataField]
public EntityUid? Contract;
/// <summary>
/// The contract component.
/// </summary>
[DataField]
public DevilContractComponent ContractComponent;
/// <summary>
/// All current clauses the entity is under the effect of.
/// </summary>
[DataField]
public List<DevilClausePrototype> CurrentClauses = [];
}

View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Polymorph;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Devil.Contract;
[Prototype("clause")]
public sealed class DevilClausePrototype : IPrototype
{
[IdDataField]
public string ID { get; private init; } = default!;
[DataField(required: true)]
public int ClauseWeight;
[DataField]
public ComponentRegistry? AddedComponents;
[DataField]
public ComponentRegistry? RemovedComponents;
[DataField]
public ComponentRegistry? OverriddenComponents; // DeltaV - Added overridden components
[DataField]
public string? DamageModifierSet;
[DataField]
public BaseDevilContractEvent? Event;
[DataField]
public List<EntProtoId>? Implants;
[DataField]
public List<EntProtoId>? SpawnedItems;
[DataField]
public ProtoId<PolymorphPrototype>? Polymorph;
}

View File

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Devil.Contract;
[RegisterComponent]
public sealed partial class DevilContractComponent : Component
{
/// <summary>
/// The entity who signed the paper, AKA, the entity who has the effects applied.
/// </summary>
[DataField]
public EntityUid? Signer;
/// <summary>
/// The entity who created the contract, AKA, the entity who gains the soul.
/// </summary>
[DataField]
public EntityUid? ContractOwner;
/// <summary>
/// All current clauses.
/// </summary>
[DataField]
public HashSet<DevilClausePrototype> CurrentClauses = [];
/// <summary>
/// Has the contract been signed by the signer?
/// </summary>
[DataField]
public bool IsVictimSigned;
/// <summary>
/// Has the contract been signed by the devil?
/// </summary>
[DataField]
public bool IsDevilSigned;
/// <summary>
/// Has the contract been signed by both the devil and the victim?
/// </summary>
public bool IsContractFullySigned => IsVictimSigned && IsDevilSigned;
public bool IsContractSignable => ContractWeight >= 0;
public bool CanApplyEffects => IsContractFullySigned && IsContractSignable && Signer != null && ContractOwner != null;
/// <summary>
/// Does the contract weigh positively or negatively?
/// </summary>
/// <remarks>
/// The higher it is, the more the cons.
/// </remarks>
[DataField]
public int ContractWeight;
}

View File

@ -0,0 +1,21 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Devil;
[Prototype("devilBranchPrototype")]
public sealed class DevilBranchPrototype : IPrototype
{
[IdDataField]
public string ID { get; set; } = default!;
[DataField("powerActions", required: true)]
public Dictionary<DevilPowerLevel, List<EntProtoId>> PowerActions = new();
}
public enum DevilPowerLevel : byte
{
None,
Weak,
Moderate,
Powerful,
}

View File

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Dataset;
using Content.Shared.Polymorph;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Content.Shared.FixedPoint;
namespace Content.Shared._Goobstation.Devil;
[RegisterComponent]
public sealed partial class DevilComponent : Component
{
[DataField]
public List<EntProtoId> BaseDevilActions = new()
{
"ActionCreateContract",
"ActionShadowJaunt",
"ActionDevilGrip",
};
[DataField]
public List<EntityUid>? ActionEntities;
/// <summary>
/// The amount of souls or successful contracts the entity has.
/// </summary>
[DataField]
public int Souls;
[DataField]
public ProtoId<DevilBranchPrototype> DevilBranchPrototype = "BaseDevilBranch";
/// <summary>
/// The true name of the devil.
/// This is auto-generated from a list in the system.
/// </summary>
[DataField]
public string TrueName = string.Empty;
/// <summary>
/// The current power level of the devil.
/// </summary>
[DataField]
public DevilPowerLevel PowerLevel = 0;
/// <summary>
/// Sound effect played when summoning a contract.
/// </summary>
[DataField]
public SoundPathSpecifier FwooshPath = new ("/Audio/_Goobstation/Effects/fwoosh.ogg");
/// <summary>
/// When the true-name stun was last triggered
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan LastTriggeredTime;
/// <summary>
/// Minimum time between true-name triggers
/// </summary>
[DataField]
public TimeSpan CooldownDuration = TimeSpan.FromSeconds(30);
[DataField]
public ProtoId<DatasetPrototype> FirstNameTrue = new("names_devil_first");
[DataField]
public ProtoId<DatasetPrototype> LastNameTrue = new("names_devil_last");
/// <summary>
/// How much damage taken when a true name is spoken. Doubled if spoken by the chaplain.
/// </summary>
[DataField]
public DamageSpecifier DamageOnTrueName = new() {DamageDict = new Dictionary<string, FixedPoint2>() {{ "Holy", 15 }}};
/// <summary>
/// Holy action damage multiplier if done by the chaplain. Also effects stums.
/// </summary>
[DataField]
public float BibleUserDamageMultiplier = 2f;
/// <summary>
/// How long the Devil is stunned when their true name is spoken. Doubled if spoken by the chaplain.
/// </summary>
[DataField]
public TimeSpan ParalyzeDurationOnTrueName = TimeSpan.FromSeconds(4);
[ViewVariables(VVAccess.ReadOnly)]
public EntityUid? DevilGrip;
// abandom all hope, all ye who enter
[DataField]
public TimeSpan PossessionDuration = TimeSpan.FromSeconds(30);
[DataField]
public EntProtoId GripPrototype = "DevilGrip";
[DataField]
public EntProtoId ContractPrototype = "PaperDevilContract";
[DataField]
public EntProtoId RevivalContractPrototype = "PaperDevilContractRevival";
[DataField]
public EntProtoId PentagramEffectProto = "Pentagram";
[DataField]
public EntProtoId FireEffectProto = "FireEffect";
[DataField]
public EntProtoId JauntAnimationProto = "PolymorphShadowJauntAnimation";
[DataField]
public ProtoId<PolymorphPrototype> JauntEntityProto = "ShadowJaunt";
[DataField]
public ProtoId<DamageModifierSetPrototype> DevilDamageModifierSet = "DevilDealPositive";
}

View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil.Contract;
using Content.Shared.Inventory;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Devil;
/// <summary>
/// Raised on a devil when their power level changes.
/// </summary>
/// <param name="User">The Devil whos power level is changing</param>
/// <param name="NewLevel">The new level they are reaching.</param>
[ByRefEvent]
public record struct PowerLevelChangedEvent(EntityUid User, DevilPowerLevel NewLevel);
/// <summary>
/// Raised on a devil when the amount of souls in their storage changes.
/// </summary>
/// <param name="User">The Devil gaining souls.</param>
/// <param name="Victim">The entity losing its soul.</param>
/// <param name="Amount">How many souls they are gaining.</param>
[ByRefEvent]
public record struct SoulAmountChangedEvent(EntityUid User, EntityUid Victim, int Amount);
/// <summary>
/// Raised on an entity to see if their eyes are covered.
/// This just checks for the identity blocker comp.
/// </summary>
/// <param name="Target"></param>
public sealed class IsEyesCoveredCheckEvent : EntityEventArgs, IInventoryRelayEvent
{
public SlotFlags TargetSlots => SlotFlags.EYES | SlotFlags.MASK | SlotFlags.HEAD;
public bool IsEyesProtected;
}
// Contract Events
[ImplicitDataDefinitionForInheritors, DataDefinition]
public abstract partial class BaseDevilContractEvent : EntityEventArgs
{
/// <summary>
/// The contract using this event.
/// </summary>
public DevilContractComponent? Contract;
/// <summary>
/// The target affected by this contract.
/// </summary>
public EntityUid Target;
}
[DataDefinition, Serializable]
public sealed partial class DevilContractSoulOwnershipEvent : BaseDevilContractEvent;
[DataDefinition, Serializable]
public sealed partial class DevilContractLoseHandEvent : BaseDevilContractEvent;
[DataDefinition, Serializable]
public sealed partial class DevilContractLoseLegEvent : BaseDevilContractEvent;
[DataDefinition, Serializable]
public sealed partial class DevilContractLoseOrganEvent : BaseDevilContractEvent;
[DataDefinition, Serializable]
public sealed partial class DevilContractChanceEvent : BaseDevilContractEvent;

View File

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Devil;
[RegisterComponent]
public sealed partial class PendingHandshakeComponent : Component
{
[DataField]
public EntityUid? Offerer;
[DataField]
public TimeSpan ExpiryTime;
}

View File

@ -0,0 +1,10 @@
using Robust.Shared.Player;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Devil.UI;
[Serializable, NetSerializable]
public sealed class RevivalContractMessage(bool accepted) : BoundUserInterfaceMessage
{
public bool Accepted { get; } = accepted;
}

View File

@ -0,0 +1,8 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Devil.UI;
[Serializable, NetSerializable]
public enum RevivalContractUiKey
{
Key,
}

View File

@ -0,0 +1,8 @@
namespace Content.Shared._Goobstation.Devour.Events;
[ByRefEvent]
public record struct BeforeSelfRevivalEvent(
EntityUid Target,
LocId PopupText,
bool Handled = false,
bool Cancelled = false);

View File

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Devour;
/// <summary>
/// Used to mark an entity as being unable to self-revive (e.g preventing Changelings from using their stasis)
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PreventSelfRevivalComponent : Component;

View File

@ -0,0 +1,26 @@
using Content.Shared._Goobstation.Devour;
using Content.Shared._Goobstation.Devour.Events;
using Content.Shared.Popups;
namespace Content.Shared._Goobstation.Devour.Systems;
public sealed class PreventSelfRevivalSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PreventSelfRevivalComponent, BeforeSelfRevivalEvent>(OnAttemptSelfRevive);
}
private void OnAttemptSelfRevive(Entity<PreventSelfRevivalComponent> ent, ref BeforeSelfRevivalEvent args)
{
if (args.Handled || args.Cancelled)
return;
_popup.PopupEntity(Loc.GetString(args.PopupText), args.Target, args.Target, PopupType.SmallCaution);
args.Cancelled = true;
}
}

View File

@ -0,0 +1,7 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Exorcism;
[Serializable, NetSerializable]
public sealed partial class ExorcismDoAfterEvent : SimpleDoAfterEvent;

View File

@ -9,6 +9,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Devil;
using Content.Shared._Goobstation.Flashbang;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
@ -23,6 +24,7 @@ public partial class GoobInventorySystem
{
base.Initialize();
SubscribeLocalEvent<InventoryComponent, FlashDurationMultiplierEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, IsEyesCoveredCheckEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<Overlays.NightVisionComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<Overlays.ThermalVisionComponent>>(RefRelayInventoryEvent);
}

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Paper;
/// <summary>
/// Raised on the paper when a sign is attempted
/// </summary>
[ByRefEvent]
public record struct BeingSignedAttemptEvent(EntityUid Paper, EntityUid Signer, bool Cancelled = false);

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Paper;
/// <summary>
/// Raised on the paper when a sign is successful.
/// </summary>
[ByRefEvent]
public record struct SignSuccessfulEvent(EntityUid Paper, EntityUid User);

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Actions;
namespace Content.Shared._Goobstation.Possession;
public sealed partial class EndPossessionEarlyEvent : InstantActionEvent;

View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Aviu00 <aviu00@protonmail.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
// SPDX-FileCopyrightText: 2025 mikusssssss <153551970+mikusssssss@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared._Goobstation.Religion;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class WeakToHolyComponent : Component
{
/// <summary>
/// Should this entity take holy damage no matter what?
/// </summary>
[DataField, AutoNetworkedField]
public bool AlwaysTakeHoly;
/// <summary>
/// Is the entity currently standing on a rune?
/// </summary>
[ViewVariables]
public bool IsColliding;
/// <summary>
/// Duration between each heal tick.
/// </summary>
[DataField]
public TimeSpan HealTickDelay = TimeSpan.FromSeconds(2);
/// <summary>
/// Used for passive healing.
/// </summary>
[ViewVariables]
public TimeSpan NextPassiveHealTick;
/// <summary>
/// DeltaV - Was this critter already holy damagable?
/// </summary>
[DataField]
public bool HadHolyWeakness = false;
/// <summary>
/// How much the entity is healed by runes each tick.
/// </summary>
[DataField]
public DamageSpecifier HealAmount = new()
{
DamageDict =
{
["Holy"] = -4,
},
};
/// <summary>
/// How much the entity is healed passively by each tick.
/// </summary>
[DataField]
public DamageSpecifier PassiveAmount = new()
{
DamageDict =
{
["Holy"] = -0.5, // if its less it dont work du to limb damage
},
};
}

View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Marcus F <199992874+thebiggestbruh@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Temperature.Components;
/// <summary>
/// Used to ensure that HighTempImmunityComponent is not overriden (when it is made eventually)
/// </summary>
[RegisterComponent]
public sealed partial class SpecialHighTempImmunityComponent : Component;

View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Marcus F <marcus2008stoke@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Temperature.Components;
/// <summary>
/// Used to ensure that LowTempImmunityComponent is not overriden (when it is made eventually)
/// </summary>
[RegisterComponent]
public sealed partial class SpecialLowTempImmunityComponent : Component;

View File

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Marcus F <199992874+thebiggestbruh@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Atmos;
namespace Content.Shared._Goobstation.Temperature;
public sealed class TemperatureImmunityEvent(float currentTemperature) : EntityEventArgs
{
public float CurrentTemperature = currentTemperature;
public readonly float IdealTemperature = Atmospherics.NormalBodyTemperature;
}

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 Aineias1 <dmitri.s.kiselev@gmail.com>
// SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 McBosserson <148172569+McBosserson@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Milon <plmilonpl@gmail.com>
// SPDX-FileCopyrightText: 2025 Piras314 <p1r4s@proton.me>
// SPDX-FileCopyrightText: 2025 Rouden <149893554+Roudenn@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Unlumination <144041835+Unlumy@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <linebarrelerenthusiast@gmail.com>
// SPDX-FileCopyrightText: 2025 username <113782077+whateverusername0@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 whateverusername0 <whateveremail>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Lavaland.Chasm;
[ByRefEvent]
public record struct BeforeChasmFallingEvent(EntityUid Entity, bool Cancelled = false);

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 Aineias1 <dmitri.s.kiselev@gmail.com>
// SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 McBosserson <148172569+McBosserson@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Milon <plmilonpl@gmail.com>
// SPDX-FileCopyrightText: 2025 Piras314 <p1r4s@proton.me>
// SPDX-FileCopyrightText: 2025 Rouden <149893554+Roudenn@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Unlumination <144041835+Unlumy@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <linebarrelerenthusiast@gmail.com>
// SPDX-FileCopyrightText: 2025 username <113782077+whateverusername0@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 whateverusername0 <whateveremail>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Lavaland.Chasm;
[RegisterComponent]
public sealed partial class PreventChasmFallingComponent : Component
{
[DataField]
public bool DeleteOnUse = true;
}

View File

@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 Aineias1 <dmitri.s.kiselev@gmail.com>
// SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 McBosserson <148172569+McBosserson@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Milon <plmilonpl@gmail.com>
// SPDX-FileCopyrightText: 2025 Piras314 <p1r4s@proton.me>
// SPDX-FileCopyrightText: 2025 Rouden <149893554+Roudenn@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Unlumination <144041835+Unlumy@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <linebarrelerenthusiast@gmail.com>
// SPDX-FileCopyrightText: 2025 pheenty <fedorlukin2006@gmail.com>
// SPDX-FileCopyrightText: 2025 username <113782077+whateverusername0@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 whateverusername0 <whateveremail>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Chasm;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Timing;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Shared._Lavaland.Chasm;
public sealed class PreventChasmFallingSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
private static readonly ResolvedPathSpecifier FallSound = new("/Audio/Items/Mining/fultext_launch.ogg"); // DeltaV - Avoid literal
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PreventChasmFallingComponent, BeforeChasmFallingEvent>(OnBeforeFall);
SubscribeLocalEvent<InventoryComponent, BeforeChasmFallingEvent>(Relay);
}
private void OnBeforeFall(EntityUid uid, PreventChasmFallingComponent comp, ref BeforeChasmFallingEvent args)
{
if (TryComp<UseDelayComponent>(uid, out var useDelay) && _delay.IsDelayed((uid, useDelay)))
return;
args.Cancelled = true;
var coordsValid = false;
var coords = Transform(args.Entity).Coordinates;
const int attempts = 20;
var curAttempts = 0;
while (!coordsValid)
{
curAttempts++;
if (curAttempts > attempts)
return; // Just to be safe from stack overflow
var newCoords = new EntityCoordinates(Transform(args.Entity).ParentUid, coords.X + _random.NextFloat(-5f, 5f), coords.Y + _random.NextFloat(-5f, 5f));
if (!_interaction.InRangeUnobstructed(args.Entity, newCoords, -1f) ||
_lookup.GetEntitiesInRange<ChasmComponent>(newCoords, 1f).Count > 0)
continue;
_transform.SetCoordinates(args.Entity, newCoords);
_transform.AttachToGridOrMap(args.Entity, Transform(args.Entity));
_audio.PlayPvs(FallSound, args.Entity); // DeltaV - Avoid literal
if (args.Entity != uid && comp.DeleteOnUse)
QueueDel(uid);
else if (useDelay != null)
_delay.TryResetDelay((uid, useDelay));
coordsValid = true;
}
}
private void Relay(EntityUid uid, InventoryComponent comp, ref BeforeChasmFallingEvent args)
{
if (!HasComp<ContainerManagerComponent>(uid))
return;
RelayEvent(uid, ref args);
}
private void RelayEvent(EntityUid uid, ref BeforeChasmFallingEvent ev)
{
if (!TryComp<ContainerManagerComponent>(uid, out var containerManager))
return;
foreach (var container in containerManager.Containers.Values)
{
if (ev.Cancelled)
break;
foreach (var entity in container.ContainedEntities)
{
RaiseLocalEvent(entity, ref ev);
if (ev.Cancelled)
break;
RelayEvent(entity, ref ev);
}
}
}
}

View File

@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2024 Fishbait <Fishbait@git.ml>
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2024 fishbait <gnesse@gmail.com>
# SPDX-FileCopyrightText: 2024 username <113782077+whateverusername0@users.noreply.github.com>
# SPDX-FileCopyrightText: 2024 whateverusername0 <whateveremail>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
# SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- files: ["devil_start.ogg"]
license: "CC-BY-SA-4.0"
copyright: "Created by SolsticeOfTheWinter"
source: "https://github.com/SolsticeOfTheWinter"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2024 username <113782077+whateverusername0@users.noreply.github.com>
# SPDX-FileCopyrightText: 2024 whateverusername0 <whateveremail>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
# SPDX-FileCopyrightText: 2025 Armok <155400926+ARMOKS@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- files: ["crack2.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from /tg/station"
source: "https://github.com/tgstation/tgstation/tree/master/sound/effects/wounds/crack2.ogg"

Binary file not shown.

View File

@ -0,0 +1,2 @@
admin-verb-text-make-devil = Make Devil
admin-verb-make-devil = Make the target into a devil

View File

@ -0,0 +1,11 @@
figurines-devil-1 = Won't you shake a poor sinner's hand?
figurines-devil-2 = Of course you can trust me! Just sign this contract!
figurines-devil-3 = The chaplain is a scammer, don't you trust him!
figurines-devil-4 = Have you heard of the next big investment crypto? It's called "Goobcoin" and it's going to the moon!
figurines-devil-5 = Contractor? Contractee? It's really the same thing.
figurines-devil-6 = CHAPLAIN!! I'M CREW!! I'M CREW!!
figurines-devil-7 = Hey! Where are you going with my codex? Give that back!
figurines-devil-8 = What? No, "Strength" will make you stronger, just trust me!
figurines-devil-9 = I said I wanted your "Soul", not "Sole". Take the shoes back!
figurines-devil-10 = No bitches? I can help with that.
figurines-devil-11 = True name? You don't need to worry about that.

View File

@ -0,0 +1,15 @@
cheat-death-component-examined = [color=red] Something tells you {SUBJECT($target)} has no fear of death. [/color]
cheat-death-component-remaining-revives = { $amount ->
[0] You have no revives left.
[one] You have one revive left.
*[other] You have {$amount} revives left.
}
cheat-death-component-remaining-revives-unlimited = You have an unlimited amount of revives.
action-cheat-death-fail-no-lives = You aren't slipping away this time.
action-cheat-death-fail-not-dead = You can't cheat death while alive!
action-cheat-death-holy-damage = Divine intervention prevents your revival.
action-cheated-death-dead = {$name} jerks awake. Bones mended, wounds sewn, organs arranged ever so perfectly.
action-cheated-death-alive = {$name} violently shakes, all manners of wounds disappearing in a flash of flames.

View File

@ -0,0 +1,49 @@
devil-contract-contractee = contractee
devil-contract-contractor = contractor
devil-contract-early-sign-failed = The contractor must wait for the contractee to sign first!
devil-contract-no-soul-sign-failed = You do not have a soul to sell!
devil-sign-invalid-user = You do not have the power to fulfill this contract!
revival-contract-no-mind = They have no mind to accept a deal!
devil-contract-mind-shielded-failed = A voice comes from your Nanotrasen™ mindshield. "Nice try."
contract-summon-flavor = {$name}'s hand bursts into flames before revealing a black roll of parchment...
burn-contract-prompt = Burn contract.
burn-contract-popup-success = The contract bursts into flames! It seems someones fate has been sealed...
burn-contract-popup-fail = You cannot burn a contract during a deal!
contract-victim-signed = A chill runs down your spine as you lift the pen...
contract-devil-signed = The contract pulses with dark energy...
contract-soul-added = You gain another soul in your possession...
contract-uneven-odds = These odds aren't fair. You need {$number} more contract weight.
devil-contract-examined = This contract has a weight of: {$weight}.
devil-deal-time-ran-out = Your vision goes black as your muscles fail you. You've run out of time.
revival-contract-accepted = You feel your connection to your body strengthen...
revival-contract-rejected = You reject the unholy offer.
revival-contract-expired = Times up.
revival-contract-prompt = {$offerer} offers to restore your life for a price... Do you accept?
revival-contract-use-success = You offer {$target} a deal no man could refuse...
revival-contract-use-failed = You cannot offer {$target} this deal.
revival-contract-prompt-reject = Reject Offer
revival-contract-prompt-accept = Accept Offer
revival-contract-menu-title = Contract From the Depths
revival-contract-menu-prompt = Sell your soul for a second chance?
default-contract-content = This binding contract made this day between:
- The Infernal Party (Hereinafter "Contractor")
- The Mortal Party (Hereinafter "Contractee")
{"["}bold]Clauses of Binding:{"["}/bold]
{"["}italic]The following parties give up the following possessions{"["}/italic{"]"}
Contractee: Soul Ownership
Contractee: Death
{"["}italic]All clauses come into effect upon signing of this paper by both parties.{"["}/italic]
{"["}italic]{"["}color=#ac3a0d]Clauses may not be broken. Ever.{"["}/color]{"["}/italic]

View File

@ -0,0 +1,42 @@
devil-roundend-name = Devil
objective-issuer-devil = Devil
roundend-prepend-devil-contracts = Someone has made [color=red]{$number}[/color] successful contracts.
roundend-prepend-devil-contracts-named = [color=white]{$name}[/color] has made [color=red]{$number}[/color] successful contracts.
devil-component-examined = [color=darkred]{CAPITALIZE(POSS-ADJ($target))} eyes glow a faint red.[/color]
condemned-component-examined = [color=chartreuse]{CAPITALIZE($target)}'s eyes are hollow and soulless.[/color]
devil-role-greeting = You are a devil from the depths of hell,
who has possessed the body of {$playerName}
Corrupt mortals and collect souls through the use of contracts.
It is not in your interest to do tasks unrelated to your deals, they are beneath you.
Your true name is {$trueName} - Try to keep it a secret, will you?
devil-true-name-heard = The act of {$speaker} commanding {$target}'s true name compels {POSS-ADJ($target)} to stop.
devil-true-name-heard-chaplain = The act of {$speaker}'s holy words commanding {$target}'s true name name burns away at {POSS-ADJ($target)} very soul.
objective-condition-contract-title = Collect Souls
objective-condition-contract-description = Collect as many souls as possible.
objective-condition-weight-title = Make profitable deals.
objective-condition-weight-description = Ensure the deals benefit you as much as possible.
hand-shake-prompt-verb = Offer handshake to {$target}
hand-shake-accept-verb = Shake {$target}'s hand
handshake-success = You feel slightly lighter as you shake {$user}'s hand.
handshake-fail = You shake {POSS-ADJ($user)} hand, but nothing happens!
handshake-offer-popup = {$user} offers {POSS-ADJ($user)} hand to you, a sly grin on {POSS-ADJ($user)} face.
handshake-offer-popup-self = You outstretch your hand to {$target}, a sly grin on your face.
weaktoholy-component-bible-sizzle = {$target}'s skin is singed with the {$item}!
devil-banish-begin = {$user} begins to perform an exorcism on {$target}!
devil-exorcised = The ground cracks and splinters beneath {$target} as they are forcefully returned to hell!
invalid-possession-target = You cannot possess this target!
devil-power-level-increase-weak = Bits of flame flare out from your fingertips. Your connection with hell has strengthened.
devil-power-level-increase-moderate = You feel your power surge. Your connection with hell has strengthened.
devil-power-level-increase-powerful = You've almost completed your tasks here. Just a few more souls.

View File

@ -0,0 +1 @@
devil-speech-grip = Stupete et cadite!!

View File

@ -0,0 +1,2 @@
roles-antag-devil-name = Devil
roles-antag-devil-description = Use your wit and silver tongue to trick crew members into devilish deals.

View File

@ -0,0 +1 @@
self-revive-fail = Can't revive in this state!

View File

@ -0,0 +1,2 @@
# Ideally, subtype names should be short
role-subtype-devil = Devil

View File

@ -0,0 +1,16 @@
possession-fail-changeling = There's no soul to tamper with!
possession-fail-heretic = The power of the mansus prevents you from possessing them!
possession-fail-ghoul = There's no mind for you to possess!
possession-fail-devil = You cannot possess another devil!
possession-fail-ghost = There's no mind for you to possess!
possession-fail-temporary = There's no mind for you to possess!
possession-fail-target-dead = There's no mind for you to possess!
possession-fail-target-shielded = A pesky shield prevents you from prying at their mind!
possession-fail-target-already-possessed = Someone else has already taken over this mind...
possession-fail-target-chaplain = A holy force prevents you from possessing this target!
possession-popup-others = {$target}'s limbs contort in a strange way, {POSS-ADJ($target)} eyes rolling back in {POSS-ADJ($target)} head.
possession-popup-self = Your soul is forced out of your body!
possession-end-popup = {$target} seizes up and falls limp to the ground.
possessed-component-examined = You have {$timeremaining} seconds left in this body.

View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
defibrillator-missing-organs = Patient lacks essential vital organs! Revival is impossible.

View File

@ -74,6 +74,11 @@
radius: 6
castShadows: false
enabled: false
- type: UserInterface
interfaces:
enum.RevivalContractUiKey.Key: # Goobstation - Devil
type: RevivalContractBoundUserInterface
requireInputValidation: false
# proto for player ghosts specifically
- type: entity

View File

@ -92,6 +92,8 @@
orGroup: SpacemenFig
- id: ToyFigurineNukieElite
orGroup: SpacemenFig
- id: ToyFigurineDevil # Goobstation
orGroup: SpacemenFig
- id: ToyFigurineFootsoldier
orGroup: SpacemenFig
- id: ToyFigurineWizardFake

View File

@ -24,6 +24,8 @@
prob: 0.5
- id: SubWizard
prob: 0.05
- id: Devil # Goob
prob: 0.02
- type: entity
parent: BaseGameRule
@ -33,6 +35,8 @@
rules:
- id: Thief
prob: 0.5
- id: Devil # Goob
prob: 0.02
- type: entity
parent: BaseGameRule
@ -382,7 +386,7 @@
selectionTime: IntraPlayerSpawn # Pre-selection before jobs; assignment doesn't really matter though, we only care about the pre-selection to block other antags.
removeUponFailedSpawn: false
definitions:
- prefRoles: [ InitialInfected, Traitor, Thief, HeadRev ]
- prefRoles: [ InitialInfected, Traitor, Thief, Devil, HeadRev ] # Goob - Inc devil
max: 2
playerRatio: 30

View File

@ -12,6 +12,7 @@
- SpaceNinja
- Wizard
- Zombies
- Devil # Goob content
- MinorAntagonists
- type: guideEntry

View File

@ -15,6 +15,8 @@
- !type:AddComponentSpecial
components:
- type: BibleUser #Lets them heal with bibles
- type: Condemned
soulOwnedNotDevil: true # This just means they won't suffer the effects of being condemned, while still not being able to sell their soul. - Goobstation
- type: PsionicBonusChance # DeltaV - makes it more likely to become psionic.
multiplier: 3

View File

@ -15,6 +15,11 @@
- Lawyer
#- Brig # DeltaV - Removed brig access
- Maintenance
special: # Goobstation - Everyone knows lawyers are soulless.
- !type:AddComponentSpecial
components:
- type: Condemned
soulOwnedNotDevil: true
- type: startingGear
id: LawyerGear

View File

@ -37,6 +37,8 @@
- type: CommandStaff
- type: PsionicBonusChance # DeltaV - makes it more likely to become psionic.
flatBonus: 0.025
- type: Condemned # Goobstation - Nanotrasen owns your soul pal.
soulOwnedNotDevil: true
- type: startingGear
id: CaptainGear

View File

@ -0,0 +1,131 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
# SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# Devil Specific
- type: entity
parent: BaseAction
id: ActionCreateContract
name: Summon infernal contract
description: Creates an infernal contract in the Devil's hand.
categories: [ HideSpawnMenu ]
components:
- type: Action
useDelay: 10
itemIconStyle: NoItem
icon:
sprite: _Goobstation/Actions/devil.rsi
state: summon-contract
- type: InstantAction
event: !type:CreateContractEvent {}
- type: entity
parent: BaseAction
id: ActionCreateRevivalContract
name: Summon revival contract
description: Creates a revival contract in the Devil's hand.
categories: [ HideSpawnMenu ]
components:
- type: Action
useDelay: 30
itemIconStyle: NoItem
icon:
sprite: _Goobstation/Actions/devil.rsi
state: summon-contract-revival
- type: InstantAction
event: !type:CreateRevivalContractEvent {}
- type: entity
parent: BaseAction
id: ActionShadowJaunt
name: Shadow jaunt
description: Melt into the shadows.
categories: [ HideSpawnMenu ]
components:
- type: Action
useDelay: 40 # Stop running you cowardly fucks.
itemIconStyle: NoItem
checkCanInteract: false
#checkConsciousness: true # DeltaV - Nuh-uh, no death teleporting
icon:
sprite: _Goobstation/Actions/devil.rsi
state: shadow-jaunt
- type: InstantAction
event: !type:ShadowJauntEvent {}
- type: entity
parent: BaseAction
id: ActionDevilPossess
name: Possess target
description: Temporarily possess a target.
categories: [ HideSpawnMenu ]
components:
- type: Action
useDelay: 70
itemIconStyle: NoItem
icon:
sprite: _Goobstation/Actions/devil.rsi
state: possess
- type: TargetAction
interactOnMiss: false
- type: EntityTargetAction
canTargetSelf: false
whitelist:
components:
- Body
event: !type:DevilPossessionEvent {}
- type: entity
parent: BaseAction
id: ActionEndPossession
name: End possession
description: Cancel this possession early.
categories: [ HideSpawnMenu ]
components:
- type: Action
itemIconStyle: NoItem
checkCanInteract: false
checkConsciousness: false
icon:
sprite: _Goobstation/Actions/devil.rsi
state: possess
- type: InstantAction
event: !type:EndPossessionEarlyEvent {}
- type: entity
parent: BaseAction
id: ActionDevilGrip
name: Devils Grip
description: What little fraction of your power remains in this realm, channeled into your hands.
categories: [ HideSpawnMenu ]
components:
- type: Action
itemIconStyle: NoItem
icon:
sprite: _Goobstation/Actions/devil.rsi
state: devil-grip
- type: InstantAction
event: !type:DevilGripEvent {}
# Deals
- type: entity
parent: BaseAction
id: ActionCheatDeath
name: Cheat Death
description: Laugh in the Grim Reaper's face. You've still got time.
categories: [ HideSpawnMenu ]
components:
- type: Action
itemIconStyle: NoItem
useDelay: 250
checkCanInteract: false
checkConsciousness: false
icon:
sprite: _Goobstation/Actions/devil.rsi
state: cheat-death
- type: InstantAction
event: !type:CheatDeathEvent {}

View File

@ -0,0 +1,24 @@
- type: damageModifierSet
id: DevilDealPositive
coefficients:
Blunt: 0.50
Slash: 0.50
Piercing: 0.50
Cold: 0.50
Heat: 0.50
Bloodloss: 0.50
Poison: 0.50
Radiation: 0.50
Holy: 1.50
- type: damageModifierSet
id: DevilDealNegative
coefficients:
Blunt: 2
Slash: 2
Piercing: 2
Cold: 2
Bloodloss: 2
Poison: 2
Radiation: 2
Holy: 2

Some files were not shown because too many files have changed in this diff Show More