Skip to content

Commit

Permalink
Psionic Refactor Version 3 Part 1 (#1383)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
VMSolidus authored Jan 1, 2025
1 parent 6e6caf8 commit eb27db6
Show file tree
Hide file tree
Showing 11 changed files with 1,516 additions and 627 deletions.
580 changes: 580 additions & 0 deletions Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.Functions.cs

Large diffs are not rendered by default.

634 changes: 260 additions & 374 deletions Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Content.Server/Chat/TelepathicChatSystem.Psychognomy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
31 changes: 15 additions & 16 deletions Content.Server/Nyanotrasen/Psionics/NPC/PsionicNpcCombatSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,24 @@ public override void Initialize()
SubscribeLocalEvent<NoosphericZapPowerComponent, NPCSteeringEvent>(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<NoosphericZapPowerComponent> 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<EntityTargetActionComponent>(action.Value);
if (actionTarget.Cooldown is {} cooldown && cooldown.End > _timing.CurTime
|| !TryComp<NPCRangedCombatComponent>(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<EntityTargetActionComponent>(action.Value);
// if (actionTarget.Cooldown is {} cooldown && cooldown.End > _timing.CurTime
// || !TryComp<NPCRangedCombatComponent>(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);
}
}
42 changes: 40 additions & 2 deletions Content.Server/Psionics/PsionicsSystem.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -87,7 +92,22 @@ private void OnStartup(EntityUid uid, PsionicComponent component, MapInitEvent a
|| !component.CanReroll)
return;

Timer.Spawn(TimeSpan.FromSeconds(30), () => DeferRollers(uid));

}

/// <summary>
/// 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.
/// </summary>
private void DeferRollers(EntityUid uid)
{
if (!Exists(uid)
|| !TryComp(uid, out PsionicComponent? component))
return;

CheckPowerCost(uid, component);
GenerateAvailablePowers(component);
_rollers.Enqueue((component, uid));
}

Expand All @@ -108,6 +128,24 @@ private void CheckPowerCost(EntityUid uid, PsionicComponent component)
component.NextPowerCost = 100 * MathF.Pow(2, powerCount);
}

/// <summary>
/// The power pool is itself a DataField, and things like Traits/Antags are allowed to modify or replace the pool.
/// </summary>
private void GenerateAvailablePowers(PsionicComponent component)
{
if (!_protoMan.TryIndex<WeightedRandomPrototype>(component.PowerPool.Id, out var pool))
return;

foreach (var id in pool.Weights)
{
if (!_protoMan.TryIndex<PsionicPowerPrototype>(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)
Expand Down Expand Up @@ -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;
}

Expand Down
40 changes: 37 additions & 3 deletions Content.Shared/Psionics/PsionicComponent.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -45,6 +46,12 @@ public sealed partial class PsionicComponent : Component
[DataField]
public float Potentia;

/// <summary>
/// The base cost for new powers.
/// </summary>
[DataField]
public float BaselinePowerCost = 100;

/// <summary>
/// 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.
Expand Down Expand Up @@ -81,6 +88,11 @@ public sealed partial class PsionicComponent : Component
[DataField]
public string MindbreakingFeedback = "mindbreaking-feedback";

/// <summary>
/// </summary>
[DataField]
public string HardMindbreakingFeedback = "hard-mindbreaking-feedback";

/// <summary>
/// How much should the odds of obtaining a Psionic Power be multiplied when rolling for one.
/// </summary>
Expand Down Expand Up @@ -139,6 +151,12 @@ private set
}
}

/// <summary>
/// Whether this entity is capable of randomly rolling for powers.
/// </summary>
[DataField]
public bool Roller = true;

/// <summary>
/// Ifrits, revenants, etc are explicitly magical beings that shouldn't get mindbroken
/// </summary>
Expand All @@ -153,10 +171,10 @@ private set
public HashSet<PsionicPowerPrototype> ActivePowers = new();

/// <summary>
/// The list of each Psionic Power by action with entityUid.
/// The list of each Psionic Power by prototype with entityUid.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<EntProtoId, EntityUid?> Actions = new();
public Dictionary<string, EntityUid?> Actions = new();

/// <summary>
/// What sources of Amplification does this Psion have?
Expand Down Expand Up @@ -202,7 +220,7 @@ private set
/// unneccesary subs for unique psionic entities like e.g. Oracle.
/// </summary>
[DataField]
public List<string>? PsychognomicDescriptors = null;
public List<string> PsychognomicDescriptors = new();

/// Used for tracking what spell a Psion is actively casting
[DataField]
Expand All @@ -228,6 +246,22 @@ private set
[DataField]
public int FamiliarLimit = 1;

/// <summary>
/// The list of all potential Assay messages that can be obtained from this Psion.
/// </summary>
[DataField]
public List<string> AssayFeedback = new();

/// <summary>
/// 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.
/// </summary>
[DataField]
public ProtoId<WeightedRandomPrototype> PowerPool = "RandomPsionicPowerPool";

[DataField]
public Dictionary<string, float> AvailablePowers = new();

[DataField]
public ProtoId<AlertPrototype> ManaAlert = "Mana";
}
Expand Down
90 changes: 29 additions & 61 deletions Content.Shared/Psionics/PsionicPowerPrototype.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -19,77 +21,43 @@ public sealed partial class PsionicPowerPrototype : IPrototype
public string Name = default!;

/// <summary>
/// The description of a power in yml, used for player notifications.
/// </summary>
[DataField(required: true)]
public string Description = default!;

/// <summary>
/// The list of each Action that this power adds in the form of ActionId and ActionEntity
/// </summary>
[DataField]
public List<EntProtoId> Actions = new();

/// <summary>
/// The list of what Components this power adds.
/// </summary>
[DataField]
public ComponentRegistry Components = new();

/// <summary>
/// What message will be sent to the player as a Popup.
/// If left blank, it will default to the Const "generic-power-initialization-feedback"
/// </summary>
[DataField]
public string? InitializationPopup;

/// <summary>
/// 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.
/// </summary>
[DataField]
public string? InitializationFeedback;

/// <summary>
/// What color will the initialization feedback display in the chat window with.
/// </summary>
[DataField]
public string InitializationFeedbackColor = "#8A00C2";

/// <summary>
/// What font size will the initialization message use in chat.
/// </summary>
[DataField]
public int InitializationFeedbackFontSize = 12;

/// <summary>
/// Which chat channel will the initialization message use.
/// What category of psionics does this power come from.
/// EG: Mentalics, Anomalists, Blood Cults, Heretics, etc.
/// </summary>
[DataField]
public ChatChannel InitializationFeedbackChannel = ChatChannel.Emotes;
public List<string> PowerCategories = new();

/// <summary>
/// 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.
/// </summary>
[DataField]
public string MetapsionicFeedback = "psionic-metapsionic-feedback-default";
[DataField(serverOnly: true)]
public PsionicPowerFunction[] InitializeFunctions { get; private set; } = Array.Empty<PsionicPowerFunction>();

/// <summary>
/// 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.
/// </summary>
[DataField]
public float AmplificationModifier = 0;

/// <summary>
/// How much this power will increase or decrease a user's Dampening.
/// </summary>
[DataField]
public float DampeningModifier = 0;
[DataField(serverOnly: true)]
public PsionicPowerFunction[] RemovalFunctions { get; private set; } = Array.Empty<PsionicPowerFunction>();

/// <summary>
/// How many "Power Slots" this power occupies.
/// </summary>
[DataField]
public int PowerSlotCost = 1;
}
}

/// 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);
}
1 change: 1 addition & 0 deletions Resources/Locale/en-US/psionics/psionic-powers.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
noMana: shadowkin-tired
- type: InnatePsionicPowers
powersToAdd:
- ShadowkinPowers
- DarkSwapPower
- type: LanguageKnowledge
speaks:
- TauCetiBasic
Expand Down
5 changes: 0 additions & 5 deletions Resources/Prototypes/Psionics/PsionicPowerPool.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit eb27db6

Please sign in to comment.