-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Description This PR Implements a new dedicated "Psionic Familiar System", allowing a new class of powers for summoning Familiars directly. Familiars are Psionic entities called from other planes of existence, who will follow and protect the caster, with their life if necessary. Familiars can never hurt their summoner, and inherit all of the faction tags of their summoner. When the summoner dies or is mindbroken, his Familiars are "sent back from whence they came". Notably, Psionic Familiars are not like Remilia or the Ifrit, they do not **depend** on being player controlled ghostroles, although they can be! If not player controlled, they instead function as an NPC, similarly to a Rat King's Servants. # TODO - [X] Implement new familiars for this system to use - [X] Get sprites ready for the summon familiar spells. - [x] Move Remilia and the Ifrit out of their Bible, and into the new Psionic Familiar System. <details><summary><h1>Media</h1></summary> <p> https://github.com/user-attachments/assets/8e89021b-3529-4b48-9909-7e8bfd72d587 ![image](https://github.com/user-attachments/assets/f89cbcc5-fb43-423a-a571-8aed9a5cfbba) </p> </details> # Changelog :cl: - add: A new class of Psionic powers has been added, "Summon Familiar". Familiars are a new kind of Psionic creature that can be summoned by Psions with the right power. Familiars will automatically follow their Master, attack anyone who attacks their Master, fight back when attacked, and attack anyone their Master attacks. Additionally, Familiars are also ghostroles, so that they can be taken over by a player, but otherwise do not require player control to function. Familiars disappear when they die, and will also disappear if their Master is either killed, or mindbroken. Psions can have a maximum of one(1) familiar at a time. - add: New psi-power "Summon Imp", as the first new Psi Familiar. Imps are small motes of living flame that follow and protect their summoner. Imps also emit an incredibly bright light, and can natively cast Pyrokinetic Flare. - add: Remilia has been updated to be a Psi Familiar, and no longer requires the Bible to summon. Chaplains now start with a Power that lets them summon Remilia once every 2 minutes. --------- Signed-off-by: VMSolidus <[email protected]> Co-authored-by: Remuchi <[email protected]>
- Loading branch information
Showing
23 changed files
with
647 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
using Content.Server.NPC; | ||
using Content.Server.NPC.Components; | ||
using Content.Server.NPC.HTN; | ||
using Content.Server.NPC.Systems; | ||
using Content.Server.Popups; | ||
using Content.Shared.Abilities.Psionics; | ||
using Content.Shared.Actions.Events; | ||
using Content.Shared.Interaction.Events; | ||
using Content.Shared.Mobs; | ||
using Robust.Shared.Map; | ||
using System.Numerics; | ||
|
||
namespace Content.Server.Abilities.Psionics; | ||
|
||
public sealed partial class PsionicFamiliarSystem : EntitySystem | ||
{ | ||
[Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!; | ||
[Dependency] private readonly NpcFactionSystem _factions = default!; | ||
[Dependency] private readonly NPCSystem _npc = default!; | ||
[Dependency] private readonly HTNSystem _htn = default!; | ||
[Dependency] private readonly PopupSystem _popup = default!; | ||
|
||
public override void Initialize() | ||
{ | ||
base.Initialize(); | ||
|
||
SubscribeLocalEvent<PsionicComponent, SummonPsionicFamiliarActionEvent>(OnSummon); | ||
SubscribeLocalEvent<PsionicFamiliarComponent, ComponentShutdown>(OnFamiliarShutdown); | ||
SubscribeLocalEvent<PsionicFamiliarComponent, AttackAttemptEvent>(OnFamiliarAttack); | ||
SubscribeLocalEvent<PsionicFamiliarComponent, MobStateChangedEvent>(OnFamiliarDeath); | ||
} | ||
|
||
private void OnSummon(EntityUid uid, PsionicComponent psionicComponent, SummonPsionicFamiliarActionEvent args) | ||
{ | ||
if (psionicComponent.Familiars.Count >= psionicComponent.FamiliarLimit | ||
|| !_psionics.OnAttemptPowerUse(args.Performer, args.PowerName, args.ManaCost, args.CheckInsulation) | ||
|| args.Handled || args.FamiliarProto is null) | ||
return; | ||
|
||
args.Handled = true; | ||
var familiar = Spawn(args.FamiliarProto, Transform(uid).Coordinates); | ||
EnsureComp<PsionicFamiliarComponent>(familiar, out var familiarComponent); | ||
familiarComponent.Master = uid; | ||
psionicComponent.Familiars.Add(familiar); | ||
Dirty(familiar, familiarComponent); | ||
Dirty(uid, psionicComponent); | ||
|
||
InheritFactions(uid, familiar, familiarComponent); | ||
HandleBlackboards(uid, familiar, args); | ||
DoGlimmerEffects(uid, psionicComponent, args); | ||
} | ||
|
||
private void InheritFactions(EntityUid uid, EntityUid familiar, PsionicFamiliarComponent familiarComponent) | ||
{ | ||
if (!familiarComponent.InheritMasterFactions | ||
|| !TryComp<NpcFactionMemberComponent>(uid, out var masterFactions) | ||
|| masterFactions.Factions.Count <= 0) | ||
return; | ||
|
||
EnsureComp<NpcFactionMemberComponent>(familiar, out var familiarFactions); | ||
foreach (var faction in masterFactions.Factions) | ||
{ | ||
if (familiarFactions.Factions.Contains(faction)) | ||
continue; | ||
|
||
_factions.AddFaction(familiar, faction, true); | ||
} | ||
} | ||
|
||
private void HandleBlackboards(EntityUid master, EntityUid familiar, SummonPsionicFamiliarActionEvent args) | ||
{ | ||
if (!args.FollowMaster | ||
|| !TryComp<HTNComponent>(familiar, out var htnComponent)) | ||
return; | ||
|
||
_npc.SetBlackboard(familiar, NPCBlackboard.FollowTarget, new EntityCoordinates(master, Vector2.Zero), htnComponent); | ||
_htn.Replan(htnComponent); | ||
} | ||
|
||
private void DoGlimmerEffects(EntityUid uid, PsionicComponent component, SummonPsionicFamiliarActionEvent args) | ||
{ | ||
if (!args.DoGlimmerEffects | ||
|| args.MinGlimmer == 0 && args.MaxGlimmer == 0) | ||
return; | ||
|
||
var minGlimmer = (int) Math.Round(MathF.MinMagnitude(args.MinGlimmer, args.MaxGlimmer) | ||
* component.CurrentAmplification - component.CurrentDampening); | ||
var maxGlimmer = (int) Math.Round(MathF.MaxMagnitude(args.MinGlimmer, args.MaxGlimmer) | ||
* component.CurrentAmplification - component.CurrentDampening); | ||
|
||
_psionics.LogPowerUsed(uid, args.PowerName, minGlimmer, maxGlimmer); | ||
} | ||
|
||
private void OnFamiliarShutdown(EntityUid uid, PsionicFamiliarComponent component, ComponentShutdown args) | ||
{ | ||
if (!Exists(component.Master) | ||
|| !TryComp<PsionicComponent>(component.Master, out var psionicComponent) | ||
|| !psionicComponent.Familiars.Contains(uid)) | ||
return; | ||
|
||
psionicComponent.Familiars.Remove(uid); | ||
} | ||
|
||
private void OnFamiliarAttack(EntityUid uid, PsionicFamiliarComponent component, AttackAttemptEvent args) | ||
{ | ||
if (component.CanAttackMaster || args.Target is null | ||
|| args.Target != component.Master) | ||
return; | ||
|
||
args.Cancel(); | ||
if (!Loc.TryGetString(component.AttackMasterText, out var attackFailMessage)) | ||
return; | ||
|
||
_popup.PopupEntity(attackFailMessage, uid, uid, component.AttackPopupType); | ||
} | ||
|
||
private void OnFamiliarDeath(EntityUid uid, PsionicFamiliarComponent component, MobStateChangedEvent args) | ||
{ | ||
if (!component.DespawnOnFamiliarDeath | ||
|| args.NewMobState != MobState.Dead) | ||
return; | ||
|
||
DespawnFamiliar(uid, component); | ||
} | ||
|
||
public void DespawnFamiliar(EntityUid uid) | ||
{ | ||
if (!TryComp<PsionicFamiliarComponent>(uid, out var familiarComponent)) | ||
return; | ||
|
||
DespawnFamiliar(uid, familiarComponent); | ||
} | ||
|
||
public void DespawnFamiliar(EntityUid uid, PsionicFamiliarComponent component) | ||
{ | ||
var popupText = Loc.GetString(component.DespawnText, ("entity", MetaData(uid).EntityName)); | ||
_popup.PopupEntity(popupText, uid, component.DespawnPopopType); | ||
QueueDel(uid); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
Content.Shared/Actions/Events/SummonPsionicFamiliarActionEvent.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
using Robust.Shared.Prototypes; | ||
|
||
namespace Content.Shared.Actions.Events; | ||
|
||
public sealed partial class SummonPsionicFamiliarActionEvent : InstantActionEvent | ||
{ | ||
/// <summary> | ||
/// The entity to be spawned by this power. | ||
/// </summary> | ||
[DataField] | ||
public EntProtoId? FamiliarProto; | ||
|
||
/// <summary> | ||
/// The name of this power, used for logging purposes. | ||
/// </summary> | ||
[DataField] | ||
public string PowerName; | ||
|
||
/// <summary> | ||
/// How much Mana this power should cost, if any. | ||
/// </summary> | ||
[DataField] | ||
public float ManaCost; | ||
|
||
/// <summary> | ||
/// Whether this power checks if the wearer is psionically insulated. | ||
/// </summary> | ||
[DataField] | ||
public bool CheckInsulation; | ||
|
||
/// <summary> | ||
/// Whether this power generates glimmer when used. | ||
/// </summary> | ||
[DataField] | ||
public bool DoGlimmerEffects; | ||
|
||
/// <summary> | ||
/// Whether the summoned entity will follow the one who summoned it. | ||
/// </summary> | ||
[DataField] | ||
public bool FollowMaster; | ||
|
||
/// <summary> | ||
/// The minimum amount of glimmer generated by this power. | ||
/// </summary> | ||
[DataField] | ||
public int MinGlimmer; | ||
|
||
/// <summary> | ||
/// The maximum amount of glimmer generated by this power. | ||
/// </summary> | ||
[DataField] | ||
public int MaxGlimmer; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.