Skip to content

Commit

Permalink
Port Sacrificing (#1110)
Browse files Browse the repository at this point in the history
# Description

This PR rebases the Sacrificing mechanic from Nyanotrasen, while
updating it to newer standards for the Reworked Psionics System. This
does not rebase Golemancy, or Items Of Power, so the system for now
exists solely to spike glimmer DOWN in the event of a code white, and
merely produces brains to be placed inside cyborgs. Notably different
from DeltaV's implementation of it, this system does not reduce a random
amount of glimmer, and instead reduces glimmer by a base of 25,
multiplied by the victim's Psionic Amplification(which is itself
semi-random). Which means that certain individuals(Especially people
with lots of powers, and/or the Power Overwhelming trait) are inherently
more useful to sacrifice than other, weaker psionics.

When I bring back Items Of Power, the chances of obtaining an Artifact
are based on the victim's Dampening stat.


https://github.com/user-attachments/assets/23e52f20-ac3e-4bd9-8122-cadec96cbdf3

# Changelog

:cl:
- add: Sacrificing Psions has been added. Psions can be sacrificed by
Epistemics upon an altar in order to dramatically reduce glimmer. The
more powerful the Psion to be sacrificed, the more glimmer is reduced.

---------

Co-authored-by: deltanedas <[email protected]>
  • Loading branch information
VMSolidus and deltanedas authored Oct 20, 2024
1 parent 54893bd commit c557d74
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 44 deletions.
5 changes: 5 additions & 0 deletions Content.Client/Chapel/SacrificialAltarSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Content.Shared.Chapel;

namespace Content.Client.Chapel;

public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem;
129 changes: 129 additions & 0 deletions Content.Server/Chapel/SacrificialAltarSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using Content.Server.Bible.Components;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Database;
using Content.Shared.Chapel;
using Content.Shared.DoAfter;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Popups;
using Content.Shared.Psionics.Glimmer;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;

namespace Content.Server.Chapel;

public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem
{
[Dependency] private readonly GlimmerSystem _glimmer = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<SacrificialAltarComponent, SacrificeDoAfterEvent>(OnDoAfter);
}

private void OnDoAfter(Entity<SacrificialAltarComponent> ent, ref SacrificeDoAfterEvent args)
{
ent.Comp.SacrificeStream = _audio.Stop(ent.Comp.SacrificeStream);
ent.Comp.DoAfter = null;

if (args.Cancelled || args.Handled || args.Args.Target is not { } target
|| !TryComp<PsionicComponent>(target, out var psionic)
|| !_mind.TryGetMind(target, out var _, out var _))
return;

_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.Args.User):player} sacrificed {ToPrettyString(target):target} on {ToPrettyString(ent):altar}");

// lower glimmer by a random amount
_glimmer.Glimmer -= (int) (ent.Comp.GlimmerReduction * psionic.CurrentAmplification);

if (ent.Comp.RewardPool is not null && _random.Prob(ent.Comp.BaseItemChance * psionic.CurrentDampening))
{
var proto = _proto.Index(_random.Pick(ent.Comp.RewardPool));
Spawn(proto.ToString(), Transform(ent).Coordinates);
}
// TODO GOLEMS: create a soul crystal and transfer mind into it

// finally gib the targets old body
if (TryComp<BodyComponent>(target, out var body))
_body.GibBody(target, gibOrgans: false, body, launchGibs: true);
else
QueueDel(target);
}

protected override void AttemptSacrifice(Entity<SacrificialAltarComponent> ent, EntityUid user, EntityUid target)
{
if (ent.Comp.DoAfter != null)
return;

// can't sacrifice yourself
if (user == target)
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-self"), ent, user, PopupType.SmallCaution);
return;
}

// you need to be psionic OR bible user
if (!HasComp<PsionicComponent>(user) && !HasComp<BibleUserComponent>(user))
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-user"), ent, user, PopupType.SmallCaution);
return;
}

// and no golems or familiars or whatever should be sacrificing
if (!HasComp<HumanoidAppearanceComponent>(user))
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-user-humanoid"), ent, user, PopupType.SmallCaution);
return;
}

// prevent psichecking SSD people...
// notably there is no check in OnDoAfter so you can't alt f4 to survive being sacrificed
if (!HasComp<ActorComponent>(target) || _mind.GetMind(target) == null)
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-target-catatonic", ("target", target)), ent, user, PopupType.SmallCaution);
return;
}

// TODO: there should be a penalty to the user for psichecking like this
if (!HasComp<PsionicComponent>(target))
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-target", ("target", target)), ent, user, PopupType.SmallCaution);
return;
}

if (!HasComp<HumanoidAppearanceComponent>(target))
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-target-humanoid", ("target", target)), ent, user, PopupType.SmallCaution);
return;
}

_popup.PopupEntity(Loc.GetString("altar-sacrifice-popup", ("user", user), ("target", target)), ent, PopupType.LargeCaution);

ent.Comp.SacrificeStream = _audio.PlayPvs(ent.Comp.SacrificeSound, ent)?.Entity;

var ev = new SacrificeDoAfterEvent();
var args = new DoAfterArgs(EntityManager, user, ent.Comp.SacrificeTime, ev, target: target, eventTarget: ent)
{
BreakOnDamage = true,
BreakOnUserMove = true,
BreakOnTargetMove = true,
BreakOnWeightlessMove = true,
NeedHand = true
};
DoAfter.TryStartDoAfter(args, out ent.Comp.DoAfter);
}
}
6 changes: 2 additions & 4 deletions Content.Shared/Chapel/SacrificeDoAfterEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using Content.Shared.DoAfter;

namespace Content.Shared.Chapel;
[Serializable, NetSerializable]
public sealed partial class SacrificeDoAfterEvent : SimpleDoAfterEvent
{

}
[Serializable, NetSerializable]
public sealed partial class SacrificeDoAfterEvent : SimpleDoAfterEvent { }
48 changes: 48 additions & 0 deletions Content.Shared/Chapel/SacrificialAltarComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Content.Shared.Random;
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;

namespace Content.Shared.Chapel;

/// <summary>
/// Altar that lets you sacrifice psionics to lower glimmer by a large amount.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedSacrificialAltarSystem))]
public sealed partial class SacrificialAltarComponent : Component
{
/// <summary>
/// DoAfter for an active sacrifice.
/// </summary>
[DataField]
public DoAfterId? DoAfter;

/// <summary>
/// How long it takes to sacrifice someone once they die.
/// This is the window to interrupt a sacrifice if you want glimmer to stay high, or need the psionic to be revived.
/// </summary>
[DataField]
public TimeSpan SacrificeTime = TimeSpan.FromSeconds(8.35);

[DataField]
public SoundSpecifier SacrificeSound = new SoundPathSpecifier("/Audio/Psionics/heartbeat_fast.ogg");

[DataField]
public EntityUid? SacrificeStream;

/// <summary>
/// Base amount to reduce glimmer by, multiplied by the victim's Amplification stat.
/// </summary>
[DataField]
public float GlimmerReduction = 25;

[DataField]
public List<ProtoId<WeightedRandomEntityPrototype>>? RewardPool;

/// <summary>
/// The base chance to generate an item of power, multiplied by the victim's Dampening stat.
/// </summary>
[DataField]
public float BaseItemChance = 0.1f;
}
61 changes: 61 additions & 0 deletions Content.Shared/Chapel/SharedSacrificialAltarSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Linq;
using Content.Shared.Buckle.Components;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Verbs;

namespace Content.Shared.Chapel;

public abstract partial class SharedSacrificialAltarSystem : EntitySystem
{
[Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<SacrificialAltarComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SacrificialAltarComponent, BuckleChangeEvent>(OnUnstrapped);
SubscribeLocalEvent<SacrificialAltarComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
}

private void OnExamined(Entity<SacrificialAltarComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("altar-examine"));
}

private void OnUnstrapped(Entity<SacrificialAltarComponent> ent, ref BuckleChangeEvent args)
{
if (ent.Comp.DoAfter is not { } id)
return;

DoAfter.Cancel(id);
ent.Comp.DoAfter = null;
}

private void OnGetVerbs(Entity<SacrificialAltarComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || ent.Comp.DoAfter != null
|| !TryComp<StrapComponent>(ent, out var strap)
|| GetFirstBuckled(strap) is not { } target)
return;

var user = args.User;
args.Verbs.Add(new AlternativeVerb()
{
Act = () => AttemptSacrifice(ent, user, target),
Text = Loc.GetString("altar-sacrifice-verb"),
Priority = 2
});
}

private EntityUid? GetFirstBuckled(StrapComponent strap)
{
if (strap.BuckledEntities.Count <= 0)
return null;

return strap.BuckledEntities.First();
}

protected virtual void AttemptSacrifice(Entity<SacrificialAltarComponent> ent, EntityUid user, EntityUid target) { }
}
11 changes: 11 additions & 0 deletions Resources/Locale/en-US/chapel/altar.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
altar-examine = [color=purple]This altar can be used to sacrifice Psionics.[/color]
altar-sacrifice-verb = Sacrifice
altar-failure-reason-self = You can't sacrifice yourself!
altar-failure-reason-user = You are not psionic or clerically trained!
altar-failure-reason-user-humanoid = You are not a humanoid!
altar-failure-reason-target = {CAPITALIZE(THE($target))} {CONJUGATE-BE($target)} not psionic!
altar-failure-reason-target-humanoid = {CAPITALIZE(THE($target))} {CONJUGATE-BE($target)} not a humanoid!
altar-failure-reason-target-catatonic = {CAPITALIZE(THE($target))} {CONJUGATE-BE($target)} braindead!
altar-sacrifice-popup = {$user} starts to sacrifice {$target}!
8 changes: 7 additions & 1 deletion Resources/Prototypes/Entities/Structures/Furniture/altar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@
- TableLayer
- type: Sprite
snapCardinals: true
- type: Climbable
- type: Clickable
- type: SacrificialAltar
- type: Strap
position: Down
rotation: -90
- type: GuideHelp
guides:
- AltarsGolemancy

- type: entity
id: AltarNanotrasen
Expand Down
4 changes: 2 additions & 2 deletions Resources/Prototypes/Guidebook/science.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
- Xenoarchaeology
- Robotics
- MachineUpgrading
# - AltarsGolemancy # When it's added # Nyanotrasen - Golemancy guidebook
- ReverseEngineering # Nyanotrasen - Reverse Engineering guidebook
- AltarsGolemancy
- ReverseEngineering

- type: guideEntry
id: Technologies
Expand Down
8 changes: 4 additions & 4 deletions Resources/Prototypes/Nyanotrasen/Guidebook/epistemics.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# - type: guideEntry # When it's added
# id: AltarsGolemancy
# name: guide-entry-altars-golemancy
# text: "/ServerInfo/Nyanotrasen/Guidebook/Epistemics/Altar.xml"
- type: guideEntry
id: AltarsGolemancy
name: guide-entry-altars-golemancy
text: "/ServerInfo/Nyanotrasen/Guidebook/Epistemics/Altar.xml"

- type: guideEntry
id: ReverseEngineering
Expand Down
21 changes: 0 additions & 21 deletions Resources/Prototypes/Nyanotrasen/psionicArtifacts.yml

This file was deleted.

17 changes: 5 additions & 12 deletions Resources/ServerInfo/Nyanotrasen/Guidebook/Epistemics/Altar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@
The chapel has a [color=#a4885c]sacrificial altar[/color]. To use it, a psionic humanoid must be placed on it, and someone with either psionics or clerical training must initiate the sacrifice.
It appears in the context menu on the altar, which can be opened with the right mouse button by default.

<GuideEntityEmbed Entity="CrystalSoul" />
Once sacrificed, the psionic humanoid's soul is trapped inside a [color=#a4885c]soul crystal[/color]. This is not the end for them; they can still talk both vocally and telepathically, and this form is much harder to destroy.

<GuideEntityEmbed Entity="PonderingOrbTelepathic" />
10% of the time, the altar will spawn a powerful psionic item along with the soul crystal. This chance increases to 50% if the sacrifice was performed by someone with clerical training, such as the [color=#a4885c]chaplain[/color] or [color=#a4885c]mystagogue[/color].
<GuideEntityEmbed Entity="MaterialBluespace1" />
<GuideEntityEmbed Entity="ClothingEyesTelegnosisSpectacles" />
As a reward for sacrificing a psionic, glimmer will be lowered substantially and you will be given some loot.
Usually this is a few bluespace crystals, but you can also get a powerful psionic item.

## Golemancy
<Box>
<GuideEntityEmbed Entity="MobGolemCult"/>
<GuideEntityEmbed Entity="MobGolemWood"/>
</Box>
Soul crystals can be installed into [color=#a4885c]golems[/color] to give the soul a new body. Golems are bound to serve their master. As constructs, they do not need to eat, drink, or breathe.
Note that for wood golems, if plants are planted on top of their head, the plants will still need those things.

[color=red]Note: Golemancy is not implemented yet. Once you sacrifice a psionic you can borg them with an MMI.[/color]
</Document>

0 comments on commit c557d74

Please sign in to comment.