diff --git a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs index 841d923a24c..bdf295615ea 100644 --- a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs +++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs @@ -12,7 +12,6 @@ using System.Linq; using Robust.Server.Player; using Content.Server.Chat.Managers; -using Content.Server.Psionics.Glimmer; namespace Content.Server.Abilities.Psionics { @@ -29,6 +28,7 @@ public sealed class PsionicAbilitiesSystem : EntitySystem [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly PsionicFamiliarSystem _psionicFamiliar = default!; private ProtoId _pool = "RandomPsionicPowerPool"; private const string GenericInitializationMessage = "generic-power-initialization-feedback"; @@ -59,6 +59,7 @@ private void OnPsionicShutdown(EntityUid uid, PsionicComponent component, Compon || HasComp(uid)) return; + KillFamiliars(component); RemoveAllPsionicPowers(uid); } @@ -215,6 +216,7 @@ public void RemoveAllPsionicPowers(EntityUid uid, bool mindbreak = false) _popups.PopupEntity(Loc.GetString(psionic.MindbreakingFeedback, ("entity", MetaData(uid).EntityName)), uid, uid, PopupType.MediumCaution); + KillFamiliars(psionic); RemComp(uid); RemComp(uid); @@ -368,5 +370,20 @@ private void RemovePsionicStatSources(EntityUid uid, PsionicPowerPrototype proto RefreshPsionicModifiers(uid, psionic); } + + private void KillFamiliars(PsionicComponent component) + { + if (component.Familiars.Count <= 0) + return; + + foreach (var familiar in component.Familiars) + { + if (!TryComp(familiar, out var familiarComponent) + || !familiarComponent.DespawnOnMasterDeath) + continue; + + _psionicFamiliar.DespawnFamiliar(familiar, familiarComponent); + } + } } } diff --git a/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs b/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs new file mode 100644 index 00000000000..d382c1f2318 --- /dev/null +++ b/Content.Server/Abilities/Psionics/PsionicFamiliarSystem.cs @@ -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(OnSummon); + SubscribeLocalEvent(OnFamiliarShutdown); + SubscribeLocalEvent(OnFamiliarAttack); + SubscribeLocalEvent(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(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(uid, out var masterFactions) + || masterFactions.Factions.Count <= 0) + return; + + EnsureComp(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(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(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(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); + } +} diff --git a/Content.Server/NPC/Components/NPCRetaliationComponent.cs b/Content.Server/NPC/Components/NPCRetaliationComponent.cs index c0bf54d76e7..21b806c4486 100644 --- a/Content.Server/NPC/Components/NPCRetaliationComponent.cs +++ b/Content.Server/NPC/Components/NPCRetaliationComponent.cs @@ -21,4 +21,10 @@ public sealed partial class NPCRetaliationComponent : Component /// todo: this needs to support timeoffsetserializer at some point [DataField("attackMemories")] public Dictionary AttackMemories = new(); + + /// + /// Whether this NPC will retaliate against a "Friendly" NPC. + /// + [DataField] + public bool RetaliateFriendlies; } diff --git a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs b/Content.Server/NPC/Components/NpcFactionMemberComponent.cs index ce7e59ea2c7..f38d8cca0f4 100644 --- a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs +++ b/Content.Server/NPC/Components/NpcFactionMemberComponent.cs @@ -4,7 +4,6 @@ namespace Content.Server.NPC.Components { [RegisterComponent] - [Access(typeof(NpcFactionSystem))] public sealed partial class NpcFactionMemberComponent : Component { /// diff --git a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs index cde8decefc4..a855c9915a0 100644 --- a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs +++ b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs @@ -47,7 +47,8 @@ public bool TryRetaliate(EntityUid uid, EntityUid target, NPCRetaliationComponen if (!HasComp(target)) return false; - if (_npcFaction.IsEntityFriendly(uid, target)) + if (!component.RetaliateFriendlies + && _npcFaction.IsEntityFriendly(uid, target)) return false; _npcFaction.AggroEntity(uid, target); diff --git a/Content.Server/Psionics/PsionicsSystem.cs b/Content.Server/Psionics/PsionicsSystem.cs index 90f69cf7967..9685334daba 100644 --- a/Content.Server/Psionics/PsionicsSystem.cs +++ b/Content.Server/Psionics/PsionicsSystem.cs @@ -16,7 +16,9 @@ using Robust.Server.Player; using Content.Server.Chat.Managers; using Robust.Shared.Prototypes; -using Content.Shared.Psionics; +using Content.Shared.Mobs; +using Content.Shared.Damage; +using Content.Shared.Interaction.Events; namespace Content.Server.Psionics; @@ -35,6 +37,8 @@ public sealed class PsionicsSystem : EntitySystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly PsionicFamiliarSystem _psionicFamiliar = default!; + [Dependency] private readonly NPCRetaliationSystem _retaliationSystem = default!; private const string BaselineAmplification = "Baseline Amplification"; private const string BaselineDampening = "Baseline Dampening"; @@ -64,6 +68,9 @@ public override void Initialize() SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnMeleeHit); SubscribeLocalEvent(OnStamHit); + SubscribeLocalEvent(OnMobstateChanged); + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnAttackAttempt); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnRemove); @@ -250,4 +257,59 @@ public void RerollPsionics(EntityUid uid, PsionicComponent? psionic = null, floa RollPsionics(uid, psionic, true, bonusMuliplier); psionic.CanReroll = false; } + + private void OnMobstateChanged(EntityUid uid, PsionicComponent component, MobStateChangedEvent args) + { + if (component.Familiars.Count <= 0 + || args.NewMobState != MobState.Dead) + return; + + foreach (var familiar in component.Familiars) + { + if (!TryComp(familiar, out var familiarComponent) + || !familiarComponent.DespawnOnMasterDeath) + continue; + + _psionicFamiliar.DespawnFamiliar(familiar, familiarComponent); + } + } + + /// + /// When a caster with active summons is attacked, aggro their familiars to the attacker. + /// + private void OnDamageChanged(EntityUid uid, PsionicComponent component, DamageChangedEvent args) + { + if (component.Familiars.Count <= 0 + || !args.DamageIncreased + || args.Origin is not { } origin + || origin == uid) + return; + + SetFamiliarTarget(origin, component); + } + + /// + /// When a caster with active summons attempts to attack something, aggro their familiars to the target. + /// + private void OnAttackAttempt(EntityUid uid, PsionicComponent component, AttackAttemptEvent args) + { + if (component.Familiars.Count <= 0 + || args.Target == uid + || args.Target is not { } target + || component.Familiars.Contains(target)) + return; + + SetFamiliarTarget(target, component); + } + + private void SetFamiliarTarget(EntityUid target, PsionicComponent component) + { + foreach (var familiar in component.Familiars) + { + if (!TryComp(familiar, out var retaliationComponent)) + continue; + + _retaliationSystem.TryRetaliate(familiar, target, retaliationComponent); + } + } } diff --git a/Content.Shared/Actions/Events/SummonPsionicFamiliarActionEvent.cs b/Content.Shared/Actions/Events/SummonPsionicFamiliarActionEvent.cs new file mode 100644 index 00000000000..0df9d86f51e --- /dev/null +++ b/Content.Shared/Actions/Events/SummonPsionicFamiliarActionEvent.cs @@ -0,0 +1,54 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Actions.Events; + +public sealed partial class SummonPsionicFamiliarActionEvent : InstantActionEvent +{ + /// + /// The entity to be spawned by this power. + /// + [DataField] + public EntProtoId? FamiliarProto; + + /// + /// The name of this power, used for logging purposes. + /// + [DataField] + public string PowerName; + + /// + /// How much Mana this power should cost, if any. + /// + [DataField] + public float ManaCost; + + /// + /// Whether this power checks if the wearer is psionically insulated. + /// + [DataField] + public bool CheckInsulation; + + /// + /// Whether this power generates glimmer when used. + /// + [DataField] + public bool DoGlimmerEffects; + + /// + /// Whether the summoned entity will follow the one who summoned it. + /// + [DataField] + public bool FollowMaster; + + /// + /// The minimum amount of glimmer generated by this power. + /// + [DataField] + public int MinGlimmer; + + /// + /// The maximum amount of glimmer generated by this power. + /// + [DataField] + public int MaxGlimmer; +} diff --git a/Content.Shared/Psionics/PsionicComponent.cs b/Content.Shared/Psionics/PsionicComponent.cs index 5a3cce19ad3..16e0f028de0 100644 --- a/Content.Shared/Psionics/PsionicComponent.cs +++ b/Content.Shared/Psionics/PsionicComponent.cs @@ -213,5 +213,17 @@ private set /// Popup to play if there no Mana left for a power to execute. public string NoMana = "no-mana"; + + /// + /// The list of Familiars currently bound to this Psion. + /// + [DataField] + public List Familiars = new(); + + /// + /// The maximum number of Familiars a Psion may bind. + /// + [DataField] + public int FamiliarLimit = 1; } } diff --git a/Content.Shared/Psionics/PsionicFamiliarComponent.cs b/Content.Shared/Psionics/PsionicFamiliarComponent.cs new file mode 100644 index 00000000000..d47b01e7e73 --- /dev/null +++ b/Content.Shared/Psionics/PsionicFamiliarComponent.cs @@ -0,0 +1,75 @@ +using Content.Shared.Popups; +using Robust.Shared.GameStates; + +namespace Content.Shared.Abilities.Psionics; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +public sealed partial class PsionicFamiliarComponent : Component +{ + /// + /// The entity that summoned this Familiar. + /// + [DataField, AutoNetworkedField] + public EntityUid? Master; + + /// + /// Whether the familiar is allowed to attack its Master. + /// + [DataField] + public bool CanAttackMaster; + + /// + /// Popup to play when a familiar that isn't allowed to attack its Master, attempts to do so. + /// + [DataField] + public string AttackMasterText = "psionic-familiar-cant-attack-master"; + + /// + /// Popup type to use when failing to attack the familiar's Master. + /// + [DataField] + public PopupType AttackPopupType = PopupType.SmallCaution; + + /// + /// Text to display when a Familiar is forced to return from whence it came. + /// + [DataField] + public string DespawnText = "psionic-familiar-despawn-text"; + + /// + /// Popup type to use when a Familiar is forced to return from whence it came. + /// + [DataField] + public PopupType DespawnPopopType = PopupType.MediumCaution; + + /// + /// Whether a Psionic Familiar is sent back from whence it came if its Master dies. + /// + [DataField] + public bool DespawnOnMasterDeath = true; + + /// + /// Whether a Psionic Familiar is sent back from whence it came if it dies. + /// + [DataField] + public bool DespawnOnFamiliarDeath = true; + + /// + /// Whether a Psionic Familiar is sent back from whence it came if its Master is mindbroken. + /// + [DataField] + public bool DespawnOnMasterMindbroken = true; + + /// + /// Should the Familiar despawn when the player controlling it disconnects. + /// + [DataField] + public bool DespawnOnPlayerDetach; + + /// + /// Whether a Psionic Familiar inherits its Master's factions. + /// This can get people into trouble if the familiar inherits a hostile faction such as Syndicate. + /// + [DataField] + public bool InheritMasterFactions = true; +} diff --git a/Resources/Locale/en-US/psionics/psionic-powers.ftl b/Resources/Locale/en-US/psionics/psionic-powers.ftl index b0e917925c1..a7cec77aa2a 100644 --- a/Resources/Locale/en-US/psionics/psionic-powers.ftl +++ b/Resources/Locale/en-US/psionics/psionic-powers.ftl @@ -70,7 +70,7 @@ revivify-power-initialization-feedback = The Secret of Life in its fullness. I feel my entire existence burning out from within, merely by knowing it. Power flows through me as a mighty river, begging to be released with a simple spoken word. revivify-power-metapsionic-feedback = {CAPITALIZE($entity)} bears the Greater Secret of Life. -revivify-start = {CAPITALIZE($entity)} enunciates a word of such divine power, that those who hear it weep from joy. +revivify-begin = {CAPITALIZE($entity)} enunciates a word of such divine power, that those who hear it weep from joy. # Telegnosis telegnosis-power-description = Create a telegnostic projection to remotely observe things. @@ -143,6 +143,21 @@ pyrokinetic-flare-power-initialization-feedback = I can recall it still, a glimpse of the fires of Gehenna. pyrokinetic-flare-power-metapsionic-feedback = Guh these don't even matter because nobody can read this line in-game and I don't know when I'm ever bringing back Narrow Pulse +# Summon Imp +action-name-summon-imp = Summon Imp +action-description-summon-imp = + Summon and bind an Imp from Gehenna to serve as your Familiar. +summon-imp-power-description = { action-description-summon-imp } +summon-imp-power-initialization-feedback = + For a brief time, I find myself wandering the blackened fields of Gehenna. I sift between the ashes, finding a smoldering coal in the shape of an eye. + I breathe upon it, and it bursts alight with flame. Before I return, the creature thanks me and tells me its name. + +# Summon Remilia +action-name-summon-remilia = Summon Remilia +action-description-summon-remilia = + Call forth your ever-loyal familiar Remilia. +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 examine-mindbroken-message = @@ -160,3 +175,12 @@ action-name-darkswap = DarkSwap action-description-darkswap = Mmra Mamm! ethereal-pickup-fail = My hand sizzles as it passes through... + +# Psionic Familiar System +psionic-familiar-cant-attack-master = I am bound by my Master, I cannot harm them. +psionic-familiar-despawn-text = {CAPITALIZE($entity)} returns from whence it came! +ghost-role-information-familiar-name = Psionic Familiar +ghost-role-information-familiar-description = An interdimensional creature bound to the will of a Psion. +ghost-role-information-familiar-rules = + Obey the one who summoned you. Do not act against the interests of your Master. You will die for your Master if it is necessary. + diff --git a/Resources/Prototypes/Actions/psionics.yml b/Resources/Prototypes/Actions/psionics.yml index d302de7a643..a372a480ac7 100644 --- a/Resources/Prototypes/Actions/psionics.yml +++ b/Resources/Prototypes/Actions/psionics.yml @@ -364,3 +364,41 @@ maxRange: 1 spawns: - EffectPyrokineticFlare + +- type: entity + id: ActionSummonImp + name: action-name-summon-imp + description: action-description-summon-imp + noSpawn: true + components: + - type: InstantAction + icon: { sprite: Interface/Actions/psionics.rsi, state: summon_imp } + useDelay: 120 + checkCanInteract: false + event: !type:SummonPsionicFamiliarActionEvent + familiarProto: MobPsionicFamiliarImp + powerName: "Summon Imp" + checkInsulation: true + doGlimmerEffects: true + followMaster: true + minGlimmer: 10 + maxGlimmer: 20 + +- type: entity + id: ActionSummonRemilia + name: action-name-summon-remilia + description: action-description-summon-remilia + noSpawn: true + components: + - type: InstantAction + icon: { sprite: Interface/Actions/psionics.rsi, state: summon_remilia } + useDelay: 120 + checkCanInteract: false + event: !type:SummonPsionicFamiliarActionEvent + familiarProto: MobBatRemilia + powerName: "Summon Remilia" + checkInsulation: true + doGlimmerEffects: true + followMaster: true + minGlimmer: 5 + maxGlimmer: 10 diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 87166f13a3e..a605e22e642 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -1,6 +1,69 @@ +- type: entity + parent: + - BaseMob + - MobCombat + - MobDamageable + id: BaseMobPsionicFamiliar + abstract: true + components: + - type: Sprite + drawdepth: Mobs + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: bat + sprite: Mobs/Animals/bat.rsi + - type: GhostRole + makeSentient: true + allowMovement: true + allowSpeech: true + name: ghost-role-information-familiar-name + description: ghost-role-information-familiar-description + rules: ghost-role-information-familiar-rules + raffle: + settings: default + - type: GhostTakeoverAvailable + - type: Tag + tags: + - DoorBumpOpener + - type: MobThresholds + thresholds: + 0: Alive + 100: Dead + - type: Damageable + damageContainer: CorporealSpirit + damageModifierSet: CorporealSpirit + - type: MindContainer + showExamineInfo: false + - type: NpcFactionMember + factions: + - PsionicInterloper + - type: Alerts + - type: Familiar + - type: Psionic + removable: false + psychognomicDescriptors: + - p-descriptor-bound + - p-descriptor-cyclic + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower + - type: HTN + rootTask: + task: MeleePsionicFamiliarCompound + - type: NPCRetaliation + attackMemoryLength: 10 + retaliateFriendlies: true + - type: PsionicFamiliar + - type: DamageOnDispel + damage: + types: + Heat: 100 + - type: RandomMetadata + nameSegments: [names_golem] + - type: entity name: Remilia - parent: MobBat + parent: BaseMobPsionicFamiliar id: MobBatRemilia description: The chaplain's familiar. Likes fruit. components: @@ -23,25 +86,43 @@ - type: Access tags: - Chapel - - type: Damageable # Nyanotrasen - Corporeal Spirit allows Holy water to do damage - damageContainer: CorporealSpirit - damageModifierSet: CorporealSpirit - - type: MindContainer - showExamineInfo: true - - type: NpcFactionMember - factions: - - PetsNT - - PsionicInterloper #Nyano - Summary: makes a part of the psionic faction. - - type: Alerts - - type: Familiar - - type: Psionic - removable: false - psychognomicDescriptors: - - p-descriptor-bound - - p-descriptor-cyclic - type: InnatePsionicPowers powersToAdd: - TelepathyPower + - XenoglossyPower + - type: MovementSpeedModifier + baseWalkSpeed : 3 + baseSprintSpeed : 6 + - type: Speech + speechSounds: Squeak + speechVerb: SmallMob + allowedEmotes: ['Squeak'] + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.25 + density: 0.8 + mask: + - FlyingMobMask + layer: + - FlyingMobLayer + - type: InteractionPopup + successChance: 0.2 + interactSuccessString: petting-success-soft-floofy + interactFailureString: petting-failure-bat + interactSuccessSpawn: EffectHearts + interactSuccessSound: + path: /Audio/Animals/fox_squeak.ogg + - type: MeleeWeapon + soundHit: + path: /Audio/Effects/bite.ogg + angle: 0 + animation: WeaponArcBite + damage: + types: + Piercing: 5 - type: entity name: Cerberus @@ -74,7 +155,7 @@ - type: NpcFactionMember factions: - Syndicate - - PsionicInterloper #Nyano - Summary: makes part of the psionoic faction. + - PsionicInterloper - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-corrupted-corgi @@ -99,7 +180,7 @@ showExamineInfo: true - type: Familiar - type: Dispellable - - type: Psionic #Nyano - Summary: makes psionic on creation. + - type: Psionic removable: false - type: InnatePsionicPowers powersToAdd: @@ -109,3 +190,45 @@ Male: Cerberus Female: Cerberus Unsexed: Cerberus + +- type: entity + name: imp familiar + parent: BaseMobPsionicFamiliar + id: MobPsionicFamiliarImp + description: A living mote of flame summoned from Gehenna. + components: + - type: Sprite + drawdepth: Mobs + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: imp + sprite: Mobs/Demons/imp.rsi + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower + - PyrokineticFlare + - XenoglossyPower + - type: MeleeWeapon + damage: + types: + Heat: 9 + soundHit: + path: /Audio/Weapons/Guns/Hits/energy_meat1.ogg + params: + variation: 0.250 + volume: -10 + - type: PointLight + radius: 2 + energy: 30 + color: "#ff4500" + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 13 + mask: + - Opaque + layer: + - MobLayer diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml index 9a0709e8e74..23ce5a36ee2 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml @@ -20,8 +20,6 @@ Burn: 10 - type: Prayable bibleUserOnly: true - - type: Summonable - specialItem: SpawnPointGhostRemilia - type: ReactionMixer mixMessage: "bible-mixing-success" reactionTypes: diff --git a/Resources/Prototypes/NPCs/psionic.yml b/Resources/Prototypes/NPCs/psionic.yml new file mode 100644 index 00000000000..0c3772bd912 --- /dev/null +++ b/Resources/Prototypes/NPCs/psionic.yml @@ -0,0 +1,25 @@ +- type: htnCompound + id: MeleePsionicFamiliarCompound + branches: + - tasks: + - !type:HTNCompoundTask + task: MeleeCombatCompound + - tasks: + - !type:HTNCompoundTask + task: FollowCompound + - tasks: + - !type:HTNCompoundTask + task: IdleCompound + +- type: htnCompound + id: RangedPsionicFamiliarCompound + branches: + - tasks: + - !type:HTNCompoundTask + task: InnateRangedCombatCompound + - tasks: + - !type:HTNCompoundTask + task: FollowCompound + - tasks: + - !type:HTNCompoundTask + task: IdleCompound diff --git a/Resources/Prototypes/Nyanotrasen/psionicPowers.yml b/Resources/Prototypes/Nyanotrasen/psionicPowers.yml index 22198ff5b02..8b2911018f9 100644 --- a/Resources/Prototypes/Nyanotrasen/psionicPowers.yml +++ b/Resources/Prototypes/Nyanotrasen/psionicPowers.yml @@ -16,3 +16,4 @@ ShadeskipPower: 0.15 TelekineticPulsePower: 0.15 PyrokineticFlare: 0.3 + SummonImpPower: 0.15 diff --git a/Resources/Prototypes/Psionics/psionics.yml b/Resources/Prototypes/Psionics/psionics.yml index dbc7f91bfbe..7ee6e193e37 100644 --- a/Resources/Prototypes/Psionics/psionics.yml +++ b/Resources/Prototypes/Psionics/psionics.yml @@ -254,3 +254,22 @@ 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 + 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 + powerSlotCost: 0 diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml index a33e29963d0..fe7175172d2 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml @@ -32,13 +32,14 @@ special: - !type:AddComponentSpecial components: - - type: BibleUser #Lets them heal with bibles + - type: BibleUser - type: Psionic powerRollMultiplier: 3 - type: InnatePsionicPowers powersToAdd: - TelepathyPower - HealingWordPower + - SummonRemiliaPower - type: startingGear id: ChaplainGear diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/meta.json b/Resources/Textures/Interface/Actions/psionics.rsi/meta.json index 9c49090db8c..735bc293d15 100644 --- a/Resources/Textures/Interface/Actions/psionics.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/psionics.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Created by leonardo_dabepis (discord)", + "copyright": "healing_word, revivify, shadeskip by leonardo_dabepis (discord), telekinetic_pulse by .mocho (discord), pyrokinetic_flare, summon_remilia, summon_bat and summon_imp by ghost581 (discord)", "size": { "x": 64, "y": 64 @@ -21,6 +21,15 @@ }, { "name": "pyrokinetic_flare" + }, + { + "name": "summon_imp" + }, + { + "name": "summon_remilia" + }, + { + "name": "summon_bat" } ] } diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/summon_bat.png b/Resources/Textures/Interface/Actions/psionics.rsi/summon_bat.png new file mode 100644 index 00000000000..60f571278b9 Binary files /dev/null and b/Resources/Textures/Interface/Actions/psionics.rsi/summon_bat.png differ diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/summon_imp.png b/Resources/Textures/Interface/Actions/psionics.rsi/summon_imp.png new file mode 100644 index 00000000000..5de7c274fc1 Binary files /dev/null and b/Resources/Textures/Interface/Actions/psionics.rsi/summon_imp.png differ diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/summon_remilia.png b/Resources/Textures/Interface/Actions/psionics.rsi/summon_remilia.png new file mode 100644 index 00000000000..fcfe2a37265 Binary files /dev/null and b/Resources/Textures/Interface/Actions/psionics.rsi/summon_remilia.png differ diff --git a/Resources/Textures/Mobs/Demons/imp.rsi/imp.png b/Resources/Textures/Mobs/Demons/imp.rsi/imp.png new file mode 100644 index 00000000000..575c223ee87 Binary files /dev/null and b/Resources/Textures/Mobs/Demons/imp.rsi/imp.png differ diff --git a/Resources/Textures/Mobs/Demons/imp.rsi/meta.json b/Resources/Textures/Mobs/Demons/imp.rsi/meta.json new file mode 100644 index 00000000000..98f33679c7b --- /dev/null +++ b/Resources/Textures/Mobs/Demons/imp.rsi/meta.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Created by ghost581(Discord)", + "states": [ + { + "name": "imp", + "delays": [[0.3, 0.3, 0.3, 0.3]] + } + ] +}