From eb27db61dc42c00ea18198c094633692bf5baf0a Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Wed, 1 Jan 2025 15:59:49 -0600 Subject: [PATCH 1/2] Psionic Refactor Version 3 Part 1 (#1383) # Description They say Rome wasn't built in a day, well this entire PR was coded in a single 6 hour Adderall binge. This PR represents the next big leap in code capability for the PsionicSystem, completely reworking how Psionic Powers are added and removed, such that like the TraitSystem, they utilize modular functions governing how they work. Instead of there being only 5 different hardcoded things that Psi Powers can do, there is now a library containing 21 different modular functions, which are slotted as desired into the power prototypes. Additionally, a significant improvement in the logical flow of this is that since each power is responsible for its own "removal codepath", it's now possible to remove individual powers from a character, as opposed to always needing to wipe the slate clean entirely. I'm not going to add any new powers in this PR, nor am I touching the code for the Psionic Actions themselves, that'll come in Part 2, in which I refactor the Psionic-Actions so that they also operate on similar stacks of modular functions. This PR also makes extensive refactors to the PsionicPowerPrototype, as well as PsionicAbilitiesSystem, so that it has all new hooks and datafields for other systems to be able to modify a psion. It is now entirely feasible to create unique "Types" of Psions, with their own distinct power lists. It's also now possible to create "Tech Trees" of powers, by setting up powers such that they write to and modify the personalized pool of available powers to generate. For example, Xenoglossy and Psychognomy are now dependent on Telepathy, and simply won't appear in the list of available powers if a Psion doesn't first have Telepathy. # Changelog :cl: - add: Psionic Refactor V3 is here! No new powers are added in this update, but the options for creating new powers has been SIGNIFICANTLY EXPANDED. - add: Xenoglossy and Psychognomy now can only be rolled if you first have the Telepathy power. - add: Breath of Life can now only be rolled if you first have the Healing Word power - add: Pyrokinesis and Summon Imp now require the Pyroknetic Flare power - add: All new Psychognomy descriptors for many pre-existing powers. Have fun being unintentionally screamed at telepathically by someone with the POWER OVERWHELMING trait. --- .../PsionicAbilitiesSystem.Functions.cs | 580 ++++++++++++++ .../Psionics/PsionicAbilitiesSystem.cs | 634 +++++++--------- .../Chat/TelepathicChatSystem.Psychognomy.cs | 2 +- .../Psionics/NPC/PsionicNpcCombatSystem.cs | 31 +- Content.Server/Psionics/PsionicsSystem.cs | 42 +- Content.Shared/Psionics/PsionicComponent.cs | 40 +- .../Psionics/PsionicPowerPrototype.cs | 90 +-- .../Locale/en-US/psionics/psionic-powers.ftl | 1 + .../Entities/Mobs/Species/shadowkin.yml | 2 +- .../Prototypes/Psionics/PsionicPowerPool.yml | 5 - Resources/Prototypes/Psionics/psionics.yml | 716 ++++++++++++++---- 11 files changed, 1516 insertions(+), 627 deletions(-) create mode 100644 Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.Functions.cs 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/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 From 71f200540802b8bdcfe9d89052cc0d853e89fb47 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Wed, 1 Jan 2025 22:00:16 +0000 Subject: [PATCH 2/2] Automatic Changelog Update (#1383) --- Resources/Changelog/Changelog.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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