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