diff --git a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.Functions.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.Functions.cs
new file mode 100644
index 00000000000..3db2a2bae4e
--- /dev/null
+++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.Functions.cs
@@ -0,0 +1,580 @@
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Content.Shared.Actions;
+using Content.Shared.Psionics;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Popups;
+using Content.Shared.Chat;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Random;
+using Content.Server.Chat.Managers;
+using Robust.Shared.Player;
+
+namespace Content.Server.Abilities.Psionics;
+
+[UsedImplicitly]
+public sealed partial class AddPsionicActions : PsionicPowerFunction
+{
+ ///
+ /// The list of each Action that this power adds in the form of ActionId and ActionEntity
+ ///
+ [DataField]
+ public List Actions = new();
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ var actions = entityManager.System();
+ foreach (var id in Actions)
+ {
+ EntityUid? actionId = null;
+ if (actions.AddAction(uid, ref actionId, id))
+ {
+ actions.StartUseDelay(actionId);
+ psionicComponent.Actions.Add(proto.ID, actionId);
+ }
+ }
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class RemovePsionicActions : PsionicPowerFunction
+{
+ // As a novelty, this does not require any DataFields.
+ // This removes all Actions directly associated with a specific power, which works with our current system of record-keeping
+ // for psi-powers.
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ var actions = entityManager.System();
+ if (psionicComponent.Actions is null
+ || !psionicComponent.Actions.ContainsKey(proto.ID))
+ return;
+
+ var copy = serializationManager.CreateCopy(psionicComponent.Actions, notNullableOverride: true);
+
+ foreach (var (id, actionUid) in copy)
+ {
+ if (id != proto.ID)
+ continue;
+
+ actions.RemoveAction(uid, actionUid);
+ }
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class AddPsionicPowerComponents : PsionicPowerFunction
+{
+ ///
+ /// The list of what Components this power adds.
+ ///
+ [DataField]
+ public ComponentRegistry Components = new();
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ foreach (var entry in Components.Values)
+ {
+ if (entityManager.HasComponent(uid, entry.Component.GetType()))
+ continue;
+
+ var comp = (Component) serializationManager.CreateCopy(entry.Component, notNullableOverride: true);
+ comp.Owner = uid;
+ entityManager.AddComponent(uid, comp);
+ }
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class RemovePsionicPowerComponents : PsionicPowerFunction
+{
+ ///
+ /// The list of what Components this power removes.
+ ///
+ [DataField]
+ public ComponentRegistry Components = new();
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ foreach (var (name, _) in Components)
+ entityManager.RemoveComponentDeferred(uid, factory.GetComponent(name).GetType());
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class AddPsionicStatSources : PsionicPowerFunction
+{
+ ///
+ /// How much this power will increase or decrease a user's Amplification.
+ ///
+ [DataField]
+ public float AmplificationModifier;
+
+ ///
+ /// How much this power will increase or decrease a user's Dampening.
+ ///
+ [DataField]
+ public float DampeningModifier;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ if (AmplificationModifier != 0)
+ psionicComponent.AmplificationSources.Add(proto.Name, AmplificationModifier);
+
+ if (DampeningModifier != 0)
+ psionicComponent.DampeningSources.Add(proto.Name, DampeningModifier);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class RemovePsionicStatSources : PsionicPowerFunction
+{
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.AmplificationSources.Remove(proto.Name);
+ psionicComponent.DampeningSources.Remove(proto.Name);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicFeedbackPopup : PsionicPowerFunction
+{
+ ///
+ /// What message will be sent to the player as a Popup.
+ /// If left blank, it will default to the Const "generic-power-initialization-feedback"
+ ///
+ [DataField]
+ public string InitializationPopup = "generic-power-initialization-feedback";
+
+ [DataField]
+ public PopupType InitPopupType = PopupType.MediumCaution;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ var popups = entityManager.System();
+ if (playerManager.TryGetSessionByEntity(uid, out var session)
+ || session is null
+ || !loc.TryGetString(InitializationPopup, out var popupString))
+ return;
+
+ popups.PopupEntity(popupString, uid, uid, InitPopupType);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicFeedbackSelfChat : PsionicPowerFunction
+{
+ ///
+ /// What message will be sent to the player as a Chat message.
+ /// If left blank, it will default to the Const "generic-power-initialization-feedback"
+ ///
+ [DataField]
+ public string FeedbackMessage = "generic-power-initialization-feedback";
+
+ ///
+ /// What color will the initialization feedback display in the chat window with.
+ ///
+ [DataField]
+ public string InitializationFeedbackColor = "#8A00C2";
+
+ ///
+ /// What font size will the initialization message use in chat.
+ ///
+ [DataField]
+ public int InitializationFeedbackFontSize = 12;
+
+
+ ///
+ /// Which chat channel will the initialization message use.
+ ///
+ [DataField]
+ public ChatChannel InitializationFeedbackChannel = ChatChannel.Emotes;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ var chatManager = IoCManager.Resolve();
+ if (playerManager.TryGetSessionByEntity(uid, out var session)
+ || session is null
+ || !loc.TryGetString(FeedbackMessage, out var feedback))
+ return;
+
+ var feedbackMessage = $"[font size={InitializationFeedbackFontSize}][color={InitializationFeedbackColor}]{feedback}[/color][/font]";
+ chatManager.ChatMessageToOne(
+ InitializationFeedbackChannel,
+ feedbackMessage,
+ feedbackMessage,
+ EntityUid.Invalid,
+ false,
+ session.Channel);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class AddPsionicAssayFeedback : PsionicPowerFunction
+{
+ ///
+ /// What message will this power generate when scanned by an Assay user.
+ /// These are also used for the Psi-Potentiometer.
+ ///
+ [DataField]
+ public string AssayFeedback = "";
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ if (AssayFeedback is "")
+ return;
+
+ psionicComponent.AssayFeedback.Add(AssayFeedback);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class RemoveAssayFeedback : PsionicPowerFunction
+{
+ [DataField]
+ public string AssayFeedback = "";
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ if (AssayFeedback is ""
+ || !psionicComponent.AssayFeedback.Contains(AssayFeedback))
+ return;
+
+ psionicComponent.AssayFeedback.Remove(AssayFeedback);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class AddPsionicPsychognomicDescriptors : PsionicPowerFunction
+{
+ [DataField]
+ public string PsychognomicDescriptor = "";
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ // It is entirely intended that this doesn't include a Contains check.
+ // The descriptors list allows duplicates, and will only ever pick one anyway.
+ if (PsychognomicDescriptor is "")
+ return;
+
+ psionicComponent.PsychognomicDescriptors.Add(PsychognomicDescriptor);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class RemovePsionicPsychognomicDescriptors : PsionicPowerFunction
+{
+ [DataField]
+ public string PsychognomicDescriptor = "";
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ if (PsychognomicDescriptor is ""
+ || !psionicComponent.PsychognomicDescriptors.Contains(PsychognomicDescriptor))
+ return;
+
+ psionicComponent.PsychognomicDescriptors.Remove(PsychognomicDescriptor);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicModifyPowerSlots : PsionicPowerFunction
+{
+ [DataField]
+ public int PowerSlotsModifier;
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.PowerSlots += PowerSlotsModifier;
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicModifyFamiliarLimit : PsionicPowerFunction
+{
+ [DataField]
+ public int FamiliarLimitModifier;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.FamiliarLimit += FamiliarLimitModifier;
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicModifyRemovable : PsionicPowerFunction
+{
+ [DataField]
+ public bool Removable;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.Removable = Removable;
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicModifyMana : PsionicPowerFunction
+{
+ [DataField]
+ public float MaxManaModifier;
+
+ [DataField]
+ public float ManaGainModifier;
+
+ [DataField]
+ public float ManaGainMultiplierModifier;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.MaxMana += MaxManaModifier;
+ psionicComponent.ManaGain += ManaGainModifier;
+ psionicComponent.ManaGainMultiplier += ManaGainMultiplierModifier;
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicModifyGlimmer : PsionicPowerFunction
+{
+ [DataField]
+ public int GlimmerModifier;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ var glimmerSystem = entityManager.System();
+ glimmerSystem.Glimmer += GlimmerModifier;
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicChangePowerPool : PsionicPowerFunction
+{
+ [DataField]
+ public ProtoId PowerPool = "RandomPsionicPowerPool";
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.PowerPool = PowerPool;
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicAddAvailablePowers : PsionicPowerFunction
+{
+ ///
+ /// I can't validate these using this method. So this is a string.
+ ///
+ [DataField]
+ public string PowerPrototype = "";
+
+ [DataField]
+ public float Weight = 1f;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ var protoMan = IoCManager.Resolve();
+ if (!protoMan.HasIndex(PowerPrototype)
+ || psionicComponent.AvailablePowers.ContainsKey(PowerPrototype))
+ return;
+
+ psionicComponent.AvailablePowers.Add(PowerPrototype, Weight);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicRemoveAvailablePowers : PsionicPowerFunction
+{
+ ///
+ /// I can't validate these using this method. So this is a string.
+ ///
+ [DataField]
+ public string PowerPrototype = "";
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.AvailablePowers.Remove(PowerPrototype);
+ }
+}
+
+[UsedImplicitly]
+public sealed partial class PsionicModifyRollChances : PsionicPowerFunction
+{
+ [DataField]
+ public float BaselinePowerCostModifier;
+
+ [DataField]
+ public float BaselineChanceModifier;
+
+ public override void OnAddPsionic(
+ EntityUid uid,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto)
+ {
+ psionicComponent.BaselinePowerCost += BaselinePowerCostModifier;
+ psionicComponent.Chance += BaselineChanceModifier;
+ }
+}
diff --git a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs
index a657af150f4..ff32809a5a4 100644
--- a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs
+++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs
@@ -1,17 +1,13 @@
using Content.Shared.Abilities.Psionics;
-using Content.Shared.Actions;
using Content.Shared.Popups;
using Content.Shared.Chat;
-using Content.Shared.Psionics.Glimmer;
-using Content.Shared.Random;
+using Content.Shared.Psionics;
using Content.Shared.Random.Helpers;
using Content.Shared.StatusEffect;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
-using Content.Shared.Psionics;
-using System.Linq;
-using Robust.Server.Player;
+using Robust.Shared.Player;
using Content.Server.Chat.Managers;
using Robust.Shared.Configuration;
using Content.Shared.CCVar;
@@ -19,413 +15,303 @@
using Content.Server.NPC.HTN;
using Content.Server.Ghost;
using Content.Server.Mind;
-namespace Content.Server.Abilities.Psionics
-{
- public sealed class PsionicAbilitiesSystem : EntitySystem
- {
- [Dependency] private readonly IComponentFactory _componentFactory = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
- [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
- [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly SharedActionsSystem _actions = default!;
- [Dependency] private readonly SharedPopupSystem _popups = default!;
- [Dependency] private readonly ISerializationManager _serialization = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IChatManager _chatManager = default!;
- [Dependency] private readonly PsionicFamiliarSystem _psionicFamiliar = default!;
- [Dependency] private readonly IConfigurationManager _config = default!;
- [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
- [Dependency] private readonly GhostSystem _ghost = default!;
- [Dependency] private readonly MindSystem _mind = default!;
-
- private ProtoId _pool = "RandomPsionicPowerPool";
- private const string GenericInitializationMessage = "generic-power-initialization-feedback";
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(InnatePowerStartup);
- SubscribeLocalEvent(OnPsionicShutdown);
- }
-
- ///
- /// Special use-case for a InnatePsionicPowers, which allows an entity to start with any number of Psionic Powers.
- ///
- private void InnatePowerStartup(EntityUid uid, InnatePsionicPowersComponent comp, MapInitEvent args)
- {
- // Any entity with InnatePowers should also be psionic, but in case they aren't already...
- EnsureComp(uid, out var psionic);
-
- foreach (var proto in comp.PowersToAdd)
- if (!psionic.ActivePowers.Contains(_prototypeManager.Index(proto)))
- InitializePsionicPower(uid, _prototypeManager.Index(proto), psionic, false);
- }
-
- private void OnPsionicShutdown(EntityUid uid, PsionicComponent component, ComponentShutdown args)
- {
- if (!EntityManager.EntityExists(uid)
- || HasComp(uid))
- return;
-
- KillFamiliars(component);
- RemoveAllPsionicPowers(uid);
- }
-
- ///
- /// The most shorthand route to creating a Psion. If an entity is not already psionic, it becomes one. This also adds a random new PsionicPower.
- /// To create a "Latent Psychic"(Psion with no powers) just add or ensure the PsionicComponent normally.
- ///
- public void AddPsionics(EntityUid uid)
- {
- if (Deleted(uid))
- return;
-
- AddRandomPsionicPower(uid);
- }
-
- ///
- /// Pretty straightforward, adds a random psionic power to a given Entity. If that Entity is not already Psychic, it will be made one.
- /// If an entity already has all possible powers, this will not add any new ones.
- ///
- public void AddRandomPsionicPower(EntityUid uid)
- {
- // We need to EnsureComp here to make sure that we aren't iterating over a component that:
- // A: Isn't fully initialized
- // B: Is in the process of being shutdown/deleted
- // Imagine my surprise when I found out Resolve doesn't check for that.
- // TODO: This EnsureComp will be 1984'd in a separate PR, when I rework how you get psionics in the first place.
- EnsureComp(uid, out var psionic);
-
- if (!_prototypeManager.TryIndex(_pool.Id, out var pool))
- return;
-
- var newPool = pool.Weights.Keys.ToList();
- newPool.RemoveAll(s =>
- _prototypeManager.TryIndex(s, out var p) &&
- psionic.ActivePowers.Contains(p));
- if (newPool.Count == 0)
- return;
+namespace Content.Server.Abilities.Psionics;
- var newProto = _random.Pick(newPool);
- if (!_prototypeManager.TryIndex(newProto, out var newPower))
- return;
-
- InitializePsionicPower(uid, newPower);
-
- _glimmerSystem.Glimmer += _random.Next(1, (int) Math.Round(1 + psionic.CurrentAmplification + psionic.CurrentDampening));
- }
+public sealed class PsionicAbilitiesSystem : EntitySystem
+{
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedPopupSystem _popups = default!;
+ [Dependency] private readonly ISerializationManager _serialization = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerManager = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly PsionicFamiliarSystem _psionicFamiliar = default!;
+ [Dependency] private readonly IConfigurationManager _config = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+ [Dependency] private readonly GhostSystem _ghost = default!;
+ [Dependency] private readonly MindSystem _mind = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(InnatePowerStartup);
+ SubscribeLocalEvent(OnPsionicShutdown);
+ }
- ///
- /// Initializes a new Psionic Power on a given entity, assuming the entity does not already have said power initialized.
- ///
- public void InitializePsionicPower(EntityUid uid, PsionicPowerPrototype proto, PsionicComponent psionic, bool playFeedback = true)
- {
- if (!_prototypeManager.HasIndex(proto.ID)
- || psionic.ActivePowers.Contains(proto))
- return;
-
- psionic.ActivePowers.Add(proto);
-
- AddPsionicActions(uid, proto, psionic);
- AddPsionicPowerComponents(uid, proto);
- AddPsionicStatSources(proto, psionic);
- RefreshPsionicModifiers(uid, psionic);
- SendFeedbackMessage(uid, proto, playFeedback);
- UpdatePowerSlots(psionic);
- //UpdatePsionicDanger(uid, psionic); // TODO: After Glimmer Refactor
- //SendFeedbackAudio(uid, proto, playPopup); // TODO: This one is coming next!
- }
+ ///
+ /// Special use-case for a InnatePsionicPowers, which allows an entity to start with any number of Psionic Powers.
+ ///
+ private void InnatePowerStartup(EntityUid uid, InnatePsionicPowersComponent comp, MapInitEvent args)
+ {
+ // Any entity with InnatePowers should also be psionic, but in case they aren't already...
+ EnsureComp(uid, out var psionic);
- ///
- /// Initializes a new Psionic Power on a given entity, assuming the entity does not already have said power initialized.
- ///
- public void InitializePsionicPower(EntityUid uid, PsionicPowerPrototype proto, bool playFeedback = true)
- {
- EnsureComp(uid, out var psionic);
+ foreach (var proto in comp.PowersToAdd)
+ if (!psionic.ActivePowers.Contains(_prototypeManager.Index(proto)))
+ InitializePsionicPower(uid, _prototypeManager.Index(proto), psionic, false);
+ }
- InitializePsionicPower(uid, proto, psionic, playFeedback);
- }
+ private void OnPsionicShutdown(EntityUid uid, PsionicComponent component, ComponentShutdown args)
+ {
+ if (!EntityManager.EntityExists(uid)
+ || HasComp(uid))
+ return;
- ///
- /// Updates a Psion's casting stats, call this anytime a system adds a new source of Amp or Damp.
- ///
- public void RefreshPsionicModifiers(EntityUid uid, PsionicComponent comp)
- {
- var ampModifier = 0f;
- var dampModifier = 0f;
- foreach (var (_, source) in comp.AmplificationSources)
- ampModifier += source;
- foreach (var (_, source) in comp.DampeningSources)
- dampModifier += source;
-
- var ev = new OnSetPsionicStatsEvent(ampModifier, dampModifier);
- RaiseLocalEvent(uid, ref ev);
- ampModifier = ev.AmplificationChangedAmount;
- dampModifier = ev.DampeningChangedAmount;
-
- comp.CurrentAmplification = ampModifier;
- comp.CurrentDampening = dampModifier;
- }
+ KillFamiliars(component);
+ RemoveAllPsionicPowers(uid);
+ }
- ///
- /// Updates a Psion's casting stats, call this anytime a system adds a new source of Amp or Damp.
- /// Variant function for systems that didn't already have the PsionicComponent.
- ///
- public void RefreshPsionicModifiers(EntityUid uid)
- {
- if (!TryComp(uid, out var comp))
- return;
+ ///
+ /// The most shorthand route to creating a Psion. If an entity is not already psionic, it becomes one. This also adds a random new PsionicPower.
+ /// To create a "Latent Psychic"(Psion with no powers) just add or ensure the PsionicComponent normally.
+ ///
+ public void AddPsionics(EntityUid uid)
+ {
+ if (Deleted(uid))
+ return;
- RefreshPsionicModifiers(uid, comp);
- }
+ AddRandomPsionicPower(uid);
+ }
- ///
- /// A more advanced form of removing powers. Mindbreaking not only removes all psionic powers,
- /// it also disables the possibility of obtaining new ones.
- ///
- public void MindBreak(EntityUid uid)
+ ///
+ /// Pretty straightforward, adds a random psionic power to a given Entity. If that Entity is not already Psychic, it will be made one.
+ /// If an entity already has all possible powers, this will not add any new ones.
+ ///
+ public void AddRandomPsionicPower(EntityUid uid, bool forced = false)
+ {
+ // We need to EnsureComp here to make sure that we aren't iterating over a component that:
+ // A: Isn't fully initialized
+ // B: Is in the process of being shutdown/deleted
+ // Imagine my surprise when I found out Resolve doesn't check for that.
+ // TODO: This EnsureComp will be 1984'd in a separate PR, when I rework how you get psionics in the first place.
+ EnsureComp(uid, out var psionic);
+ if (!psionic.Roller && !forced)
+ return;
+
+ // Since this can be called by systems other than the original roundstart initialization, we need to check that the available powers list
+ // doesn't contain duplicates of powers we already have.
+ var copy = _serialization.CreateCopy(psionic.AvailablePowers, notNullableOverride: true);
+ foreach (var weight in copy)
{
- if (!HasComp(uid))
- return;
-
- RemoveAllPsionicPowers(uid, true);
- if (_config.GetCVar(CCVars.ScarierMindbreaking))
- ScarierMindbreak(uid);
- }
+ if (!_prototypeManager.TryIndex(weight.Key, out var copyPower)
+ || !psionic.ActivePowers.Contains(copyPower))
+ continue;
- ///
- /// An even more advanced form of Mindbreaking. Turn the victim into an NPC.
- /// For the people who somehow didn't intuit from the absolutely horrifying text that mindbreaking people is very fucking bad.
- ///
- public void ScarierMindbreak(EntityUid uid)
- {
- if (!_playerManager.TryGetSessionByEntity(uid, out var session) || session is null)
- return;
-
- var feedbackMessage = $"[font size=24][color=#ff0000]{"Your characters personhood has been obliterated. If you wish to continue playing, consider respawning as a new character."}[/color][/font]";
- _chatManager.ChatMessageToOne(
- ChatChannel.Emotes,
- feedbackMessage,
- feedbackMessage,
- EntityUid.Invalid,
- false,
- session.Channel);
-
- if (!_mind.TryGetMind(session, out var mindId, out var mind))
- return;
-
- _ghost.SpawnGhost((mindId, mind), Transform(uid).Coordinates, false);
- _npcFaction.AddFaction(uid, "SimpleNeutral");
- var htn = EnsureComp(uid);
- htn.RootTask = new HTNCompoundTask() { Task = "IdleCompound" };
+ psionic.AvailablePowers.Remove(copyPower.ID);
}
- ///
- /// Remove all Psionic powers, with accompanying actions, components, and casting stat sources, from a given Psion.
- /// Optionally, the Psion can also be rendered permanently non-Psionic.
- ///
- public void RemoveAllPsionicPowers(EntityUid uid, bool mindbreak = false)
- {
- if (!TryComp(uid, out var psionic)
- || !psionic.Removable)
- return;
-
- RemovePsionicActions(uid, psionic);
+ if (psionic.AvailablePowers.Count <= 0)
+ return;
+ var proto = _random.Pick(psionic.AvailablePowers);
+ if (!_prototypeManager.TryIndex(proto, out var newPower))
+ return;
- var newPsionic = psionic.ActivePowers.ToList();
- foreach (var proto in newPsionic)
- {
- if (!_prototypeManager.TryIndex(proto.ID, out var power))
- continue;
+ InitializePsionicPower(uid, newPower);
+ }
- RemovePsionicPowerComponents(uid, proto);
+ ///
+ /// Initializes a new Psionic Power on a given entity, assuming the entity does not already have said power initialized.
+ ///
+ public void InitializePsionicPower(EntityUid uid, PsionicPowerPrototype proto, PsionicComponent psionic, bool playFeedback = true)
+ {
+ if (!_prototypeManager.HasIndex(proto.ID)
+ || psionic.ActivePowers.Contains(proto))
+ return;
+
+ psionic.ActivePowers.Add(proto);
+
+ foreach (var function in proto.InitializeFunctions)
+ function.OnAddPsionic(uid,
+ _componentFactory,
+ EntityManager,
+ _serialization,
+ _playerManager,
+ Loc,
+ psionic,
+ proto);
+
+ RefreshPsionicModifiers(uid, psionic);
+ UpdatePowerSlots(psionic);
+ }
- // If we're mindbreaking, we can skip the casting stats since the PsionicComponent is getting 1984'd.
- if (!mindbreak)
- RemovePsionicStatSources(uid, power, psionic);
- }
+ ///
+ /// Initializes a new Psionic Power on a given entity, assuming the entity does not already have said power initialized.
+ ///
+ public void InitializePsionicPower(EntityUid uid, PsionicPowerPrototype proto, bool playFeedback = true)
+ {
+ EnsureComp(uid, out var psionic);
- if (mindbreak)
- {
- EnsureComp(uid);
- _statusEffectsSystem.TryAddStatusEffect(uid, psionic.MindbreakingStutterCondition,
- TimeSpan.FromMinutes(psionic.MindbreakingStutterTime * psionic.CurrentAmplification * psionic.CurrentDampening),
- false,
- psionic.MindbreakingStutterAccent);
+ InitializePsionicPower(uid, proto, psionic, playFeedback);
+ }
- _popups.PopupEntity(Loc.GetString(psionic.MindbreakingFeedback, ("entity", MetaData(uid).EntityName)), uid, uid, PopupType.MediumCaution);
+ ///
+ /// Updates a Psion's casting stats, call this anytime a system adds a new source of Amp or Damp.
+ ///
+ public void RefreshPsionicModifiers(EntityUid uid, PsionicComponent comp)
+ {
+ var ampModifier = 0f;
+ var dampModifier = 0f;
+ foreach (var (_, source) in comp.AmplificationSources)
+ ampModifier += source;
+ foreach (var (_, source) in comp.DampeningSources)
+ dampModifier += source;
+
+ var ev = new OnSetPsionicStatsEvent(ampModifier, dampModifier);
+ RaiseLocalEvent(uid, ref ev);
+ ampModifier = ev.AmplificationChangedAmount;
+ dampModifier = ev.DampeningChangedAmount;
+
+ comp.CurrentAmplification = ampModifier;
+ comp.CurrentDampening = dampModifier;
+ }
- KillFamiliars(psionic);
- RemComp(uid);
- RemComp(uid);
+ ///
+ /// Updates a Psion's casting stats, call this anytime a system adds a new source of Amp or Damp.
+ /// Variant function for systems that didn't already have the PsionicComponent.
+ ///
+ public void RefreshPsionicModifiers(EntityUid uid)
+ {
+ if (!TryComp(uid, out var comp))
+ return;
- var ev = new OnMindbreakEvent();
- RaiseLocalEvent(uid, ref ev);
+ RefreshPsionicModifiers(uid, comp);
+ }
- return;
- }
- RefreshPsionicModifiers(uid, psionic);
- }
+ ///
+ /// A more advanced form of removing powers. Mindbreaking not only removes all psionic powers,
+ /// it also disables the possibility of obtaining new ones.
+ ///
+ public void MindBreak(EntityUid uid)
+ {
+ if (!TryComp(uid, out var psionic))
+ return;
- ///
- /// Add all actions associated with a specific Psionic Power
- ///
- private void AddPsionicActions(EntityUid uid, PsionicPowerPrototype proto, PsionicComponent psionic)
- {
- foreach (var id in proto.Actions)
- {
- EntityUid? actionId = null;
- if (_actions.AddAction(uid, ref actionId, id))
- {
- _actions.StartUseDelay(actionId);
- psionic.Actions.Add(id, actionId);
- }
- }
- }
+ RemoveAllPsionicPowers(uid, true);
+ EnsureComp(uid);
+ _statusEffectsSystem.TryAddStatusEffect(uid, psionic.MindbreakingStutterCondition,
+ TimeSpan.FromMinutes(psionic.MindbreakingStutterTime * psionic.CurrentAmplification * psionic.CurrentDampening),
+ false,
+ psionic.MindbreakingStutterAccent);
- ///
- /// Add all components associated with a specific Psionic power.
- ///
- private void AddPsionicPowerComponents(EntityUid uid, PsionicPowerPrototype proto)
- {
- if (proto.Components is null)
- return;
-
- foreach (var entry in proto.Components.Values)
- {
- if (HasComp(uid, entry.Component.GetType()))
- continue;
-
- var comp = (Component) _serialization.CreateCopy(entry.Component, notNullableOverride: true);
- comp.Owner = uid;
- EntityManager.AddComponent(uid, comp);
- }
- }
+ _popups.PopupEntity(Loc.GetString(psionic.MindbreakingFeedback, ("entity", MetaData(uid).EntityName)), uid, uid, PopupType.MediumCaution);
- ///
- /// Update the Amplification and Dampening sources of a Psion to include a new Power.
- ///
- private void AddPsionicStatSources(PsionicPowerPrototype proto, PsionicComponent psionic)
- {
- if (proto.AmplificationModifier != 0)
- psionic.AmplificationSources.Add(proto.Name, proto.AmplificationModifier);
+ KillFamiliars(psionic);
+ RemComp(uid);
+ RemComp(uid);
- if (proto.DampeningModifier != 0)
- psionic.DampeningSources.Add(proto.Name, proto.DampeningModifier);
- }
+ var ev = new OnMindbreakEvent();
+ RaiseLocalEvent(uid, ref ev);
- ///
- /// Displays a message to alert the player when they have obtained a new psionic power. These generally will not play for Innate powers.
- /// Chat messages of this nature should be written in the first-person.
- /// Popup feedback should be no more than a sentence, while the full Initialization Feedback can be as much as a paragraph of text.
- ///
- private void SendFeedbackMessage(EntityUid uid, PsionicPowerPrototype proto, bool playFeedback = true)
- {
- if (!playFeedback
- || !_playerManager.TryGetSessionByEntity(uid, out var session)
- || session is null)
- return;
-
- if (proto.InitializationPopup is null)
- _popups.PopupEntity(Loc.GetString(GenericInitializationMessage), uid, uid, PopupType.MediumCaution);
- else _popups.PopupEntity(Loc.GetString(proto.InitializationPopup), uid, uid, PopupType.MediumCaution);
-
- if (proto.InitializationFeedback is null)
- return;
-
- if (!Loc.TryGetString(proto.InitializationFeedback, out var feedback))
- return;
- var feedbackMessage = $"[font size={proto.InitializationFeedbackFontSize}][color={proto.InitializationFeedbackColor}]{feedback}[/color][/font]";
- _chatManager.ChatMessageToOne(
- proto.InitializationFeedbackChannel,
- feedbackMessage,
- feedbackMessage,
- EntityUid.Invalid,
- false,
- session.Channel);
- }
+ if (_config.GetCVar(CCVars.ScarierMindbreaking))
+ ScarierMindbreak(uid, psionic);
+ }
- private void UpdatePowerSlots(PsionicComponent psionic)
- {
- var slotsUsed = 0;
- foreach (var power in psionic.ActivePowers)
- slotsUsed += power.PowerSlotCost;
+ ///
+ /// An even more advanced form of Mindbreaking. Turn the victim into an NPC.
+ /// For the people who somehow didn't intuit from the absolutely horrifying text that mindbreaking people is very fucking bad.
+ ///
+ public void ScarierMindbreak(EntityUid uid, PsionicComponent component)
+ {
+ if (!_playerManager.TryGetSessionByEntity(uid, out var session) || session is null)
+ return;
+
+ var popup = Loc.GetString(component.HardMindbreakingFeedback);
+ var feedbackMessage = $"[font size=24][color=#ff0000]{popup}[/color][/font]";
+ _chatManager.ChatMessageToOne(
+ ChatChannel.Emotes,
+ feedbackMessage,
+ feedbackMessage,
+ EntityUid.Invalid,
+ false,
+ session.Channel);
+
+ if (!_mind.TryGetMind(session, out var mindId, out var mind))
+ return;
+
+ _ghost.SpawnGhost((mindId, mind), Transform(uid).Coordinates, false);
+ _npcFaction.AddFaction(uid, "SimpleNeutral");
+ var htn = EnsureComp(uid);
+ htn.RootTask = new HTNCompoundTask() { Task = "IdleCompound" };
+ }
- psionic.PowerSlotsTaken = slotsUsed;
- }
+ ///
+ /// Remove all Psionic powers, with accompanying actions, components, and casting stat sources, from a given Psion.
+ /// Optionally, the Psion can also be rendered permanently non-Psionic.
+ ///
+ public void RemoveAllPsionicPowers(EntityUid uid, bool mindbreak = false)
+ {
+ if (!TryComp(uid, out var psionic)
+ || !psionic.Removable)
+ return;
- ///
- /// Psions over a certain power threshold become a glimmer source. This cannot be fully implemented until after I rework Glimmer
- ///
- //private void UpdatePsionicDanger(EntityUid uid, PsionicComponent psionic)
- //{
- // if (psionic.PowerSlotsTaken <= psionic.PowerSlots)
- // return;
- //
- // EnsureComp(uid, out var glimmerSource);
- // glimmerSource.SecondsPerGlimmer = 10 / (psionic.PowerSlotsTaken - psionic.PowerSlots);
- //}
-
- ///
- /// Remove all Psychic Actions listed in an entity's Psionic Component. Unfortunately, removing actions associated with a specific Power Prototype is not supported.
- ///
- private void RemovePsionicActions(EntityUid uid, PsionicComponent psionic)
- {
- if (psionic.Actions is null)
- return;
+ foreach (var proto in psionic.ActivePowers)
+ RemovePsionicPower(uid, psionic, proto, mindbreak);
- foreach (var action in psionic.Actions)
- _actionsSystem.RemoveAction(uid, action.Value);
- }
+ if (mindbreak)
+ return;
- ///
- /// Remove all Components associated with a specific Psionic Power.
- ///
- private void RemovePsionicPowerComponents(EntityUid uid, PsionicPowerPrototype proto)
- {
- if (proto.Components is null)
- return;
+ RefreshPsionicModifiers(uid, psionic);
+ }
- foreach (var comp in proto.Components)
- {
- var powerComp = (Component) _componentFactory.GetComponent(comp.Key);
- if (!EntityManager.HasComponent(uid, powerComp.GetType()))
- continue;
+ public void RemovePsionicPower(EntityUid uid, PsionicComponent psionicComponent, PsionicPowerPrototype psionicPower, bool forced = false)
+ {
+ if (!psionicComponent.ActivePowers.Contains(psionicPower)
+ || !psionicComponent.Removable && !forced)
+ return;
+
+ foreach (var function in psionicPower.RemovalFunctions)
+ function.OnAddPsionic(uid,
+ _componentFactory,
+ EntityManager,
+ _serialization,
+ _playerManager,
+ Loc,
+ psionicComponent,
+ psionicPower);
+ }
- EntityManager.RemoveComponent(uid, powerComp.GetType());
- }
- }
+ public void RemovePsionicPower(EntityUid uid, PsionicPowerPrototype psionicPower, bool forced = false)
+ {
+ if (!TryComp(uid, out var psionicComponent)
+ || !psionicComponent.ActivePowers.Contains(psionicPower)
+ || !psionicComponent.Removable && !forced)
+ return;
+
+ foreach (var function in psionicPower.RemovalFunctions)
+ function.OnAddPsionic(uid,
+ _componentFactory,
+ EntityManager,
+ _serialization,
+ _playerManager,
+ Loc,
+ psionicComponent,
+ psionicPower);
+ }
- ///
- /// Remove all stat sources associated with a specific Psionic Power.
- ///
- private void RemovePsionicStatSources(EntityUid uid, PsionicPowerPrototype proto, PsionicComponent psionic)
- {
- if (proto.AmplificationModifier != 0)
- psionic.AmplificationSources.Remove(proto.Name);
+ private void UpdatePowerSlots(PsionicComponent psionic)
+ {
+ var slotsUsed = 0;
+ foreach (var power in psionic.ActivePowers)
+ slotsUsed += power.PowerSlotCost;
- if (proto.DampeningModifier != 0)
- psionic.DampeningSources.Remove(proto.Name);
+ psionic.PowerSlotsTaken = slotsUsed;
+ }
- RefreshPsionicModifiers(uid, psionic);
- }
+ private void KillFamiliars(PsionicComponent component)
+ {
+ if (component.Familiars.Count <= 0)
+ return;
- private void KillFamiliars(PsionicComponent component)
+ foreach (var familiar in component.Familiars)
{
- if (component.Familiars.Count <= 0)
- return;
-
- foreach (var familiar in component.Familiars)
- {
- if (!TryComp(familiar, out var familiarComponent)
- || !familiarComponent.DespawnOnMasterDeath)
- continue;
+ if (!TryComp(familiar, out var familiarComponent)
+ || !familiarComponent.DespawnOnMasterDeath)
+ continue;
- _psionicFamiliar.DespawnFamiliar(familiar, familiarComponent);
- }
+ _psionicFamiliar.DespawnFamiliar(familiar, familiarComponent);
}
}
}
diff --git a/Content.Server/Chat/TelepathicChatSystem.Psychognomy.cs b/Content.Server/Chat/TelepathicChatSystem.Psychognomy.cs
index 4ba990466c6..383096c03d6 100644
--- a/Content.Server/Chat/TelepathicChatSystem.Psychognomy.cs
+++ b/Content.Server/Chat/TelepathicChatSystem.Psychognomy.cs
@@ -130,7 +130,7 @@ private void DescribeGlimmerSource(EntityUid uid, GlimmerSourceComponent compone
// This one's also a bit of a catch-all for "lacks component"
private void DescribePsion(EntityUid uid, PsionicComponent component, GetPsychognomicDescriptorEvent ev)
{
- if (component.PsychognomicDescriptors != null)
+ if (component.PsychognomicDescriptors.Count > 0)
{
foreach (var descriptor in component.PsychognomicDescriptors)
{
diff --git a/Content.Server/Nyanotrasen/Psionics/NPC/PsionicNpcCombatSystem.cs b/Content.Server/Nyanotrasen/Psionics/NPC/PsionicNpcCombatSystem.cs
index 9caef36a752..1f18176fac6 100644
--- a/Content.Server/Nyanotrasen/Psionics/NPC/PsionicNpcCombatSystem.cs
+++ b/Content.Server/Nyanotrasen/Psionics/NPC/PsionicNpcCombatSystem.cs
@@ -27,25 +27,24 @@ public override void Initialize()
SubscribeLocalEvent(ZapCombat);
NoosphericZap = _protoMan.Index(NoosphericZapProto);
- DebugTools.Assert(NoosphericZap.Actions.Count == 1, "I can't account for this, so it's your problem now");
}
private void ZapCombat(Entity ent, ref NPCSteeringEvent args)
{
- PsionicComponent? psionics = null;
- if (!Resolve(ent, ref psionics, logMissing: true)
- || !psionics.Actions.TryGetValue(NoosphericZap.Actions[0], out var action)
- || action is null)
- return;
-
- var actionTarget = Comp(action.Value);
- if (actionTarget.Cooldown is {} cooldown && cooldown.End > _timing.CurTime
- || !TryComp(ent, out var combat)
- || !_actions.ValidateEntityTarget(ent, combat.Target, (action.Value, actionTarget))
- || actionTarget.Event is not {} ev)
- return;
-
- ev.Target = combat.Target;
- _actions.PerformAction(ent, null, action.Value, actionTarget, ev, _timing.CurTime, predicted: false);
+ // Nothing uses this anyway, what the hell it's pure shitcode?
+ // PsionicComponent? psionics = null;
+ // if (!Resolve(ent, ref psionics, logMissing: true)
+ // || !psionics.ActivePowers.Contains(NoosphericZap))
+ // return;
+
+ // var actionTarget = Comp(action.Value);
+ // if (actionTarget.Cooldown is {} cooldown && cooldown.End > _timing.CurTime
+ // || !TryComp(ent, out var combat)
+ // || !_actions.ValidateEntityTarget(ent, combat.Target, (action.Value, actionTarget))
+ // || actionTarget.Event is not {} ev)
+ // return;
+
+ // ev.Target = combat.Target;
+ // _actions.PerformAction(ent, null, action.Value, actionTarget, ev, _timing.CurTime, predicted: false);
}
}
diff --git a/Content.Server/Psionics/PsionicsSystem.cs b/Content.Server/Psionics/PsionicsSystem.cs
index 5f43e730ad4..20e55576721 100644
--- a/Content.Server/Psionics/PsionicsSystem.cs
+++ b/Content.Server/Psionics/PsionicsSystem.cs
@@ -1,6 +1,8 @@
using Content.Shared.Abilities.Psionics;
using Content.Shared.StatusEffect;
+using Content.Shared.Psionics;
using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Random;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Damage.Events;
using Content.Shared.CCVar;
@@ -19,9 +21,9 @@
using Content.Shared.Mobs;
using Content.Shared.Damage;
using Content.Shared.Interaction.Events;
+using Timer = Robust.Shared.Timing.Timer;
using Content.Shared.Alert;
using Content.Shared.Rounding;
-using Content.Shared.Psionics;
namespace Content.Server.Psionics;
@@ -62,6 +64,9 @@ public sealed class PsionicsSystem : EntitySystem
public override void Update(float frameTime)
{
base.Update(frameTime);
+ if (!_cfg.GetCVar(CCVars.PsionicRollsEnabled))
+ return;
+
foreach (var roller in _rollers)
RollPsionics(roller.uid, roller.component, true);
_rollers.Clear();
@@ -87,7 +92,22 @@ private void OnStartup(EntityUid uid, PsionicComponent component, MapInitEvent a
|| !component.CanReroll)
return;
+ Timer.Spawn(TimeSpan.FromSeconds(30), () => DeferRollers(uid));
+
+ }
+
+ ///
+ /// We wait a short time before starting up the rolled powers, so that other systems have a chance to modify the list first.
+ /// This is primarily for the sake of TraitSystem and AddJobSpecial.
+ ///
+ private void DeferRollers(EntityUid uid)
+ {
+ if (!Exists(uid)
+ || !TryComp(uid, out PsionicComponent? component))
+ return;
+
CheckPowerCost(uid, component);
+ GenerateAvailablePowers(component);
_rollers.Enqueue((component, uid));
}
@@ -108,6 +128,24 @@ private void CheckPowerCost(EntityUid uid, PsionicComponent component)
component.NextPowerCost = 100 * MathF.Pow(2, powerCount);
}
+ ///
+ /// The power pool is itself a DataField, and things like Traits/Antags are allowed to modify or replace the pool.
+ ///
+ private void GenerateAvailablePowers(PsionicComponent component)
+ {
+ if (!_protoMan.TryIndex(component.PowerPool.Id, out var pool))
+ return;
+
+ foreach (var id in pool.Weights)
+ {
+ if (!_protoMan.TryIndex(id.Key, out var power)
+ || component.ActivePowers.Contains(power))
+ continue;
+
+ component.AvailablePowers.Add(id.Key, id.Value);
+ }
+ }
+
private void OnMeleeHit(EntityUid uid, AntiPsionicWeaponComponent component, MeleeHitEvent args)
{
foreach (var entity in args.HitEntities)
@@ -200,7 +238,7 @@ private bool HandlePotentiaCalculations(EntityUid uid, PsionicComponent componen
component.Potentia -= component.NextPowerCost;
_psionicAbilitiesSystem.AddPsionics(uid);
- component.NextPowerCost = 100 * MathF.Pow(2, component.PowerSlotsTaken);
+ component.NextPowerCost = component.BaselinePowerCost * MathF.Pow(2, component.PowerSlotsTaken);
return true;
}
diff --git a/Content.Shared/Psionics/PsionicComponent.cs b/Content.Shared/Psionics/PsionicComponent.cs
index 299dc71340b..58118dbd700 100644
--- a/Content.Shared/Psionics/PsionicComponent.cs
+++ b/Content.Shared/Psionics/PsionicComponent.cs
@@ -1,6 +1,7 @@
using Content.Shared.Alert;
using Content.Shared.DoAfter;
using Content.Shared.Psionics;
+using Content.Shared.Random;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -45,6 +46,12 @@ public sealed partial class PsionicComponent : Component
[DataField]
public float Potentia;
+ ///
+ /// The base cost for new powers.
+ ///
+ [DataField]
+ public float BaselinePowerCost = 100;
+
///
/// Each time a Psion rolls for a new power, they roll a number between 0 and 100, adding any relevant modifiers. This number is then added to Potentia,
/// meaning that it carries over between rolls. When a character has an amount of potentia equal to at least 100 * 2^(total powers), the potentia is then spent, and a power is generated.
@@ -81,6 +88,11 @@ public sealed partial class PsionicComponent : Component
[DataField]
public string MindbreakingFeedback = "mindbreaking-feedback";
+ ///
+ ///
+ [DataField]
+ public string HardMindbreakingFeedback = "hard-mindbreaking-feedback";
+
///
/// How much should the odds of obtaining a Psionic Power be multiplied when rolling for one.
///
@@ -139,6 +151,12 @@ private set
}
}
+ ///
+ /// Whether this entity is capable of randomly rolling for powers.
+ ///
+ [DataField]
+ public bool Roller = true;
+
///
/// Ifrits, revenants, etc are explicitly magical beings that shouldn't get mindbroken
///
@@ -153,10 +171,10 @@ private set
public HashSet ActivePowers = new();
///
- /// The list of each Psionic Power by action with entityUid.
+ /// The list of each Psionic Power by prototype with entityUid.
///
[ViewVariables(VVAccess.ReadOnly)]
- public Dictionary Actions = new();
+ public Dictionary Actions = new();
///
/// What sources of Amplification does this Psion have?
@@ -202,7 +220,7 @@ private set
/// unneccesary subs for unique psionic entities like e.g. Oracle.
///
[DataField]
- public List? PsychognomicDescriptors = null;
+ public List PsychognomicDescriptors = new();
/// Used for tracking what spell a Psion is actively casting
[DataField]
@@ -228,6 +246,22 @@ private set
[DataField]
public int FamiliarLimit = 1;
+ ///
+ /// The list of all potential Assay messages that can be obtained from this Psion.
+ ///
+ [DataField]
+ public List AssayFeedback = new();
+
+ ///
+ /// The list of powers that this Psion is eligible to roll new abilities from.
+ /// This generates the initial ability pool, but can also be modified by other systems.
+ ///
+ [DataField]
+ public ProtoId PowerPool = "RandomPsionicPowerPool";
+
+ [DataField]
+ public Dictionary AvailablePowers = new();
+
[DataField]
public ProtoId ManaAlert = "Mana";
}
diff --git a/Content.Shared/Psionics/PsionicPowerPrototype.cs b/Content.Shared/Psionics/PsionicPowerPrototype.cs
index d81ae05be23..ecc988d5d67 100644
--- a/Content.Shared/Psionics/PsionicPowerPrototype.cs
+++ b/Content.Shared/Psionics/PsionicPowerPrototype.cs
@@ -1,5 +1,7 @@
-using Content.Shared.Chat;
+using Content.Shared.Abilities.Psionics;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
namespace Content.Shared.Psionics;
@@ -19,77 +21,43 @@ public sealed partial class PsionicPowerPrototype : IPrototype
public string Name = default!;
///
- /// The description of a power in yml, used for player notifications.
- ///
- [DataField(required: true)]
- public string Description = default!;
-
- ///
- /// The list of each Action that this power adds in the form of ActionId and ActionEntity
- ///
- [DataField]
- public List Actions = new();
-
- ///
- /// The list of what Components this power adds.
- ///
- [DataField]
- public ComponentRegistry Components = new();
-
- ///
- /// What message will be sent to the player as a Popup.
- /// If left blank, it will default to the Const "generic-power-initialization-feedback"
- ///
- [DataField]
- public string? InitializationPopup;
-
- ///
- /// What message will be sent to the chat window when the power is initialized. Leave it blank to send no message.
- /// Initialization messages won't play for powers that are Innate, only powers obtained during the round.
- /// These should generally also be written in the first person, and can be far lengthier than popups.
- ///
- [DataField]
- public string? InitializationFeedback;
-
- ///
- /// What color will the initialization feedback display in the chat window with.
- ///
- [DataField]
- public string InitializationFeedbackColor = "#8A00C2";
-
- ///
- /// What font size will the initialization message use in chat.
- ///
- [DataField]
- public int InitializationFeedbackFontSize = 12;
-
- ///
- /// Which chat channel will the initialization message use.
+ /// What category of psionics does this power come from.
+ /// EG: Mentalics, Anomalists, Blood Cults, Heretics, etc.
///
[DataField]
- public ChatChannel InitializationFeedbackChannel = ChatChannel.Emotes;
+ public List PowerCategories = new();
///
- /// What message will this power generate when scanned by a Metempsionic Focused Pulse.
+ /// These functions are called when a Psionic Power is added to a Psion.
///
- [DataField]
- public string MetapsionicFeedback = "psionic-metapsionic-feedback-default";
+ [DataField(serverOnly: true)]
+ public PsionicPowerFunction[] InitializeFunctions { get; private set; } = Array.Empty();
///
- /// How much this power will increase or decrease a user's Amplification.
+ /// These functions are called when a Psionic Power is removed from a Psion,
+ /// as a rule of thumb these should do the exact opposite of most of a power's init functions.
///
- [DataField]
- public float AmplificationModifier = 0;
-
- ///
- /// How much this power will increase or decrease a user's Dampening.
- ///
- [DataField]
- public float DampeningModifier = 0;
+ [DataField(serverOnly: true)]
+ public PsionicPowerFunction[] RemovalFunctions { get; private set; } = Array.Empty();
///
/// How many "Power Slots" this power occupies.
///
[DataField]
public int PowerSlotCost = 1;
-}
\ No newline at end of file
+}
+
+/// This serves as a hook for psionic powers to modify the psionic component.
+[ImplicitDataDefinitionForInheritors]
+public abstract partial class PsionicPowerFunction
+{
+ public abstract void OnAddPsionic(
+ EntityUid mob,
+ IComponentFactory factory,
+ IEntityManager entityManager,
+ ISerializationManager serializationManager,
+ ISharedPlayerManager playerManager,
+ ILocalizationManager loc,
+ PsionicComponent psionicComponent,
+ PsionicPowerPrototype proto);
+}
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index 341c7bb92a6..23c8fd9bc23 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -8901,3 +8901,27 @@ Entries:
id: 6603
time: '2025-01-01T03:33:47.0000000+00:00'
url: https://github.com/Simple-Station/Einstein-Engines/pull/1390
+- author: VMSolidus
+ changes:
+ - type: Add
+ message: >-
+ Psionic Refactor V3 is here! No new powers are added in this update, but
+ the options for creating new powers has been SIGNIFICANTLY EXPANDED.
+ - type: Add
+ message: >-
+ Xenoglossy and Psychognomy now can only be rolled if you first have the
+ Telepathy power.
+ - type: Add
+ message: >-
+ Breath of Life can now only be rolled if you first have the Healing Word
+ power
+ - type: Add
+ message: Pyrokinesis and Summon Imp now require the Pyroknetic Flare power
+ - type: Add
+ message: >-
+ All new Psychognomy descriptors for many pre-existing powers. Have fun
+ being unintentionally screamed at telepathically by someone with the
+ POWER OVERWHELMING trait.
+ id: 6604
+ time: '2025-01-01T21:59:50.0000000+00:00'
+ url: https://github.com/Simple-Station/Einstein-Engines/pull/1383
diff --git a/Resources/Locale/en-US/psionics/psionic-powers.ftl b/Resources/Locale/en-US/psionics/psionic-powers.ftl
index ae3cfb383ed..ab2a991e06f 100644
--- a/Resources/Locale/en-US/psionics/psionic-powers.ftl
+++ b/Resources/Locale/en-US/psionics/psionic-powers.ftl
@@ -160,6 +160,7 @@ summon-remilia-power-description = { action-description-summon-remilia }
# Psionic System Messages
mindbreaking-feedback = The light of life vanishes from {CAPITALIZE($entity)}'s eyes, leaving behind a husk pretending at sapience
+hard-mindbreaking-feedback = Your character's personhood has been obliterated. If you wish to continue playing, consider respawning as a new character.
examine-mindbroken-message =
Eyes unblinking, staring deep into the horizon. {CAPITALIZE($entity)} is a sack of meat pretending it has a soul.
There is nothing behind its gaze, no evidence there can be found of the divine light of creation.
diff --git a/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml b/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
index a0694465b55..3dd7b2c651c 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
@@ -229,7 +229,7 @@
noMana: shadowkin-tired
- type: InnatePsionicPowers
powersToAdd:
- - ShadowkinPowers
+ - DarkSwapPower
- type: LanguageKnowledge
speaks:
- TauCetiBasic
diff --git a/Resources/Prototypes/Psionics/PsionicPowerPool.yml b/Resources/Prototypes/Psionics/PsionicPowerPool.yml
index a5cfccbbae5..f1d4f69b05a 100644
--- a/Resources/Prototypes/Psionics/PsionicPowerPool.yml
+++ b/Resources/Prototypes/Psionics/PsionicPowerPool.yml
@@ -5,16 +5,11 @@
DispelPower: 1
#TelegnosisPower: 1
PsionicRegenerationPower: 1
- XenoglossyPower: 0.75
- PsychognomyPower: 0.75
MassSleepPower: 0.3
# PsionicInvisibilityPower: 0.15
MindSwapPower: 0.15
TelepathyPower: 1
HealingWordPower: 0.85
- RevivifyPower: 0.1
ShadeskipPower: 0.15
TelekineticPulsePower: 0.15
PyrokineticFlare: 0.3
- SummonImpPower: 0.15
- DarkSwapPower: 0.1
diff --git a/Resources/Prototypes/Psionics/psionics.yml b/Resources/Prototypes/Psionics/psionics.yml
index 9b526665851..548881e61f7 100644
--- a/Resources/Prototypes/Psionics/psionics.yml
+++ b/Resources/Prototypes/Psionics/psionics.yml
@@ -1,275 +1,663 @@
- type: psionicPower
id: DispelPower
name: Dispel
- description: dispel-power-description
- actions:
- - ActionDispel
- components:
- - type: DispelPower
- initializationFeedback: dispel-power-initialization-feedback
- metapsionicFeedback: dispel-power-metapsionic-feedback
- dampeningModifier: 1
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionDispel
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: DispelPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: dispel-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: -4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: dispel-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ dampeningModifier: 1
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: DispelPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: dispel-power-metapsionic-feedback
- type: psionicPower
id: MassSleepPower
name: Mass Sleep
- description: mass-sleep-power-description
- actions:
- - ActionMassSleep
- components:
- - type: MassSleepPower
- # initializationFeedback: mass-sleep-power-initialization-feedback # I apologize, I don't feel like writing a paragraph of feedback for a power that's getting replaced with a new one.
- metapsionicFeedback: mass-sleep-power-metapsionic-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionMassSleep
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: MassSleepPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 5
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: mass-sleep-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: MassSleepPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: mass-sleep-power-metapsionic-feedback
- type: psionicPower
id: MindSwapPower
name: Mind Swap
- description: mind-swap-power-description
- actions:
- - ActionMindSwap
- components:
- - type: MindSwapPower
- initializationFeedback: mind-swap-power-initialization-feedback
- metapsionicFeedback: mind-swap-power-metapsionic-feedback
- amplificationModifier: 1
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionMindSwap
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: MindSwapPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: mind-swap-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: mind-swap-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 1
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: MindSwapPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: mind-swap-power-metapsionic-feedback
- type: psionicPower
id: NoosphericZapPower
name: Noospheric Zap
- description: noospheric-zap-power-description
- actions:
- - ActionNoosphericZap
- components:
- - type: NoosphericZapPower
- initializationFeedback: noospheric-zap-power-initialization-feedback
- metapsionicFeedback: noospheric-zap-power-metapsionic-feedback
- amplificationModifier: 1
+ powerCategories:
+ - Anomalist
+ - Electrokinesis
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionNoosphericZap
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: NoosphericZapPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: noospheric-zap-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: noospheric-zap-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 1
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: NoosphericZapPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: noospheric-zap-power-metapsionic-feedback
- type: psionicPower
id: PyrokinesisPower
name: Pyrokinesis
- description: pyrokinesis-power-description
- actions:
- - ActionPyrokinesis
- components:
- - type: PyrokinesisPower
- initializationFeedback: pyrokinesis-power-initialization-feedback
- metapsionicFeedback: pyrokinesis-power-metapsionic-feedback
- amplificationModifier: 1
+ powerCategories:
+ - Anomalist
+ - Pyrokinesis
+ - Dangerous
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionPyrokinesis
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: PyrokinesisPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: pyrokinesis-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: pyrokinesis-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 1
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: PyrokinesisPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: pyrokinesis-power-metapsionic-feedback
- type: psionicPower
id: MetapsionicPower
name: Metapsionic Pulse
- description: metapsionic-power-description
- actions:
- - ActionMetapsionic
- components:
- - type: MetapsionicPower
- initializationFeedback: metapsionic-power-initialization-feedback
- metapsionicFeedback: metapsionic-power-metapsionic-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionMetapsionic
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: MetapsionicPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: metapsionic-power-initialization-feedback
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: metapsionic-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: MetapsionicPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: metapsionic-power-metapsionic-feedback
- type: psionicPower
id: PsionicRegenerationPower
name: Psionic Regeneration
- description: psionic-regeneration-power-description
- actions:
- - ActionPsionicRegeneration
- components:
- - type: PsionicRegenerationPower
- initializationFeedback: psionic-regeneration-power-initialization-feedback
- metapsionicFeedback: psionic-regeneration-power-metapsionic-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionPsionicRegeneration
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: PsionicRegenerationPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: psionic-regeneration-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: psionic-regeneration-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: PsionicRegenerationPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: psionic-regeneration-power-metapsionic-feedback
- type: psionicPower
id: TelegnosisPower
name: Telegnosis
- description: telegnosis-power-description
- actions:
- - ActionTelegnosis
- components:
- - type: TelegnosisPower
- initializationFeedback: telegnosis-power-initialization-feedback
- metapsionicFeedback: telegnosis-power-metapsionic-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionTelegnosis
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: TelegnosisPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: telegnosis-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: telegnosis-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: TelegnosisPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: telegnosis-power-metapsionic-feedback
- type: psionicPower
id: PsionicInvisibilityPower
name: Psionic Invisibility
- description: psionic-invisibility-power-description
- actions:
- - ActionDispel
- components:
- - type: PsionicInvisibilityPower
- initializationFeedback: psionic-invisibility-power-initialization-feedback
- metapsionicFeedback: psionic-invisibility-power-metapsionic-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionPsionicInvisibility
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: PsionicInvisibilityPower
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: psionic-invisibility-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: psionic-invisibility-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: PsionicInvisibilityPower
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: psionic-invisibility-power-metapsionic-feedback
- type: psionicPower
id: XenoglossyPower
name: Xenoglossy
- description: xenoglossy-power-description
- components:
- - type: UniversalLanguageSpeaker
- initializationFeedback: xenoglossy-power-initialization-feedback
- metapsionicFeedback: psionic-language-power-feedback # Reuse for telepathy, clairaudience, etc
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: UniversalLanguageSpeaker
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: xenoglossy-power-initialization-feedback
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: psionic-language-power-feedback
+ removalFunctions:
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: Telepathy
+ - !type:RemoveAssayFeedback
+ assayFeedback: psionic-language-power-feedback
powerSlotCost: 0
- type: psionicPower
id: PsychognomyPower #i.e. reverse physiognomy
name: Psychognomy #psycho- + -gnomy. I reccomend starting with your language's equilvalent of "physiognomy" and working backwards. i.e. психо(г)номика
- description: psychognomy-power-description
- components:
- - type: Psychognomist
- initializationFeedback: psychognomy-power-initialization-feedback
- metapsionicFeedback: psionic-language-power-feedback
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: Psychognomist
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: psychognomy-power-initialization-feedback
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: psionic-language-power-feedback
+ removalFunctions:
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: Psychognomist
+ - !type:RemoveAssayFeedback
+ assayFeedback: psionic-language-power-feedback
powerSlotCost: 0
- type: psionicPower
id: TelepathyPower
name: Telepathy
- description: telepathy-power-description
- components:
- - type: Telepathy
- initializationFeedback: telepathy-power-initialization-feedback
- metapsionicFeedback: psionic-language-power-feedback # Reuse for telepathy, clairaudience, etc
+ powerCategories:
+ - Mentalic
+ initializeFunctions:
+ - !type:AddPsionicPowerComponents
+ components:
+ - type: Telepathy
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: telepathy-power-initialization-feedback
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: psionic-language-power-feedback
+ - !type:PsionicAddAvailablePowers
+ powerPrototype: XenoglossyPower
+ weight: 0.75
+ - !type:PsionicAddAvailablePowers
+ powerPrototype: PsychognomyPower
+ weight: 0.75
+ removalFunctions:
+ - !type:RemovePsionicPowerComponents
+ components:
+ - type: Telepathy
+ - !type:RemoveAssayFeedback
+ assayFeedback: psionic-language-power-feedback
+ - !type:PsionicRemoveAvailablePowers
+ powerPrototype: XenoglossyPower
+ - !type:PsionicRemoveAvailablePowers
+ powerPrototype: PsychognomyPower
powerSlotCost: 0
- type: psionicPower
id: HealingWordPower
name: HealingWord
- description: healing-word-power-description
- actions:
- - ActionHealingWord
- initializationFeedback: healing-word-power-initialization-feedback
- metapsionicFeedback: healing-word-power-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
+ powerCategories:
+ - Anomalist
+ - Life
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionHealingWord
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: healing-word-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: healing-word-power-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ - !type:PsionicAddAvailablePowers
+ powerPrototype: RevivifyPower
+ weight: 0.1
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: healing-word-power-feedback
+ - !type:PsionicRemoveAvailablePowers
+ powerPrototype: RevivifyPower
- type: psionicPower
id: RevivifyPower
name: Revivify
- description: revivify-power-description
- actions:
- - ActionRevivify
- initializationFeedback: revivify-power-initialization-feedback
- metapsionicFeedback: revivify-power-feedback
- amplificationModifier: 2.5 # An extremely rare and dangerous power
- powerSlotCost: 2
+ powerCategories:
+ - Anomalist
+ - Life
+ - Dangerous
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionRevivify
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: revivify-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 15
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: revivify-power-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 2.5
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: revivify-power-feedback
- type: psionicPower
id: LowAmplification
name: LowAmplification
- description: low-amplification-power-description
- amplificationModifier: -0.25
+ powerCategories:
+ - Passive
+ initializeFunctions:
+ - !type:AddPsionicStatSources
+ amplificationModifier: -0.25
+ removalFunctions:
+ - !type:RemovePsionicStatSources
powerSlotCost: 0
- type: psionicPower
id: HighAmplification
name: HighAmplification
- description: high-amplification-power-description
- amplificationModifier: 0.25
+ powerCategories:
+ - Passive
+ initializeFunctions:
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.25
+ removalFunctions:
+ - !type:RemovePsionicStatSources
powerSlotCost: 0
- type: psionicPower
id: PowerOverwhelming
name: PowerOverwhelming
- description: power-overwhelming-power-description
- metapsionicFeedback: power-overwhelming-power-feedback
- amplificationModifier: 2
+ powerCategories:
+ - Passive
+ - Dangerous
+ initializeFunctions:
+ - !type:AddPsionicStatSources
+ amplificationModifier: 2
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: power-overwhelming-power-feedback
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: OVERWHELMING
+ removalFunctions:
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: power-overwhelming-power-feedback
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: OVERWHELMING
powerSlotCost: 2
- type: psionicPower
id: LowDampening
name: LowDampening
- description: low-dampening-power-description
- dampeningModifier: -0.25
+ powerCategories:
+ - Passive
+ initializeFunctions:
+ - !type:AddPsionicStatSources
+ dampeningModifier: -0.25
+ removalFunctions:
+ - !type:RemovePsionicStatSources
powerSlotCost: 0
- type: psionicPower
id: HighDampening
name: HighDampening
- description: high-dampening-power-description
- dampeningModifier: 0.25
+ powerCategories:
+ - Passive
+ initializeFunctions:
+ - !type:AddPsionicStatSources
+ dampeningModifier: 0.25
+ removalFunctions:
+ - !type:RemovePsionicStatSources
powerSlotCost: 0
- type: psionicPower
id: ShadeskipPower
name: Shadeskip
- description: shadeskip-power-description
- actions:
- - ActionShadeskip
- initializationFeedback: shadeskip-power-initialization-feedback
- metapsionicFeedback: shadeskip-power-metapsionic-feedback
- amplificationModifier: 1
+ powerCategories:
+ - Anomalist
+ - Shadow
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionShadeskip
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: shadeskip-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: shadeskip-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 1
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: tenebrous
+ - !type:PsionicAddAvailablePowers
+ powerPrototype: DarkSwapPower
+ weight: 0.1
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: shadeskip-power-metapsionic-feedback
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: tenebrous
+ - !type:PsionicRemoveAvailablePowers
+ powerPrototype: DarkSwapPower
- type: psionicPower
id: TelekineticPulsePower
name: Telekinetic Pulse
- description: telekinetic-pulse-power-description
- actions:
- - ActionTelekineticPulse
- initializationFeedback: telekinetic-pulse-power-initialization-feedback
- metapsionicFeedback: telekinetic-pulse-power-metapsionic-feedback
- amplificationModifier: 1
-
-- type: psionicPower
- id: ShadowkinPowers
- name: Shadowkin Powers
- description: shadowkin-powers-description
- actions:
- - ActionDarkSwap
- powerSlotCost: 0
+ powerCategories:
+ - Anomalist
+ - Kinetic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionTelekineticPulse
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: telekinetic-pulse-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 10
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: telekinetic-pulse-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 1
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: kinetic
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: telekinetic-pulse-power-metapsionic-feedback
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: kinetic
-- type: psionicPower
- id: EtherealVisionPower
- name: Ethereal Vision
- description: ethereal-vision-powers-description
- components:
- - type: ShowEthereal
- powerSlotCost: 0
+# - type: psionicPower
+# id: EtherealVisionPower
+# name: Ethereal Vision
+# description: ethereal-vision-powers-description
+# components:
+# - type: ShowEthereal
+# powerSlotCost: 0
- type: psionicPower
id: DarkSwapPower
name: DarkSwap
- description: darkswap-power-description
- actions:
- - ActionDarkSwap
- powerSlotCost: 1
- initializationFeedback: darkswap-power-initialization-feedback
+ powerCategories:
+ - Anomalist
+ - Shadow
+ - Dangerous
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionDarkSwap
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: darkswap-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 10
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: tenebrous
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: tenebrous
+ powerSlotCost: 2
- type: psionicPower
id: PyrokineticFlare
name: Pyrokinetic Flare
- description: pyrokinetic-flare-power-description
- actions:
- - ActionPyrokineticFlare
+ powerCategories:
+ - Anomalist
+ - Pyrokinetic
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionPyrokineticFlare
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: pyrokinetic-flare-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 4
+ - !type:AddPsionicAssayFeedback
+ assayFeedback: pyrokinetic-flare-power-metapsionic-feedback
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.25
+ - !type:PsionicAddAvailablePowers
+ powerPrototype: SummonImpPower
+ weight: 0.3
+ - !type:PsionicAddAvailablePowers
+ powerPrototype: PyrokinesisPower
+ weight: 0.1
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: pyre
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemoveAssayFeedback
+ assayFeedback: pyrokinetic-flare-power-metapsionic-feedback
+ - !type:PsionicRemoveAvailablePowers
+ powerPrototype: SummonImpPower
+ - !type:PsionicRemoveAvailablePowers
+ powerPrototype: PyrokinesisPower
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: pyre
powerSlotCost: 1
- initializationFeedback: pyrokinetic-flare-power-initialization-feedback
- metapsionicFeedback: pyrokinetic-flare-power-metapsionic-feedback
- amplificationModifier: 0.25
- type: psionicPower
id: SummonImpPower
name: Summon Imp
- description: summon-imp-power-description
- actions:
- - ActionSummonImp
+ powerCategories:
+ - Anomalist
+ - Pyrokinetic
+ - Dangerous
+ - Summoning
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionSummonImp
+ - !type:PsionicFeedbackPopup
+ - !type:PsionicFeedbackSelfChat
+ feedbackMessage: summon-imp-power-initialization-feedback
+ - !type:PsionicModifyGlimmer
+ glimmerModifier: 10
+ - !type:AddPsionicStatSources
+ amplificationModifier: 0.5
+ dampeningModifier: 0.5
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: pyre
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: calling
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicStatSources
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: pyre
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: calling
powerSlotCost: 1
- initializationFeedback: summon-imp-power-initialization-feedback
- amplificationModifier: 0.5
- dampeningModifier: 0.5
- type: psionicPower
id: SummonRemiliaPower
name: Summon Remilia
- description: summon-imp-power-description
- actions:
- - ActionSummonRemilia
+ powerCategories:
+ - Mentalic
+ - Unique
+ - Summoning
+ initializeFunctions:
+ - !type:AddPsionicActions
+ actions:
+ - ActionSummonRemilia
+ - !type:AddPsionicPsychognomicDescriptors
+ psychognomicDescriptor: calling
+ removalFunctions:
+ - !type:RemovePsionicActions
+ - !type:RemovePsionicPsychognomicDescriptors
+ psychognomicDescriptor: calling
powerSlotCost: 0