diff --git a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
index 5b156644a7..9d5005e9e8 100644
--- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
+++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
@@ -2,6 +2,7 @@ using System.Numerics;
using System.Threading;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
+using Content.Client.UserInterface.Systems.Actions;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Timer = Robust.Shared.Timing.Timer;
@@ -16,7 +17,7 @@ namespace Content.Client.ContextMenu.UI
///
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
///
- public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged
+ public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged, IOnSystemChanged
{
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
@@ -216,6 +217,12 @@ namespace Content.Client.ContextMenu.UI
Close();
}
+ private void OnChargingUpdated(bool charging)
+ {
+ if (charging)
+ Close();
+ }
+
public void OnSystemLoaded(CombatModeSystem system)
{
system.LocalPlayerCombatModeUpdated += OnCombatModeUpdated;
@@ -225,5 +232,15 @@ namespace Content.Client.ContextMenu.UI
{
system.LocalPlayerCombatModeUpdated -= OnCombatModeUpdated;
}
+
+ public void OnSystemLoaded(ChargeActionSystem system)
+ {
+ system.ChargingUpdated += OnChargingUpdated;
+ }
+
+ public void OnSystemUnloaded(ChargeActionSystem system)
+ {
+ system.ChargingUpdated -= OnChargingUpdated;
+ }
}
}
diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
index 09663ba82c..e6df68d933 100644
--- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
+++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
@@ -22,6 +22,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
+using Robust.Shared.Map;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Content.Client.Actions.ActionsSystem;
@@ -29,8 +30,7 @@ using static Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow;
using static Robust.Client.UserInterface.Control;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.LineEdit;
-using static Robust.Client.UserInterface.Controls.MultiselectOptionButton<
- Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow.Filters>;
+using static Robust.Client.UserInterface.Controls.MultiselectOptionButton;
using static Robust.Client.UserInterface.Controls.TextureRect;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
@@ -128,25 +128,45 @@ public sealed class ActionUIController : UIController, IOnStateChanged ToggleWindow()))
- .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
- typeof(ConstructionSystem), typeof(DragDropSystem))
- .BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
+ .Bind(ContentKeyFunctions.OpenActionsMenu, InputCmdHandler.FromDelegate(_ => ToggleWindow()))
+ .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true), typeof(ConstructionSystem), typeof(DragDropSystem))
+ .BindBefore(ContentKeyFunctions.AltActivateItemInWorld, new PointerInputCmdHandler(AltUse, outsidePrediction: true))
.Register();
}
- private bool TargetingCancel(in PointerInputCmdArgs args)
+ private bool AltUse(in PointerInputCmdArgs args)
{
- if (!_timing.IsFirstTimePredicted)
+ if (!_timing.IsFirstTimePredicted || _actionsSystem == null || SelectingTargetFor is not { } actionId)
return false;
- // only do something for actual target-based actions
- if (SelectingTargetFor == null)
+ if (_playerManager.LocalEntity is not { } user)
return false;
- StopTargeting();
- return true;
+ if (!EntityManager.TryGetComponent(user, out ActionsComponent? comp))
+ return false;
+
+ if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
+ baseAction is not BaseTargetActionComponent action || !action.IsAltEnabled)
+ {
+ return false;
+ }
+
+ // Is the action currently valid?
+ if (!action.Enabled
+ || action is { Charges: 0, RenewCharges: false }
+ || action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
+ {
+ // The user is targeting with this action, but it is not valid. Maybe mark this click as
+ // handled and prevent further interactions.
+ return !action.InteractOnMiss;
+ }
+
+ return action switch
+ {
+ WorldTargetActionComponent mapTarget => TryTargetWorld(args.Coordinates, actionId, mapTarget, user, comp,
+ ActionUseType.AltUse, target: args.EntityUid) || !mapTarget.InteractOnMiss,
+ _ => false
+ };
}
///
@@ -179,28 +199,23 @@ public sealed class ActionUIController : UIController, IOnStateChanged TryTargetWorld(args.Coordinates, actionId, mapTarget, user, comp) ||
+ !mapTarget.InteractOnMiss,
+ EntityTargetActionComponent entTarget => TryTargetEntity(args.EntityUid, actionId, entTarget, user, comp) ||
+ !entTarget.InteractOnMiss,
+ _ => false
+ };
}
- private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, WorldTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
+ public bool TryTargetWorld(EntityCoordinates coordinates, EntityUid actionId, WorldTargetActionComponent action,
+ EntityUid user, ActionsComponent actionComp, ActionUseType type = ActionUseType.Default, int chargeLevel = 0, EntityUid? target = default)
{
if (_actionsSystem == null)
return false;
- var coords = args.Coordinates;
-
- if (!_actionsSystem.ValidateWorldTarget(user, coords, action))
+ if (!_actionsSystem.ValidateWorldTarget(user, coordinates, action))
{
// Invalid target.
if (action.DeselectOnMiss)
@@ -213,14 +228,24 @@ public sealed class ActionUIController : UIController, IOnStateChanged actions)
{
if (_window is not { Disposed: false, IsOpen: true })
@@ -432,7 +456,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged? ChargingUpdated;
+
+ private bool _charging;
+ private bool _prevCharging;
+
+ private float _chargeTime;
+ private int _chargeLevel;
+ private int _prevChargeLevel;
+
+ private bool _isChargingPlaying;
+ private bool _isChargedPlaying;
+
+ private const float LevelChargeTime = 1.5f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _controller = _uiManager.GetUIController();
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (_playerManager.LocalEntity is not { } user)
+ return;
+
+ if (!_timing.IsFirstTimePredicted || _controller == null || _controller.SelectingTargetFor is not { } actionId)
+ return;
+
+ if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
+ baseAction is not BaseTargetActionComponent action || !action.IsChargeEnabled)
+ return;
+
+ if (!action.Enabled
+ || action is { Charges: 0, RenewCharges: false }
+ || action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
+ {
+ return;
+ }
+
+ var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary);
+ switch (altDown)
+ {
+ case BoundKeyState.Down:
+ _prevCharging = _charging;
+ _charging = true;
+ _chargeTime += frameTime;
+ _chargeLevel = (int) (_chargeTime / LevelChargeTime) + 1;
+ _chargeLevel = Math.Clamp(_chargeLevel, 1, action.MaxChargeLevel);
+ break;
+ case BoundKeyState.Up when _charging:
+ _prevCharging = _charging;
+ _charging = false;
+ _chargeTime = 0f;
+ _isChargingPlaying = false;
+ _isChargedPlaying = false;
+
+ HandleAction(actionId, action, user, _chargeLevel);
+ _chargeLevel = 0;
+
+ RaiseNetworkEvent(new RequestAudioSpellStop());
+ RaiseNetworkEvent(new RemoveWizardChargeEvent());
+ break;
+ case BoundKeyState.Up:
+ _prevCharging = _charging;
+ _chargeLevel = 0;
+ _charging = false;
+ _chargeTime = 0f;
+ _isChargingPlaying = false;
+ _isChargedPlaying = false;
+
+ RaiseNetworkEvent(new RequestAudioSpellStop());
+ RaiseNetworkEvent(new RemoveWizardChargeEvent());
+ break;
+ }
+
+ if (_chargeLevel != _prevChargeLevel)
+ {
+ if (_chargeLevel > 0 && _charging)
+ {
+ RaiseNetworkEvent(new AddWizardChargeEvent(action.ChargeProto));
+ }
+ _prevChargeLevel = _chargeLevel;
+ }
+
+ if (_prevCharging != _charging)
+ {
+ ChargingUpdated?.Invoke(_charging);
+ }
+
+ if (_charging && !_isChargingPlaying)
+ {
+ _isChargingPlaying = true;
+ RaiseNetworkEvent(new RequestSpellChargingAudio(action.ChargingSound, action.LoopCharging));
+ }
+
+ if (_chargeLevel >= action.MaxChargeLevel && !_isChargedPlaying && _charging)
+ {
+ _isChargedPlaying = true;
+ RaiseNetworkEvent(new RequestSpellChargedAudio(action.MaxChargedSound, action.LoopMaxCharged));
+ }
+ }
+
+ private void HandleAction(EntityUid actionId, BaseTargetActionComponent action, EntityUid user, int chargeLevel)
+ {
+ var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
+ if (mousePos.MapId == MapId.Nullspace)
+ return;
+
+ var coordinates = EntityCoordinates.FromMap(_mapManager.TryFindGridAt(mousePos, out var gridUid, out _)
+ ? gridUid
+ : _mapManager.GetMapEntityId(mousePos.MapId), mousePos, _transformSystem, EntityManager);
+
+ if (!EntityManager.TryGetComponent(user, out ActionsComponent? comp))
+ return;
+
+ switch (action)
+ {
+ case WorldTargetActionComponent mapTarget:
+ _controller?.TryTargetWorld(coordinates, actionId, mapTarget, user, comp, ActionUseType.Charge, chargeLevel);
+ break;
+ }
+
+ RaiseNetworkEvent(new RequestAudioSpellStop());
+ RaiseNetworkEvent(new RemoveWizardChargeEvent());
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+
+ _controller = null;
+
+ _charging = false;
+ _prevCharging = false;
+ _chargeTime = 0f;
+ _chargeLevel = 0;
+ _prevChargeLevel = 0;
+ _isChargingPlaying = false;
+ _isChargedPlaying = false;
+ }
+}
diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
index c1064f62f7..0298b5de46 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
@@ -17,8 +17,6 @@ using Robust.Client.State;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
namespace Content.Client.Weapons.Melee;
diff --git a/Content.Client/_White/Wizard/Scrolls/ScrollSystem.cs b/Content.Client/_White/Wizard/Scrolls/ScrollSystem.cs
new file mode 100644
index 0000000000..79aa123a89
--- /dev/null
+++ b/Content.Client/_White/Wizard/Scrolls/ScrollSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared._White.Wizard.ScrollSystem;
+
+namespace Content.Client._White.Wizard.Scrolls;
+
+public sealed class ScrollSystem : SharedScrollSystem
+{
+}
diff --git a/Content.Server/EnergyDome/EnergyDomeComponent.cs b/Content.Server/EnergyDome/EnergyDomeComponent.cs
new file mode 100644
index 0000000000..b1efc631ec
--- /dev/null
+++ b/Content.Server/EnergyDome/EnergyDomeComponent.cs
@@ -0,0 +1,15 @@
+namespace Content.Server.EnergyDome;
+
+///
+/// marker component that allows linking the dome generator with the dome itself
+///
+
+[RegisterComponent, Access(typeof(EnergyDomeSystem))]
+public sealed partial class EnergyDomeComponent : Component
+{
+ ///
+ /// A linked generator that uses energy
+ ///
+ [DataField]
+ public EntityUid? Generator;
+}
diff --git a/Content.Server/EnergyDome/EnergyDomeGeneratorComponent.cs b/Content.Server/EnergyDome/EnergyDomeGeneratorComponent.cs
new file mode 100644
index 0000000000..24189f518f
--- /dev/null
+++ b/Content.Server/EnergyDome/EnergyDomeGeneratorComponent.cs
@@ -0,0 +1,85 @@
+using Content.Shared.DeviceLinking;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.EnergyDome;
+
+///
+/// component, allows an entity to generate a battery-powered energy dome of a specific type.
+///
+[RegisterComponent, Access(typeof(EnergyDomeSystem))] //Access add
+public sealed partial class EnergyDomeGeneratorComponent : Component
+{
+ [DataField]
+ public bool Enabled = false;
+
+ ///
+ /// How much energy will be spent from the battery per unit of damage taken by the shield.
+ ///
+ [DataField]
+ public float DamageEnergyDraw = 10f;
+
+ ///
+ /// Whether or not the dome can be toggled via standard interactions
+ /// (alt verbs, using in hand, etc)
+ ///
+ [DataField]
+ public bool CanInteractUse = true;
+
+ ///
+ /// Can the NetworkDevice system activate and deactivate the barrier?
+ ///
+ [DataField]
+ public bool CanDeviceNetworkUse = false;
+
+ //Dome
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public EntProtoId DomePrototype = "EnergyDomeSmallRed";
+
+ [DataField]
+ public EntityUid? SpawnedDome;
+
+ ///
+ /// the entity on which the shield will be hung. This is either the container containing
+ /// the item or the item itself. Determined when the shield is activated,
+ /// it is stored in the component for changing the protected entity.
+ ///
+ [DataField]
+ public EntityUid? DomeParentEntity;
+
+ //Action
+ [DataField]
+ public EntProtoId ToggleAction = "ActionToggleDome";
+
+ [DataField]
+ public EntityUid? ToggleActionEntity;
+
+ //Sounds
+ [DataField]
+ public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
+
+ [DataField]
+ public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg");
+
+ [DataField]
+ public SoundSpecifier EnergyOutSound = new SoundPathSpecifier("/Audio/Machines/energyshield_down.ogg");
+
+ [DataField]
+ public SoundSpecifier TurnOffSound = new SoundPathSpecifier("/Audio/Machines/button.ogg");
+
+ [DataField]
+ public SoundSpecifier ParrySound = new SoundPathSpecifier("/Audio/Machines/energyshield_parry.ogg")
+ {
+ Params = AudioParams.Default.WithVariation(0.05f)
+ };
+
+ //Ports
+ [DataField]
+ public ProtoId TogglePort = "Toggle";
+
+ [DataField]
+ public ProtoId OnPort = "On";
+
+ [DataField]
+ public ProtoId OffPort = "Off";
+}
diff --git a/Content.Server/EnergyDome/EnergyDomeSystem.cs b/Content.Server/EnergyDome/EnergyDomeSystem.cs
new file mode 100644
index 0000000000..1439c38c2a
--- /dev/null
+++ b/Content.Server/EnergyDome/EnergyDomeSystem.cs
@@ -0,0 +1,329 @@
+using Content.Server.DeviceLinking.Events;
+using Content.Server.DeviceLinking.Systems;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.PowerCell;
+using Content.Shared.Actions;
+using Content.Shared.Damage;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.PowerCell;
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Timing;
+using Content.Shared.Toggleable;
+using Content.Shared.Verbs;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+
+namespace Content.Server.EnergyDome;
+
+public sealed partial class EnergyDomeSystem : EntitySystem
+{
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly UseDelaySystem _useDelay = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly PowerCellSystem _powerCell = default!;
+ [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ //Generator events
+ SubscribeLocalEvent(OnInit);
+
+ SubscribeLocalEvent(OnActivatedInWorld);
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnSignalReceived);
+ SubscribeLocalEvent(OnGetActions);
+ SubscribeLocalEvent(OnToggleAction);
+
+ SubscribeLocalEvent(OnPowerCellChanged);
+ SubscribeLocalEvent(OnPowerCellSlotEmpty);
+ SubscribeLocalEvent(OnChargeChanged);
+
+ SubscribeLocalEvent(OnParentChanged);
+
+ SubscribeLocalEvent>(AddToggleDomeVerb);
+ SubscribeLocalEvent(OnExamine);
+
+
+ SubscribeLocalEvent(OnComponentRemove);
+
+ //Dome events
+ SubscribeLocalEvent(OnDomeDamaged);
+ }
+
+
+ private void OnInit(Entity generator, ref MapInitEvent args)
+ {
+ if (generator.Comp.CanDeviceNetworkUse)
+ _signalSystem.EnsureSinkPorts(generator, generator.Comp.TogglePort, generator.Comp.OnPort, generator.Comp.OffPort);
+ }
+
+ //different ways of use
+
+ private void OnSignalReceived(Entity generator, ref SignalReceivedEvent args)
+ {
+ if (!generator.Comp.CanDeviceNetworkUse)
+ return;
+
+ if (args.Port == generator.Comp.OnPort)
+ {
+ AttemptToggle(generator, true);
+ }
+ if (args.Port == generator.Comp.OffPort)
+ {
+ AttemptToggle(generator, false);
+ }
+ if (args.Port == generator.Comp.TogglePort)
+ {
+ AttemptToggle(generator, !generator.Comp.Enabled);
+ }
+ }
+
+ private void OnAfterInteract(Entity generator, ref AfterInteractEvent args)
+ {
+ if (generator.Comp.CanInteractUse)
+ AttemptToggle(generator, !generator.Comp.Enabled);
+ }
+
+ private void OnActivatedInWorld(Entity generator, ref ActivateInWorldEvent args)
+ {
+ if (generator.Comp.CanInteractUse)
+ AttemptToggle(generator, !generator.Comp.Enabled);
+ }
+
+ private void OnExamine(Entity generator, ref ExaminedEvent args)
+ {
+ args.PushMarkup(Loc.GetString(
+ (generator.Comp.Enabled)
+ ? "energy-dome-on-examine-is-on-message"
+ : "energy-dome-on-examine-is-off-message"
+ ));
+ }
+
+ private void AddToggleDomeVerb(Entity generator, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || !generator.Comp.CanInteractUse)
+ return;
+
+ var @event = args;
+ ActivationVerb verb = new()
+ {
+ Text = Loc.GetString("energy-dome-verb-toggle"),
+ Act = () => AttemptToggle(generator, !generator.Comp.Enabled)
+ };
+
+ args.Verbs.Add(verb);
+ }
+ private void OnGetActions(Entity generator, ref GetItemActionsEvent args)
+ {
+ if (generator.Comp.CanInteractUse)
+ args.AddAction(ref generator.Comp.ToggleActionEntity, generator.Comp.ToggleAction);
+ }
+
+ private void OnToggleAction(Entity generator, ref ToggleActionEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ AttemptToggle(generator, !generator.Comp.Enabled);
+
+ args.Handled = true;
+ }
+
+ // System interactions
+
+ private void OnPowerCellSlotEmpty(Entity generator, ref PowerCellSlotEmptyEvent args)
+ {
+ TurnOff(generator, true);
+ }
+
+ private void OnPowerCellChanged(Entity generator, ref PowerCellChangedEvent args)
+ {
+ if (args.Ejected || !_powerCell.HasDrawCharge(generator))
+ TurnOff(generator, true);
+ }
+
+ private void OnChargeChanged(Entity generator, ref ChargeChangedEvent args)
+ {
+ if (args.Charge == 0)
+ TurnOff(generator, true);
+ }
+ private void OnDomeDamaged(Entity dome, ref DamageChangedEvent args)
+ {
+ if (dome.Comp.Generator == null)
+ return;
+
+ var generatorUid = dome.Comp.Generator.Value;
+
+ if (!TryComp(generatorUid, out var generatorComp))
+ return;
+
+ if (args.DamageDelta == null)
+ return;
+
+ float totalDamage = args.DamageDelta.GetTotal().Float();
+ var energyLeak = totalDamage * generatorComp.DamageEnergyDraw;
+
+ _audio.PlayPvs(generatorComp.ParrySound, dome);
+
+ if (HasComp(generatorUid))
+ {
+ _powerCell.TryGetBatteryFromSlot(generatorUid, out var cell);
+ if (cell != null)
+ {
+ _battery.UseCharge(cell.Owner, energyLeak);
+
+ if (cell.Charge == 0)
+ TurnOff((generatorUid, generatorComp), true);
+ }
+ }
+
+ //it seems to me it would not work well to hang both a powercell and an internal battery with wire charging on the object....
+ if (TryComp(generatorUid, out var battery)) {
+ _battery.UseCharge(generatorUid, energyLeak);
+
+ if (battery.Charge == 0)
+ TurnOff((generatorUid, generatorComp), true);
+ }
+ }
+
+ private void OnParentChanged(Entity generator, ref EntParentChangedMessage args)
+ {
+ //To do: taking the active barrier in hand for some reason does not manage to change the parent in this case,
+ //and the barrier is not turned off.
+ //
+ //Laying down works well (-_-)
+ if (GetProtectedEntity(generator) != generator.Comp.DomeParentEntity)
+ TurnOff(generator, false);
+ }
+
+ private void OnComponentRemove(Entity generator, ref ComponentRemove args)
+ {
+ TurnOff(generator, false);
+ }
+
+ // Functional
+
+ public bool AttemptToggle(Entity generator, bool status)
+ {
+ if (TryComp(generator, out var useDelay) && _useDelay.IsDelayed(new Entity(generator, useDelay)))
+ {
+ _audio.PlayPvs(generator.Comp.TurnOffSound, generator);
+ _popup.PopupEntity(
+ Loc.GetString("energy-dome-recharging"),
+ generator);
+ return false;
+ }
+
+ if (TryComp(generator, out var powerCellSlot))
+ {
+ if (!_powerCell.TryGetBatteryFromSlot(generator, out var cell) && !TryComp(generator, out cell))
+ {
+ _audio.PlayPvs(generator.Comp.TurnOffSound, generator);
+ _popup.PopupEntity(
+ Loc.GetString("energy-dome-no-cell"),
+ generator);
+ return false;
+ }
+
+ if (!_powerCell.HasDrawCharge(generator))
+ {
+ _audio.PlayPvs(generator.Comp.TurnOffSound, generator);
+ _popup.PopupEntity(
+ Loc.GetString("energy-dome-no-power"),
+ generator);
+ return false;
+ }
+ }
+
+ if (TryComp(generator, out var battery))
+ {
+ if (battery.Charge == 0)
+ {
+ _audio.PlayPvs(generator.Comp.TurnOffSound, generator);
+ _popup.PopupEntity(
+ Loc.GetString("energy-dome-no-power"),
+ generator);
+ return false;
+ }
+ }
+
+ Toggle(generator, status);
+ return true;
+ }
+
+ private void Toggle(Entity generator, bool status)
+ {
+ if (status)
+ TurnOn(generator);
+ else
+ TurnOff(generator, false);
+ }
+
+ private void TurnOn(Entity generator)
+ {
+ if (generator.Comp.Enabled)
+ return;
+
+ var protectedEntity = GetProtectedEntity(generator);
+
+ var newDome = Spawn(generator.Comp.DomePrototype, Transform(protectedEntity).Coordinates);
+ generator.Comp.DomeParentEntity = protectedEntity;
+ _transform.SetParent(newDome, protectedEntity);
+
+ if (TryComp(newDome, out var domeComp))
+ {
+ domeComp.Generator = generator;
+ }
+
+ _powerCell.SetPowerCellDrawEnabled(generator, true);
+ if (TryComp(generator, out var recharger)) {
+ recharger.AutoRecharge = true;
+ }
+
+ generator.Comp.SpawnedDome = newDome;
+ _audio.PlayPvs(generator.Comp.TurnOnSound, generator);
+ generator.Comp.Enabled = true;
+ }
+
+ private void TurnOff(Entity generator, bool startReloading)
+ {
+ if (!generator.Comp.Enabled)
+ return;
+
+ generator.Comp.Enabled = false;
+ QueueDel(generator.Comp.SpawnedDome);
+
+ _powerCell.SetPowerCellDrawEnabled(generator, false);
+ if (TryComp(generator, out var recharger))
+ {
+ recharger.AutoRecharge = false;
+ }
+
+ _audio.PlayPvs(generator.Comp.TurnOffSound, generator);
+ if (startReloading)
+ {
+ _audio.PlayPvs(generator.Comp.EnergyOutSound, generator);
+ if (TryComp(generator, out var useDelay))
+ {
+ _useDelay.TryResetDelay(new Entity(generator, useDelay));
+ }
+ }
+ }
+
+ // Util
+
+ private EntityUid GetProtectedEntity(EntityUid entity)
+ {
+ return (_container.TryGetOuterContainer(entity, Transform(entity), out var container))
+ ? container.Owner
+ : entity;
+ }
+}
diff --git a/Content.Server/Lightning/LightningSystem.cs b/Content.Server/Lightning/LightningSystem.cs
index 4f975a60fd..6f5a86b0bb 100644
--- a/Content.Server/Lightning/LightningSystem.cs
+++ b/Content.Server/Lightning/LightningSystem.cs
@@ -57,7 +57,6 @@ public sealed class LightningSystem : SharedLightningSystem
}
}
-
///
/// Looks for objects with a LightningTarget component in the radius, prioritizes them, and hits the highest priority targets with lightning.
///
@@ -78,9 +77,9 @@ public sealed class LightningSystem : SharedLightningSystem
_random.Shuffle(targets);
targets.Sort((x, y) => y.Priority.CompareTo(x.Priority));
- int shootedCount = 0;
- int count = -1;
- while(shootedCount < boltCount)
+ var shootCount = 0;
+ var count = -1;
+ while(shootCount < boltCount)
{
count++;
@@ -95,7 +94,7 @@ public sealed class LightningSystem : SharedLightningSystem
{
ShootRandomLightnings(targets[count].Owner, range, 1, lightningPrototype, arcDepth - targets[count].LightningResistance, triggerLightningEvents);
}
- shootedCount++;
+ shootCount++;
}
}
}
diff --git a/Content.Server/Magic/Components/SpellbookComponent.cs b/Content.Server/Magic/Components/SpellbookComponent.cs
deleted file mode 100644
index ebc3c88043..0000000000
--- a/Content.Server/Magic/Components/SpellbookComponent.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
-
-namespace Content.Server.Magic.Components;
-
-///
-/// Spellbooks for having an entity learn spells as long as they've read the book and it's in their hand.
-///
-[RegisterComponent]
-public sealed partial class SpellbookComponent : Component
-{
- ///
- /// List of spells that this book has. This is a combination of the WorldSpells, EntitySpells, and InstantSpells.
- ///
- [ViewVariables]
- public readonly List Spells = new();
-
- ///
- /// The three fields below is just used for initialization.
- ///
- [DataField("spells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
- [ViewVariables(VVAccess.ReadWrite)]
- public Dictionary SpellActions = new();
-
- [DataField("learnTime")]
- [ViewVariables(VVAccess.ReadWrite)]
- public float LearnTime = .75f;
-
- ///
- /// If true, the spell action stays even after the book is removed
- ///
- [DataField("learnPermanently")]
- [ViewVariables(VVAccess.ReadWrite)]
- public bool LearnPermanently;
-}
diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs
index bb11c1f014..53963879fe 100644
--- a/Content.Server/Magic/MagicSystem.cs
+++ b/Content.Server/Magic/MagicSystem.cs
@@ -1,17 +1,15 @@
+using System.Linq;
using System.Numerics;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chat.Systems;
using Content.Server.Doors.Systems;
-using Content.Server.Magic.Components;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Actions;
using Content.Shared.Body.Components;
using Content.Shared.Coordinates.Helpers;
-using Content.Shared.DoAfter;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
-using Content.Shared.Interaction.Events;
using Content.Shared.Magic;
using Content.Shared.Magic.Events;
using Content.Shared.Maps;
@@ -21,6 +19,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
@@ -33,31 +32,25 @@ namespace Content.Server.Magic;
///
public sealed class MagicSystem : EntitySystem
{
- [Dependency] private readonly ISerializationManager _seriMan = default!;
+ [Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DoorBoltSystem _boltsSystem = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly GunSystem _gunSystem = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ChatSystem _chat = default!;
- [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnUse);
- SubscribeLocalEvent(OnDoAfter);
-
SubscribeLocalEvent(OnInstantSpawn);
SubscribeLocalEvent(OnTeleportSpell);
SubscribeLocalEvent(OnKnockSpell);
@@ -67,73 +60,8 @@ public sealed class MagicSystem : EntitySystem
SubscribeLocalEvent(OnChangeComponentsSpell);
}
- private void OnDoAfter(EntityUid uid, SpellbookComponent component, DoAfterEvent args)
- {
- if (args.Handled || args.Cancelled)
- return;
-
- args.Handled = true;
- if (!component.LearnPermanently)
- {
- _actionsSystem.GrantActions(args.Args.User, component.Spells, uid);
- return;
- }
-
- foreach (var (id, charges) in component.SpellActions)
- {
- // TOOD store spells entity ids on some sort of innate magic user component or something like that.
- EntityUid? actionId = null;
- if (_actionsSystem.AddAction(args.Args.User, ref actionId, id))
- _actionsSystem.SetCharges(actionId, charges < 0 ? null : charges);
- }
-
- component.SpellActions.Clear();
- }
-
- private void OnInit(EntityUid uid, SpellbookComponent component, MapInitEvent args)
- {
- if (component.LearnPermanently)
- return;
-
- foreach (var (id, charges) in component.SpellActions)
- {
- var spell = _actionContainer.AddAction(uid, id);
- if (spell == null)
- continue;
-
- _actionsSystem.SetCharges(spell, charges < 0 ? null : charges);
- component.Spells.Add(spell.Value);
- }
- }
-
- private void OnUse(EntityUid uid, SpellbookComponent component, UseInHandEvent args)
- {
- if (args.Handled)
- return;
-
- AttemptLearn(uid, component, args);
-
- args.Handled = true;
- }
-
- private void AttemptLearn(EntityUid uid, SpellbookComponent component, UseInHandEvent args)
- {
- var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.LearnTime, new SpellbookDoAfterEvent(), uid, target: uid)
- {
- BreakOnTargetMove = true,
- BreakOnUserMove = true,
- BreakOnDamage = true,
- NeedHand = true //What, are you going to read with your eyes only??
- };
-
- _doAfter.TryStartDoAfter(doAfterEventArgs);
- }
-
#region Spells
- ///
- /// Handles the instant action (i.e. on the caster) attempting to spawn an entity.
- ///
private void OnInstantSpawn(InstantSpawnSpellEvent args)
{
if (args.Handled)
@@ -145,11 +73,11 @@ public sealed class MagicSystem : EntitySystem
{
var ent = Spawn(args.Prototype, position.SnapToGrid(EntityManager, _mapManager));
- if (args.PreventCollideWithCaster)
- {
- var comp = EnsureComp(ent);
- comp.Uid = args.Performer;
- }
+ if (!args.PreventCollideWithCaster)
+ continue;
+
+ var comp = EnsureComp(ent);
+ comp.Uid = args.Performer;
}
Speak(args);
@@ -166,22 +94,17 @@ public sealed class MagicSystem : EntitySystem
var xform = Transform(ev.Performer);
- // var userVelocity = _physics.GetMapLinearVelocity(ev.Performer); WD EDIT
-
foreach (var pos in GetSpawnPositions(xform, ev.Pos))
{
- // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
- var mapPos = pos.ToMap(EntityManager);
- var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out var grid) // WD EDIT
+ var mapPos = _transformSystem.ToMapCoordinates(pos);
+ var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out var grid)
? pos.WithEntityId(gridUid, EntityManager)
- : new(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
+ : new EntityCoordinates(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
- // WD EDIT
var userVelocity = Vector2.Zero;
if (grid != null && TryComp(gridUid, out PhysicsComponent? physics))
userVelocity = physics.LinearVelocity;
- // WD EDIT
var ent = Spawn(ev.Prototype, spawnCoords);
var direction = ev.Target.ToMapPos(EntityManager, _transformSystem) -
@@ -194,7 +117,9 @@ public sealed class MagicSystem : EntitySystem
{
if (ev.Handled)
return;
+
ev.Handled = true;
+
Speak(ev);
foreach (var toRemove in ev.ToRemove)
@@ -209,75 +134,12 @@ public sealed class MagicSystem : EntitySystem
continue;
var component = (Component) _compFact.GetComponent(name);
- component.Owner = ev.Target;
var temp = (object) component;
- _seriMan.CopyTo(data.Component, ref temp);
+ _serializationManager.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(ev.Target, (Component) temp!);
}
}
- private List GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data)
- {
- switch (data)
- {
- case TargetCasterPos:
- return new List(1) {casterXform.Coordinates};
- case TargetInFront:
- {
- // This is shit but you get the idea.
- var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
-
- if (!_mapManager.TryGetGrid(casterXform.GridUid, out var mapGrid))
- return new List();
-
- if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
- return new List();
-
- var tileIndex = tileReference.Value.GridIndices;
- var coords = mapGrid.GridTileToLocal(tileIndex);
- EntityCoordinates coordsPlus;
- EntityCoordinates coordsMinus;
-
- var dir = casterXform.LocalRotation.GetCardinalDir();
- switch (dir)
- {
- case Direction.North:
- case Direction.South:
- {
- coordsPlus = mapGrid.GridTileToLocal(tileIndex + (1, 0));
- coordsMinus = mapGrid.GridTileToLocal(tileIndex + (-1, 0));
- return new List(3)
- {
- coords,
- coordsPlus,
- coordsMinus,
- };
- }
- case Direction.East:
- case Direction.West:
- {
- coordsPlus = mapGrid.GridTileToLocal(tileIndex + (0, 1));
- coordsMinus = mapGrid.GridTileToLocal(tileIndex + (0, -1));
- return new List(3)
- {
- coords,
- coordsPlus,
- coordsMinus,
- };
- }
- }
-
- return new List();
- }
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- ///
- /// Teleports the user to the clicked location
- ///
- ///
private void OnTeleportSpell(TeleportSpellEvent args)
{
if (args.Handled)
@@ -285,19 +147,16 @@ public sealed class MagicSystem : EntitySystem
var transform = Transform(args.Performer);
- if (transform.MapID != args.Target.GetMapId(EntityManager)) return;
+ if (transform.MapID != args.Target.GetMapId(EntityManager))
+ return;
_transformSystem.SetCoordinates(args.Performer, args.Target);
- transform.AttachToGridOrMap();
+ _transformSystem.AttachToGridOrMap(args.Performer);
_audio.PlayPvs(args.BlinkSound, args.Performer, AudioParams.Default.WithVolume(args.BlinkVolume));
Speak(args);
args.Handled = true;
}
- ///
- /// Opens all doors within range
- ///
- ///
private void OnKnockSpell(KnockSpellEvent args)
{
if (args.Handled)
@@ -306,13 +165,11 @@ public sealed class MagicSystem : EntitySystem
args.Handled = true;
Speak(args);
- //Get the position of the player
var transform = Transform(args.Performer);
var coords = transform.Coordinates;
_audio.PlayPvs(args.KnockSound, args.Performer, AudioParams.Default.WithVolume(args.KnockVolume));
- //Look for doors and don't open them if they're already open.
foreach (var entity in _lookup.GetEntitiesInRange(coords, args.Range))
{
if (TryComp(entity, out var bolts))
@@ -329,9 +186,10 @@ public sealed class MagicSystem : EntitySystem
return;
ev.Handled = true;
+
Speak(ev);
- var direction = Transform(ev.Target).MapPosition.Position - Transform(ev.Performer).MapPosition.Position;
+ var direction = _transformSystem.GetMapCoordinates(ev.Target).Position - _transformSystem.GetMapCoordinates(ev.Performer).Position;
var impulseVector = direction * 10000;
_physics.ApplyLinearImpulse(ev.Target, impulseVector);
@@ -339,28 +197,17 @@ public sealed class MagicSystem : EntitySystem
if (!TryComp(ev.Target, out var body))
return;
- var ents = _bodySystem.GibBody(ev.Target, true, body);
+ var entities = _bodySystem.GibBody(ev.Target, true, body);
if (!ev.DeleteNonBrainParts)
return;
- foreach (var part in ents)
+ foreach (var part in entities.Where(part => HasComp(part) && !HasComp(part)))
{
- // just leaves a brain and clothes
- if (HasComp(part) && !HasComp(part))
- {
- QueueDel(part);
- }
+ QueueDel(part);
}
}
- ///
- /// Spawns entity prototypes from a list within range of click.
- ///
- ///
- /// It will offset mobs after the first mob based on the OffsetVector2 property supplied.
- ///
- /// The Spawn Spell Event args.
private void OnWorldSpawn(WorldSpawnSpellEvent args)
{
if (args.Handled)
@@ -373,24 +220,85 @@ public sealed class MagicSystem : EntitySystem
args.Handled = true;
}
- ///
- /// Loops through a supplied list of entity prototypes and spawns them
- ///
- ///
- /// If an offset of 0, 0 is supplied then the entities will all spawn on the same tile.
- /// Any other offset will spawn entities starting from the source Map Coordinates and will increment the supplied
- /// offset
- ///
- /// The list of Entities to spawn in
- /// Map Coordinates where the entities will spawn
- /// Check to see if the entities should self delete
- /// A Vector2 offset that the entities will spawn in
- private void SpawnSpellHelper(List entityEntries, EntityCoordinates entityCoords, float? lifetime, Vector2 offsetVector2)
+ #endregion
+
+ #region Helpers
+
+ public List GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data)
{
- var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random);
+ return data switch
+ {
+ TargetCasterPos => GetCasterPosition(casterXform),
+ TargetInFront => GetPositionsInFront(casterXform),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public List GetCasterPosition(TransformComponent casterXform)
+ {
+ return new List(1) { casterXform.Coordinates };
+ }
+
+ public List GetPositionsInFront(TransformComponent casterXform)
+ {
+ var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
+
+ if (!TryComp(casterXform.GridUid, out var mapGrid) ||
+ !directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
+ {
+ return new List();
+ }
+
+ var tileIndex = tileReference.Value.GridIndices;
+ var coords = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex);
+
+ var directions = GetCardinalDirections(casterXform.LocalRotation.GetCardinalDir());
+ var spawnPositions = new List(3);
+
+ foreach (var direction in directions)
+ {
+ var offset = GetOffsetForDirection(direction);
+ var coordinates = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + offset);
+ spawnPositions.Add(coordinates);
+ }
+
+ spawnPositions.Add(coords);
+ return spawnPositions;
+ }
+
+ public IEnumerable GetCardinalDirections(Direction dir)
+ {
+ switch (dir)
+ {
+ case Direction.North:
+ case Direction.South:
+ return new[] { Direction.North, Direction.South };
+ case Direction.East:
+ case Direction.West:
+ return new[] { Direction.East, Direction.West };
+ default:
+ return Array.Empty();
+ }
+ }
+
+ public (int, int) GetOffsetForDirection(Direction direction)
+ {
+ return direction switch
+ {
+ Direction.North => (1, 0),
+ Direction.South => (-1, 0),
+ Direction.East => (0, 1),
+ Direction.West => (0, -1),
+ _ => (0, 0)
+ };
+ }
+
+ public void SpawnSpellHelper(List entityEntries, EntityCoordinates entityCoords, float? lifetime, Vector2 offsetVector2)
+ {
+ var getPrototypes = EntitySpawnCollection.GetSpawns(entityEntries, _random);
var offsetCoords = entityCoords;
- foreach (var proto in getProtos)
+ foreach (var proto in getPrototypes)
{
// TODO: Share this code with instant because they're both doing similar things for positioning.
var entity = Spawn(proto, offsetCoords);
@@ -404,8 +312,6 @@ public sealed class MagicSystem : EntitySystem
}
}
- #endregion
-
private void Speak(BaseActionEvent args)
{
if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech))
@@ -414,4 +320,6 @@ public sealed class MagicSystem : EntitySystem
_chat.TrySendInGameICMessage(args.Performer, Loc.GetString(speak.Speech),
InGameICChatType.Speak, false);
}
+
+ #endregion
}
diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs
index c844988b06..7971c8195d 100644
--- a/Content.Server/Power/EntitySystems/BatterySystem.cs
+++ b/Content.Server/Power/EntitySystems/BatterySystem.cs
@@ -84,8 +84,17 @@ namespace Content.Server.Power.EntitySystems
while (query.MoveNext(out var uid, out var comp, out var batt))
{
if (!comp.AutoRecharge) continue;
- if (batt.IsFullyCharged) continue;
- SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
+
+ if (comp.AutoRechargeRate > 0)
+ {
+ if (batt.IsFullyCharged) continue;
+ SetCharge(uid, batt.CurrentCharge + comp.AutoRechargeRate * frameTime, batt);
+ }
+ if (comp.AutoRechargeRate < 0) //self discharging
+ {
+ if (batt.Charge == 0) continue;
+ UseCharge(uid, -comp.AutoRechargeRate * frameTime, batt);
+ }
}
}
diff --git a/Content.Server/_White/IncorporealSystem/IncorporealSystem.cs b/Content.Server/_White/IncorporealSystem/IncorporealSystem.cs
index 969f8af6ea..3231e4420b 100644
--- a/Content.Server/_White/IncorporealSystem/IncorporealSystem.cs
+++ b/Content.Server/_White/IncorporealSystem/IncorporealSystem.cs
@@ -2,6 +2,8 @@
using Content.Shared.Eye;
using Content.Shared.Movement.Systems;
using Content.Shared.Physics;
+using Content.Shared.Stealth;
+using Content.Shared.Stealth.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
@@ -10,11 +12,10 @@ namespace Content.Server._White.IncorporealSystem;
public sealed class IncorporealSystem : EntitySystem
{
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
-
+ [Dependency] private readonly SharedStealthSystem _stealth = default!;
public override void Initialize()
{
@@ -41,6 +42,9 @@ public sealed class IncorporealSystem : EntitySystem
_visibilitySystem.RefreshVisibility(uid);
}
+ Spawn("EffectEmpPulse", Transform(uid).Coordinates);
+ EnsureComp(uid);
+ _stealth.SetVisibility(uid, -1);
_movement.RefreshMovementSpeedModifiers(uid);
}
@@ -50,8 +54,8 @@ public sealed class IncorporealSystem : EntitySystem
{
var fixture = fixtures.Fixtures.First();
- _physics.SetCollisionMask(uid, fixture.Key, fixture.Value, (int) (CollisionGroup.FlyingMobMask | CollisionGroup.GhostImpassable), fixtures);
- _physics.SetCollisionLayer(uid, fixture.Key, fixture.Value, (int) CollisionGroup.FlyingMobLayer, fixtures);
+ _physics.SetCollisionMask(uid, fixture.Key, fixture.Value, (int) (CollisionGroup.MobMask | CollisionGroup.GhostImpassable), fixtures);
+ _physics.SetCollisionLayer(uid, fixture.Key, fixture.Value, (int) CollisionGroup.MobLayer, fixtures);
}
if (TryComp(uid, out var visibility))
@@ -62,6 +66,10 @@ public sealed class IncorporealSystem : EntitySystem
}
component.MovementSpeedBuff = 1;
+
+ Spawn("EffectEmpPulse", Transform(uid).Coordinates);
+ _stealth.SetVisibility(uid, 1);
+ RemComp(uid);
_movement.RefreshMovementSpeedModifiers(uid);
}
diff --git a/Content.Server/_White/Wizard/Charging/ChargingSystem.cs b/Content.Server/_White/Wizard/Charging/ChargingSystem.cs
new file mode 100644
index 0000000000..f5e8f840fe
--- /dev/null
+++ b/Content.Server/_White/Wizard/Charging/ChargingSystem.cs
@@ -0,0 +1,183 @@
+using Content.Shared._White.Wizard;
+using Content.Shared._White.Wizard.Charging;
+using Content.Shared.Follower;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Player;
+
+namespace Content.Server._White.Wizard.Charging;
+
+public sealed class ChargingSystem : SharedChargingSystem
+{
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly FollowerSystem _followerSystem = default!;
+
+ private readonly Dictionary> _charges = new();
+
+ private readonly Dictionary _chargingLoops = new();
+ private readonly Dictionary _chargedLoop = new();
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnCharging);
+ SubscribeNetworkEvent(OnCharged);
+ SubscribeNetworkEvent(OnStop);
+ SubscribeLocalEvent(OnDetach);
+
+ SubscribeNetworkEvent(Add);
+ SubscribeNetworkEvent(Remove);
+ }
+
+ #region Audio
+
+ private void OnCharging(RequestSpellChargingAudio msg, EntitySessionEventArgs args)
+ {
+ var user = args.SenderSession?.AttachedEntity;
+ if (user == null)
+ return;
+
+ var shouldLoop = msg.Loop;
+ var sound = msg.Sound;
+
+ if (!shouldLoop)
+ {
+ _audio.PlayPvs(sound, user.Value);
+ return;
+ }
+
+ if (_chargingLoops.TryGetValue(user.Value, out var currentStream))
+ {
+ _audio.Stop(currentStream);
+ _chargingLoops.Remove(user.Value);
+ }
+
+ var newStream = _audio.PlayPvs(sound, user.Value, AudioParams.Default.WithLoop(true));
+
+ if (newStream.HasValue)
+ {
+ _chargingLoops[user.Value] = newStream.Value.Entity;
+ }
+ }
+
+ private void OnCharged(RequestSpellChargedAudio msg, EntitySessionEventArgs args)
+ {
+ var user = args.SenderSession?.AttachedEntity;
+ if (user == null)
+ return;
+
+ if (_chargingLoops.TryGetValue(user.Value, out var currentStream))
+ {
+ _audio.Stop(currentStream);
+ _chargingLoops.Remove(user.Value);
+ }
+
+ var shouldLoop = msg.Loop;
+ var sound = msg.Sound;
+
+ if (!shouldLoop)
+ {
+ _audio.PlayPvs(sound, user.Value);
+ return;
+ }
+
+ if (_chargedLoop.TryGetValue(user.Value, out var chargedLoop))
+ {
+ _audio.Stop(chargedLoop);
+ _chargedLoop.Remove(user.Value);
+ }
+
+ var newStream = _audio.PlayPvs(sound, user.Value, AudioParams.Default.WithLoop(true));
+
+ if (newStream.HasValue)
+ {
+ _chargedLoop[user.Value] = newStream.Value.Entity;
+ }
+ }
+
+ private void OnStop(RequestAudioSpellStop msg, EntitySessionEventArgs args)
+ {
+ var user = args.SenderSession?.AttachedEntity;
+ if (user == null)
+ return;
+
+ if (_chargingLoops.TryGetValue(user.Value, out var currentStream))
+ {
+ _audio.Stop(currentStream);
+ _chargingLoops.Remove(user.Value);
+ }
+
+ if (_chargedLoop.TryGetValue(user.Value, out var chargedLoop))
+ {
+ _audio.Stop(chargedLoop);
+ _chargedLoop.Remove(user.Value);
+ }
+ }
+
+ private void OnDetach(PlayerDetachedEvent msg, EntitySessionEventArgs args)
+ {
+ var user = msg.Entity;
+
+ if (_chargingLoops.TryGetValue(user, out var currentStream))
+ {
+ _audio.Stop(currentStream);
+ _chargingLoops.Remove(user);
+ }
+
+ if (_chargedLoop.TryGetValue(user, out var chargedLoop))
+ {
+ _audio.Stop(chargedLoop);
+ _chargedLoop.Remove(user);
+ }
+ }
+
+ #endregion
+
+ #region Charges
+
+ private void Add(AddWizardChargeEvent msg, EntitySessionEventArgs args)
+ {
+ if (args.SenderSession.AttachedEntity != null)
+ AddCharge(args.SenderSession.AttachedEntity.Value, msg.ChargeProto);
+ }
+
+ private void Remove(RemoveWizardChargeEvent msg, EntitySessionEventArgs args)
+ {
+ if (args.SenderSession.AttachedEntity != null)
+ RemoveAllCharges(args.SenderSession.AttachedEntity.Value);
+ }
+
+ #endregion
+
+ #region Helpers
+
+ public void AddCharge(EntityUid uid, string msgChargeProto)
+ {
+ var itemEnt = Spawn(msgChargeProto, Transform(uid).Coordinates);
+ _followerSystem.StartFollowingEntity(itemEnt, uid);
+
+ if (!_charges.ContainsKey(uid))
+ {
+ _charges[uid] = new List();
+ }
+
+ _charges[uid].Add(itemEnt);
+ }
+
+ public void RemoveAllCharges(EntityUid uid)
+ {
+ if (!_charges.ContainsKey(uid))
+ return;
+
+ foreach (var followerEnt in _charges[uid])
+ {
+ Del(followerEnt);
+ }
+
+ _charges.Remove(uid);
+ }
+
+ #endregion
+}
diff --git a/Content.Server/_White/Wizard/Magic/Amaterasu/AmaterasuComponent.cs b/Content.Server/_White/Wizard/Magic/Amaterasu/AmaterasuComponent.cs
new file mode 100644
index 0000000000..6cf0d7a5c6
--- /dev/null
+++ b/Content.Server/_White/Wizard/Magic/Amaterasu/AmaterasuComponent.cs
@@ -0,0 +1,6 @@
+namespace Content.Server._White.Wizard.Magic.Amaterasu;
+
+[RegisterComponent]
+public sealed partial class AmaterasuComponent : Component
+{
+}
diff --git a/Content.Server/_White/Wizard/Magic/Amaterasu/AmaterasuSystem.cs b/Content.Server/_White/Wizard/Magic/Amaterasu/AmaterasuSystem.cs
new file mode 100644
index 0000000000..6c16a7f52f
--- /dev/null
+++ b/Content.Server/_White/Wizard/Magic/Amaterasu/AmaterasuSystem.cs
@@ -0,0 +1,34 @@
+using Content.Server.Atmos.Components;
+using Content.Server.Body.Systems;
+using Content.Shared.Mobs;
+
+namespace Content.Server._White.Wizard.Magic.Amaterasu;
+
+public sealed class AmaterasuSystem : EntitySystem
+{
+ [Dependency] private readonly BodySystem _bodySystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMobState);
+ }
+
+ private void OnMobState(EntityUid uid, AmaterasuComponent component, MobStateChangedEvent args)
+ {
+ if (args.NewMobState is MobState.Critical or MobState.Dead)
+ {
+ if(!TryComp(uid, out var flammable))
+ return;
+
+ if (flammable.OnFire)
+ {
+ _bodySystem.GibBody(uid);
+ return;
+ }
+
+ RemComp(uid);
+ }
+ }
+}
diff --git a/Content.Server/_White/Wizard/Magic/Other/InstantRecallComponent.cs b/Content.Server/_White/Wizard/Magic/Other/InstantRecallComponent.cs
new file mode 100644
index 0000000000..e20a7ac19e
--- /dev/null
+++ b/Content.Server/_White/Wizard/Magic/Other/InstantRecallComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._White.Wizard.Magic.Other;
+
+[RegisterComponent]
+public sealed partial class InstantRecallComponent : Component
+{
+ public EntityUid? Item;
+}
diff --git a/Content.Server/_White/Wizard/Magic/TeslaProjectile/TeslaProjectileComponent.cs b/Content.Server/_White/Wizard/Magic/TeslaProjectile/TeslaProjectileComponent.cs
new file mode 100644
index 0000000000..9ffce14fd6
--- /dev/null
+++ b/Content.Server/_White/Wizard/Magic/TeslaProjectile/TeslaProjectileComponent.cs
@@ -0,0 +1,4 @@
+namespace Content.Server._White.Wizard.Magic.TeslaProjectile;
+
+[RegisterComponent]
+public sealed partial class TeslaProjectileComponent : Component {}
diff --git a/Content.Server/_White/Wizard/Magic/TeslaProjectile/TeslaProjectileSystem.cs b/Content.Server/_White/Wizard/Magic/TeslaProjectile/TeslaProjectileSystem.cs
new file mode 100644
index 0000000000..44740f704d
--- /dev/null
+++ b/Content.Server/_White/Wizard/Magic/TeslaProjectile/TeslaProjectileSystem.cs
@@ -0,0 +1,21 @@
+using Content.Server.Lightning;
+using Content.Shared.Projectiles;
+
+namespace Content.Server._White.Wizard.Magic.TeslaProjectile;
+
+public sealed class TeslaProjectileSystem : EntitySystem
+{
+ [Dependency] private readonly LightningSystem _lightning = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStartCollide);
+ }
+
+ private void OnStartCollide(Entity ent, ref ProjectileHitEvent args)
+ {
+ _lightning.ShootRandomLightnings(ent, 2, 4, arcDepth:2);
+ }
+}
diff --git a/Content.Server/_White/Wizard/Magic/WizardSpellsSystem.cs b/Content.Server/_White/Wizard/Magic/WizardSpellsSystem.cs
new file mode 100644
index 0000000000..9a4cd57b25
--- /dev/null
+++ b/Content.Server/_White/Wizard/Magic/WizardSpellsSystem.cs
@@ -0,0 +1,701 @@
+using System.Linq;
+using System.Numerics;
+using Content.Server._White.IncorporealSystem;
+using Content.Server._White.Wizard.Magic.Amaterasu;
+using Content.Server._White.Wizard.Magic.Other;
+using Content.Server.Abilities.Mime;
+using Content.Server.Administration.Commands;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.Emp;
+using Content.Server.Lightning;
+using Content.Server.Magic;
+using Content.Server.Singularity.EntitySystems;
+using Content.Server.Weapons.Ranged.Systems;
+using Content.Shared._White.Wizard;
+using Content.Shared._White.Wizard.Magic;
+using Content.Shared.Actions;
+using Content.Shared.Cluwne;
+using Content.Shared.Coordinates.Helpers;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Humanoid;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Inventory.VirtualItem;
+using Content.Shared.Item;
+using Content.Shared.Magic;
+using Content.Shared.Maps;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Physics;
+using Content.Shared.Popups;
+using Content.Shared.StatusEffect;
+using Content.Shared.Throwing;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server._White.Wizard.Magic;
+
+public sealed class WizardSpellsSystem : EntitySystem
+{
+ #region Dependencies
+
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly GunSystem _gunSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly LightningSystem _lightning = default!;
+ [Dependency] private readonly MagicSystem _magicSystem = default!;
+ [Dependency] private readonly GravityWellSystem _gravityWell = default!;
+ [Dependency] private readonly FlammableSystem _flammableSystem = default!;
+ [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+ [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly TurfSystem _turf = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly EmpSystem _empSystem = default!;
+
+ #endregion
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInstantRecallSpell);
+ SubscribeLocalEvent(OnMimeTouchSpell);
+ SubscribeLocalEvent(OnBananaTouchSpell);
+ SubscribeLocalEvent(OnCluwneCurseSpell);
+ SubscribeLocalEvent(OnEmpSpell);
+ SubscribeLocalEvent(OnJauntSpell);
+ SubscribeLocalEvent(OnBlinkSpell);
+ SubscribeLocalEvent(OnForcewallSpell);
+ SubscribeLocalEvent(OnCardsSpell);
+ SubscribeLocalEvent(OnFireballSpell);
+ SubscribeLocalEvent(OnForceSpell);
+ SubscribeLocalEvent(OnArcSpell);
+
+ SubscribeLocalEvent(OnBeforeCastSpell);
+ }
+
+ #region Instant Recall
+
+ private void OnInstantRecallSpell(InstantRecallSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ if (!TryComp(msg.Performer, out var handsComponent))
+ return;
+
+ if (!TryComp(msg.Action, out var recallComponent))
+ {
+ _popupSystem.PopupEntity("Что-то поломалось!", msg.Performer, msg.Performer);
+ return;
+ }
+
+ if (handsComponent.ActiveHandEntity != null)
+ {
+ if (HasComp(handsComponent.ActiveHandEntity.Value))
+ {
+ _popupSystem.PopupEntity("Не могу работать с этим!", msg.Performer, msg.Performer);
+ return;
+ }
+
+ recallComponent.Item = handsComponent.ActiveHandEntity.Value;
+ _popupSystem.PopupEntity($"Сопряжено с {MetaData(handsComponent.ActiveHandEntity.Value).EntityName}", msg.Performer, msg.Performer);
+ return;
+ }
+
+ if (handsComponent.ActiveHandEntity == null && recallComponent.Item != null)
+ {
+ var coordsItem = Transform(recallComponent.Item.Value).Coordinates;
+ var coordsPerformer = Transform(msg.Performer).Coordinates;
+
+ Spawn("EffectEmpPulse", coordsItem);
+
+ _transformSystem.SetCoordinates(recallComponent.Item.Value, coordsPerformer);
+ _transformSystem.AttachToGridOrMap(recallComponent.Item.Value);
+
+ _handsSystem.TryForcePickupAnyHand(msg.Performer, recallComponent.Item.Value);
+
+ msg.Handled = true;
+ return;
+ }
+
+ _popupSystem.PopupEntity("Нет привязки.", msg.Performer, msg.Performer);
+ }
+
+ #endregion
+
+ #region Mime Touch
+
+ private void OnMimeTouchSpell(MimeTouchSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ if (!HasComp(msg.Target))
+ {
+ _popupSystem.PopupEntity("Работает только на людях!", msg.Performer, msg.Performer);
+ return;
+ }
+
+ SetOutfitCommand.SetOutfit(msg.Target, "MimeGear", EntityManager);
+ EnsureComp(msg.Target);
+
+ Spawn("AdminInstantEffectSmoke3", Transform(msg.Target).Coordinates);
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ #endregion
+
+ #region Banana Touch
+
+ private void OnBananaTouchSpell(BananaTouchSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ if (!HasComp(msg.Target))
+ {
+ _popupSystem.PopupEntity("Работает только на людях!", msg.Performer, msg.Performer);
+ return;
+ }
+
+ SetOutfitCommand.SetOutfit(msg.Target, "ClownGear", EntityManager);
+ EnsureComp(msg.Target);
+
+ Spawn("AdminInstantEffectSmoke3", Transform(msg.Target).Coordinates);
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ #endregion
+
+ #region Cluwne Curse
+
+ private void OnCluwneCurseSpell(CluwneCurseSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ if (!HasComp(msg.Target))
+ {
+ _popupSystem.PopupEntity("Работает только на людях!", msg.Performer, msg.Performer);
+ return;
+ }
+
+ EnsureComp(msg.Target);
+
+ Spawn("AdminInstantEffectSmoke3", Transform(msg.Target).Coordinates);
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ #endregion
+
+ #region EMP
+
+ private void OnEmpSpell(EmpSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ var coords = _transformSystem.ToMapCoordinates(Transform(msg.Performer).Coordinates);
+
+ _empSystem.EmpPulse(coords, 15, 1000000, 60f);
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ #endregion
+
+ #region Ethereal Jaunt
+
+ private void OnJauntSpell(EtherealJauntSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ if (_statusEffectsSystem.HasStatusEffect(msg.Performer, "Incorporeal"))
+ {
+ _popupSystem.PopupEntity("Вы уже в потустороннем мире", msg.Performer, msg.Performer);
+ return;
+ }
+
+ Spawn("AdminInstantEffectSmoke10", Transform(msg.Performer).Coordinates);
+
+ _statusEffectsSystem.TryAddStatusEffect(msg.Performer, "Incorporeal", TimeSpan.FromSeconds(10), false);
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ #endregion
+
+ #region Blink
+
+ private void OnBlinkSpell(BlinkSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ var transform = Transform(msg.Performer);
+
+ var oldCoords = transform.Coordinates;
+
+ EntityCoordinates coords = default;
+ var foundTeleportPos = false;
+ var attempts = 10;
+
+ while (attempts > 0)
+ {
+ attempts--;
+
+ var random = new Random().Next(10, 20);
+ var offset = transform.LocalRotation.ToWorldVec().Normalized();
+ var direction = transform.LocalRotation.GetDir().ToVec();
+ var newOffset = offset + direction * random;
+ coords = transform.Coordinates.Offset(newOffset).SnapToGrid(EntityManager);
+
+ var tile = coords.GetTileRef();
+
+ if (tile != null && _turf.IsTileBlocked(tile.Value, CollisionGroup.AllMask))
+ continue;
+
+ foundTeleportPos = true;
+ break;
+ }
+
+ if (!foundTeleportPos)
+ return;
+
+ _transformSystem.SetCoordinates(msg.Performer, coords);
+ _transformSystem.AttachToGridOrMap(msg.Performer);
+
+ _audio.PlayPvs("/Audio/White/Cult/veilin.ogg", coords);
+ _audio.PlayPvs("/Audio/White/Cult/veilout.ogg", oldCoords);
+
+ Spawn("AdminInstantEffectSmoke10", oldCoords);
+ Spawn("AdminInstantEffectSmoke10", coords);
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ #endregion
+
+ #region Forcewall
+
+ private void OnForcewallSpell(ForceWallSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ switch (msg.ActionUseType)
+ {
+ case ActionUseType.Default:
+ ForcewallSpellDefault(msg);
+ break;
+ case ActionUseType.Charge:
+ ForcewallSpellCharge(msg);
+ break;
+ case ActionUseType.AltUse:
+ ForcewallSpellAlt(msg);
+ break;
+ }
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ private void ForcewallSpellDefault(ForceWallSpellEvent msg)
+ {
+ var transform = Transform(msg.Performer);
+
+ foreach (var position in _magicSystem.GetPositionsInFront(transform))
+ {
+ var ent = Spawn(msg.Prototype, position.SnapToGrid(EntityManager, _mapManager));
+
+ var comp = EnsureComp(ent);
+ comp.Uid = msg.Performer;
+ }
+ }
+
+ private void ForcewallSpellCharge(ForceWallSpellEvent msg)
+ {
+ var xform = Transform(msg.Performer);
+
+ var positions = GetArenaPositions(xform, msg.ChargeLevel);
+
+ foreach (var position in positions)
+ {
+ var ent = Spawn(msg.Prototype, position);
+
+ var comp = EnsureComp(ent);
+ comp.Uid = msg.Performer;
+ }
+ }
+
+ private void ForcewallSpellAlt(ForceWallSpellEvent msg)
+ {
+ var xform = Transform(msg.TargetUid);
+
+ var positions = GetArenaPositions(xform, 2);
+
+ foreach (var direction in positions)
+ {
+ var ent = Spawn(msg.Prototype, direction);
+
+ var comp = EnsureComp(ent);
+ comp.Uid = msg.Performer;
+ }
+ }
+
+ #endregion
+
+ #region Cards
+
+ private void OnCardsSpell(CardsSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ switch (msg.ActionUseType)
+ {
+ case ActionUseType.Default:
+ CardsSpellDefault(msg);
+ break;
+ case ActionUseType.Charge:
+ CardsSpellCharge(msg);
+ break;
+ case ActionUseType.AltUse:
+ CardsSpellAlt(msg);
+ break;
+ }
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ private void CardsSpellDefault(CardsSpellEvent msg)
+ {
+ var xform = Transform(msg.Performer);
+
+ for (var i = 0; i < 10; i++)
+ {
+ foreach (var pos in _magicSystem.GetSpawnPositions(xform, msg.Pos))
+ {
+ var mapPos = _transformSystem.ToMapCoordinates(pos);
+ var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out _)
+ ? pos.WithEntityId(gridUid, EntityManager)
+ : new EntityCoordinates(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
+
+ var ent = Spawn(msg.Prototype, spawnCoords);
+
+ var direction = msg.Target.ToMapPos(EntityManager, _transformSystem) - spawnCoords.ToMapPos(EntityManager, _transformSystem);
+ var randomizedDirection = direction + new Vector2(_random.Next(-2, 2), _random.Next(-2, 2));
+
+ _throwingSystem.TryThrow(ent, randomizedDirection, 60, msg.Performer);
+ }
+ }
+ }
+
+ private void CardsSpellCharge(CardsSpellEvent msg)
+ {
+ var xform = Transform(msg.Performer);
+
+ var count = 5 * msg.ChargeLevel;
+ var angleStep = 360f / count;
+
+ for (var i = 0; i < count; i++)
+ {
+ var angle = i * angleStep;
+
+ var direction = new Vector2(MathF.Cos(MathHelper.DegreesToRadians(angle)), MathF.Sin(MathHelper.DegreesToRadians(angle)));
+
+ foreach (var pos in _magicSystem.GetSpawnPositions(xform, msg.Pos))
+ {
+ var mapPos = _transformSystem.ToMapCoordinates(pos);
+
+ var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out _)
+ ? pos.WithEntityId(gridUid, EntityManager)
+ : new EntityCoordinates(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
+
+ var ent = Spawn(msg.Prototype, spawnCoords);
+
+ _throwingSystem.TryThrow(ent, direction, 60, msg.Performer);
+ }
+ }
+ }
+
+ private void CardsSpellAlt(CardsSpellEvent msg)
+ {
+ if (!HasComp(msg.TargetUid))
+ return;
+
+ Del(msg.TargetUid);
+ var item = Spawn(msg.Prototype);
+ _handsSystem.TryPickupAnyHand(msg.Performer, item);
+ }
+
+ #endregion
+
+ #region Fireball
+
+ private void OnFireballSpell(FireballSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ switch (msg.ActionUseType)
+ {
+ case ActionUseType.Default:
+ FireballSpellDefault(msg);
+ break;
+ case ActionUseType.Charge:
+ FireballSpellCharge(msg);
+ break;
+ case ActionUseType.AltUse:
+ FireballSpellAlt(msg);
+ break;
+ }
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ private void FireballSpellDefault(FireballSpellEvent msg)
+ {
+ var xform = Transform(msg.Performer);
+
+ foreach (var pos in _magicSystem.GetSpawnPositions(xform, msg.Pos))
+ {
+ var mapPos = _transformSystem.ToMapCoordinates(pos);
+ var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out var grid)
+ ? pos.WithEntityId(gridUid, EntityManager)
+ : new EntityCoordinates(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
+
+ var userVelocity = Vector2.Zero;
+
+ if (grid != null && TryComp(gridUid, out PhysicsComponent? physics))
+ userVelocity = physics.LinearVelocity;
+
+ var ent = Spawn(msg.Prototype, spawnCoords);
+ var direction = msg.Target.ToMapPos(EntityManager, _transformSystem) - spawnCoords.ToMapPos(EntityManager, _transformSystem);
+ _gunSystem.ShootProjectile(ent, direction, userVelocity, msg.Performer, msg.Performer);
+ }
+ }
+
+ private void FireballSpellCharge(FireballSpellEvent msg)
+ {
+ var coords = Transform(msg.Performer).Coordinates;
+
+ var targets = _lookup.GetEntitiesInRange(coords, 2 * msg.ChargeLevel);
+
+ foreach (var target in targets.Where(target => target.Owner != msg.Performer))
+ {
+ target.Comp.FireStacks += 3;
+ _flammableSystem.Ignite(target, msg.Performer);
+ }
+ }
+
+ private void FireballSpellAlt(FireballSpellEvent msg)
+ {
+ if (!TryComp(msg.TargetUid, out var flammableComponent))
+ return;
+
+ flammableComponent.FireStacks += 4;
+
+ _flammableSystem.Ignite(msg.TargetUid, msg.Performer);
+
+ EnsureComp(msg.TargetUid);
+ }
+
+ #endregion
+
+ #region Force
+
+ private void OnForceSpell(ForceSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ switch (msg.ActionUseType)
+ {
+ case ActionUseType.Default:
+ ForceSpellDefault(msg);
+ break;
+ case ActionUseType.Charge:
+ ForceSpellCharge(msg);
+ break;
+ case ActionUseType.AltUse:
+ ForceSpellAlt(msg);
+ break;
+ }
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ private void ForceSpellDefault(ForceSpellEvent msg)
+ {
+ Spawn("AdminInstantEffectMinusGravityWell", msg.Target);
+ }
+
+ private void ForceSpellCharge(ForceSpellEvent msg)
+ {
+ _gravityWell.GravPulse(msg.Performer, 15, 0, -80 * msg.ChargeLevel, -2 * msg.ChargeLevel);
+ }
+
+ private void ForceSpellAlt(ForceSpellEvent msg)
+ {
+ _gravityWell.GravPulse(msg.Target, 10, 0, 200, 10);
+ }
+
+ #endregion
+
+ #region Arc
+
+ private void OnArcSpell(ArcSpellEvent msg)
+ {
+ if (msg.Handled || !CheckRequirements(msg.Action, msg.Performer))
+ return;
+
+ switch (msg.ActionUseType)
+ {
+ case ActionUseType.Default:
+ ArcSpellDefault(msg);
+ break;
+ case ActionUseType.Charge:
+ ArcSpellCharge(msg);
+ break;
+ case ActionUseType.AltUse:
+ ArcSpellAlt(msg);
+ break;
+ }
+
+ msg.Handled = true;
+ Speak(msg);
+ }
+
+ private void ArcSpellDefault(ArcSpellEvent msg)
+ {
+ const int possibleEntitiesCount = 2;
+
+ var entitiesInRange = _lookup.GetEntitiesInRange(msg.Target, 1);
+ var entitiesToHit = entitiesInRange.Where(HasComp).Take(possibleEntitiesCount);
+
+ foreach (var entity in entitiesToHit)
+ {
+ _lightning.ShootLightning(msg.Performer, entity);
+ }
+ }
+
+ private void ArcSpellCharge(ArcSpellEvent msg)
+ {
+ _lightning.ShootRandomLightnings(msg.Performer, 2 * msg.ChargeLevel, msg.ChargeLevel * 2, arcDepth: 2);
+ }
+
+ private void ArcSpellAlt(ArcSpellEvent msg)
+ {
+ var xform = Transform(msg.Performer);
+
+ foreach (var pos in _magicSystem.GetSpawnPositions(xform, msg.Pos))
+ {
+ var mapPos = _transformSystem.ToMapCoordinates(pos);
+ var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out var grid)
+ ? pos.WithEntityId(gridUid, EntityManager)
+ : new EntityCoordinates(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
+
+ var userVelocity = Vector2.Zero;
+
+ if (grid != null && TryComp(gridUid, out PhysicsComponent? physics))
+ userVelocity = physics.LinearVelocity;
+
+ var ent = Spawn(msg.Prototype, spawnCoords);
+ var direction = msg.Target.ToMapPos(EntityManager, _transformSystem) - spawnCoords.ToMapPos(EntityManager, _transformSystem);
+ _gunSystem.ShootProjectile(ent, direction, userVelocity, msg.Performer, msg.Performer);
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private void Speak(BaseActionEvent args)
+ {
+ if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech))
+ return;
+
+ _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(speak.Speech),
+ InGameICChatType.Speak, false);
+ }
+
+ private List GetArenaPositions(TransformComponent casterXform, int arenaSize)
+ {
+ var positions = new List();
+
+ arenaSize--;
+
+ for (var i = -arenaSize; i <= arenaSize; i++)
+ {
+ for (var j = -arenaSize; j <= arenaSize; j++)
+ {
+ var position = new Vector2(i, j);
+ var coordinates = casterXform.Coordinates.Offset(position);
+ positions.Add(coordinates);
+ }
+ }
+
+ return positions;
+ }
+
+ private bool CheckRequirements(EntityUid spell, EntityUid performer)
+ {
+ var ev = new BeforeCastSpellEvent(performer);
+ RaiseLocalEvent(spell, ref ev);
+ return !ev.Cancelled;
+ }
+
+ private void OnBeforeCastSpell(Entity ent, ref BeforeCastSpellEvent args)
+ {
+ var comp = ent.Comp;
+ var hasReqs = false;
+
+ if (comp.RequiresClothes)
+ {
+ var enumerator = _inventory.GetSlotEnumerator(args.Performer, SlotFlags.OUTERCLOTHING | SlotFlags.HEAD);
+ while (enumerator.MoveNext(out var containerSlot))
+ {
+ if (containerSlot.ContainedEntity is { } item)
+ hasReqs = HasComp(item);
+ else
+ hasReqs = false;
+
+ if (!hasReqs)
+ break;
+ }
+ }
+
+ if (!hasReqs)
+ {
+ args.Cancelled = true;
+ _popupSystem.PopupEntity("Missing Requirements! You need to wear your robe and hat!", args.Performer, args.Performer);
+ }
+ }
+
+ #endregion
+}
diff --git a/Content.Server/_White/Wizard/Scrolls/ScrollSystem.cs b/Content.Server/_White/Wizard/Scrolls/ScrollSystem.cs
new file mode 100644
index 0000000000..a1e1a33d27
--- /dev/null
+++ b/Content.Server/_White/Wizard/Scrolls/ScrollSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared._White.Wizard.ScrollSystem;
+
+namespace Content.Server._White.Wizard.Scrolls;
+
+public sealed class ScrollSystem : SharedScrollSystem
+{
+ protected override void BurnScroll(EntityUid uid) => Del(uid);
+}
diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs
index 4715f03c70..a256167410 100644
--- a/Content.Shared/Actions/ActionEvents.cs
+++ b/Content.Shared/Actions/ActionEvents.cs
@@ -81,8 +81,10 @@ public sealed class GetItemActionsEvent : EntityEventArgs
public sealed class RequestPerformActionEvent : EntityEventArgs
{
public readonly NetEntity Action;
- public readonly NetEntity? EntityTarget;
+ public NetEntity? EntityTarget;
public readonly NetCoordinates? EntityCoordinatesTarget;
+ public ActionUseType ActionUseType = ActionUseType.Default;
+ public int ChargeLevel;
public RequestPerformActionEvent(NetEntity action)
{
@@ -148,6 +150,8 @@ public abstract partial class WorldTargetActionEvent : BaseActionEvent
/// The coordinates of the location that the user targeted.
///
public EntityCoordinates Target;
+
+ public EntityUid TargetUid;
}
///
@@ -161,4 +165,18 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs
/// The user performing the action.
///
public EntityUid Performer;
+
+ public EntityUid Action;
+
+ public ActionUseType ActionUseType = ActionUseType.Default;
+
+ public int ChargeLevel;
+}
+
+[Serializable, NetSerializable]
+public enum ActionUseType
+{
+ Default, // left mouse click.
+ Charge, // Holding right mouse click(has 4 charges).
+ AltUse // Alt + left mouse click.
}
diff --git a/Content.Shared/Actions/BaseTargetActionComponent.cs b/Content.Shared/Actions/BaseTargetActionComponent.cs
index 7e40b10c32..b96ac9ea38 100644
--- a/Content.Shared/Actions/BaseTargetActionComponent.cs
+++ b/Content.Shared/Actions/BaseTargetActionComponent.cs
@@ -1,4 +1,5 @@
using Content.Shared.Interaction;
+using Robust.Shared.Audio;
namespace Content.Shared.Actions;
@@ -40,4 +41,28 @@ public abstract partial class BaseTargetActionComponent : BaseActionComponent
/// over lay in place of the currently held item "held item".
///
[DataField("targetingIndicator")] public bool TargetingIndicator = true;
+
+ [DataField]
+ public bool IsAltEnabled;
+
+ [DataField]
+ public bool IsChargeEnabled;
+
+ [DataField]
+ public string ChargeProto = "MagicFollowerEntity";
+
+ [DataField]
+ public int MaxChargeLevel = 4;
+
+ [DataField]
+ public SoundSpecifier ChargingSound = new SoundPathSpecifier("/Audio/White/Magic/chargingfallback.ogg");
+
+ [DataField]
+ public bool LoopCharging = true;
+
+ [DataField]
+ public SoundSpecifier MaxChargedSound = new SoundPathSpecifier("/Audio/White/Magic/maxchargefallback.ogg");
+
+ [DataField]
+ public bool LoopMaxCharged;
}
diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs
index df469f4ac1..dbe3fc8673 100644
--- a/Content.Shared/Actions/SharedActionsSystem.cs
+++ b/Content.Shared/Actions/SharedActionsSystem.cs
@@ -413,6 +413,8 @@ public abstract class SharedActionsSystem : EntitySystem
if (worldAction.Event != null)
{
worldAction.Event.Target = entityCoordinatesTarget;
+ if (ev.EntityTarget != null)
+ worldAction.Event.TargetUid = GetEntity(ev.EntityTarget.Value);
Dirty(actionEnt, worldAction);
performEvent = worldAction.Event;
}
@@ -430,7 +432,12 @@ public abstract class SharedActionsSystem : EntitySystem
}
if (performEvent != null)
+ {
performEvent.Performer = user;
+ performEvent.Action = actionEnt;
+ performEvent.ActionUseType = ev.ActionUseType;
+ performEvent.ChargeLevel = ev.ChargeLevel;
+ }
// All checks passed. Perform the action!
PerformAction(user, component, actionEnt, action, performEvent, curTime);
@@ -677,6 +684,8 @@ public abstract class SharedActionsSystem : EntitySystem
/// Entity to receive the actions
/// The actions to add
/// The entity that enables these actions (e.g., flashlight). May be null (innate actions).
+ /// ActionsComponent.
+ /// ActionContainerComponent.
public void GrantActions(EntityUid performer, IEnumerable actions, EntityUid container, ActionsComponent? comp = null, ActionsContainerComponent? containerComp = null)
{
if (!Resolve(container, ref containerComp))
diff --git a/Content.Shared/_White/Wizard/Charging/SharedChargingSystem.cs b/Content.Shared/_White/Wizard/Charging/SharedChargingSystem.cs
new file mode 100644
index 0000000000..2f5984bd4c
--- /dev/null
+++ b/Content.Shared/_White/Wizard/Charging/SharedChargingSystem.cs
@@ -0,0 +1,5 @@
+namespace Content.Shared._White.Wizard.Charging;
+
+public abstract class SharedChargingSystem : EntitySystem
+{
+}
diff --git a/Content.Shared/_White/Wizard/Magic/MagicComponent.cs b/Content.Shared/_White/Wizard/Magic/MagicComponent.cs
new file mode 100644
index 0000000000..a43001ca53
--- /dev/null
+++ b/Content.Shared/_White/Wizard/Magic/MagicComponent.cs
@@ -0,0 +1,13 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._White.Wizard.Magic;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class MagicComponent : Component
+{
+ ///
+ /// Does this spell require Wizard Robes & Hat?
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public bool RequiresClothes;
+}
diff --git a/Content.Shared/_White/Wizard/Magic/WizardClothesComponent.cs b/Content.Shared/_White/Wizard/Magic/WizardClothesComponent.cs
new file mode 100644
index 0000000000..13283f849d
--- /dev/null
+++ b/Content.Shared/_White/Wizard/Magic/WizardClothesComponent.cs
@@ -0,0 +1,9 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._White.Wizard.Magic;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class WizardClothesComponent : Component
+{
+
+}
diff --git a/Content.Shared/_White/Wizard/ScrollSystem/ScrollComponent.cs b/Content.Shared/_White/Wizard/ScrollSystem/ScrollComponent.cs
new file mode 100644
index 0000000000..f14576519b
--- /dev/null
+++ b/Content.Shared/_White/Wizard/ScrollSystem/ScrollComponent.cs
@@ -0,0 +1,43 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._White.Wizard.ScrollSystem;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedScrollSystem))]
+public sealed partial class ScrollComponent : Component
+{
+ ///
+ /// ActionId to give on use.
+ ///
+ [DataField]
+ [ViewVariables]
+ public string ActionId;
+
+ ///
+ /// How time it takes to learn.
+ ///
+ [DataField]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float LearnTime = 5f;
+
+ ///
+ /// Popup on learn.
+ ///
+ [DataField]
+ [ViewVariables]
+ public string LearnPopup;
+
+ ///
+ /// Sound to play on use.
+ ///
+ [DataField]
+ [ViewVariables]
+ public SoundSpecifier UseSound;
+
+ ///
+ /// Sound to play after use.
+ ///
+ [DataField]
+ [ViewVariables]
+ public SoundSpecifier AfterUseSound;
+}
diff --git a/Content.Shared/_White/Wizard/ScrollSystem/SharedScrollSystem.cs b/Content.Shared/_White/Wizard/ScrollSystem/SharedScrollSystem.cs
new file mode 100644
index 0000000000..6d267eaf17
--- /dev/null
+++ b/Content.Shared/_White/Wizard/ScrollSystem/SharedScrollSystem.cs
@@ -0,0 +1,87 @@
+using Content.Shared.Actions;
+using Content.Shared.DoAfter;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Popups;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
+
+namespace Content.Shared._White.Wizard.ScrollSystem;
+
+public abstract class SharedScrollSystem : EntitySystem
+{
+ #region Dependencies
+
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+
+ #endregion
+
+ #region Init
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnScrollUse);
+ SubscribeLocalEvent(OnScrollDoAfter);
+ }
+
+ #endregion
+
+ #region Handlers
+
+ private void OnScrollUse(EntityUid uid, ScrollComponent component, UseInHandEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.LearnTime, new ScrollDoAfterEvent(), uid, target: uid)
+ {
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnDamage = true,
+ NeedHand = true
+ };
+
+ if (_net.IsServer)
+ {
+ _audioSystem.PlayPvs(component.UseSound, args.User);
+ }
+
+ _popupSystem.PopupClient($"You start learning about {component.LearnPopup}.", args.User, args.User, PopupType.Medium);
+
+ _doAfterSystem.TryStartDoAfter(doAfterEventArgs);
+
+ args.Handled = true;
+ }
+
+ private void OnScrollDoAfter(EntityUid uid, ScrollComponent component, ScrollDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled)
+ return;
+
+ _actionsSystem.AddAction(args.User, component.ActionId);
+
+ if (_net.IsServer)
+ {
+ _audioSystem.PlayEntity(component.AfterUseSound, args.User, args.User);
+ }
+
+ _popupSystem.PopupClient($"You learned much about {component.LearnPopup}. The scroll is slowly burning in your hands.", args.User, args.User, PopupType.Medium);
+
+ BurnScroll(uid);
+
+ args.Handled = true;
+ }
+
+ #endregion
+
+ #region Helpers
+
+ protected virtual void BurnScroll(EntityUid uid) {}
+
+ #endregion
+}
diff --git a/Content.Shared/_White/Wizard/WizardEvents.cs b/Content.Shared/_White/Wizard/WizardEvents.cs
new file mode 100644
index 0000000000..c5d6d7e055
--- /dev/null
+++ b/Content.Shared/_White/Wizard/WizardEvents.cs
@@ -0,0 +1,175 @@
+using Content.Shared.Actions;
+using Content.Shared.DoAfter;
+using Content.Shared.Magic;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared._White.Wizard;
+
+#region HelperEvents
+
+[Serializable, NetSerializable]
+public sealed partial class ScrollDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
+[ByRefEvent]
+public struct BeforeCastSpellEvent
+{
+ public EntityUid Performer;
+
+ public bool Cancelled;
+
+ public BeforeCastSpellEvent(EntityUid performer)
+ {
+ Performer = performer;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed partial class AddWizardChargeEvent : EntityEventArgs
+{
+ public string ChargeProto;
+
+ public AddWizardChargeEvent(string chargeProto)
+ {
+ ChargeProto = chargeProto;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed partial class RemoveWizardChargeEvent : EntityEventArgs
+{
+}
+
+[Serializable, NetSerializable]
+public sealed partial class RequestSpellChargingAudio : EntityEventArgs
+{
+ public SoundSpecifier Sound;
+ public bool Loop;
+
+ public RequestSpellChargingAudio(SoundSpecifier sound, bool loop)
+ {
+ Sound = sound;
+ Loop = loop;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed partial class RequestSpellChargedAudio : EntityEventArgs
+{
+ public SoundSpecifier Sound;
+ public bool Loop;
+
+ public RequestSpellChargedAudio(SoundSpecifier sound, bool loop)
+ {
+ Sound = sound;
+ Loop = loop;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed partial class RequestAudioSpellStop : EntityEventArgs
+{
+}
+
+#endregion
+
+#region Spells
+
+public sealed partial class ArcSpellEvent : WorldTargetActionEvent, ISpeakSpell
+{
+ [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+
+ [DataField("posData")]
+ public MagicSpawnData Pos = new TargetCasterPos();
+
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class ForceSpellEvent : WorldTargetActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class FireballSpellEvent : WorldTargetActionEvent, ISpeakSpell
+{
+ [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+
+ [DataField("posData")]
+ public MagicSpawnData Pos = new TargetCasterPos();
+
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class CardsSpellEvent : WorldTargetActionEvent, ISpeakSpell
+{
+ [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+
+ [DataField("posData")]
+ public MagicSpawnData Pos = new TargetCasterPos();
+
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class ForceWallSpellEvent : WorldTargetActionEvent, ISpeakSpell
+{
+ [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class BlinkSpellEvent : InstantActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class EtherealJauntSpellEvent : InstantActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class EmpSpellEvent : InstantActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class CluwneCurseSpellEvent : EntityTargetActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class BananaTouchSpellEvent : EntityTargetActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class MimeTouchSpellEvent : EntityTargetActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+public sealed partial class InstantRecallSpellEvent : InstantActionEvent, ISpeakSpell
+{
+ [DataField("speech")]
+ public string? Speech { get; private set; }
+}
+
+#endregion
diff --git a/Resources/Audio/Machines/attributions.yml b/Resources/Audio/Machines/attributions.yml
index a0f1c9f7e7..5ef62f6d5d 100644
--- a/Resources/Audio/Machines/attributions.yml
+++ b/Resources/Audio/Machines/attributions.yml
@@ -152,3 +152,23 @@
license: "CC0-1.0"
copyright: "dakamakat on freesound.org"
source: "https://freesound.org/people/Dakamakat/sounds/717370/"
+
+- files: ["energyshield_up.ogg"]
+ license: "CC0-1.0"
+ copyright: "unfa on freesound.org"
+ source: "https://freesound.org/people/unfa/sounds/584173/"
+
+- files: ["energyshield_down.ogg"]
+ license: "CC-BY-4.0"
+ copyright: "SilverIllusionist on freesound.org"
+ source: "https://freesound.org/people/SilverIllusionist/sounds/673556/"
+
+- files: ["energyshield_ambient.ogg"]
+ license: "CC0-1.0"
+ copyright: "julianmateo_ on freesound.org"
+ source: "https://freesound.org/people/julianmateo_/sounds/524165/"
+
+- files: ["energyshield_parry.ogg"]
+ license: "CC-BY-4.0"
+ copyright: "Robinhood76 on freesound.org"
+ source: "https://freesound.org/people/Robinhood76/sounds/107613/"
\ No newline at end of file
diff --git a/Resources/Audio/Machines/energyshield_ambient.ogg b/Resources/Audio/Machines/energyshield_ambient.ogg
new file mode 100644
index 0000000000..ea560a076b
Binary files /dev/null and b/Resources/Audio/Machines/energyshield_ambient.ogg differ
diff --git a/Resources/Audio/Machines/energyshield_down.ogg b/Resources/Audio/Machines/energyshield_down.ogg
new file mode 100644
index 0000000000..a92915ff07
Binary files /dev/null and b/Resources/Audio/Machines/energyshield_down.ogg differ
diff --git a/Resources/Audio/Machines/energyshield_parry.ogg b/Resources/Audio/Machines/energyshield_parry.ogg
new file mode 100644
index 0000000000..4c5d1517a2
Binary files /dev/null and b/Resources/Audio/Machines/energyshield_parry.ogg differ
diff --git a/Resources/Audio/Machines/energyshield_up.ogg b/Resources/Audio/Machines/energyshield_up.ogg
new file mode 100644
index 0000000000..9f0121a7b5
Binary files /dev/null and b/Resources/Audio/Machines/energyshield_up.ogg differ
diff --git a/Resources/Audio/White/Items/scroll/after_use.ogg b/Resources/Audio/White/Items/scroll/after_use.ogg
new file mode 100644
index 0000000000..bd87b67f08
Binary files /dev/null and b/Resources/Audio/White/Items/scroll/after_use.ogg differ
diff --git a/Resources/Audio/White/Items/scroll/use.ogg b/Resources/Audio/White/Items/scroll/use.ogg
new file mode 100644
index 0000000000..9789a13817
Binary files /dev/null and b/Resources/Audio/White/Items/scroll/use.ogg differ
diff --git a/Resources/Audio/White/Magic/Arc/cast.ogg b/Resources/Audio/White/Magic/Arc/cast.ogg
new file mode 100644
index 0000000000..1562ecbb1b
Binary files /dev/null and b/Resources/Audio/White/Magic/Arc/cast.ogg differ
diff --git a/Resources/Audio/White/Magic/Arc/charge.ogg b/Resources/Audio/White/Magic/Arc/charge.ogg
new file mode 100644
index 0000000000..94b6abbd9e
Binary files /dev/null and b/Resources/Audio/White/Magic/Arc/charge.ogg differ
diff --git a/Resources/Audio/White/Magic/Arc/max.ogg b/Resources/Audio/White/Magic/Arc/max.ogg
new file mode 100644
index 0000000000..cb2bfa608c
Binary files /dev/null and b/Resources/Audio/White/Magic/Arc/max.ogg differ
diff --git a/Resources/Audio/White/Magic/Cards/cast.ogg b/Resources/Audio/White/Magic/Cards/cast.ogg
new file mode 100644
index 0000000000..bc8cf03553
Binary files /dev/null and b/Resources/Audio/White/Magic/Cards/cast.ogg differ
diff --git a/Resources/Audio/White/Magic/Cards/charge.ogg b/Resources/Audio/White/Magic/Cards/charge.ogg
new file mode 100644
index 0000000000..ee9957ae2c
Binary files /dev/null and b/Resources/Audio/White/Magic/Cards/charge.ogg differ
diff --git a/Resources/Audio/White/Magic/Cards/max.ogg b/Resources/Audio/White/Magic/Cards/max.ogg
new file mode 100644
index 0000000000..cb2bfa608c
Binary files /dev/null and b/Resources/Audio/White/Magic/Cards/max.ogg differ
diff --git a/Resources/Audio/White/Magic/Force/cast.ogg b/Resources/Audio/White/Magic/Force/cast.ogg
new file mode 100644
index 0000000000..88d12fbd89
Binary files /dev/null and b/Resources/Audio/White/Magic/Force/cast.ogg differ
diff --git a/Resources/Audio/White/Magic/Force/charge.ogg b/Resources/Audio/White/Magic/Force/charge.ogg
new file mode 100644
index 0000000000..a227dfc0e2
Binary files /dev/null and b/Resources/Audio/White/Magic/Force/charge.ogg differ
diff --git a/Resources/Audio/White/Magic/Force/max.ogg b/Resources/Audio/White/Magic/Force/max.ogg
new file mode 100644
index 0000000000..cb2bfa608c
Binary files /dev/null and b/Resources/Audio/White/Magic/Force/max.ogg differ
diff --git a/Resources/Audio/White/Magic/chargingfallback.ogg b/Resources/Audio/White/Magic/chargingfallback.ogg
new file mode 100644
index 0000000000..4a7ba5bd80
Binary files /dev/null and b/Resources/Audio/White/Magic/chargingfallback.ogg differ
diff --git a/Resources/Audio/White/Magic/maxchargefallback.ogg b/Resources/Audio/White/Magic/maxchargefallback.ogg
new file mode 100644
index 0000000000..1a34e0484a
Binary files /dev/null and b/Resources/Audio/White/Magic/maxchargefallback.ogg differ
diff --git a/Resources/Locale/en-US/EnergyDome/energydome.ftl b/Resources/Locale/en-US/EnergyDome/energydome.ftl
new file mode 100644
index 0000000000..e9a42503d8
--- /dev/null
+++ b/Resources/Locale/en-US/EnergyDome/energydome.ftl
@@ -0,0 +1,9 @@
+energy-dome-access-denied = Access denied
+energy-dome-recharging = Recharging...
+energy-dome-no-power = Low battery
+energy-dome-no-cell = There is no power source
+
+energy-dome-on-examine-is-on-message = The energy barrier is [color=darkgreen]up[/color].
+energy-dome-on-examine-is-off-message = The energy barrier is [color=darkred]down[/color].
+
+energy-dome-verb-toggle = Toggle energy dome
\ No newline at end of file
diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl
index 081e46fbff..ce26762eaf 100644
--- a/Resources/Locale/en-US/research/technologies.ftl
+++ b/Resources/Locale/en-US/research/technologies.ftl
@@ -32,6 +32,7 @@ research-technology-wave-particle-harnessing = Wave Particle Harnessing
research-technology-advanced-riot-control = Advanced Riot Control
research-technology-portable-microfusion-weaponry = Portable Microfusion Weaponry
research-technology-experimental-battery-ammo = Experimental Battery Ammo
+research-technology-energy_barriers = Energy Barriers
research-technology-basic-shuttle-armament = Shuttle basic armament
research-technology-advanced-shuttle-weapon = Advanced shuttle weapons
diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl
index 7f5c8cebfc..b445c31571 100644
--- a/Resources/Locale/en-US/store/uplink-catalog.ftl
+++ b/Resources/Locale/en-US/store/uplink-catalog.ftl
@@ -306,6 +306,9 @@ uplink-hardsuit-syndieelite-desc = An elite version of the blood-red hardsuit, w
uplink-clothing-outer-hardsuit-juggernaut-name = Cybersun Juggernaut Suit
uplink-clothing-outer-hardsuit-juggernaut-desc = Hyper resilient armor made of materials tested in the Tau chromosphere facility. The only thing that's going to be slowing you down is this suit... and tasers.
+uplink-energy-dome-name = Personal energy dome
+uplink-energy-dome-desc = A personal shield generator that protects the wearer from lasers and bullets but prevents from using ranged weapons himself. Comes with a small power cell.
+
# Misc
uplink-cyberpen-name = Cybersun Pen
uplink-cyberpen-desc = Cybersun's legal department pen, invaluable for forging documents and escaping prisons. Smells vaguely of hard-light and war profiteering.
diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml
index 5abcaf9bc3..ab650286bc 100644
--- a/Resources/Prototypes/Actions/types.yml
+++ b/Resources/Prototypes/Actions/types.yml
@@ -33,6 +33,16 @@
iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
event: !type:ToggleActionEvent
+- type: entity
+ id: ActionToggleDome
+ name: Toggle energy dome
+ description: Turn the energy barrier on or off.
+ noSpawn: true
+ components:
+ - type: InstantAction
+ icon: { sprite: Objects/Weapons/Melee/e_shield.rsi, state: eshield-on }
+ event: !type:ToggleActionEvent
+
- type: entity
id: ActionOpenStorageImplant
name: Open Storage Implant
diff --git a/Resources/Prototypes/Damage/modifier_sets.yml b/Resources/Prototypes/Damage/modifier_sets.yml
index 31dd47a9e1..e850785863 100644
--- a/Resources/Prototypes/Damage/modifier_sets.yml
+++ b/Resources/Prototypes/Damage/modifier_sets.yml
@@ -171,6 +171,16 @@
flatReductions:
Heat: 3
+- type: damageModifierSet
+ id: HardLightBarrier
+ coefficients:
+ Heat: 0.8
+ Blunt: 0.8
+ Slash: 0.8
+ Piercing: 0.8
+ Cold: 0.8
+ Shock: 1.6
+
- type: damageModifierSet
id: Scale # Skin tougher, bones weaker, strong stomachs, cold-blooded (kindof)
coefficients:
diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
index 42ca6ebfbf..0307a01ef1 100644
--- a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
+++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
@@ -664,12 +664,36 @@
Heat: 0.25
Radiation: 0.25
Caustic: 0.75
- - type: ClothingSpeedModifier
- walkModifier: 0.8
- sprintModifier: 0.8
- - type: HeldSpeedModifier
- type: ToggleableClothing
clothingPrototype: ClothingHeadHelmetHardsuitWizard
+ - type: ContainerContainer
+ containers:
+ cell_slot: !type:ContainerSlot
+ toggleable-clothing: !type:ContainerSlot
+ - type: PowerCellSlot
+ cellSlotId: cell_slot
+ - type: ItemSlots
+ slots:
+ cell_slot:
+ name: power-cell-slot-component-slot-name-default
+ startingItem: PowerCellMicroreactor
+ whitelist:
+ tags:
+ - PowerCell
+ - PowerCellSmall
+ - PowerCellHyper
+ - PowerCellMicroreactor
+ - type: EnergyDomeGenerator
+ damageEnergyDraw: 3
+ domePrototype: EnergyDomeSmallPink
+ - type: ClothingSpeedModifier
+ walkModifier: 1
+ sprintModifier: 1
+ - type: PowerCellDraw
+ drawRate: 8
+ useRate: 0
+ - type: UseDelay
+ delay: 10.0
#Ling Space Suit
- type: entity
diff --git a/Resources/Prototypes/Entities/Effects/admin_triggers.yml b/Resources/Prototypes/Entities/Effects/admin_triggers.yml
index e1f366678d..d61847f907 100644
--- a/Resources/Prototypes/Entities/Effects/admin_triggers.yml
+++ b/Resources/Prototypes/Entities/Effects/admin_triggers.yml
@@ -7,7 +7,7 @@
sprite: /Textures/Objects/Fun/goldbikehorn.rsi
visible: false
state: icon
- - type: TriggerOnSpawn
+ - type: TriggerOnSpawn
- type: TimedDespawn
lifetime: 5
@@ -27,9 +27,9 @@
components:
- type: FlashOnTrigger
range: 7
- - type: SpawnOnTrigger
+ - type: SpawnOnTrigger
proto: GrenadeFlashEffect
-
+
- type: entity
id: AdminInstantEffectSmoke3
suffix: Smoke (03 sec)
@@ -43,7 +43,7 @@
- type: TimerTriggerVisuals
primingSound:
path: /Audio/Effects/Smoke-grenade.ogg
-
+
- type: entity
id: AdminInstantEffectSmoke10
suffix: Smoke (10 sec)
@@ -57,7 +57,7 @@
- type: TimerTriggerVisuals
primingSound:
path: /Audio/Effects/Smoke-grenade.ogg
-
+
- type: entity
id: AdminInstantEffectSmoke30
suffix: Smoke (30 sec)
@@ -89,7 +89,7 @@
id: AdminInstantEffectGravityWell
suffix: Gravity Well
parent: AdminInstantEffectBase
- components:
+ components:
- type: SoundOnTrigger
removeOnTrigger: true
sound:
@@ -103,10 +103,34 @@
path: /Audio/Effects/Grenades/Supermatter/supermatter_loop.ogg
- type: GravityWell
maxRange: 8
- baseRadialAcceleration: 10
+ baseRadialAcceleration: 250
baseTangentialAcceleration: 0
gravPulsePeriod: 0.01
- type: SingularityDistortion
intensity: 10
falloffPower: 1.5
-
+
+- type: entity
+ id: AdminInstantEffectMinusGravityWell
+ suffix: Gravity Well
+ parent: AdminInstantEffectBase
+ components:
+ - type: SoundOnTrigger
+ removeOnTrigger: true
+ sound:
+ path: /Audio/Effects/Grenades/Supermatter/supermatter_start.ogg
+ volume: 5
+ - type: AmbientSound
+ enabled: true
+ volume: -5
+ range: 14
+ sound:
+ path: /Audio/Effects/Grenades/Supermatter/supermatter_loop.ogg
+ - type: GravityWell
+ maxRange: 10
+ baseRadialAcceleration: -200
+ baseTangentialAcceleration: -5
+ gravPulsePeriod: 0.01
+ - type: SingularityDistortion
+ intensity: 10
+ falloffPower: 1.5
diff --git a/Resources/Prototypes/Entities/Effects/dome.yml b/Resources/Prototypes/Entities/Effects/dome.yml
new file mode 100644
index 0000000000..77a3113e0f
--- /dev/null
+++ b/Resources/Prototypes/Entities/Effects/dome.yml
@@ -0,0 +1,129 @@
+- type: entity
+ id: EnergyDomeBase
+ abstract: true
+ components:
+ - type: Sprite
+ drawdepth: Effects
+ noRot: true
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeCircle
+ radius: 0.8
+ density: 0
+ mask:
+ - None
+ layer:
+ - BulletImpassable
+ - Opaque
+ - type: Physics
+ bodyType: Static
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: HardLightBarrier
+ - type: AmbientSound
+ volume: 35
+ range: 5
+ sound:
+ path: /Audio/Machines/energyshield_ambient.ogg
+ - type: EnergyDome
+ - type: Tag
+ tags:
+ - HideContextMenu
+
+- type: entity
+ id: EnergyDomeSmallPink
+ noSpawn: true
+ parent: EnergyDomeBase
+ components:
+ - type: Sprite
+ sprite: Effects/EnergyDome/energydome_small.rsi
+ layers:
+ - state: small
+ color: "#f5166b"
+ - type: PointLight
+ enabled: true
+ radius: 5
+ power: 2
+ color: "#f5166b"
+
+- type: entity
+ id: EnergyDomeSmallRed
+ noSpawn: true
+ parent: EnergyDomeBase
+ components:
+ - type: Sprite
+ drawdepth: Effects
+ noRot: true
+ sprite: Effects/EnergyDome/energydome_small.rsi
+ layers:
+ - state: small
+ color: "#b00000"
+ - type: PointLight
+ enabled: true
+ radius: 5
+ power: 2
+ color: "#b00000"
+
+- type: entity
+ id: EnergyDomeMediumBlue
+ noSpawn: true
+ parent: EnergyDomeBase
+ components:
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeCircle
+ radius: 1.8
+ density: 0
+ mask:
+ - None
+ layer:
+ - BulletImpassable
+ - Opaque
+ - type: Sprite
+ sprite: Effects/EnergyDome/energydome_medium.rsi
+ layers:
+ - state: medium
+ color: "#64b9de"
+ - type: PointLight
+ enabled: true
+ radius: 5
+ power: 10
+ color: "#64b9de"
+
+- type: entity
+ id: EnergyDomeSlowing
+ noSpawn: true
+ parent: EnergyDomeBase
+ components:
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeCircle
+ radius: 2.8
+ density: 0
+ hard: false
+ mask:
+ - None
+ layer:
+ - MidImpassable
+ - type: Sprite
+ drawdepth: LowFloors
+ sprite: Effects/EnergyDome/energydome_slowdown_big.rsi
+ layers:
+ - state: big
+ color: "#a3d177"
+ - type: PointLight
+ enabled: true
+ radius: 5
+ power: 30
+ color: "#a3d177"
+ - type: DamageContacts
+ damage:
+ types:
+ Slash: -1.5
+ Piercing: -1.5
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml
index 5ca7620f41..6782f9f9ad 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/base.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml
@@ -297,6 +297,7 @@
- Muted
- Pacified
- StaminaModifier
+ - Incorporeal
- type: Blindable
# Other
- type: Temperature
diff --git a/Resources/Prototypes/Entities/Objects/Magic/books.yml b/Resources/Prototypes/Entities/Objects/Magic/books.yml
deleted file mode 100644
index 89acd9e7da..0000000000
--- a/Resources/Prototypes/Entities/Objects/Magic/books.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-- type: entity
- id: BaseSpellbook
- name: spellbook
- parent: BaseItem
- abstract: true
- components:
- - type: Sprite
- sprite: Objects/Misc/books.rsi
- layers:
- - state: book_demonomicon
- - type: Spellbook
- - type: Tag
- tags:
- - Spellbook
-
-- type: entity
- id: SpawnSpellbook
- name: spawn spellbook
- parent: BaseSpellbook
- components:
- - type: Spellbook
- spells:
- ActionSpawnMagicarpSpell: -1
-
-- type: entity
- id: ForceWallSpellbook
- name: force wall spellbook
- parent: BaseSpellbook
- components:
- - type: Sprite
- sprite: Objects/Magic/spellbooks.rsi
- layers:
- - state: bookforcewall
- - type: Spellbook
- spells:
- ActionForceWall: -1
-
-- type: entity
- id: BlinkBook
- name: blink spellbook
- parent: BaseSpellbook
- components:
- - type: Sprite
- sprite: Objects/Magic/spellbooks.rsi
- layers:
- - state: spellbook
- - type: Spellbook
- spells:
- ActionBlink: -1
-
-- type: entity
- id: SmiteBook
- name: smite spellbook
- parent: BaseSpellbook
- components:
- - type: Sprite
- sprite: Objects/Magic/spellbooks.rsi
- layers:
- - state: spellbook
- - type: Spellbook
- spells:
- ActionSmite: -1
-
-- type: entity
- id: KnockSpellbook
- name: knock spellbook
- parent: BaseSpellbook
- components:
- - type: Sprite
- sprite: Objects/Magic/spellbooks.rsi
- layers:
- - state: bookknock
- - type: Spellbook
- spells:
- ActionKnock: -1
-
-- type: entity
- id: FireballSpellbook
- name: fireball spellbook
- parent: BaseSpellbook
- components:
- - type: Sprite
- sprite: Objects/Magic/spellbooks.rsi
- layers:
- - state: bookfireball
- - type: Spellbook
- spells:
- ActionFireball: -1
-
-- type: entity
- id: ScrollRunes
- name: scroll of runes
- parent: BaseSpellbook
- components:
- - type: Item
- size: Normal
- - type: Sprite
- sprite: Objects/Magic/magicactions.rsi
- layers:
- - state: spell_default
- - type: Spellbook
- spells:
- ActionFlashRune: -1
- ActionExplosionRune: -1
- ActionIgniteRune: -1
- ActionStunRune: -1
diff --git a/Resources/Prototypes/Entities/Objects/Tools/energydome.yml b/Resources/Prototypes/Entities/Objects/Tools/energydome.yml
new file mode 100644
index 0000000000..615bf2a9ae
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Tools/energydome.yml
@@ -0,0 +1,179 @@
+- type: entity
+ name: blood red personal shield generator
+ description: A personal shield generator that protects the wearer from lasers and bullets but prevents from using ranged weapons himself. Uses a power cell.
+ id: EnergyDomeGeneratorPersonalSyndie
+ parent: BaseItem
+ components:
+ - type: Item
+ size: Ginormous
+ - type: Clothing
+ quickEquip: false
+ slots:
+ - Belt
+ - type: Sprite
+ sprite: Objects/Tools/EnergyDome/syndie.rsi
+ layers:
+ - state: icon
+ - type: ContainerContainer
+ containers:
+ cell_slot: !type:ContainerSlot
+ - type: PowerCellSlot
+ cellSlotId: cell_slot
+ - type: ItemSlots
+ slots:
+ cell_slot:
+ name: power-cell-slot-component-slot-name-default
+ startingItem: PowerCellSmall
+ whitelist:
+ tags:
+ - PowerCell
+ - PowerCellSmall
+ - type: EnergyDomeGenerator
+ damageEnergyDraw: 5
+ domePrototype: EnergyDomeSmallRed
+ - type: PowerCellDraw
+ drawRate: 10
+ useRate: 0
+ - type: UseDelay
+ delay: 10.0
+
+- type: entity
+ name: BR-40c "Turtle"
+ description: A two-handed and heavy energy barrier with extremely low passive energy consumption. Can be tethered with a multitool.
+ id: EnergyDomeDirectionalTurtle
+ parent: BaseItem
+ components:
+ - type: Sprite
+ sprite: Objects/Tools/EnergyDome/reinhardt.rsi
+ layers:
+ - state: icon
+ - type: InteractionOutline
+ - type: MultiHandedItem
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.25,-0.25,0.25,0.25"
+ density: 20
+ mask:
+ - ItemMask
+ restitution: 0.3
+ friction: 0.2
+ - type: Item
+ size: Ginormous
+ - type: HeldSpeedModifier
+ walkModifier: 0.7
+ sprintModifier: 0.7
+ - type: ContainerContainer
+ containers:
+ cell_slot: !type:ContainerSlot
+ - type: PowerCellSlot
+ cellSlotId: cell_slot
+ - type: ItemSlots
+ slots:
+ cell_slot:
+ name: power-cell-slot-component-slot-name-default
+ startingItem: PowerCellSmall
+ whitelist:
+ tags:
+ - PowerCell
+ - PowerCellSmall
+ - type: EnergyDomeGenerator
+ damageEnergyDraw: 7
+ domePrototype: EnergyDomeMediumBlue
+ canDeviceNetworkUse: true
+ - type: PowerCellDraw
+ drawRate: 2
+ useRate: 0
+ - type: UseDelay
+ delay: 10.0
+ - type: DeviceNetwork
+ deviceNetId: Wireless
+ receiveFrequencyId: BasicDevice
+ - type: WirelessNetworkConnection
+ range: 200
+ - type: DeviceLinkSink
+ ports:
+ - Toggle
+ - On
+ - Off
+
+- type: entity
+ id: EnergyDomeWiredTest
+ name: Static Dome
+ description: Test energy barrier powered by station wiring. I don't know how the hell to balance it.....
+ parent: BaseMachine
+ suffix: DO NOT MERGE
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Transform
+ anchored: true
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.45,-0.45,0.45,0.45"
+ density: 190
+ mask:
+ - MachineMask
+ layer:
+ - MachineLayer
+ - type: Sprite
+ sprite: Structures/Power/Generation/Tesla/coil.rsi
+ snapCardinals: true
+ noRot: true
+ layers:
+ - state: coil
+ - type: ExaminableBattery
+ - type: Battery
+ maxCharge: 30000 #<- max supply
+ startingCharge: 10000
+ - type: PowerNetworkBattery
+ maxSupply: 30000
+ maxChargeRate: 1000 #<- passive charging frow power net
+ supplyRampTolerance: 500
+ supplyRampRate: 50
+ - type: BatteryCharger
+ voltage: Medium
+ - type: NodeContainer
+ examinable: true
+ nodes:
+ input:
+ !type:CableDeviceNode
+ nodeGroupID: MVPower
+ - type: BatterySelfRecharger
+ autoRecharge: false # true only when active
+ autoRechargeRate: -800 #<- discharge per second while active
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: Metallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 200
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - type: UseDelay
+ delay: 30.0
+ - type: DeviceNetwork
+ deviceNetId: Wireless
+ receiveFrequencyId: BasicDevice
+ - type: WirelessNetworkConnection
+ range: 200
+ - type: DeviceLinkSink
+ ports:
+ - Toggle
+ - On
+ - Off
+ - type: EnergyDomeGenerator
+ enabled: true
+ damageEnergyDraw: 100
+ domePrototype: EnergyDomeSlowing
+ canDeviceNetworkUse: true
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml
index e67c41c9bf..097d7703e8 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml
@@ -204,3 +204,34 @@
radius: 2.0
energy: 7.0
- type: BloodBoilProjectile
+
+- type: entity
+ id: ProjectileTeslaBall
+ name: teslaball
+ description: You better GITTAH WEIGH.
+ parent: BulletRocket
+ noSpawn: true
+ components:
+ - type: PointLight
+ color: "#B3CEFF"
+ radius: 2.0
+ energy: 5.0
+ - type: Projectile
+ damage:
+ types:
+ Caustic: 10
+ - type: Sprite
+ sprite: Structures/Power/Generation/Tesla/energy_miniball.rsi
+ layers:
+ - state: tesla_projectile
+ shader: unshaded
+ - type: Explosive
+ explosionType: Default
+ maxIntensity: 100
+ intensitySlope: 0.1
+ totalIntensity: 0.3
+ maxTileBreak: 0
+ - type: StunOnCollide
+ stunAmount: 2
+ knockdownAmount: 2
+ - type: TeslaProjectile
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml
index c68feff0b5..788e95e8e1 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml
@@ -50,3 +50,37 @@
# also limits the crew's use
- type: TimedDespawn
lifetime: 30
+
+- type: entity
+ parent: BaseItem
+ id: ThrowingCard
+ name: card
+ components:
+ - type: Sprite
+ sprite: Objects/Magic/card.rsi
+ layers:
+ - state: icon
+ - type: Appearance
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape: !type:PhysShapeCircle
+ radius: 0.2
+ density: 5
+ mask:
+ - ItemMask
+ restitution: 0.3
+ friction: 0.2
+ - type: EmbeddableProjectile
+ sound: /Audio/Weapons/star_hit.ogg
+ - type: DamageOtherOnHit
+ damage:
+ types:
+ Slash: 8
+ Piercing: 10
+ - type: StaminaDamageOnCollide
+ damage: 45
+ - type: StaminaDamageOnEmbed
+ damage: 10
+ - type: TimedDespawn
+ lifetime: 60
diff --git a/Resources/Prototypes/Magic/white.yml b/Resources/Prototypes/Magic/white.yml
new file mode 100644
index 0000000000..65726f9f55
--- /dev/null
+++ b/Resources/Prototypes/Magic/white.yml
@@ -0,0 +1,253 @@
+- type: entity
+ id: ActionElectricArcSpell
+ name: Electric arc
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: WorldTargetAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ checkCanAccess: false
+ range: 10
+ sound: !type:SoundPathSpecifier
+ path: /Audio/White/Magic/Arc/cast.ogg
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: thunder
+ isChargeEnabled: true
+ chargingSound:
+ path: /Audio/White/Magic/Arc/charge.ogg
+ chargeProto: MagicFollowerArcEntity
+ maxChargedSound:
+ path: /Audio/White/Magic/Arc/max.ogg
+ isAltEnabled: true
+ event: !type:ArcSpellEvent
+ speech: "KUH, ABAH'RAH"
+ prototype: ProjectileTeslaBall
+ posData: !type:TargetCasterPos
+
+- type: entity
+ id: ActionForceSpell
+ name: Force
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: WorldTargetAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ checkCanAccess: false
+ range: 10
+ sound: !type:SoundPathSpecifier
+ path: /Audio/White/Magic/Force/cast.ogg
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: push
+ isChargeEnabled: true
+ chargingSound:
+ path: /Audio/White/Magic/Force/charge.ogg
+ chargeProto: MagicFollowerForceEntity
+ maxChargedSound:
+ path: /Audio/White/Magic/Force/max.ogg
+ isAltEnabled: true
+ event: !type:ForceSpellEvent
+ speech: "EL DRITCH!"
+
+- type: entity
+ id: ActionFireballSpell
+ name: Fireball
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: WorldTargetAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ checkCanAccess: false
+ range: 45
+ isChargeEnabled: true
+ chargeProto: MagicFollowerFireEntity
+ isAltEnabled: true
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Magic/fireball.ogg
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: fireball
+ event: !type:FireballSpellEvent
+ prototype: ProjectileFireball
+ posData: !type:TargetCasterPos
+ speech: action-speech-spell-fireball
+
+- type: entity
+ id: ActionCardSpell
+ name: Cards
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: WorldTargetAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ checkCanAccess: false
+ range: 45
+ isChargeEnabled: true
+ chargingSound:
+ path: /Audio/White/Magic/Cards/charge.ogg
+ chargeProto: MagicFollowerCardEntity
+ maxChargedSound:
+ path: /Audio/White/Magic/Cards/max.ogg
+ isAltEnabled: true
+ sound: !type:SoundPathSpecifier
+ path: /Audio/White/Magic/Cards/cast.ogg
+ icon:
+ sprite: Objects/Magic/card.rsi
+ state: icon
+ event: !type:CardsSpellEvent
+ prototype: ThrowingCard
+ posData: !type:TargetCasterPos
+ speech: "SHIZO NERO!"
+
+- type: entity
+ id: ActionForcewallSpell
+ name: Forcewall
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: WorldTargetAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ checkCanAccess: false
+ range: 10
+ sound: !type:SoundPathSpecifier
+ path: /Audio/White/Magic/Force/cast.ogg
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: shield
+ isChargeEnabled: true
+ chargingSound:
+ path: /Audio/White/Magic/Force/charge.ogg
+ chargeProto: MagicFollowerForceEntity
+ maxChargedSound:
+ path: /Audio/White/Magic/Force/max.ogg
+ isAltEnabled: true
+ event: !type:ForceWallSpellEvent
+ speech: "TARCOL MINTI ZHERI!"
+ prototype: WallForce
+
+- type: entity
+ id: ActionBlinkSpell
+ name: Blink
+ noSpawn: true
+ components:
+ - type: InstantAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: blink
+ event: !type:BlinkSpellEvent
+ speech: "SYCAR TYN!"
+
+- type: entity
+ id: ActionEtherealJauntSpell
+ name: Ethereal Jaunt
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: InstantAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: jaunt
+ event: !type:EtherealJauntSpellEvent
+ speech: "SCYAR NILA!"
+
+- type: entity
+ id: ActionEmpSpell
+ name: Emp
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: InstantAction
+ useDelay: 60
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: emp_new
+ event: !type:EmpSpellEvent
+ speech: "OCYAR TRINA!"
+
+- type: entity
+ id: ActionCluwneCurseSpell
+ name: Cluwne Curse
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: EntityTargetAction
+ canTargetSelf: false
+ range: 2
+ useDelay: 300
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: cluwne
+ event: !type:CluwneCurseSpellEvent
+ speech: "CLUWNE FOR ME!"
+
+- type: entity
+ id: ActionBananaTouchSpell
+ name: Banana Touch
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: EntityTargetAction
+ canTargetSelf: false
+ range: 2
+ useDelay: 300
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: clown
+ event: !type:BananaTouchSpellEvent
+ speech: "HONK FOR ME!"
+
+- type: entity
+ id: ActionMimeTouchSpell
+ name: Mime Touch
+ noSpawn: true
+ components:
+ - type: Magic
+ requiresClothes: true
+ - type: EntityTargetAction
+ canTargetSelf: false
+ range: 2
+ useDelay: 300
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: mime_curse
+ event: !type:MimeTouchSpellEvent
+ speech: "SILENCE!"
+
+- type: entity
+ id: ActionInstantRecallSpell
+ name: Instant Recall
+ noSpawn: true
+ components:
+ - type: InstantRecall
+ - type: Magic
+ requiresClothes: true
+ - type: InstantAction
+ useDelay: 10
+ itemIconStyle: BigAction
+ icon:
+ sprite: Objects/Magic/magicactions.rsi
+ state: summons
+ event: !type:InstantRecallSpellEvent
diff --git a/Resources/Prototypes/White/Entities/Clothing/nigger.yml b/Resources/Prototypes/White/Entities/Clothing/nigger.yml
new file mode 100644
index 0000000000..a4724eb870
--- /dev/null
+++ b/Resources/Prototypes/White/Entities/Clothing/nigger.yml
@@ -0,0 +1,35 @@
+- type: entity
+ parent: ClothingHeadHatWizard
+ id: ClothingHeadHatRealWizardBlue
+ components:
+ - type: WizardClothes
+
+- type: entity
+ parent: ClothingOuterWizard
+ id: ClothingOuterRealWizardBlue
+ components:
+ - type: WizardClothes
+
+- type: entity
+ parent: ClothingHeadHatRedwizard
+ id: ClothingHeadHatRealWizardRed
+ components:
+ - type: WizardClothes
+
+- type: entity
+ parent: ClothingOuterWizardRed
+ id: ClothingOuterRealWizardRed
+ components:
+ - type: WizardClothes
+
+- type: entity
+ parent: ClothingHeadHatVioletwizard
+ id: ClothingHeadHatRealWizardViolet
+ components:
+ - type: WizardClothes
+
+- type: entity
+ parent: ClothingOuterWizardViolet
+ id: ClothingOuterRealWizardViolet
+ components:
+ - type: WizardClothes
diff --git a/Resources/Prototypes/White/Objects/Scrolls/magic.yml b/Resources/Prototypes/White/Objects/Scrolls/magic.yml
new file mode 100644
index 0000000000..171f566025
--- /dev/null
+++ b/Resources/Prototypes/White/Objects/Scrolls/magic.yml
@@ -0,0 +1,119 @@
+- type: entity
+ id: MagicFollowerEntity
+ name: magic
+ components:
+ - type: Physics
+ bodyType: Dynamic
+ fixedRotation: false
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/Cores/bluespace_core.rsi
+ noRot: true
+ layers:
+ - state: core
+ - state: pulse
+ map: ["decay"]
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.AnomalyCoreVisuals.Decaying:
+ decay:
+ True: { visible: true }
+ False: { visible: false }
+ - type: PointLight
+ radius: 1.5
+ energy: 3.5
+ color: "#00ccff"
+ castShadows: false
+
+- type: entity
+ id: MagicFollowerArcEntity
+ name: magic
+ components:
+ - type: Physics
+ bodyType: Dynamic
+ fixedRotation: false
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/Cores/electric_core.rsi
+ noRot: true
+ layers:
+ - state: core
+ - state: pulse
+ map: ["decay"]
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.AnomalyCoreVisuals.Decaying:
+ decay:
+ True: { visible: true }
+ False: { visible: false }
+ - type: PointLight
+ radius: 1.5
+ energy: 3.5
+ color: "#ccf404"
+ castShadows: false
+
+- type: entity
+ id: MagicFollowerForceEntity
+ name: magic
+ components:
+ - type: Physics
+ bodyType: Dynamic
+ fixedRotation: false
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/Cores/bluespace_core.rsi
+ noRot: true
+ layers:
+ - state: core
+ - state: pulse
+ map: ["decay"]
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.AnomalyCoreVisuals.Decaying:
+ decay:
+ True: { visible: true }
+ False: { visible: false }
+ - type: PointLight
+ radius: 1.5
+ energy: 3.5
+ color: "#00ccff"
+ castShadows: false
+
+- type: entity
+ id: MagicFollowerFireEntity
+ name: magic
+ components:
+ - type: Physics
+ bodyType: Dynamic
+ fixedRotation: false
+ - type: Sprite
+ sprite: Structures/Specific/Anomalies/Cores/pyro_core.rsi
+ noRot: true
+ layers:
+ - state: core
+ - state: pulse
+ map: ["decay"]
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.AnomalyCoreVisuals.Decaying:
+ decay:
+ True: { visible: true }
+ False: { visible: false }
+ - type: PointLight
+ radius: 1.5
+ energy: 3.5
+ color: "#ce5a25"
+ castShadows: false
+
+- type: entity
+ id: MagicFollowerCardEntity
+ name: magic
+ components:
+ - type: Physics
+ bodyType: Dynamic
+ fixedRotation: false
+ - type: Sprite
+ sprite: Objects/Magic/card.rsi
+ layers:
+ - state: icon
diff --git a/Resources/Prototypes/White/Objects/Scrolls/scrolls.yml b/Resources/Prototypes/White/Objects/Scrolls/scrolls.yml
new file mode 100644
index 0000000000..f95198ccfa
--- /dev/null
+++ b/Resources/Prototypes/White/Objects/Scrolls/scrolls.yml
@@ -0,0 +1,133 @@
+- type: entity
+ id: BaseScroll
+ parent: BaseItem
+ name: "Magic Scroll"
+ description: "A relic of arcane lore, this ancient parchment bears witness to countless mystical incantations and forgotten spells."
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: White/Misc/scrolls.rsi
+ layers:
+ - state: scroll
+ - type: Scroll
+ useSound:
+ path: /Audio/White/Items/scroll/use.ogg
+ afterUseSound:
+ path: /Audio/White/Items/scroll/after_use.ogg
+
+- type: entity
+ id: ScrollFireball
+ parent: BaseScroll
+ name: "Fireball scroll"
+ components:
+ - type: Scroll
+ actionId: ActionFireballSpell
+ learnPopup: fireball
+
+- type: entity
+ id: ScrollForcewall
+ parent: BaseScroll
+ name: "Forcewall scroll"
+ components:
+ - type: Scroll
+ actionId: ActionForcewallSpell
+ learnPopup: forcewall
+
+- type: entity
+ id: ScrollKnock
+ parent: BaseScroll
+ name: "Knock scroll"
+ components:
+ - type: Scroll
+ actionId: ActionKnock
+ learnPopup: knock-knock
+
+- type: entity
+ id: ScrollArc
+ parent: BaseScroll
+ name: "Electric Arc scroll"
+ components:
+ - type: Scroll
+ actionId: ActionElectricArcSpell
+ learnPopup: lightning
+
+- type: entity
+ id: ScrollForce
+ parent: BaseScroll
+ name: "Force scroll"
+ components:
+ - type: Scroll
+ actionId: ActionForceSpell
+ learnPopup: force
+
+- type: entity
+ id: ScrollCards
+ parent: BaseScroll
+ name: "Cards scroll"
+ components:
+ - type: Scroll
+ actionId: ActionCardSpell
+ learnPopup: cards
+
+- type: entity
+ id: ScrollBlink
+ parent: BaseScroll
+ name: "Blink scroll"
+ components:
+ - type: Scroll
+ actionId: ActionBlinkSpell
+ learnPopup: blink
+
+- type: entity
+ id: ScrollEtherealJaunt
+ parent: BaseScroll
+ name: "Ethereal Jaunt scroll"
+ components:
+ - type: Scroll
+ actionId: ActionEtherealJauntSpell
+ learnPopup: jaunt
+
+- type: entity
+ id: ScrollEmp
+ parent: BaseScroll
+ name: "Emp scroll"
+ components:
+ - type: Scroll
+ actionId: ActionEmpSpell
+ learnPopup: emp
+
+- type: entity
+ id: ScrollCluwneCurse
+ parent: BaseScroll
+ name: "Cluwne curse scroll"
+ components:
+ - type: Scroll
+ actionId: ActionCluwneCurseSpell
+ learnPopup: curse
+
+- type: entity
+ id: ScrollBananaTouch
+ parent: BaseScroll
+ name: "Banana touch scroll"
+ components:
+ - type: Scroll
+ actionId: ActionBananaTouchSpell
+ learnPopup: banana
+
+- type: entity
+ id: ScrollMimeTouch
+ parent: BaseScroll
+ name: "Mime touch scroll"
+ components:
+ - type: Scroll
+ actionId: ActionMimeTouchSpell
+ learnPopup: silence
+
+- type: entity
+ id: ScrollInstantRecall
+ parent: BaseScroll
+ name: "Instant recall scroll"
+ components:
+ - type: Scroll
+ actionId: ActionInstantRecallSpell
+ learnPopup: recall
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_big.rsi/big.png b/Resources/Textures/Effects/EnergyDome/energydome_big.rsi/big.png
new file mode 100644
index 0000000000..eb8e2a5bf5
Binary files /dev/null and b/Resources/Textures/Effects/EnergyDome/energydome_big.rsi/big.png differ
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_big.rsi/meta.json b/Resources/Textures/Effects/EnergyDome/energydome_big.rsi/meta.json
new file mode 100644
index 0000000000..a8ac036154
--- /dev/null
+++ b/Resources/Textures/Effects/EnergyDome/energydome_big.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 192,
+ "y": 192
+ },
+ "states": [
+ {
+ "name": "big",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_directed.rsi/meta.json b/Resources/Textures/Effects/EnergyDome/energydome_directed.rsi/meta.json
new file mode 100644
index 0000000000..7097ca861e
--- /dev/null
+++ b/Resources/Textures/Effects/EnergyDome/energydome_directed.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 96,
+ "y": 64
+ },
+ "states": [
+ {
+ "name": "small",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_directed.rsi/small.png b/Resources/Textures/Effects/EnergyDome/energydome_directed.rsi/small.png
new file mode 100644
index 0000000000..cabd6d68cc
Binary files /dev/null and b/Resources/Textures/Effects/EnergyDome/energydome_directed.rsi/small.png differ
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_medium.rsi/medium.png b/Resources/Textures/Effects/EnergyDome/energydome_medium.rsi/medium.png
new file mode 100644
index 0000000000..823012cea3
Binary files /dev/null and b/Resources/Textures/Effects/EnergyDome/energydome_medium.rsi/medium.png differ
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_medium.rsi/meta.json b/Resources/Textures/Effects/EnergyDome/energydome_medium.rsi/meta.json
new file mode 100644
index 0000000000..fc29522c65
--- /dev/null
+++ b/Resources/Textures/Effects/EnergyDome/energydome_medium.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 128,
+ "y": 128
+ },
+ "states": [
+ {
+ "name": "medium",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_slowdown_big.rsi/big.png b/Resources/Textures/Effects/EnergyDome/energydome_slowdown_big.rsi/big.png
new file mode 100644
index 0000000000..9e403c300a
Binary files /dev/null and b/Resources/Textures/Effects/EnergyDome/energydome_slowdown_big.rsi/big.png differ
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_slowdown_big.rsi/meta.json b/Resources/Textures/Effects/EnergyDome/energydome_slowdown_big.rsi/meta.json
new file mode 100644
index 0000000000..a8ac036154
--- /dev/null
+++ b/Resources/Textures/Effects/EnergyDome/energydome_slowdown_big.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 192,
+ "y": 192
+ },
+ "states": [
+ {
+ "name": "big",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_small.rsi/meta.json b/Resources/Textures/Effects/EnergyDome/energydome_small.rsi/meta.json
new file mode 100644
index 0000000000..c0c836ef1e
--- /dev/null
+++ b/Resources/Textures/Effects/EnergyDome/energydome_small.rsi/meta.json
@@ -0,0 +1,27 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 64,
+ "y": 64
+ },
+ "states": [
+ {
+ "name": "small",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Effects/EnergyDome/energydome_small.rsi/small.png b/Resources/Textures/Effects/EnergyDome/energydome_small.rsi/small.png
new file mode 100644
index 0000000000..96cfa6029d
Binary files /dev/null and b/Resources/Textures/Effects/EnergyDome/energydome_small.rsi/small.png differ
diff --git a/Resources/Textures/Objects/Magic/card.rsi/icon.png b/Resources/Textures/Objects/Magic/card.rsi/icon.png
new file mode 100644
index 0000000000..d56463103d
Binary files /dev/null and b/Resources/Textures/Objects/Magic/card.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Magic/card.rsi/meta.json b/Resources/Textures/Objects/Magic/card.rsi/meta.json
new file mode 100644
index 0000000000..b24d676f83
--- /dev/null
+++ b/Resources/Textures/Objects/Magic/card.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": null,
+ "copyright": null,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/clown.png b/Resources/Textures/Objects/Magic/magicactions.rsi/clown.png
new file mode 100644
index 0000000000..8a902c6fdb
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/clown.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/cluwne.png b/Resources/Textures/Objects/Magic/magicactions.rsi/cluwne.png
new file mode 100644
index 0000000000..dd62ff1e91
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/cluwne.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/emp_new.png b/Resources/Textures/Objects/Magic/magicactions.rsi/emp_new.png
new file mode 100644
index 0000000000..6e3bcf3d8b
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/emp_new.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/jaunt.png b/Resources/Textures/Objects/Magic/magicactions.rsi/jaunt.png
new file mode 100644
index 0000000000..a9209a4b21
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/jaunt.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json b/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json
index 9bf76bbe77..a6b494771c 100644
--- a/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json
+++ b/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json
@@ -27,6 +27,30 @@
},
{
"name": "gib"
- }
+ },
+ {
+ "name": "push"
+ },
+ {
+ "name": "thunder"
+ },
+ {
+ "name": "clown"
+ },
+ {
+ "name": "cluwne"
+ },
+ {
+ "name": "emp_new"
+ },
+ {
+ "name": "jaunt"
+ },
+ {
+ "name": "mime_curse"
+ },
+ {
+ "name": "summons"
+ }
]
-}
\ No newline at end of file
+}
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/mime_curse.png b/Resources/Textures/Objects/Magic/magicactions.rsi/mime_curse.png
new file mode 100644
index 0000000000..59f0306cb2
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/mime_curse.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/push.png b/Resources/Textures/Objects/Magic/magicactions.rsi/push.png
new file mode 100644
index 0000000000..7f78a8c4b3
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/push.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/summons.png b/Resources/Textures/Objects/Magic/magicactions.rsi/summons.png
new file mode 100644
index 0000000000..0e44c2f17f
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/summons.png differ
diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/thunder.png b/Resources/Textures/Objects/Magic/magicactions.rsi/thunder.png
new file mode 100644
index 0000000000..5ced92857f
Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/thunder.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/icon.png b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/icon.png
new file mode 100644
index 0000000000..24e7549af4
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/inhand-left.png b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/inhand-left.png
new file mode 100644
index 0000000000..9a8a439994
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/inhand-right.png b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/inhand-right.png
new file mode 100644
index 0000000000..9a8a439994
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/meta.json b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/meta.json
new file mode 100644
index 0000000000..cc4ea1dab9
--- /dev/null
+++ b/Resources/Textures/Objects/Tools/EnergyDome/reinhardt.rsi/meta.json
@@ -0,0 +1,22 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/equipped-BELT.png b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/equipped-BELT.png
new file mode 100644
index 0000000000..5df8249cd2
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/equipped-BELT.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/icon.png b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/icon.png
new file mode 100644
index 0000000000..4824044f0b
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/inhand-left.png b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/inhand-left.png
new file mode 100644
index 0000000000..74dbff9699
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/inhand-right.png b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/inhand-right.png
new file mode 100644
index 0000000000..8dda54bcc0
Binary files /dev/null and b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/meta.json b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/meta.json
new file mode 100644
index 0000000000..52a03b1a90
--- /dev/null
+++ b/Resources/Textures/Objects/Tools/EnergyDome/syndie.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by TheShuEd (github) for Space Station 14",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "equipped-BELT",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/White/Charge/charge.rsi/flux.png b/Resources/Textures/White/Charge/charge.rsi/flux.png
new file mode 100644
index 0000000000..ab138701ab
Binary files /dev/null and b/Resources/Textures/White/Charge/charge.rsi/flux.png differ
diff --git a/Resources/Textures/White/Charge/charge.rsi/meta.json b/Resources/Textures/White/Charge/charge.rsi/meta.json
new file mode 100644
index 0000000000..c514308bf4
--- /dev/null
+++ b/Resources/Textures/White/Charge/charge.rsi/meta.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "license": null,
+ "copyright": null,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "flux",
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 1.5,
+ 0.1,
+ 0.1,
+ 1.5,
+ 0.1,
+ 0.1,
+ 1.5,
+ 0.1,
+ 0.1,
+ 1.5
+ ]
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/White/Misc/scrolls.rsi/meta.json b/Resources/Textures/White/Misc/scrolls.rsi/meta.json
new file mode 100644
index 0000000000..6eacf06a14
--- /dev/null
+++ b/Resources/Textures/White/Misc/scrolls.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": null,
+ "copyright": null,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "scroll"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/White/Misc/scrolls.rsi/scroll.png b/Resources/Textures/White/Misc/scrolls.rsi/scroll.png
new file mode 100644
index 0000000000..2f00a23401
Binary files /dev/null and b/Resources/Textures/White/Misc/scrolls.rsi/scroll.png differ