500 lines
18 KiB
C#
500 lines
18 KiB
C#
// 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
|
|
}
|