Magic spells backend (#358)

* add EntityEffect aability support

* delayed actions

* renaming

* delayed projectile spells

* spawn on self + entityEffect on self spells

* spawn on point spell

* rename

* clean up base species components

* magic alert

* move magic energy to Shared, add manacost to spells

* magic recoil

* improve magic recoil

* verbal aspect

* somatic aspect

* add simple vfx proto and sprites

* add casting VFX

* add TODO
This commit is contained in:
Ed
2024-07-28 17:26:47 +03:00
committed by GitHub
parent 6f2e1d6d9f
commit 655b32b720
51 changed files with 1213 additions and 81 deletions

View File

@@ -0,0 +1,37 @@
using Content.Server.Chat.Systems;
using Content.Shared._CP14.Magic;
using Content.Shared._CP14.Magic.Components;
using Content.Shared._CP14.Magic.Events;
namespace Content.Server._CP14.Magic;
public sealed partial class CP14MagicSystem : CP14SharedMagicSystem
{
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
SubscribeLocalEvent<CP14MagicEffectVerbalAspectComponent, CP14VerbalAspectSpeechEvent>(OnSpellSpoken);
SubscribeLocalEvent<CP14MagicEffectCastingVisualComponent, CP14StartCastMagicEffectEvent>(OnSpawnMagicVisualEffect);
SubscribeLocalEvent<CP14MagicEffectCastingVisualComponent, CP14StopCastMagicEffectEvent>(OnDespawnMagicVisualEffect);
}
private void OnSpellSpoken(Entity<CP14MagicEffectVerbalAspectComponent> ent, ref CP14VerbalAspectSpeechEvent args)
{
if (args.Performer is not null && args.Speech is not null)
_chat.TrySendInGameICMessage(args.Performer.Value, args.Speech, InGameICChatType.Speak, false);
}
private void OnSpawnMagicVisualEffect(Entity<CP14MagicEffectCastingVisualComponent> ent, ref CP14StartCastMagicEffectEvent args)
{
var vfx = SpawnAttachedTo(ent.Comp.Proto, Transform(args.Performer).Coordinates);
ent.Comp.SpawnedEntity = vfx;
}
private void OnDespawnMagicVisualEffect(Entity<CP14MagicEffectCastingVisualComponent> ent, ref CP14StopCastMagicEffectEvent args)
{
QueueDel(ent.Comp.SpawnedEntity);
ent.Comp.SpawnedEntity = null;
}
}

View File

@@ -2,7 +2,6 @@ using Content.Server._CP14.MagicEnergy.Components;
using Content.Shared._CP14.MagicEnergy;
using Content.Shared._CP14.MagicEnergy.Components;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
@@ -18,6 +17,7 @@ public sealed partial class CP14MagicEnergySystem : SharedCP14MagicEnergySystem
public override void Initialize()
{
SubscribeLocalEvent<CP14MagicEnergyDrawComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<CP14MagicEnergyPointLightControllerComponent, CP14MagicEnergyLevelChangeEvent>(OnEnergyChange);
SubscribeLocalEvent<CP14MagicEnergyExaminableComponent, ExaminedEvent>(OnExamined);
@@ -58,8 +58,6 @@ public sealed partial class CP14MagicEnergySystem : SharedCP14MagicEnergySystem
args.PushMarkup(GetEnergyExaminedText(ent, magicContainer));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -92,61 +90,4 @@ public sealed partial class CP14MagicEnergySystem : SharedCP14MagicEnergySystem
ChangeEnergy(energyEnt.Value, energyComp, draw.Energy, draw.Safe);
}
}
public bool TryConsumeEnergy(EntityUid uid, FixedPoint2 energy, CP14MagicEnergyContainerComponent? component = null, bool safe = false)
{
if (!Resolve(uid, ref component))
return false;
if (energy <= 0)
return true;
// Attempting to absorb more energy than is contained in the carrier will still waste all the energy
if (component.Energy < energy)
{
ChangeEnergy(uid, component, -component.Energy);
return false;
}
ChangeEnergy(uid, component, -energy, safe);
return true;
}
public void ChangeEnergy(EntityUid uid, CP14MagicEnergyContainerComponent component, FixedPoint2 energy, bool safe = false)
{
if (!safe)
{
//Overload
if (component.Energy + energy > component.MaxEnergy)
{
RaiseLocalEvent(uid, new CP14MagicEnergyOverloadEvent()
{
OverloadEnergy = (component.Energy + energy) - component.MaxEnergy,
});
}
//Burn out
if (component.Energy + energy < 0)
{
RaiseLocalEvent(uid, new CP14MagicEnergyBurnOutEvent()
{
BurnOutEnergy = -energy - component.Energy
});
}
}
var oldEnergy = component.Energy;
var newEnergy = Math.Clamp((float)component.Energy + (float)energy, 0, (float)component.MaxEnergy);
component.Energy = newEnergy;
if (oldEnergy != newEnergy)
{
RaiseLocalEvent(uid, new CP14MagicEnergyLevelChangeEvent()
{
OldValue = component.Energy,
NewValue = newEnergy,
MaxValue = component.MaxEnergy,
});
}
}
}

View File

@@ -0,0 +1,13 @@
namespace Content.Shared._CP14.Magic;
public interface ICP14DelayedMagicEffect // The speak n spell interface
{
/// <summary>
/// Localized string spoken by the caster when casting this spell.
/// </summary>
public float Delay { get; }
public bool BreakOnMove { get; }
public bool BreakOnDamage { get; }
}

View File

@@ -0,0 +1,130 @@
using Content.Shared._CP14.Magic.Components.Spells;
using Content.Shared._CP14.Magic.Events;
using Content.Shared.EntityEffects;
namespace Content.Shared._CP14.Magic;
public partial class CP14SharedMagicSystem
{
private void InitializeSpells()
{
// Instants
SubscribeLocalEvent<CP14DelayedSpawnEntitiesSpellComponent, CP14DelayedInstantActionDoAfterEvent>(OnCastEntitiesSpawn);
SubscribeLocalEvent<CP14DelayedSelfEntityEffectSpellComponent, CP14DelayedInstantActionDoAfterEvent>(OnCastSelfEntityEffects);
//Entity Target
SubscribeLocalEvent<CP14DelayedApplyEntityEffectsSpellComponent, CP14DelayedEntityTargetActionDoAfterEvent>(OnCastApplyEntityEffects);
//World Target
SubscribeLocalEvent<CP14DelayedProjectileSpellComponent, CP14DelayedWorldTargetActionDoAfterEvent>(OnCastProjectileSpell);
SubscribeLocalEvent<CP14DelayedSpawnOnWorldTargetSpellComponent, CP14DelayedWorldTargetActionDoAfterEvent>(OnCastSpawnOnPoint);
}
//TODO: Fuck,there's a lot of code repetition here that needs to be squeezed together somehow. Event calls, checks, and all this stuff
private void OnCastEntitiesSpawn(Entity<CP14DelayedSpawnEntitiesSpellComponent> spell, ref CP14DelayedInstantActionDoAfterEvent args)
{
var stopEv = new CP14StopCastMagicEffectEvent();
RaiseLocalEvent(spell, ref stopEv);
if (args.Cancelled || args.Handled || !_net.IsServer)
return;
args.Handled = true;
foreach (var spawn in spell.Comp.Spawns)
{
SpawnAtPosition(spawn, Transform(args.User).Coordinates);
}
var ev = new CP14AfterCastMagicEffectEvent {Performer = args.User};
RaiseLocalEvent(spell, ref ev);
}
private void OnCastSelfEntityEffects(Entity<CP14DelayedSelfEntityEffectSpellComponent> spell, ref CP14DelayedInstantActionDoAfterEvent args)
{
var stopEv = new CP14StopCastMagicEffectEvent();
RaiseLocalEvent(spell, ref stopEv);
if (args.Cancelled || args.Handled)
return;
args.Handled = true;
foreach (var effect in spell.Comp.Effects)
{
effect.Effect(new EntityEffectBaseArgs(args.User, EntityManager));
}
var ev = new CP14AfterCastMagicEffectEvent {Performer = args.User};
RaiseLocalEvent(spell, ref ev);
}
private void OnCastApplyEntityEffects(Entity<CP14DelayedApplyEntityEffectsSpellComponent> spell, ref CP14DelayedEntityTargetActionDoAfterEvent args)
{
var stopEv = new CP14StopCastMagicEffectEvent();
RaiseLocalEvent(spell, ref stopEv);
if (args.Cancelled || args.Handled || args.Target == null)
return;
args.Handled = true;
foreach (var effect in spell.Comp.Effects)
{
effect.Effect(new EntityEffectBaseArgs(args.Target.Value, EntityManager));
}
var ev = new CP14AfterCastMagicEffectEvent {Performer = args.User};
RaiseLocalEvent(spell, ref ev);
}
private void OnCastProjectileSpell(Entity<CP14DelayedProjectileSpellComponent> spell, ref CP14DelayedWorldTargetActionDoAfterEvent args)
{
var stopEv = new CP14StopCastMagicEffectEvent();
RaiseLocalEvent(spell, ref stopEv);
if (args.Cancelled || args.Handled || !_net.IsServer)
return;
args.Handled = true;
var xform = Transform(args.User);
var fromCoords = xform.Coordinates;
var toCoords = GetCoordinates(args.Target);
var userVelocity = _physics.GetMapLinearVelocity(args.User);
// If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
var fromMap = fromCoords.ToMap(EntityManager, _transform);
var spawnCoords = _mapManager.TryFindGridAt(fromMap, out var gridUid, out _)
? fromCoords.WithEntityId(gridUid, EntityManager)
: new(_mapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
var ent = Spawn(spell.Comp.Prototype, spawnCoords);
var direction = toCoords.ToMapPos(EntityManager, _transform) -
spawnCoords.ToMapPos(EntityManager, _transform);
_gunSystem.ShootProjectile(ent, direction, userVelocity, args.User, args.User);
var ev = new CP14AfterCastMagicEffectEvent {Performer = args.User};
RaiseLocalEvent(spell, ref ev);
}
private void OnCastSpawnOnPoint(Entity<CP14DelayedSpawnOnWorldTargetSpellComponent> spell, ref CP14DelayedWorldTargetActionDoAfterEvent args)
{
var stopEv = new CP14StopCastMagicEffectEvent();
RaiseLocalEvent(spell, ref stopEv);
if (args.Cancelled || args.Handled || !_net.IsServer)
return;
args.Handled = true;
var xform = Transform(args.User);
var toCoords = GetCoordinates(args.Target);
foreach (var spawn in spell.Comp.Spawns)
{
SpawnAtPosition(spawn, toCoords);
}
var ev = new CP14AfterCastMagicEffectEvent {Performer = args.User};
RaiseLocalEvent(spell, ref ev);
}
}

View File

@@ -0,0 +1,219 @@
using Content.Shared._CP14.Magic.Components;
using Content.Shared._CP14.Magic.Events;
using Content.Shared._CP14.MagicEnergy;
using Content.Shared._CP14.MagicEnergy.Components;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Speech.Muting;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
namespace Content.Shared._CP14.Magic;
public partial class CP14SharedMagicSystem : EntitySystem
{
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedGunSystem _gunSystem = default!;
[Dependency] private readonly SharedCP14MagicEnergySystem _magicEnergy = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CP14MagicEffectComponent, CP14BeforeCastMagicEffectEvent>(OnBeforeCastMagicEffect);
SubscribeLocalEvent<CP14MagicEffectSomaticAspectComponent, CP14BeforeCastMagicEffectEvent>(OnSomaticAspectBeforeCast);
SubscribeLocalEvent<CP14MagicEffectVerbalAspectComponent, CP14BeforeCastMagicEffectEvent>(OnVerbalAspectBeforeCast);
SubscribeLocalEvent<CP14MagicEffectVerbalAspectComponent, CP14AfterCastMagicEffectEvent>(OnVerbalAspectAfterCast);
SubscribeLocalEvent<CP14MagicEffectComponent, CP14AfterCastMagicEffectEvent>(OnAfterCastMagicEffect);
SubscribeLocalEvent<CP14DelayedInstantActionEvent>(OnInstantAction);
SubscribeLocalEvent<CP14DelayedEntityTargetActionEvent>(OnEntityTargetAction);
SubscribeLocalEvent<CP14DelayedWorldTargetActionEvent>(OnWorldTargetAction);
InitializeSpells();
}
private void OnSomaticAspectBeforeCast(Entity<CP14MagicEffectSomaticAspectComponent> ent, ref CP14BeforeCastMagicEffectEvent args)
{
if (TryComp<HandsComponent>(args.Performer, out var hands) || hands is not null)
{
foreach (var hand in hands.Hands)
{
if (hand.Value.IsEmpty)
return;
}
}
args.PushReason(Loc.GetString("cp14-magic-spell-need-somatic-component"));
args.Cancel();
}
private void OnVerbalAspectBeforeCast(Entity<CP14MagicEffectVerbalAspectComponent> ent, ref CP14BeforeCastMagicEffectEvent args)
{
if (HasComp<MutedComponent>(args.Performer))
{
args.PushReason(Loc.GetString("cp14-magic-spell-need-verbal-component"));
args.Cancel();
}
else
{
if (!args.Cancelled)
{
var ev = new CP14VerbalAspectSpeechEvent
{
Performer = args.Performer,
Speech = ent.Comp.StartSpeech,
};
RaiseLocalEvent(ent, ref ev);
}
}
}
private void OnVerbalAspectAfterCast(Entity<CP14MagicEffectVerbalAspectComponent> ent, ref CP14AfterCastMagicEffectEvent args)
{
var ev = new CP14VerbalAspectSpeechEvent
{
Performer = args.Performer,
Speech = ent.Comp.EndSpeech,
};
RaiseLocalEvent(ent, ref ev);
}
private void OnInstantAction(CP14DelayedInstantActionEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (args is not ICP14DelayedMagicEffect delayedEffect)
return;
if (!TryCastSpell(args.Action, args.Performer))
return;
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Performer, delayedEffect.Delay, new CP14DelayedInstantActionDoAfterEvent(), args.Action)
{
BreakOnMove = delayedEffect.BreakOnMove,
BreakOnDamage = delayedEffect.BreakOnDamage,
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
private void OnWorldTargetAction(CP14DelayedWorldTargetActionEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (args is not ICP14DelayedMagicEffect delayedEffect)
return;
if (!TryCastSpell(args.Action, args.Performer))
return;
var doAfter = new CP14DelayedWorldTargetActionDoAfterEvent()
{
Target = EntityManager.GetNetCoordinates(args.Target)
};
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Performer, delayedEffect.Delay, doAfter, args.Action)
{
BreakOnMove = delayedEffect.BreakOnMove,
BreakOnDamage = delayedEffect.BreakOnDamage,
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
private void OnEntityTargetAction(CP14DelayedEntityTargetActionEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (args is not ICP14DelayedMagicEffect delayedEffect)
return;
if (!TryCastSpell(args.Action, args.Performer))
return;
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Performer, delayedEffect.Delay, new CP14DelayedEntityTargetActionDoAfterEvent(), args.Action, args.Target)
{
BreakOnMove = delayedEffect.BreakOnMove,
BreakOnDamage = delayedEffect.BreakOnDamage,
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
private bool TryCastSpell(EntityUid spell, EntityUid performer)
{
var ev = new CP14BeforeCastMagicEffectEvent
{
Performer = performer,
};
RaiseLocalEvent(spell, ref ev);
if (ev.Reason != string.Empty && _net.IsServer)
{
_popup.PopupEntity(ev.Reason, performer, performer);
}
if (!ev.Cancelled)
{
var evStart = new CP14StartCastMagicEffectEvent()
{
Performer = performer,
};
RaiseLocalEvent(spell, ref evStart);
}
return !ev.Cancelled;
}
private void OnBeforeCastMagicEffect(Entity<CP14MagicEffectComponent> ent, ref CP14BeforeCastMagicEffectEvent args)
{
if (!TryComp<CP14MagicEnergyContainerComponent>(args.Performer, out var magicContainer))
{
args.Cancel();
return;
}
if (!_magicEnergy.HasEnergy(args.Performer, ent.Comp.ManaCost, magicContainer, ent.Comp.Safe))
{
args.PushReason(Loc.GetString("cp14-magic-spell-not-enough-mana"));
args.Cancel();
}
else if(!_magicEnergy.HasEnergy(args.Performer, ent.Comp.ManaCost, magicContainer, true) && _net.IsServer)
{
_popup.PopupEntity(Loc.GetString("cp14-magic-spell-not-enough-mana-cast-warning-"+_random.Next(5)), args.Performer, args.Performer, PopupType.SmallCaution);
}
}
private void OnAfterCastMagicEffect(Entity<CP14MagicEffectComponent> ent, ref CP14AfterCastMagicEffectEvent args)
{
if (_net.IsClient)
return;
if (!HasComp<CP14MagicEnergyContainerComponent>(args.Performer))
return;
_magicEnergy.TryConsumeEnergy(args.Performer.Value, ent.Comp.ManaCost, safe: ent.Comp.Safe);
}
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Magic.Components;
/// <summary>
/// Creates a temporary entity that exists while the spell is cast, and disappears at the end. For visual special effects.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14MagicEffectCastingVisualComponent : Component
{
[DataField]
public EntityUid? SpawnedEntity;
[DataField(required: true)]
public EntProtoId Proto = default!;
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.FixedPoint;
namespace Content.Shared._CP14.Magic.Components;
/// <summary>
/// Restricts the use of this action, by spending mana or user requirements.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14MagicEffectComponent : Component
{
[DataField]
public FixedPoint2 ManaCost = 0f;
[DataField]
public bool Safe = false;
}

View File

@@ -0,0 +1,9 @@
namespace Content.Shared._CP14.Magic.Components;
/// <summary>
/// Requires the user to have at least one free hand to use this spell
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14MagicEffectSomaticAspectComponent : Component
{
}

View File

@@ -0,0 +1,27 @@
using Content.Shared.FixedPoint;
namespace Content.Shared._CP14.Magic.Components;
/// <summary>
/// Requires the user to be able to speak in order to use this spell. Also forces the user to use certain phrases at the beginning and end of a spell cast
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14MagicEffectVerbalAspectComponent : Component
{
[DataField]
public string StartSpeech = string.Empty;
[DataField]
public string EndSpeech = string.Empty;
}
/// <summary>
/// patch to send an event to the server for saying a phrase out loud
/// </summary>
[ByRefEvent]
public sealed class CP14VerbalAspectSpeechEvent : EntityEventArgs
{
public EntityUid? Performer { get; init; }
public string? Speech { get; init; }
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.EntityEffects;
namespace Content.Shared._CP14.Magic.Components.Spells;
/// <summary>
/// Stores a list of effects for delayed actions.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14DelayedApplyEntityEffectsSpellComponent : Component
{
[DataField(required: true, serverOnly: true)]
public List<EntityEffect> Effects = new();
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Magic.Components.Spells;
/// <summary>
/// Stores a list of effects for delayed actions.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14DelayedProjectileSpellComponent : Component
{
/// <summary>
/// What entity should be spawned.
/// </summary>
[DataField(required: true)]
public EntProtoId Prototype;
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.EntityEffects;
namespace Content.Shared._CP14.Magic.Components.Spells;
/// <summary>
/// Stores a list of effects for delayed actions.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14DelayedSelfEntityEffectSpellComponent : Component
{
[DataField(required: true, serverOnly: true)]
public List<EntityEffect> Effects = new();
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Magic.Components.Spells;
/// <summary>
/// Stores a list of effects for delayed actions.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14DelayedSpawnEntitiesSpellComponent : Component
{
/// <summary>
/// What entities should be spawned.
/// </summary>
[DataField(required: true)]
public HashSet<EntProtoId> Spawns = new();
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Magic.Components.Spells;
/// <summary>
/// Stores a list of effects for delayed actions.
/// </summary>
[RegisterComponent, Access(typeof(CP14SharedMagicSystem))]
public sealed partial class CP14DelayedSpawnOnWorldTargetSpellComponent : Component
{
/// <summary>
/// What entities should be spawned.
/// </summary>
[DataField(required: true)]
public HashSet<EntProtoId> Spawns = new();
}

View File

@@ -0,0 +1,41 @@
namespace Content.Shared._CP14.Magic.Events;
[ByRefEvent]
public sealed class CP14BeforeCastMagicEffectEvent : CancellableEntityEventArgs
{
/// <summary>
/// The Performer of the event, to check if they meet the requirements.
/// </summary>
public EntityUid Performer { get; init; }
public string Reason = string.Empty;
public void PushReason(string reason)
{
Reason += $"{reason}\n";
}
}
[ByRefEvent]
public sealed class CP14AfterCastMagicEffectEvent : EntityEventArgs
{
public EntityUid? Performer { get; init; }
}
/// <summary>
/// is invoked if all conditions are met and the spell has begun to be cast
/// </summary>
[ByRefEvent]
public sealed class CP14StartCastMagicEffectEvent : EntityEventArgs
{
public EntityUid Performer { get; init; }
}
/// <summary>
/// is invoked on the spell itself when the spell process has been completed or interrupted
/// </summary>
[ByRefEvent]
public sealed class CP14StopCastMagicEffectEvent : EntityEventArgs
{
}

View File

@@ -0,0 +1,64 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Magic.Events;
//World target
public sealed partial class CP14DelayedWorldTargetActionEvent : WorldTargetActionEvent, ICP14DelayedMagicEffect
{
[DataField]
public float Delay { get; private set; } = 1f;
[DataField]
public bool BreakOnMove { get; private set; } = true;
[DataField]
public bool BreakOnDamage { get; private set; } = true;
}
[Serializable, NetSerializable]
public sealed partial class CP14DelayedWorldTargetActionDoAfterEvent : DoAfterEvent
{
[DataField]
public NetCoordinates Target;
public override DoAfterEvent Clone() => this;
}
//Entity Target
public sealed partial class CP14DelayedEntityTargetActionEvent : EntityTargetActionEvent, ICP14DelayedMagicEffect
{
[DataField]
public float Delay { get; private set; } = 1f;
[DataField]
public bool BreakOnMove { get; private set; } = true;
[DataField]
public bool BreakOnDamage { get; private set; } = true;
}
[Serializable, NetSerializable]
public sealed partial class CP14DelayedEntityTargetActionDoAfterEvent : SimpleDoAfterEvent
{
}
//Instant
public sealed partial class CP14DelayedInstantActionEvent : InstantActionEvent, ICP14DelayedMagicEffect
{
[DataField]
public float Delay { get; private set; } = 1f;
[DataField]
public bool BreakOnMove { get; private set; } = true;
[DataField]
public bool BreakOnDamage { get; private set; } = true;
}
[Serializable, NetSerializable]
public sealed partial class CP14DelayedInstantActionDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -1,4 +1,6 @@
using Content.Shared.Alert;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.MagicEnergy.Components;
@@ -13,4 +15,7 @@ public sealed partial class CP14MagicEnergyContainerComponent : Component
[DataField]
public FixedPoint2 MaxEnergy = 100f;
[DataField]
public ProtoId<AlertPrototype>? MagicAlert = null;
}

View File

@@ -1,12 +1,34 @@
using Content.Shared._CP14.MagicEnergy.Components;
using Content.Shared.Examine;
using Content.Shared.Alert;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Rounding;
namespace Content.Shared._CP14.MagicEnergy;
public partial class SharedCP14MagicEnergySystem : EntitySystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
public override void Initialize()
{
SubscribeLocalEvent<CP14MagicEnergyContainerComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<CP14MagicEnergyContainerComponent, ComponentShutdown>(OnComponentShutdown);
}
private void OnComponentStartup(Entity<CP14MagicEnergyContainerComponent> ent, ref ComponentStartup args)
{
UpdateMagicAlert(ent);
}
private void OnComponentShutdown(Entity<CP14MagicEnergyContainerComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.MagicAlert == null)
return;
_alerts.ClearAlert(ent, ent.Comp.MagicAlert.Value);
}
public string GetEnergyExaminedText(EntityUid uid, CP14MagicEnergyContainerComponent ent)
{
var power = (int)((ent.Energy / ent.MaxEnergy) * 100);
@@ -22,6 +44,92 @@ public partial class SharedCP14MagicEnergySystem : EntitySystem
("power", power),
("color", color));
}
public void ChangeEnergy(EntityUid uid, CP14MagicEnergyContainerComponent component, FixedPoint2 energy, bool safe = false)
{
if (!safe)
{
//Overload
if (component.Energy + energy > component.MaxEnergy)
{
RaiseLocalEvent(uid, new CP14MagicEnergyOverloadEvent()
{
OverloadEnergy = (component.Energy + energy) - component.MaxEnergy,
});
}
//Burn out
if (component.Energy + energy < 0)
{
RaiseLocalEvent(uid, new CP14MagicEnergyBurnOutEvent()
{
BurnOutEnergy = -energy - component.Energy
});
}
}
var oldEnergy = component.Energy;
var newEnergy = Math.Clamp((float)component.Energy + (float)energy, 0, (float)component.MaxEnergy);
component.Energy = newEnergy;
if (oldEnergy != newEnergy)
{
RaiseLocalEvent(uid, new CP14MagicEnergyLevelChangeEvent()
{
OldValue = component.Energy,
NewValue = newEnergy,
MaxValue = component.MaxEnergy,
});
}
UpdateMagicAlert((uid, component));
}
public bool HasEnergy(EntityUid uid, FixedPoint2 energy, CP14MagicEnergyContainerComponent? component = null, bool safe = false)
{
if (!Resolve(uid, ref component))
return false;
if (safe == false)
return true;
return component.Energy > energy;
}
public bool TryConsumeEnergy(EntityUid uid, FixedPoint2 energy, CP14MagicEnergyContainerComponent? component = null, bool safe = false)
{
if (!Resolve(uid, ref component))
return false;
if (energy <= 0)
return true;
// Attempting to absorb more energy than is contained in the container available only in non-safe methods (with container destruction)
if (component.Energy < energy)
{
if (safe)
{
return false;
}
else
{
ChangeEnergy(uid, component, -energy, safe);
return true;
}
}
ChangeEnergy(uid, component, -energy, safe);
return true;
}
private void UpdateMagicAlert(Entity<CP14MagicEnergyContainerComponent> ent)
{
if (ent.Comp.MagicAlert == null)
return;
var level = ContentHelpers.RoundToLevels(MathF.Max(0f, (float) ent.Comp.Energy), (float) ent.Comp.MaxEnergy, 10);
_alerts.ShowAlert(ent, ent.Comp.MagicAlert.Value, (short)level);
}
}
/// <summary>

View File

@@ -0,0 +1,21 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
namespace Content.Shared._CP14.MagicWeakness;
/// <summary>
/// imposes damage on excessive use of magic
/// </summary>
[RegisterComponent, Access(typeof(CP14MagicWeaknessSystem))]
public sealed partial class CP14MagicUnsafeDamageComponent : Component
{
[DataField]
public DamageSpecifier DamagePerEnergy = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{"Blunt", 0.5},
{"Heat", 0.5},
},
};
}

View File

@@ -0,0 +1,20 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
namespace Content.Shared._CP14.MagicWeakness;
/// <summary>
/// imposes debuffs on excessive use of magic
/// </summary>
[RegisterComponent, Access(typeof(CP14MagicWeaknessSystem))]
public sealed partial class CP14MagicUnsafeSleepComponent : Component
{
[DataField]
public float SleepPerEnergy = 0.5f;
/// <summary>
/// At the specified amount of extra mana expenditure, the character falls asleep.
/// </summary>
[DataField]
public FixedPoint2 SleepThreshold = 20f;
}

View File

@@ -0,0 +1,43 @@
using Content.Shared._CP14.MagicEnergy;
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
using Content.Shared.Popups;
using Content.Shared.StatusEffect;
namespace Content.Shared._CP14.MagicWeakness;
public partial class CP14MagicWeaknessSystem : EntitySystem
{
[ValidatePrototypeId<StatusEffectPrototype>]
private const string StatusEffectKey = "ForcedSleep";
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CP14MagicUnsafeDamageComponent, CP14MagicEnergyBurnOutEvent>(OnMagicEnergyBurnOutDamage);
SubscribeLocalEvent<CP14MagicUnsafeSleepComponent, CP14MagicEnergyBurnOutEvent>(OnMagicEnergyBurnOutSleep);
}
private void OnMagicEnergyBurnOutSleep(Entity<CP14MagicUnsafeSleepComponent> ent, ref CP14MagicEnergyBurnOutEvent args)
{
if (args.BurnOutEnergy > ent.Comp.SleepThreshold)
{
_popup.PopupEntity(Loc.GetString("cp14-magic-energy-damage-burn-out-fall"), ent, ent, PopupType.LargeCaution);
_statusEffects.TryAddStatusEffect<ForcedSleepingComponent>(ent,
StatusEffectKey,
TimeSpan.FromSeconds(ent.Comp.SleepPerEnergy * (float)args.BurnOutEnergy),
false);
}
}
private void OnMagicEnergyBurnOutDamage(Entity<CP14MagicUnsafeDamageComponent> ent, ref CP14MagicEnergyBurnOutEvent args)
{
_popup.PopupEntity(Loc.GetString("cp14-magic-energy-damage-burn-out"), ent, ent, PopupType.LargeCaution);
_damageable.TryChangeDamage(ent, ent.Comp.DamagePerEnergy * args.BurnOutEnergy);
}
}

View File

@@ -0,0 +1,2 @@
cp14-alerts-magic-energy-name = Magic energy
cp14-alerts-magic-energy-desc = A reserve of your internal magic power that allows you to use spells.

View File

@@ -4,4 +4,4 @@ cp14-magic-energy-crystal-slot-name = Energy crystal
cp14-magic-energy-no-crystal = No energy crystal!
cp14-magic-energy-insufficient = Not enough energy!
cp14-magic-energy-insufficient-unsafe = Crystal cracks from lack of energy!
cp14-magic-energy-insufficient-unsafe = Crystal cracks from lack of energy!

View File

@@ -0,0 +1,10 @@
cp14-magic-spell-not-enough-mana = Not enough mana!
cp14-magic-spell-not-enough-mana-cast-warning-0 = Everything inside you starts to whine unpleasantly...
cp14-magic-spell-not-enough-mana-cast-warning-1 = Your head starts to buzz...
cp14-magic-spell-not-enough-mana-cast-warning-2 = Your hands are shaking...
cp14-magic-spell-not-enough-mana-cast-warning-3 = Your throat starts to lump...
cp14-magic-spell-not-enough-mana-cast-warning-4 = Your head becomes heavy...
cp14-magic-energy-damage-burn-out = The pain is piercing your body!
cp14-magic-energy-damage-burn-out-fall = You pass out from the intense pain!

View File

@@ -0,0 +1,2 @@
cp14-alerts-magic-energy-name = Магическая энергия
cp14-alerts-magic-energy-desc = Запас вашей внутренней магической силы, позволяющий вам использовать заклинания.

View File

@@ -4,4 +4,4 @@ cp14-magic-energy-crystal-slot-name = Энергокристалл
cp14-magic-energy-no-crystal = Отсутствует энергокристалл!
cp14-magic-energy-insufficient = Не достаточно энергии!
cp14-magic-energy-insufficient-unsafe = Кристалл трескается от нехватки энергии!
cp14-magic-energy-insufficient-unsafe = Кристалл трескается от нехватки энергии!

View File

@@ -0,0 +1,13 @@
cp14-magic-spell-not-enough-mana = Недостаточно магической энергии!
cp14-magic-spell-not-enough-mana-cast-warning-0 = Внутри вас все начинает неприятно ныть...
cp14-magic-spell-not-enough-mana-cast-warning-1 = Голова начинает шуметь...
cp14-magic-spell-not-enough-mana-cast-warning-2 = Ваши руки дрожат...
cp14-magic-spell-not-enough-mana-cast-warning-3 = К горлу подступает ком...
cp14-magic-spell-not-enough-mana-cast-warning-4 = Голова наливается свинцом...
cp14-magic-energy-damage-burn-out = Боль пронзает ваше тело!
cp14-magic-energy-damage-burn-out-fall = Вы теряете сознание от сильной боли!
cp14-magic-spell-need-verbal-component = Вы не можете произнести заклинание вслух.
cp14-magic-spell-need-somatic-component = Вам нужна свободная рука.

View File

@@ -5,6 +5,7 @@
id: BaseAlertOrder
order:
- category: Health
- alertType: CP14MagicEnergy
- category: Stamina
- alertType: SuitPower
- category: Internals

View File

@@ -207,7 +207,7 @@
id: MobRespirator
abstract: true
components:
- type: Internals
#- type: Internals #CP14 dont need this
- type: Respirator
damage:
types:

View File

@@ -0,0 +1,29 @@
- type: alert
id: CP14MagicEnergy
icons:
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_0
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_1
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_2
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_3
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_4
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_5
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_6
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_7
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_8
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_9
- sprite: /Textures/_CP14/Interface/Alerts/magic_energy.rsi
state: mana_10
name: cp14-alerts-magic-energy-name
description: cp14-alerts-magic-energy-desc
minSeverity: 0
maxSeverity: 10

View File

@@ -0,0 +1,153 @@
- type: entity
id: CP14ActionSpellHealingWord
name: Healing word
description: bruh
components:
- type: CP14MagicEffect
manaCost: 10
- type: EntityTargetAction
useDelay: 1
itemIconStyle: BigAction
interactOnMiss: true
sound: !type:SoundPathSpecifier
path: /Audio/Magic/rumble.ogg
icon:
sprite: Objects/Magic/magicactions.rsi
state: gib
event: !type:CP14DelayedEntityTargetActionEvent
- type: CP14DelayedApplyEntityEffectsSpell
effects:
- !type:HealthChange
damage:
types:
Slash: -5
Blunt: -5
Piercing: -5
Heat: -10
- !type:Jitter
- type: entity
id: CP14ActionSpellFireball
name: Fireball
description: Fires an explosive fireball towards the clicked location.
components:
- type: CP14MagicEffect
manaCost: 70
- type: CP14MagicEffectVerbalAspect
startSpeech: "O tenebrae, ubi lux non penetrat..."
endSpeech: "Quaeso, quemdam inter vos quaero!"
- type: CP14MagicEffectSomaticAspect
- type: CP14MagicEffectCastingVisual
proto: CP14FireballRune
- type: WorldTargetAction
useDelay: 3
itemIconStyle: BigAction
checkCanAccess: false
raiseOnUser: true
range: 60
sound: !type:SoundPathSpecifier
path: /Audio/Magic/fireball.ogg
icon:
sprite: Objects/Magic/magicactions.rsi
state: fireball
event: !type:CP14DelayedWorldTargetActionEvent
delay: 3
- type: CP14DelayedProjectileSpell
prototype: ProjectileFireball
- type: entity
id: CP14ActionSpellSelfHeal
name: Self Heal
description: Toggles your suit's phase cloak. Beware that if you are hit, all abilities are disabled for 5 seconds, including your cloak!
components:
- type: InstantAction
# have to plan (un)cloaking ahead of time
useDelay: 5
priority: -9
event: !type:CP14DelayedInstantActionEvent
- type: CP14DelayedSelfEntityEffectSpell
effects:
- !type:HealthChange
damage:
types:
Slash: -5
Blunt: -5
Piercing: -5
Heat: -10
- !type:Jitter
- type: entity
id: CP14ActionSpelldaggerSpawn
name: Gravity well
description: Toggles your suit's phase cloak. Beware that if you are hit, all abilities are disabled for 5 seconds, including your cloak!
components:
- type: CP14MagicEffect
manaCost: 20
- type: WorldTargetAction
useDelay: 1
itemIconStyle: BigAction
checkCanAccess: false
raiseOnUser: true
range: 4
sound: !type:SoundPathSpecifier
path: /Audio/Magic/fireball.ogg
icon:
sprite: Objects/Magic/magicactions.rsi
state: fireball
event: !type:CP14DelayedWorldTargetActionEvent
- type: CP14DelayedSpawnOnWorldTargetSpell
spawns:
- AdminInstantEffectGravityWell
- type: listing
id: CP14SpellbookJutter
name: jit
description: jit
productAction: CP14ActionSpellHealingWord
cost:
WizCoin: 1
categories:
- SpellbookOffensive
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: CP14SpellbookFireball
name: fifire
description: fifire
productAction: CP14ActionSpellFireball
cost:
WizCoin: 1
categories:
- SpellbookOffensive
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: CP14SpellbookFireball3
name: selfheal
description: selfheal
productAction: CP14ActionSpellSelfHeal
cost:
WizCoin: 1
categories:
- SpellbookOffensive
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: CP14SpellbookFireball33
name: Gravity Well
description: well...
productAction: CP14ActionSpelldaggerSpawn
cost:
WizCoin: 1
categories:
- SpellbookOffensive
conditions:
- !type:ListingLimitedStockCondition
stock: 1

View File

@@ -0,0 +1,31 @@
- type: entity
id: CP14BaseMagicRune
name: magic rune
description: manifestation of magical energy in the physical plane
abstract: true
components:
- type: Sprite
drawDepth: FloorTiles
sprite: _CP14/Effects/Magic/cast_rune.rsi
- type: Tag
tags:
- HideContextMenu
- type: PointLight
radius: 1.5
energy: 2.5
netsync: false
- type: entity
id: CP14FireballRune
parent: CP14BaseMagicRune
components:
- type: PointLight
color: "#d47d26"
- type: Sprite
layers:
- state: medium_circle
color: "#d47d26"
shader: unshaded
- state: sun
color: "#d47d26"
shader: unshaded

View File

@@ -54,10 +54,6 @@
sprite: Objects/Misc/handcuffs.rsi
state: body-overlay-2
visible: false
- map: [ "clownedon" ] # Dynamically generated
sprite: "Effects/creampie.rsi"
state: "creampie_human"
visible: false
- type: DamageVisuals
thresholds: [ 10, 20, 30, 50, 70, 100 ]
targetLayers:
@@ -144,9 +140,7 @@
prototype: CP14Human
requiredLegs: 2
- type: Identity
- type: IdExaminable
- type: Hands
- type: Internals
- type: Inventory
- type: InventorySlots
- type: FloatingVisuals
@@ -171,8 +165,6 @@
- type: SleepEmitSound
- type: SSDIndicator
- type: StandingState
- type: Fingerprint
- type: Dna
- type: MindContainer
showExamineInfo: true
- type: InteractionPopup
@@ -180,11 +172,9 @@
interactSuccessString: hugging-success-generic
interactSuccessSound: /Audio/Effects/thudswoosh.ogg
messagePerceivedByOthers: hugging-success-generic-others
- type: CanHostGuardian
- type: NpcFactionMember
factions:
- NanoTrasen
- type: CreamPied
- type: Stripping
- type: Strippable
- type: UserInterface
@@ -215,9 +205,16 @@
deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less.
- type: Tag
tags:
- CanPilot
- FootstepSound
- DoorBumpOpener
- type: CP14MagicEnergyContainer
magicAlert: CP14MagicEnergy
maxEnergy: 100
energy: 100
- type: CP14MagicEnergyDraw
energy: 5
delay: 5
- type: CP14MagicUnsafeDamage
- type: CP14MagicUnsafeSleep
- type: entity
@@ -342,10 +339,6 @@
sprite: Objects/Misc/handcuffs.rsi
state: body-overlay-2
visible: false
- map: [ "clownedon" ] # Dynamically generated
sprite: "Effects/creampie.rsi"
state: "creampie_human"
visible: false
- type: Appearance
- type: HumanoidAppearance
species: CP14Human

View File

@@ -77,4 +77,3 @@
32:
sprite: _CP14/Mobs/Species/Human/displacement.rsi
state: female_shirt

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,71 @@
{
"version": 1,
"size": {
"x": 48,
"y": 48
},
"license": "All rights reserved for the CrystallPunk14 project only",
"copyright": "Created by TheShuEd for CrystallPunk",
"states": [
{
"name": "double_outer",
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "medium_circle",
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "medium_line",
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
},
{
"name": "sun",
"delays": [
[
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1,44 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Created by TheShuEd (Github) for CrystallPunk",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "mana_0"
},
{
"name": "mana_1"
},
{
"name": "mana_2"
},
{
"name": "mana_3"
},
{
"name": "mana_4"
},
{
"name": "mana_5"
},
{
"name": "mana_6"
},
{
"name": "mana_7"
},
{
"name": "mana_8"
},
{
"name": "mana_9"
},
{
"name": "mana_10"
}
]
}