Skip to content

Commit

Permalink
Assay Psionic Power (#1450)
Browse files Browse the repository at this point in the history
# Description

This PR adds a new psionic power to the game, called Assay. Assay is a
more advanced information gathering tool that is available roundstart to
the Mantis. It essentially acts as the first ingame method for
characters to discover the underlying math behind Psionics, displaying
to them the target's casting stats, potentia, and metapsionic feedback
messages. These messages don't tell the caster directly which powers a
target has, instead providing hints as to what those powers might be.

<details><summary><h1>Media</h1></summary>
<p>


![image](https://github.com/user-attachments/assets/d729e373-0406-4b0b-a47c-ae7c853e72b3)


![image](https://github.com/user-attachments/assets/08457e25-56c3-4ff9-856f-ce917bd40d5a)

</p>
</details>

# Changelog

:cl:
- add: Added Assay as a new psi-power, which is available roundstart to
the Psionic Mantis. Assay allows the caster to obtain more direct
information about the statistics of a specific Psion, as well as vague
hints as to what their powers may be, if any.

---------

Signed-off-by: VMSolidus <[email protected]>
Co-authored-by: Skubman <[email protected]>
Co-authored-by: flyingkarii <[email protected]>
Co-authored-by: sleepyyapril <[email protected]>
Co-authored-by: SimpleStation Changelogs <[email protected]>
  • Loading branch information
5 people authored Jan 11, 2025
1 parent 7f32e00 commit df8ffb0
Show file tree
Hide file tree
Showing 18 changed files with 358 additions and 1 deletion.
140 changes: 140 additions & 0 deletions Content.Server/Abilities/Psionics/Abilities/AssayPowerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using Content.Server.Chat.Managers;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Actions.Events;
using Content.Shared.Chat;
using Content.Shared.DoAfter;
using Content.Shared.Popups;
using Content.Shared.Psionics.Events;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Server.Player;
using Robust.Shared.Timing;
using Robust.Shared.Player;

namespace Content.Server.Abilities.Psionics;

public sealed class AssayPowerSystem : EntitySystem
{
[Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPopupSystem _popups = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PsionicComponent, AssayPowerActionEvent>(OnPowerUsed);
SubscribeLocalEvent<PsionicComponent, AssayDoAfterEvent>(OnDoAfter);
}

/// <summary>
/// This power activates when scanning any entity, displaying to the player's chat window a variety of psionic related statistics about the target.
/// </summary>
private void OnPowerUsed(EntityUid uid, PsionicComponent psionic, AssayPowerActionEvent args)
{
if (!_psionics.OnAttemptPowerUse(args.Performer, "assay")
|| psionic.DoAfter is not null)
return;

var ev = new AssayDoAfterEvent(_gameTiming.CurTime, args.FontSize, args.FontColor);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.Performer, args.UseDelay - TimeSpan.FromSeconds(psionic.CurrentAmplification), ev, args.Performer, args.Target, args.Performer)
{
BlockDuplicate = true,
BreakOnMove = true,
BreakOnDamage = true,
}, out var doAfterId);
psionic.DoAfter = doAfterId;

_popups.PopupEntity(Loc.GetString(args.PopupTarget, ("entity", args.Target)), args.Performer, PopupType.Medium);

_audioSystem.PlayPvs(args.SoundUse, args.Performer, AudioParams.Default.WithVolume(8f).WithMaxDistance(1.5f).WithRolloffFactor(3.5f));
_psionics.LogPowerUsed(args.Performer, args.PowerName, args.MinGlimmer, args.MaxGlimmer);
args.Handled = true;
}

/// <summary>
/// Assuming the DoAfter wasn't canceled, the user wasn't mindbroken, and the target still exists, prepare the scan results!
/// </summary>
private void OnDoAfter(EntityUid uid, PsionicComponent component, AssayDoAfterEvent args)
{
if (component is null)
return;
component.DoAfter = null;

var user = uid;
var target = args.Target;
if (target == null || args.Cancelled
|| !_playerManager.TryGetSessionByEntity(user, out var session))
return;

if (InspectSelf(uid, args, session))
return;

if (!TryComp<PsionicComponent>(target, out var targetPsionic))
{
var noPowers = Loc.GetString("no-powers", ("entity", target));
_popups.PopupEntity(noPowers, user, user, PopupType.LargeCaution);

// Incredibly spooky message for non-psychic targets.
var noPowersFeedback = $"[font size={args.FontSize}][color={args.FontColor}]{noPowers}[/color][/font]";
SendDescToChat(noPowersFeedback, session);
return;
}

InspectOther(targetPsionic, args, session);
}

/// <summary>
/// This is a special use-case for scanning yourself with the power. The player receives a unique feedback message if they do so.
/// It however displays significantly less information when doing so. Consider this an intriguing easter egg.
/// </summary>
private bool InspectSelf(EntityUid uid, AssayDoAfterEvent args, ICommonSession session)
{
if (args.Target != args.User)
return false;

var user = uid;
var target = args.Target;

var assaySelf = Loc.GetString("assay-self", ("entity", target!.Value));
_popups.PopupEntity(assaySelf, user, user, PopupType.LargeCaution);

var assaySelfFeedback = $"[font size=20][color=#ff0000]{assaySelf}[/color][/font]";
SendDescToChat(assaySelfFeedback, session);
return true;
}

/// <summary>
/// If the target turns out to be a psychic, display their feedback messages in chat.
/// </summary>
private void InspectOther(PsionicComponent targetPsionic, AssayDoAfterEvent args, ICommonSession session)
{
var target = args.Target;
var targetAmp = MathF.Round(targetPsionic.CurrentAmplification, 2).ToString("#.##");
var targetDamp = MathF.Round(targetPsionic.CurrentDampening, 2).ToString("#.##");
var targetPotentia = MathF.Round(targetPsionic.Potentia, 2).ToString("#.##");
var message = $"[font size={args.FontSize}][color={args.FontColor}]{Loc.GetString("assay-body", ("entity", target!.Value), ("amplification", targetAmp), ("dampening", targetDamp), ("potentia", targetPotentia))}[/color][/font]";
SendDescToChat(message, session);

foreach (var feedback in targetPsionic.AssayFeedback)
{
var locale = Loc.GetString(feedback, ("entity", target!.Value));
var feedbackMessage = $"[font size={args.FontSize}][color={args.FontColor}]{locale}[/color][/font]";
SendDescToChat(feedbackMessage, session);
}
}

private void SendDescToChat(string feedbackMessage, ICommonSession session)
{
_chatManager.ChatMessageToOne(
ChatChannel.Emotes,
feedbackMessage,
feedbackMessage,
EntityUid.Invalid,
false,
session.Channel);
}
}
30 changes: 30 additions & 0 deletions Content.Shared/Actions/Events/AssayPowerActionEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Robust.Shared.Audio;

namespace Content.Shared.Actions.Events;

public sealed partial class AssayPowerActionEvent : EntityTargetActionEvent
{
[DataField]
public TimeSpan UseDelay = TimeSpan.FromSeconds(8f);

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

[DataField]
public string PopupTarget = "assay-begin";

[DataField]
public int FontSize = 12;

[DataField]
public string FontColor = "#8A00C2";

[DataField]
public int MinGlimmer = 3;

[DataField]
public int MaxGlimmer = 6;

[DataField]
public string PowerName = "assay";
}
27 changes: 27 additions & 0 deletions Content.Shared/Psionics/Events.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Robust.Shared.Serialization;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Abilities.Psionics;

namespace Content.Shared.Psionics.Events;

Expand Down Expand Up @@ -67,3 +68,29 @@ public PsionicHealOtherDoAfterEvent(TimeSpan startedAt)

public override DoAfterEvent Clone() => this;
}

[Serializable, NetSerializable]
public sealed partial class AssayDoAfterEvent : DoAfterEvent
{
[DataField(required: true)]
public TimeSpan StartedAt;

[DataField]
public int FontSize = 12;

[DataField]
public string FontColor = "#8A00C2";

private AssayDoAfterEvent()
{
}

public AssayDoAfterEvent(TimeSpan startedAt, int fontSize, string fontColor)
{
StartedAt = startedAt;
FontSize = fontSize;
FontColor = fontColor;
}

public override DoAfterEvent Clone() => this;
}
3 changes: 3 additions & 0 deletions Resources/Locale/en-US/abilities/psionic.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ action-description-psychokinesis = Bend the fabric of space to instantly move ac
action-name-rf-sensitivity = Toggle RF Sensitivity
action-desc-rf-sensitivity = Toggle your ability to interpret radio waves on and off.
action-name-assay = Assay
action-description-assay = Probe an entity at close range to glean metaphorical information about any powers they may have
30 changes: 30 additions & 0 deletions Resources/Locale/en-US/psionics/psionic-powers.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,33 @@ ghost-role-information-familiar-description = An interdimensional creature bound
ghost-role-information-familiar-rules =
Obey the one who summoned you. Do not act against the interests of your Master. You will die for your Master if it is necessary.
# Assay Power
assay-begin = The air around {CAPITALIZE($entity)} begins to shimmer faintly
assay-self = I AM.
no-powers = {CAPITALIZE($entity)} will never awaken from the dream in this life
assay-body = "My will cast upon {CAPITALIZE($entity)} divines these. Amplification: {$amplification} Dampening: {$dampening} Potentia: {$potentia}"
assay-power-initialization-feedback =
I descend into the dreamlight once more, there I drink more fully of the cup of knowledge. The touch of the noosphere upon others becomes known to me,
I can cast my will upon them, divining the inner nature of others.
assay-power-metapsionic-feedback = {CAPITALIZE($entity)} bears a spark of the divine's judgment, they have drunk deeply of the cup of knowledge.
# Entity Specific Feedback Messages
ifrit-feedback = A spirit of Gehenna, bound by the will of a powerful psychic
prober-feedback = A mirror into the end of time, the screaming of dead stars emanates from this machine
drain-feedback = A mirror into a realm where the stars sit still forever, a cold and distant malevolence stares back
sophic-grammateus-feedback = SEEKER, YOU NEED ONLY ASK FOR MY WISDOM.
oracle-feedback = WHY DO YOU BOTHER ME SEEKER? HAVE I NOT MADE MY DESIRES CLEAR?
orecrab-feedback = Heralds of the Lord of Earth, summoned to this realm from Grome's kingdom
reagent-slime-feedback = Heralds of the Lord of Water, summoned to this realm from Straasha's kingdom.
flesh-golem-feedback = Abominations pulled from dead realms, twisted amalgamations of those fallen to the influence of primordial Chaos
glimmer-mite-feedback = A semi-corporeal parasite native to the dreamlight, its presence here brings forth the screams of dead stars.
anomaly-pyroclastic-feedback = A small mirror to the plane of Gehenna, truth lies within the Secret of Fire
anomaly-gravity-feedback = Violet and crimson, blue of blue, impossibly dark yet greater than the whitest of white, a black star shines weakly at the end of it all
anomaly-electricity-feedback = A mirror to a realm tiled by silicon, the lifeblood of artificial thought flows from it
anomaly-flesh-feedback = From within it comes the suffering of damned mutants howling for all eternity
anomaly-bluespace-feedback = A bridge of dreamlight, crossing into the space between realms of the multiverse
anomaly-ice-feedback = Walls of blackened stone, ruin and famine wait for those who fall within
anomaly-rock-feedback = A vast old oak dwells high over a plane of stone, it turns to stare back
anomaly-flora-feedback = Musical notes drift around you, playfully beckoning, they wish to feast
anomaly-liquid-feedback = A realm of twisting currents. Its placidity is a lie. The eyes within stare hungrilly
anomaly-shadow-feedback = At the end of time, when all suns have set forever, there amidst the void stands a monument to past sins.
19 changes: 19 additions & 0 deletions Resources/Prototypes/Actions/psionics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,22 @@
followMaster: true
minGlimmer: 5
maxGlimmer: 10

- type: entity
id: ActionAssay
name: action-name-assay
description: action-description-assay
categories: [ HideSpawnMenu ]
components:
- type: EntityTargetAction
icon: { sprite: Interface/Actions/psionics.rsi, state: assay }
useDelay: 45
checkCanAccess: false
range: 2
itemIconStyle: BigAction
canTargetSelf: true
blacklist:
components:
- PsionicInsulation
- Mindbroken
event: !type:AssayPowerActionEvent
2 changes: 2 additions & 0 deletions Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
nameSegments: [names_golem]
- type: Psionic
removable: false
assayFeedback:
- ifrit-feedback
psychognomicDescriptors:
- p-descriptor-bound
- p-descriptor-cyclic
Expand Down
7 changes: 7 additions & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
- type: ZombieImmune
- type: ClothingRequiredStepTriggerImmune
slots: All
- type: Dispellable

- type: entity
abstract: true
Expand Down Expand Up @@ -104,6 +105,9 @@
damageContainer: StructuralInorganic
- type: Psionic
removable: false
roller: false
assayFeedback:
- orecrab-feedback
- type: InnatePsionicPowers
powersToAdd:
- TelepathyPower
Expand Down Expand Up @@ -303,6 +307,9 @@
solution: bloodstream
- type: Psionic
removable: false
roller: false
assayFeedback:
- reagent-slime-feedback
- type: InnatePsionicPowers
powersToAdd:
- TelepathyPower
Expand Down
5 changes: 5 additions & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
Slash: 6
- type: ReplacementAccent
accent: genericAggressive
- type: Psionic
removable: false
roller: false
assayFeedback:
- flesh-golem-feedback

- type: entity
parent: BaseMobFlesh
Expand Down
4 changes: 4 additions & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/glimmer_creatures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
- ReagentId: Ectoplasm
Quantity: 15
- type: Psionic
removable: false
roller: false
assayFeedback:
- glimmer-mite-feedback
- type: GlimmerSource
- type: AmbientSound
range: 6
Expand Down
Loading

0 comments on commit df8ffb0

Please sign in to comment.