diff --git a/Content.Server/Abilities/Psionics/Abilities/AssayPowerSystem.cs b/Content.Server/Abilities/Psionics/Abilities/AssayPowerSystem.cs new file mode 100644 index 00000000000..4fe08f303ce --- /dev/null +++ b/Content.Server/Abilities/Psionics/Abilities/AssayPowerSystem.cs @@ -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(OnPowerUsed); + SubscribeLocalEvent(OnDoAfter); + } + + /// + /// This power activates when scanning any entity, displaying to the player's chat window a variety of psionic related statistics about the target. + /// + 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; + } + + /// + /// Assuming the DoAfter wasn't canceled, the user wasn't mindbroken, and the target still exists, prepare the scan results! + /// + 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(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); + } + + /// + /// 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. + /// + 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; + } + + /// + /// If the target turns out to be a psychic, display their feedback messages in chat. + /// + 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); + } +} diff --git a/Content.Shared/Actions/Events/AssayPowerActionEvent.cs b/Content.Shared/Actions/Events/AssayPowerActionEvent.cs new file mode 100644 index 00000000000..dc25d343313 --- /dev/null +++ b/Content.Shared/Actions/Events/AssayPowerActionEvent.cs @@ -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"; +} diff --git a/Content.Shared/Psionics/Events.cs b/Content.Shared/Psionics/Events.cs index 68ec3097e7d..6705e940cd5 100644 --- a/Content.Shared/Psionics/Events.cs +++ b/Content.Shared/Psionics/Events.cs @@ -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; @@ -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; +} diff --git a/Resources/Locale/en-US/abilities/psionic.ftl b/Resources/Locale/en-US/abilities/psionic.ftl index d0e8db72f8c..c8eb9635946 100644 --- a/Resources/Locale/en-US/abilities/psionic.ftl +++ b/Resources/Locale/en-US/abilities/psionic.ftl @@ -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 diff --git a/Resources/Locale/en-US/psionics/psionic-powers.ftl b/Resources/Locale/en-US/psionics/psionic-powers.ftl index ab2a991e06f..f7d225f58fc 100644 --- a/Resources/Locale/en-US/psionics/psionic-powers.ftl +++ b/Resources/Locale/en-US/psionics/psionic-powers.ftl @@ -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. diff --git a/Resources/Prototypes/Actions/psionics.yml b/Resources/Prototypes/Actions/psionics.yml index c6d9e17c2ac..97d19aae5f6 100644 --- a/Resources/Prototypes/Actions/psionics.yml +++ b/Resources/Prototypes/Actions/psionics.yml @@ -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 diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml index 12b5f7f2007..42c12b1fd64 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml @@ -92,6 +92,8 @@ nameSegments: [names_golem] - type: Psionic removable: false + assayFeedback: + - ifrit-feedback psychognomicDescriptors: - p-descriptor-bound - p-descriptor-cyclic diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index 94ba7ec4188..bc5e0239d6a 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -74,6 +74,7 @@ - type: ZombieImmune - type: ClothingRequiredStepTriggerImmune slots: All + - type: Dispellable - type: entity abstract: true @@ -104,6 +105,9 @@ damageContainer: StructuralInorganic - type: Psionic removable: false + roller: false + assayFeedback: + - orecrab-feedback - type: InnatePsionicPowers powersToAdd: - TelepathyPower @@ -303,6 +307,9 @@ solution: bloodstream - type: Psionic removable: false + roller: false + assayFeedback: + - reagent-slime-feedback - type: InnatePsionicPowers powersToAdd: - TelepathyPower diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml index 06ab02dedc9..bdf87ec87d9 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml @@ -54,6 +54,11 @@ Slash: 6 - type: ReplacementAccent accent: genericAggressive + - type: Psionic + removable: false + roller: false + assayFeedback: + - flesh-golem-feedback - type: entity parent: BaseMobFlesh diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/glimmer_creatures.yml b/Resources/Prototypes/Entities/Mobs/NPCs/glimmer_creatures.yml index 52f3844c25f..5fcada3f7f3 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/glimmer_creatures.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/glimmer_creatures.yml @@ -23,6 +23,10 @@ - ReagentId: Ectoplasm Quantity: 15 - type: Psionic + removable: false + roller: false + assayFeedback: + - glimmer-mite-feedback - type: GlimmerSource - type: AmbientSound range: 6 diff --git a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml index 82945860483..02184fc6139 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml @@ -50,6 +50,7 @@ path: /Audio/Effects/teleport_arrival.ogg - type: Psionic removable: false + roller: false - type: InnatePsionicPowers powersToAdd: - TelepathyPower @@ -106,6 +107,11 @@ - type: IgniteOnCollide fixtureId: fix1 fireStacks: 1 + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-pyroclastic-feedback - type: entity id: AnomalyGravity @@ -138,6 +144,11 @@ - type: SingularityDistortion intensity: 1000 falloffPower: 2.7 + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-gravity-feedback - type: entity id: AnomalyElectricity @@ -161,6 +172,11 @@ castShadows: false - type: ElectricityAnomaly - type: Electrified + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-electricity-feedback - type: entity id: AnomalyFlesh @@ -254,6 +270,11 @@ - MobFleshClamp - MobFleshLover - FleshKudzu + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-flesh-feedback - type: entity id: AnomalyBluespace @@ -304,6 +325,11 @@ anomalyContactDamage: types: Radiation: 10 + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-bluespace-feedback - type: entity id: AnomalyIce @@ -352,6 +378,11 @@ releasedGas: 8 # Frezon. Please replace if there is a better way to specify this releaseOnMaxSeverity: true spawnRadius: 0 + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-ice-feedback - type: entity id: AnomalyRockBase @@ -390,6 +421,11 @@ maxAmount: 50 maxRange: 12 floor: FloorAsteroidTile + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-rock-feedback - type: entity id: AnomalyRockUranium @@ -649,6 +685,11 @@ maxRange: 6 spawns: - KudzuFlowerAngry + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-flora-feedback - type: entity id: AnomalyFloraBulb @@ -808,6 +849,11 @@ solution: anomaly - type: InjectableSolution solution: beaker + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-liquid-feedback - type: entity id: AnomalyShadow @@ -864,3 +910,8 @@ - type: Tag tags: - SpookyFog + - type: Psionic + removable: false + roller: false + assayFeedback: + - anomaly-shadow-feedback diff --git a/Resources/Prototypes/Entities/Structures/Specific/oracle.yml b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml index 3e2ed9508e3..e908ecab66f 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/oracle.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml @@ -18,6 +18,8 @@ - type: Actions - type: Psionic removable: false + assayFeedback: + - oracle-feedback psychognomicDescriptors: - p-descriptor-old - p-descriptor-demiurgic diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml index 0eede9c2810..1bb752ade46 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml @@ -6,6 +6,8 @@ components: - type: Psionic removable: false + assayFeedback: + - prober-feedback - type: InnatePsionicPowers powersToAdd: - TelepathyPower @@ -93,6 +95,8 @@ components: - type: Psionic removable: false + assayFeedback: + - drain-feedback - type: GlimmerSource addToGlimmer: false - type: Construction diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml index f92c9b07400..6f7b02cb6b8 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml @@ -25,6 +25,8 @@ - type: Actions - type: Psionic removable: false + assayFeedback: + - sophic-grammateus-feedback psychognomicDescriptors: - p-descriptor-old - p-descriptor-demiurgic diff --git a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml index f7a31205706..961bbf05fd0 100644 --- a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml +++ b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml @@ -39,6 +39,7 @@ powersToAdd: - MetapsionicPower - TelepathyPower + - AssayPower - type: startingGear id: ForensicMantisGear diff --git a/Resources/Prototypes/Psionics/psionics.yml b/Resources/Prototypes/Psionics/psionics.yml index 548881e61f7..b1e9d783a8e 100644 --- a/Resources/Prototypes/Psionics/psionics.yml +++ b/Resources/Prototypes/Psionics/psionics.yml @@ -170,6 +170,9 @@ - !type:AddPsionicStatSources amplificationModifier: 0.5 dampeningModifier: 0.5 + - !type:PsionicAddAvailablePowers + powerPrototype: AssayPower + weight: 0.1 removalFunctions: - !type:RemovePsionicActions - !type:RemovePsionicPowerComponents @@ -178,6 +181,8 @@ - !type:RemovePsionicStatSources - !type:RemoveAssayFeedback assayFeedback: metapsionic-power-metapsionic-feedback + - !type:PsionicRemoveAvailablePowers + powerPrototype: AssayPower - type: psionicPower id: PsionicRegenerationPower @@ -661,3 +666,25 @@ - !type:RemovePsionicPsychognomicDescriptors psychognomicDescriptor: calling powerSlotCost: 0 + +- type: psionicPower + id: AssayPower + name: Assay + powerCategories: + - Mentalic + initializeFunctions: + - !type:AddPsionicActions + actions: + - ActionAssay + - !type:PsionicFeedbackPopup + - !type:PsionicFeedbackSelfChat + feedbackMessage: assay-power-initialization-feedback + - !type:AddPsionicAssayFeedback + assayFeedback: assay-power-metapsionic-feedback + - !type:AddPsionicStatSources + dampeningModifier: 0.5 + removalFunctions: + - !type:RemovePsionicActions + - !type:RemovePsionicStatSources + - !type:RemoveAssayFeedback + assayFeedback: assay-power-metapsionic-feedback diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/assay.png b/Resources/Textures/Interface/Actions/psionics.rsi/assay.png new file mode 100644 index 00000000000..f245ca9e5e3 Binary files /dev/null and b/Resources/Textures/Interface/Actions/psionics.rsi/assay.png differ diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/meta.json b/Resources/Textures/Interface/Actions/psionics.rsi/meta.json index 735bc293d15..dd9833b560b 100644 --- a/Resources/Textures/Interface/Actions/psionics.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/psionics.rsi/meta.json @@ -1,12 +1,15 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "healing_word, revivify, shadeskip by leonardo_dabepis (discord), telekinetic_pulse by .mocho (discord), pyrokinetic_flare, summon_remilia, summon_bat and summon_imp by ghost581 (discord)", + "copyright": "assay, healing_word, revivify, shadeskip by leonardo_dabepis (discord), telekinetic_pulse by .mocho (discord), pyrokinetic_flare, summon_remilia, summon_bat and summon_imp by ghost581 (discord)", "size": { "x": 64, "y": 64 }, "states": [ + { + "name": "assay" + }, { "name": "healing_word" },