diff --git a/Content.Client/Magic/MagicSystem.cs b/Content.Client/Magic/MagicSystem.cs new file mode 100644 index 0000000000..03aa9eb56d --- /dev/null +++ b/Content.Client/Magic/MagicSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Magic; + +namespace Content.Client.Magic; + +public sealed class MagicSystem : SharedMagicSystem; diff --git a/Content.Server/Actions/ActionOnInteractSystem.cs b/Content.Server/Actions/ActionOnInteractSystem.cs index 657ab46d60..b6eec0ce0f 100644 --- a/Content.Server/Actions/ActionOnInteractSystem.cs +++ b/Content.Server/Actions/ActionOnInteractSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Actions; using Content.Shared.Interaction; using Robust.Shared.Random; @@ -38,10 +39,18 @@ public sealed class ActionOnInteractSystem : EntitySystem private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args) { - if (args.Handled || component.ActionEntities == null) + if (args.Handled) return; - var options = GetValidActions(component.ActionEntities); + if (component.ActionEntities is not {} actionEnts) + { + if (!TryComp(uid, out var actionsContainerComponent)) + return; + + actionEnts = actionsContainerComponent.Container.ContainedEntities.ToList(); + } + + var options = GetValidActions(actionEnts); if (options.Count == 0) return; @@ -58,13 +67,21 @@ public sealed class ActionOnInteractSystem : EntitySystem private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, AfterInteractEvent args) { - if (args.Handled || component.ActionEntities == null) + if (args.Handled) return; + if (component.ActionEntities is not {} actionEnts) + { + if (!TryComp(uid, out var actionsContainerComponent)) + return; + + actionEnts = actionsContainerComponent.Container.ContainedEntities.ToList(); + } + // First, try entity target actions if (args.Target != null) { - var entOptions = GetValidActions(component.ActionEntities, args.CanReach); + var entOptions = GetValidActions(actionEnts, args.CanReach); for (var i = entOptions.Count - 1; i >= 0; i--) { var action = entOptions[i]; diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index ac519b4c2e..b1fb67cce7 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -78,6 +78,7 @@ namespace Content.Server.Ghost SubscribeLocalEvent(OnEntityStorageInsertAttempt); SubscribeLocalEvent(_ => MakeVisible(true)); + SubscribeLocalEvent(OnToggleGhostVisibilityToAll); } private void OnGhostHearingAction(EntityUid uid, GhostComponent component, ToggleGhostHearingActionEvent args) @@ -363,6 +364,15 @@ namespace Content.Server.Ghost args.Cancelled = true; } + private void OnToggleGhostVisibilityToAll(ToggleGhostVisibilityToAllEvent ev) + { + if (ev.Handled) + return; + + ev.Handled = true; + MakeVisible(true); + } + /// /// When the round ends, make all players able to see ghosts. /// diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index f7250c01ba..2cf5136b42 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -1,407 +1,22 @@ -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; -using Content.Shared.Physics; -using Content.Shared.Storage; -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.Random; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Spawners; namespace Content.Server.Magic; -/// -/// Handles learning and using spells (actions) -/// -public sealed class MagicSystem : EntitySystem +public sealed class MagicSystem : SharedMagicSystem { - [Dependency] private readonly ISerializationManager _seriMan = default!; - [Dependency] private readonly IComponentFactory _compFact = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IRobustRandom _random = 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); - SubscribeLocalEvent(OnSmiteSpell); - SubscribeLocalEvent(OnWorldSpawn); - SubscribeLocalEvent(OnProjectileSpell); - SubscribeLocalEvent(OnChangeComponentsSpell); + SubscribeLocalEvent(OnSpellSpoken); } - private void OnDoAfter(EntityUid uid, SpellbookComponent component, DoAfterEvent args) + private void OnSpellSpoken(ref SpeakSpellEvent 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) - { - BreakOnDamage = true, - BreakOnMove = 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) - return; - - var transform = Transform(args.Performer); - - foreach (var position in GetSpawnPositions(transform, args.Pos)) - { - var ent = Spawn(args.Prototype, position.SnapToGrid(EntityManager, _mapManager)); - - if (args.PreventCollideWithCaster) - { - var comp = EnsureComp(ent); - comp.Uid = args.Performer; - } - } - - Speak(args); - args.Handled = true; - } - - private void OnProjectileSpell(ProjectileSpellEvent ev) - { - if (ev.Handled) - return; - - ev.Handled = true; - Speak(ev); - - var xform = Transform(ev.Performer); - var userVelocity = _physics.GetMapLinearVelocity(ev.Performer); - - 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, _transformSystem); - var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out _) - ? pos.WithEntityId(gridUid, EntityManager) - : new(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position); - - var ent = Spawn(ev.Prototype, spawnCoords); - var direction = ev.Target.ToMapPos(EntityManager, _transformSystem) - - spawnCoords.ToMapPos(EntityManager, _transformSystem); - _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer); - } - } - - private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev) - { - if (ev.Handled) - return; - ev.Handled = true; - Speak(ev); - - foreach (var toRemove in ev.ToRemove) - { - if (_compFact.TryGetRegistration(toRemove, out var registration)) - RemComp(ev.Target, registration.Type); - } - - foreach (var (name, data) in ev.ToAdd) - { - if (HasComp(ev.Target, data.Component.GetType())) - continue; - - var component = (Component) _compFact.GetComponent(name); - component.Owner = ev.Target; - var temp = (object) component; - _seriMan.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 (!TryComp(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) - return; - - var transform = Transform(args.Performer); - - if (transform.MapID != args.Target.GetMapId(EntityManager)) return; - - _transformSystem.SetCoordinates(args.Performer, args.Target); - transform.AttachToGridOrMap(); - _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) - return; - - 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)) - _doorSystem.SetBoltsDown((entity, bolts), false); - - if (TryComp(entity, out var doorComp) && doorComp.State is not DoorState.Open) - _doorSystem.StartOpening(entity); - } - } - - private void OnSmiteSpell(SmiteSpellEvent ev) - { - if (ev.Handled) - return; - - ev.Handled = true; - Speak(ev); - - var direction = Transform(ev.Target).MapPosition.Position - Transform(ev.Performer).MapPosition.Position; - var impulseVector = direction * 10000; - - _physics.ApplyLinearImpulse(ev.Target, impulseVector); - - if (!TryComp(ev.Target, out var body)) - return; - - var ents = _bodySystem.GibBody(ev.Target, true, body); - - if (!ev.DeleteNonBrainParts) - return; - - foreach (var part in ents) - { - // just leaves a brain and clothes - if (HasComp(part) && !HasComp(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) - return; - - var targetMapCoords = args.Target; - - SpawnSpellHelper(args.Contents, targetMapCoords, args.Lifetime, args.Offset); - Speak(args); - 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) - { - var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random); - - var offsetCoords = entityCoords; - foreach (var proto in getProtos) - { - // TODO: Share this code with instant because they're both doing similar things for positioning. - var entity = Spawn(proto, offsetCoords); - offsetCoords = offsetCoords.Offset(offsetVector2); - - if (lifetime != null) - { - var comp = EnsureComp(entity); - comp.Lifetime = lifetime.Value; - } - } - } - - #endregion - - 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); + _chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), InGameICChatType.Speak, false); } } diff --git a/Content.Server/Store/Systems/StoreSystem.Refund.cs b/Content.Server/Store/Systems/StoreSystem.Refund.cs index d59ee75e3e..5a8be4be2b 100644 --- a/Content.Server/Store/Systems/StoreSystem.Refund.cs +++ b/Content.Server/Store/Systems/StoreSystem.Refund.cs @@ -1,4 +1,4 @@ -using Content.Server.Store.Components; +using Content.Server.Store.Components; using Robust.Shared.Containers; namespace Content.Server.Store.Systems; diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index fa363c54c1..0a1a8d19f3 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -215,11 +215,11 @@ public sealed partial class StoreSystem { HandleRefundComp(uid, component, actionId.Value); - if (listing.ProductUpgradeID != null) + if (listing.ProductUpgradeId != null) { foreach (var upgradeListing in component.Listings) { - if (upgradeListing.ID == listing.ProductUpgradeID) + if (upgradeListing.ID == listing.ProductUpgradeId) { upgradeListing.ProductActionEntity = actionId.Value; break; @@ -229,7 +229,7 @@ public sealed partial class StoreSystem } } - if (listing is { ProductUpgradeID: not null, ProductActionEntity: not null }) + if (listing is { ProductUpgradeId: not null, ProductActionEntity: not null }) { if (listing.ProductActionEntity != null) { diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs index c6002d0d4a..6cc50bc21b 100644 --- a/Content.Shared/Actions/ActionEvents.cs +++ b/Content.Shared/Actions/ActionEvents.cs @@ -157,7 +157,7 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs public EntityUid Performer; /// - /// The action that was performed. + /// The action the event belongs to. /// public EntityUid Action; } diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 315d2725b2..30687c9322 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -569,13 +569,12 @@ public abstract class SharedActionsSystem : EntitySystem handled = actionEvent.Handled; } - _audio.PlayPredicted(action.Sound, performer,predicted ? performer : null); - handled |= action.Sound != null; - if (!handled) return; // no interaction occurred. - // reduce charges, start cooldown, and mark as dirty (if required). + // play sound, reduce charges, start cooldown, and mark as dirty (if required). + + _audio.PlayPredicted(action.Sound, performer,predicted ? performer : null); var dirty = toggledBefore == action.Toggled; diff --git a/Content.Shared/Ghost/GhostComponent.cs b/Content.Shared/Ghost/GhostComponent.cs index f7717e8d23..96e9b717b9 100644 --- a/Content.Shared/Ghost/GhostComponent.cs +++ b/Content.Shared/Ghost/GhostComponent.cs @@ -101,4 +101,6 @@ public sealed partial class ToggleLightingActionEvent : InstantActionEvent { } public sealed partial class ToggleGhostHearingActionEvent : InstantActionEvent { } +public sealed partial class ToggleGhostVisibilityToAllEvent : InstantActionEvent { } + public sealed partial class BooActionEvent : InstantActionEvent { } diff --git a/Content.Shared/Magic/Components/MagicComponent.cs b/Content.Shared/Magic/Components/MagicComponent.cs new file mode 100644 index 0000000000..bcc11063b7 --- /dev/null +++ b/Content.Shared/Magic/Components/MagicComponent.cs @@ -0,0 +1,39 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Magic.Components; + +// TODO: Rename to MagicActionComponent or MagicRequirementsComponent +[RegisterComponent, NetworkedComponent, Access(typeof(SharedMagicSystem))] +public sealed partial class MagicComponent : Component +{ + // TODO: Split into different components? + // This could be the MagicRequirementsComp - which just is requirements for the spell + // Magic comp could be on the actual entities itself + // Could handle lifetime, ignore caster, etc? + // Magic caster comp would be on the caster, used for what I'm not sure + + // TODO: Do After here or in actions + + // TODO: Spell requirements + // A list of requirements to cast the spell + // Hands + // Any item in hand + // Spell takes up an inhand slot + // May be an action toggle or something + + // TODO: List requirements in action desc + /// + /// Does this spell require Wizard Robes & Hat? + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool RequiresClothes; + + /// + /// Does this spell require the user to speak? + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool RequiresSpeech; + + // TODO: FreeHand - should check if toggleable action + // Check which hand is free to toggle action in +} diff --git a/Content.Server/Magic/Components/SpellbookComponent.cs b/Content.Shared/Magic/Components/SpellbookComponent.cs similarity index 60% rename from Content.Server/Magic/Components/SpellbookComponent.cs rename to Content.Shared/Magic/Components/SpellbookComponent.cs index ebc3c88043..f1b307c245 100644 --- a/Content.Server/Magic/Components/SpellbookComponent.cs +++ b/Content.Shared/Magic/Components/SpellbookComponent.cs @@ -1,12 +1,13 @@ using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; -namespace Content.Server.Magic.Components; +namespace Content.Shared.Magic.Components; /// -/// Spellbooks for having an entity learn spells as long as they've read the book and it's in their hand. +/// Spellbooks can grant one or more spells to the user. If marked as it will teach +/// the performer the spells and wipe the book. +/// Default behavior requires the book to be held in hand /// -[RegisterComponent] +[RegisterComponent, Access(typeof(SpellbookSystem))] public sealed partial class SpellbookComponent : Component { /// @@ -18,18 +19,18 @@ public sealed partial class SpellbookComponent : Component /// /// The three fields below is just used for initialization. /// - [DataField("spells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + [DataField] [ViewVariables(VVAccess.ReadWrite)] - public Dictionary SpellActions = new(); + public Dictionary SpellActions = new(); - [DataField("learnTime")] + [DataField] [ViewVariables(VVAccess.ReadWrite)] public float LearnTime = .75f; /// /// If true, the spell action stays even after the book is removed /// - [DataField("learnPermanently")] + [DataField] [ViewVariables(VVAccess.ReadWrite)] public bool LearnPermanently; } diff --git a/Content.Shared/Magic/Components/WizardClothesComponent.cs b/Content.Shared/Magic/Components/WizardClothesComponent.cs new file mode 100644 index 0000000000..063cf56c33 --- /dev/null +++ b/Content.Shared/Magic/Components/WizardClothesComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Magic.Components; + +/// +/// The checks this if a spell requires wizard clothes +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedMagicSystem))] +public sealed partial class WizardClothesComponent : Component; diff --git a/Content.Shared/Magic/Events/BeforeCastSpellEvent.cs b/Content.Shared/Magic/Events/BeforeCastSpellEvent.cs new file mode 100644 index 0000000000..afb5c1f090 --- /dev/null +++ b/Content.Shared/Magic/Events/BeforeCastSpellEvent.cs @@ -0,0 +1,12 @@ +namespace Content.Shared.Magic.Events; + +[ByRefEvent] +public struct BeforeCastSpellEvent(EntityUid performer) +{ + /// + /// The Performer of the event, to check if they meet the requirements. + /// + public EntityUid Performer = performer; + + public bool Cancelled; +} diff --git a/Content.Shared/Magic/Events/ChargeSpellEvent.cs b/Content.Shared/Magic/Events/ChargeSpellEvent.cs new file mode 100644 index 0000000000..8898761ec2 --- /dev/null +++ b/Content.Shared/Magic/Events/ChargeSpellEvent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Magic.Events; + +/// +/// Adds provided Charge to the held wand +/// +public sealed partial class ChargeSpellEvent : InstantActionEvent, ISpeakSpell +{ + [DataField(required: true)] + public int Charge; + + [DataField] + public string WandTag = "WizardWand"; + + [DataField] + public string? Speech { get; private set; } +} diff --git a/Content.Shared/Magic/Events/InstantSpawnSpellEvent.cs b/Content.Shared/Magic/Events/InstantSpawnSpellEvent.cs index ef8d689862..1405b15827 100644 --- a/Content.Shared/Magic/Events/InstantSpawnSpellEvent.cs +++ b/Content.Shared/Magic/Events/InstantSpawnSpellEvent.cs @@ -1,6 +1,5 @@ using Content.Shared.Actions; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Magic.Events; @@ -9,17 +8,18 @@ public sealed partial class InstantSpawnSpellEvent : InstantActionEvent, ISpeakS /// /// What entity should be spawned. /// - [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Prototype = default!; + [DataField(required: true)] + public EntProtoId Prototype; - [DataField("preventCollide")] + [DataField] public bool PreventCollideWithCaster = true; - [DataField("speech")] + [DataField] public string? Speech { get; private set; } /// /// Gets the targeted spawn positons; may lead to multiple entities being spawned. /// - [DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos(); + [DataField] + public MagicInstantSpawnData PosData = new TargetCasterPos(); } diff --git a/Content.Shared/Magic/Events/KnockSpellEvent.cs b/Content.Shared/Magic/Events/KnockSpellEvent.cs index a3b0be5575..24a1700d21 100644 --- a/Content.Shared/Magic/Events/KnockSpellEvent.cs +++ b/Content.Shared/Magic/Events/KnockSpellEvent.cs @@ -1,5 +1,4 @@ using Content.Shared.Actions; -using Robust.Shared.Audio; namespace Content.Shared.Magic.Events; @@ -7,20 +6,12 @@ public sealed partial class KnockSpellEvent : InstantActionEvent, ISpeakSpell { /// /// The range this spell opens doors in - /// 4f is the default + /// 10f is the default + /// Should be able to open all doors/lockers in visible sight /// - [DataField("range")] - public float Range = 4f; + [DataField] + public float Range = 10f; - [DataField("knockSound")] - public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg"); - - /// - /// Volume control for the spell. - /// - [DataField("knockVolume")] - public float KnockVolume = 5f; - - [DataField("speech")] + [DataField] public string? Speech { get; private set; } } diff --git a/Content.Shared/Magic/Events/ProjectileSpellEvent.cs b/Content.Shared/Magic/Events/ProjectileSpellEvent.cs index 4496625769..336ea03346 100644 --- a/Content.Shared/Magic/Events/ProjectileSpellEvent.cs +++ b/Content.Shared/Magic/Events/ProjectileSpellEvent.cs @@ -1,6 +1,5 @@ using Content.Shared.Actions; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Magic.Events; @@ -9,14 +8,9 @@ public sealed partial class ProjectileSpellEvent : WorldTargetActionEvent, ISpea /// /// What entity should be spawned. /// - [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Prototype = default!; + [DataField(required: true)] + public EntProtoId Prototype; - /// - /// Gets the targeted spawn positions; may lead to multiple entities being spawned. - /// - [DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos(); - - [DataField("speech")] + [DataField] public string? Speech { get; private set; } } diff --git a/Content.Shared/Magic/Events/SmiteSpellEvent.cs b/Content.Shared/Magic/Events/SmiteSpellEvent.cs index 08ec63c05e..74ca116ad5 100644 --- a/Content.Shared/Magic/Events/SmiteSpellEvent.cs +++ b/Content.Shared/Magic/Events/SmiteSpellEvent.cs @@ -4,12 +4,13 @@ namespace Content.Shared.Magic.Events; public sealed partial class SmiteSpellEvent : EntityTargetActionEvent, ISpeakSpell { + // TODO: Make part of gib method /// - /// Should this smite delete all parts/mechanisms gibbed except for the brain? + /// Should this smite delete all parts/mechanisms gibbed except for the brain? /// - [DataField("deleteNonBrainParts")] + [DataField] public bool DeleteNonBrainParts = true; - [DataField("speech")] + [DataField] public string? Speech { get; private set; } } diff --git a/Content.Shared/Magic/Events/SpeakSpellEvent.cs b/Content.Shared/Magic/Events/SpeakSpellEvent.cs new file mode 100644 index 0000000000..1b3f7af63c --- /dev/null +++ b/Content.Shared/Magic/Events/SpeakSpellEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Magic.Events; + +[ByRefEvent] +public readonly struct SpeakSpellEvent(EntityUid performer, string speech) +{ + public readonly EntityUid Performer = performer; + public readonly string Speech = speech; +} diff --git a/Content.Shared/Magic/Events/TeleportSpellEvent.cs b/Content.Shared/Magic/Events/TeleportSpellEvent.cs index b24f6ec72f..525c1e5105 100644 --- a/Content.Shared/Magic/Events/TeleportSpellEvent.cs +++ b/Content.Shared/Magic/Events/TeleportSpellEvent.cs @@ -1,19 +1,19 @@ using Content.Shared.Actions; -using Robust.Shared.Audio; namespace Content.Shared.Magic.Events; +// TODO: Can probably just be an entity or something public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakSpell { - [DataField("blinkSound")] - public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg"); - - [DataField("speech")] + [DataField] public string? Speech { get; private set; } + // TODO: Move to magic component + // TODO: Maybe not since sound specifier is a thing + // Keep here to remind what the volume was set as /// /// Volume control for the spell. /// - [DataField("blinkVolume")] + [DataField] public float BlinkVolume = 5f; } diff --git a/Content.Shared/Magic/Events/WorldSpawnSpellEvent.cs b/Content.Shared/Magic/Events/WorldSpawnSpellEvent.cs index 4355cab842..2f50c67b3e 100644 --- a/Content.Shared/Magic/Events/WorldSpawnSpellEvent.cs +++ b/Content.Shared/Magic/Events/WorldSpawnSpellEvent.cs @@ -4,29 +4,31 @@ using Content.Shared.Storage; namespace Content.Shared.Magic.Events; +// TODO: This class needs combining with InstantSpawnSpellEvent + public sealed partial class WorldSpawnSpellEvent : WorldTargetActionEvent, ISpeakSpell { - // TODO:This class needs combining with InstantSpawnSpellEvent - /// /// The list of prototypes this spell will spawn /// - [DataField("prototypes")] - public List Contents = new(); + [DataField] + public List Prototypes = new(); // TODO: This offset is liable for deprecation. + // TODO: Target tile via code instead? /// /// The offset the prototypes will spawn in on relative to the one prior. /// Set to 0,0 to have them spawn on the same tile. /// - [DataField("offset")] + [DataField] public Vector2 Offset; /// /// Lifetime to set for the entities to self delete /// - [DataField("lifetime")] public float? Lifetime; + [DataField] + public float? Lifetime; - [DataField("speech")] + [DataField] public string? Speech { get; private set; } } diff --git a/Content.Shared/Magic/MagicInstantSpawnData.cs b/Content.Shared/Magic/MagicInstantSpawnData.cs new file mode 100644 index 0000000000..5dcc1453ed --- /dev/null +++ b/Content.Shared/Magic/MagicInstantSpawnData.cs @@ -0,0 +1,25 @@ +namespace Content.Shared.Magic; + +// TODO: If still needed, move to magic component +[ImplicitDataDefinitionForInheritors] +public abstract partial class MagicInstantSpawnData; + +/// +/// Spawns underneath caster. +/// +public sealed partial class TargetCasterPos : MagicInstantSpawnData; + +/// +/// Spawns 3 tiles wide in front of the caster. +/// +public sealed partial class TargetInFront : MagicInstantSpawnData +{ + [DataField] + public int Width = 3; +} + + +/// +/// Spawns 1 tile in front of caster +/// +public sealed partial class TargetInFrontSingle : MagicInstantSpawnData; diff --git a/Content.Shared/Magic/MagicSpawnData.cs b/Content.Shared/Magic/MagicSpawnData.cs deleted file mode 100644 index cd96d4ad76..0000000000 --- a/Content.Shared/Magic/MagicSpawnData.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Content.Shared.Magic; - -[ImplicitDataDefinitionForInheritors] -public abstract partial class MagicSpawnData -{ - -} - -/// -/// Spawns 1 at the caster's feet. -/// -public sealed partial class TargetCasterPos : MagicSpawnData {} - -/// -/// Targets the 3 tiles in front of the caster. -/// -public sealed partial class TargetInFront : MagicSpawnData -{ - [DataField("width")] public int Width = 3; -} diff --git a/Content.Shared/Magic/SharedMagicSystem.cs b/Content.Shared/Magic/SharedMagicSystem.cs new file mode 100644 index 0000000000..cc7a297aa4 --- /dev/null +++ b/Content.Shared/Magic/SharedMagicSystem.cs @@ -0,0 +1,519 @@ +using System.Numerics; +using Content.Shared.Actions; +using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; +using Content.Shared.Coordinates.Helpers; +using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Lock; +using Content.Shared.Magic.Components; +using Content.Shared.Magic.Events; +using Content.Shared.Maps; +using Content.Shared.Physics; +using Content.Shared.Popups; +using Content.Shared.Speech.Muting; +using Content.Shared.Storage; +using Content.Shared.Tag; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Network; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Spawners; + +namespace Content.Shared.Magic; + +/// +/// Handles learning and using spells (actions) +/// +public abstract class SharedMagicSystem : EntitySystem +{ + [Dependency] private readonly ISerializationManager _seriMan = 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 SharedGunSystem _gunSystem = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedBodySystem _body = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedDoorSystem _door = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly LockSystem _lock = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly TagSystem _tag = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnBeforeCastSpell); + + SubscribeLocalEvent(OnInstantSpawn); + SubscribeLocalEvent(OnTeleportSpell); + SubscribeLocalEvent(OnWorldSpawn); + SubscribeLocalEvent(OnProjectileSpell); + SubscribeLocalEvent(OnChangeComponentsSpell); + SubscribeLocalEvent(OnSmiteSpell); + SubscribeLocalEvent(OnKnockSpell); + SubscribeLocalEvent(OnChargeSpell); + + // Spell wishlist + // A wishlish of spells that I'd like to implement or planning on implementing in a future PR + + // TODO: InstantDoAfterSpell and WorldDoafterSpell + // Both would be an action that take in an event, that passes an event to trigger once the doafter is done + // This would be three events: + // 1 - Event that triggers from the action that starts the doafter + // 2 - The doafter event itself, which passes the event with it + // 3 - The event to trigger once the do-after finishes + + // TODO: Inanimate objects to life ECS + // AI sentience + + // TODO: Flesh2Stone + // Entity Target spell + // Synergy with Inanimate object to life (detects player and allows player to move around) + + // TODO: Lightning Spell + // Should just fire lightning, try to prevent arc back to caster + + // TODO: Magic Missile (homing projectile ecs) + // Instant action, target any player (except self) on screen + + // TODO: Random projectile ECS for magic-carp, wand of magic + + // TODO: Recall Spell + // mark any item in hand to recall + // ItemRecallComponent + // Event adds the component if it doesn't exist and the performer isn't stored in the comp + // 2nd firing of the event checks to see if the recall comp has this uid, and if it does it calls it + // if no free hands, summon at feet + // if item deleted, clear stored item + + // TODO: Jaunt (should be its own ECS) + // Instant action + // When clicked, disappear/reappear (goes to paused map) + // option to restrict to tiles + // option for requiring entry/exit (blood jaunt) + // speed option + + // TODO: Summon Events + // List of wizard events to add into the event pool that frequently activate + // floor is lava + // change places + // ECS that when triggered, will periodically trigger a random GameRule + // Would need a controller/controller entity? + + // TODO: Summon Guns + // Summon a random gun at peoples feet + // Get every alive player (not in cryo, not a simplemob) + // TODO: After Antag Rework - Rare chance of giving gun collector status to people + + // TODO: Summon Magic + // Summon a random magic wand at peoples feet + // Get every alive player (not in cryo, not a simplemob) + // TODO: After Antag Rework - Rare chance of giving magic collector status to people + + // TODO: Bottle of Blood + // Summons Slaughter Demon + // TODO: Slaughter Demon + // Also see Jaunt + + // TODO: Field Spells + // Should be able to specify a grid of tiles (3x3 for example) that it effects + // Timed despawn - so it doesn't last forever + // Ignore caster - for spells that shouldn't effect the caster (ie if timestop should effect the caster) + + // TODO: Touch toggle spell + // 1 - When toggled on, show in hand + // 2 - Block hand when toggled on + // - Require free hand + // 3 - use spell event when toggled & click + } + + private void OnBeforeCastSpell(Entity ent, ref BeforeCastSpellEvent args) + { + var comp = ent.Comp; + var hasReqs = true; + + 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 (comp.RequiresSpeech && HasComp(args.Performer)) + hasReqs = false; + + if (hasReqs) + return; + + args.Cancelled = true; + _popup.PopupClient(Loc.GetString("spell-requirements-failed"), args.Performer, args.Performer); + + // TODO: Pre-cast do after, either here or in SharedActionsSystem + } + + private bool PassesSpellPrerequisites(EntityUid spell, EntityUid performer) + { + var ev = new BeforeCastSpellEvent(performer); + RaiseLocalEvent(spell, ref ev); + return !ev.Cancelled; + } + + #region Spells + #region Instant Spawn Spells + /// + /// Handles the instant action (i.e. on the caster) attempting to spawn an entity. + /// + private void OnInstantSpawn(InstantSpawnSpellEvent args) + { + if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer)) + return; + + var transform = Transform(args.Performer); + + foreach (var position in GetInstantSpawnPositions(transform, args.PosData)) + { + SpawnSpellHelper(args.Prototype, position, args.Performer, preventCollide: args.PreventCollideWithCaster); + } + + Speak(args); + args.Handled = true; + } + + /// + /// Gets spawn positions listed on + /// + /// + private List GetInstantSpawnPositions(TransformComponent casterXform, MagicInstantSpawnData data) + { + switch (data) + { + case TargetCasterPos: + return new List(1) {casterXform.Coordinates}; + case TargetInFrontSingle: + { + var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized()); + + if (!TryComp(casterXform.GridUid, out var mapGrid)) + return new List(); + if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager)) + return new List(); + + var tileIndex = tileReference.Value.GridIndices; + return new List(1) { _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex) }; + } + case TargetInFront: + { + var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized()); + + if (!TryComp(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 = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex); + EntityCoordinates coordsPlus; + EntityCoordinates coordsMinus; + + var dir = casterXform.LocalRotation.GetCardinalDir(); + switch (dir) + { + case Direction.North: + case Direction.South: + { + coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (1, 0)); + coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (-1, 0)); + return new List(3) + { + coords, + coordsPlus, + coordsMinus, + }; + } + case Direction.East: + case Direction.West: + { + coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, 1)); + coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, -1)); + return new List(3) + { + coords, + coordsPlus, + coordsMinus, + }; + } + } + + return new List(); + } + default: + throw new ArgumentOutOfRangeException(); + } + } + // End Instant Spawn Spells + #endregion + #region World Spawn Spells + /// + /// Spawns entities from a list within range of click. + /// + /// + /// It will offset entities after the first entity based on the OffsetVector2. + /// + /// The Spawn Spell Event args. + private void OnWorldSpawn(WorldSpawnSpellEvent args) + { + if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer)) + return; + + var targetMapCoords = args.Target; + + WorldSpawnSpellHelper(args.Prototypes, targetMapCoords, args.Performer, args.Lifetime, args.Offset); + Speak(args); + 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 WorldSpawnSpellHelper(List entityEntries, EntityCoordinates entityCoords, EntityUid performer, float? lifetime, Vector2 offsetVector2) + { + var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random); + + var offsetCoords = entityCoords; + foreach (var proto in getProtos) + { + SpawnSpellHelper(proto, offsetCoords, performer, lifetime); + offsetCoords = offsetCoords.Offset(offsetVector2); + } + } + // End World Spawn Spells + #endregion + #region Projectile Spells + private void OnProjectileSpell(ProjectileSpellEvent ev) + { + if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !_net.IsServer) + return; + + ev.Handled = true; + Speak(ev); + + var xform = Transform(ev.Performer); + var fromCoords = xform.Coordinates; + var toCoords = ev.Target; + var userVelocity = _physics.GetMapLinearVelocity(ev.Performer); + + // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. + var fromMap = fromCoords.ToMap(EntityManager, _transform); + var spawnCoords = _mapManager.TryFindGridAt(fromMap, out var gridUid, out _) + ? fromCoords.WithEntityId(gridUid, EntityManager) + : new(_mapManager.GetMapEntityId(fromMap.MapId), fromMap.Position); + + var ent = Spawn(ev.Prototype, spawnCoords); + var direction = toCoords.ToMapPos(EntityManager, _transform) - + spawnCoords.ToMapPos(EntityManager, _transform); + _gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer); + } + // End Projectile Spells + #endregion + #region Change Component Spells + // staves.yml ActionRGB light + private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev) + { + if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer)) + return; + + ev.Handled = true; + Speak(ev); + + foreach (var toRemove in ev.ToRemove) + { + if (_compFact.TryGetRegistration(toRemove, out var registration)) + RemComp(ev.Target, registration.Type); + } + + foreach (var (name, data) in ev.ToAdd) + { + if (HasComp(ev.Target, data.Component.GetType())) + continue; + + var component = (Component) _compFact.GetComponent(name); + component.Owner = ev.Target; + var temp = (object) component; + _seriMan.CopyTo(data.Component, ref temp); + EntityManager.AddComponent(ev.Target, (Component) temp!); + } + } + // End Change Component Spells + #endregion + #region Teleport Spells + // TODO: Rename to teleport clicked spell? + /// + /// Teleports the user to the clicked location + /// + /// + private void OnTeleportSpell(TeleportSpellEvent args) + { + if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer)) + return; + + var transform = Transform(args.Performer); + + if (transform.MapID != args.Target.GetMapId(EntityManager) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true)) + return; + + _transform.SetCoordinates(args.Performer, args.Target); + _transform.AttachToGridOrMap(args.Performer, transform); + Speak(args); + args.Handled = true; + } + // End Teleport Spells + #endregion + #region Spell Helpers + private void SpawnSpellHelper(string? proto, EntityCoordinates position, EntityUid performer, float? lifetime = null, bool preventCollide = false) + { + if (!_net.IsServer) + return; + + var ent = Spawn(proto, position.SnapToGrid(EntityManager, _mapManager)); + + if (lifetime != null) + { + var comp = EnsureComp(ent); + comp.Lifetime = lifetime.Value; + } + + if (preventCollide) + { + var comp = EnsureComp(ent); + comp.Uid = performer; + } + } + // End Spell Helpers + #endregion + #region Smite Spells + private void OnSmiteSpell(SmiteSpellEvent ev) + { + if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer)) + return; + + ev.Handled = true; + Speak(ev); + + var direction = _transform.GetMapCoordinates(ev.Target, Transform(ev.Target)).Position - _transform.GetMapCoordinates(ev.Performer, Transform(ev.Performer)).Position; + var impulseVector = direction * 10000; + + _physics.ApplyLinearImpulse(ev.Target, impulseVector); + + if (!TryComp(ev.Target, out var body)) + return; + + _body.GibBody(ev.Target, true, body); + } + // End Smite Spells + #endregion + #region Knock Spells + /// + /// Opens all doors and locks within range + /// + /// + private void OnKnockSpell(KnockSpellEvent args) + { + if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer)) + return; + + args.Handled = true; + Speak(args); + + var transform = Transform(args.Performer); + + // Look for doors and lockers, and don't open/unlock them if they're already opened/unlocked. + foreach (var target in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(args.Performer, transform), args.Range, flags: LookupFlags.Dynamic | LookupFlags.Static)) + { + if (!_interaction.InRangeUnobstructed(args.Performer, target, range: 0, collisionMask: CollisionGroup.Opaque)) + continue; + + if (TryComp(target, out var doorBoltComp) && doorBoltComp.BoltsDown) + _door.SetBoltsDown((target, doorBoltComp), false, predicted: true); + + if (TryComp(target, out var doorComp) && doorComp.State is not DoorState.Open) + _door.StartOpening(target); + + if (TryComp(target, out var lockComp) && lockComp.Locked) + _lock.Unlock(target, args.Performer, lockComp); + } + } + // End Knock Spells + #endregion + #region Charge Spells + // TODO: Future support to charge other items + private void OnChargeSpell(ChargeSpellEvent ev) + { + if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !TryComp(ev.Performer, out var handsComp)) + return; + + EntityUid? wand = null; + foreach (var item in _hands.EnumerateHeld(ev.Performer, handsComp)) + { + if (!_tag.HasTag(item, ev.WandTag)) + continue; + + wand = item; + } + + ev.Handled = true; + Speak(ev); + + if (wand == null || !TryComp(wand, out var basicAmmoComp) || basicAmmoComp.Count == null) + return; + + _gunSystem.UpdateBasicEntityAmmoCount(wand.Value, basicAmmoComp.Count.Value + ev.Charge, basicAmmoComp); + } + // End Charge Spells + #endregion + // End Spells + #endregion + + // When any spell is cast it will raise this as an event, so then it can be played in server or something. At least until chat gets moved to shared + // TODO: Temp until chat is in shared + private void Speak(BaseActionEvent args) + { + if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech)) + return; + + var ev = new SpeakSpellEvent(args.Performer, speak.Speech); + RaiseLocalEvent(ref ev); + } +} diff --git a/Content.Shared/Magic/SpellbookSystem.cs b/Content.Shared/Magic/SpellbookSystem.cs new file mode 100644 index 0000000000..84b2b23298 --- /dev/null +++ b/Content.Shared/Magic/SpellbookSystem.cs @@ -0,0 +1,96 @@ +using Content.Shared.Actions; +using Content.Shared.DoAfter; +using Content.Shared.Interaction.Events; +using Content.Shared.Magic.Components; +using Content.Shared.Mind; +using Robust.Shared.Network; + +namespace Content.Shared.Magic; + +public sealed class SpellbookSystem : EntitySystem +{ + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly ActionContainerSystem _actionContainer = default!; + [Dependency] private readonly INetManager _netManager = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnInit, before: [typeof(SharedMagicSystem)]); + SubscribeLocalEvent(OnUse); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnInit(Entity ent, ref MapInitEvent args) + { + foreach (var (id, charges) in ent.Comp.SpellActions) + { + var spell = _actionContainer.AddAction(ent, id); + if (spell == null) + continue; + + int? charge = charges; + if (_actions.GetCharges(spell) != null) + charge = _actions.GetCharges(spell); + + _actions.SetCharges(spell, charge < 0 ? null : charge); + ent.Comp.Spells.Add(spell.Value); + } + } + + private void OnUse(Entity ent, ref UseInHandEvent args) + { + if (args.Handled) + return; + + AttemptLearn(ent, args); + + args.Handled = true; + } + + private void OnDoAfter(Entity ent, ref T args) where T : DoAfterEvent // Sometimes i despise this language + { + if (args.Handled || args.Cancelled) + return; + + args.Handled = true; + + if (!ent.Comp.LearnPermanently) + { + _actions.GrantActions(args.Args.User, ent.Comp.Spells, ent); + return; + } + + if (_mind.TryGetMind(args.Args.User, out var mindId, out _)) + { + var mindActionContainerComp = EnsureComp(mindId); + + if (_netManager.IsServer) + _actionContainer.TransferAllActionsWithNewAttached(ent, mindId, args.Args.User, newContainer: mindActionContainerComp); + } + else + { + foreach (var (id, charges) in ent.Comp.SpellActions) + { + EntityUid? actionId = null; + if (_actions.AddAction(args.Args.User, ref actionId, id)) + _actions.SetCharges(actionId, charges < 0 ? null : charges); + } + } + + ent.Comp.SpellActions.Clear(); + } + + private void AttemptLearn(Entity ent, UseInHandEvent args) + { + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, ent.Comp.LearnTime, new SpellbookDoAfterEvent(), ent, target: ent) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true //What, are you going to read with your eyes only?? + }; + + _doAfter.TryStartDoAfter(doAfterEventArgs); + } +} diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs index d3d2e13cdf..559c2a33bf 100644 --- a/Content.Shared/Store/ListingPrototype.cs +++ b/Content.Shared/Store/ListingPrototype.cs @@ -75,14 +75,14 @@ public partial class ListingData : IEquatable, ICloneable public EntProtoId? ProductAction; /// - /// The listing ID of the related upgrade listing. Can be used to link a to an - /// upgrade or to use standalone as an upgrade + /// The listing ID of the related upgrade listing. Can be used to link a to an + /// upgrade or to use standalone as an upgrade /// [DataField] - public ProtoId? ProductUpgradeID; + public ProtoId? ProductUpgradeId; /// - /// Keeps track of the current action entity this is tied to, for action upgrades + /// Keeps track of the current action entity this is tied to, for action upgrades /// [DataField] [NonSerialized] @@ -161,7 +161,7 @@ public partial class ListingData : IEquatable, ICloneable Priority = Priority, ProductEntity = ProductEntity, ProductAction = ProductAction, - ProductUpgradeID = ProductUpgradeID, + ProductUpgradeId = ProductUpgradeId, ProductActionEntity = ProductActionEntity, ProductEvent = ProductEvent, PurchaseAmount = PurchaseAmount, diff --git a/Resources/Locale/en-US/magic/magic.ftl b/Resources/Locale/en-US/magic/magic.ftl new file mode 100644 index 0000000000..4c8a5fc51d --- /dev/null +++ b/Resources/Locale/en-US/magic/magic.ftl @@ -0,0 +1 @@ +spell-requirements-failed = Missing requirements to cast this spell! diff --git a/Resources/Locale/en-US/store/categories.ftl b/Resources/Locale/en-US/store/categories.ftl index 17247b84f4..4ebeff3b23 100644 --- a/Resources/Locale/en-US/store/categories.ftl +++ b/Resources/Locale/en-US/store/categories.ftl @@ -15,3 +15,11 @@ store-category-pointless = Pointless # Revenant store-category-abilities = Abilities + +# Wizard +store-caregory-spellbook-offensive = Offensive Spells +store-caregory-spellbook-defensive = Defensive Spells +store-caregory-spellbook-utility = Utility Spells +store-caregory-spellbook-equipment = Wizard Equipment +store-caregory-spellbook-events = Event Spells + diff --git a/Resources/Locale/en-US/store/currency.ftl b/Resources/Locale/en-US/store/currency.ftl index ed28391531..ada70b5597 100644 --- a/Resources/Locale/en-US/store/currency.ftl +++ b/Resources/Locale/en-US/store/currency.ftl @@ -9,3 +9,4 @@ store-currency-display-debugdollar = {$amount -> } store-currency-display-telecrystal = TC store-currency-display-stolen-essence = Stolen Essence +store-currency-display-wizcoin = Wiz€oin™ diff --git a/Resources/Locale/en-US/store/spellbook-catalog.ftl b/Resources/Locale/en-US/store/spellbook-catalog.ftl new file mode 100644 index 0000000000..457f02916f --- /dev/null +++ b/Resources/Locale/en-US/store/spellbook-catalog.ftl @@ -0,0 +1,35 @@ +# Spells +spellbook-fireball-name = Fireball +spellbook-fireball-desc = Get most crew exploding with rage when they see this fireball heading toward them! + +spellbook-blink-name = Blink +spellbook-blink-desc = Don't blink or you'll miss yourself teleporting away. + +spellbook-force-wall-name = Force Wall +spellbook-force-wall-desc = Make three walls of pure force that you can pass through, but other's can't. + +spellbook-polymoprh-spider-name = Spider Polymoprh +spellbook-polymorph-spider-desc = Transforms you into a spider, man! + +spellbook-polymorph-rod-name = Rod Polymorph +spellbook-polymorph-rod-desc = Change into an Immovable Rod with limited movement. + +spellbook-charge-name = Charge +spellbook-charge-desc = Adds a charge back to your wand! + +# Equipment + +spellbook-wand-polymorph-door-name = Wand of Entrance +spellbook-wand-polymorph-door-description = For when you need a get-away route. + +spellbook-wand-polymorph-carp-name = Wand of Carp Polymorph +spellbook-wand-polymorph-carp-description = For when you need a carp filet quick and the clown is looking juicy. + +# Events + +spellbook-event-summon-ghosts-name = Summon Ghosts +spellbook-event-summon-ghosts-description = Who ya gonna call? + +# Upgrades +spellbook-upgrade-fireball-name = Upgrade Fireball +spellbook-upgrade-fireball-description = Upgrades Fireball to a maximum of level 3! diff --git a/Resources/Prototypes/Actions/polymorph.yml b/Resources/Prototypes/Actions/polymorph.yml index 7472fc0062..445dc8d9f5 100644 --- a/Resources/Prototypes/Actions/polymorph.yml +++ b/Resources/Prototypes/Actions/polymorph.yml @@ -14,3 +14,33 @@ - type: InstantAction event: !type:PolymorphActionEvent itemIconStyle: NoItem + +- type: entity + id: ActionPolymorphWizardSpider + name: Spider Polymorph + description: Polymorphs you into a Spider. + noSpawn: true + components: + - type: InstantAction + useDelay: 60 + event: !type:PolymorphActionEvent + protoId: WizardSpider + itemIconStyle: NoItem + icon: + sprite: Mobs/Animals/spider.rsi + state: tarantula + +- type: entity + id: ActionPolymorphWizardRod + name: Rod Form + description: CLANG! + noSpawn: true + components: + - type: InstantAction + useDelay: 60 + event: !type:PolymorphActionEvent + protoId: WizardRod + itemIconStyle: NoItem + icon: + sprite: Objects/Fun/immovable_rod.rsi + state: icon diff --git a/Resources/Prototypes/Catalog/spellbook_catalog.yml b/Resources/Prototypes/Catalog/spellbook_catalog.yml new file mode 100644 index 0000000000..38b95c3273 --- /dev/null +++ b/Resources/Prototypes/Catalog/spellbook_catalog.yml @@ -0,0 +1,140 @@ +# Offensive +- type: listing + id: SpellbookFireball + name: spellbook-fireball-name + description: spellbook-fireball-desc + productAction: ActionFireball + productUpgradeId: SpellbookFireballUpgrade + cost: + WizCoin: 2 + categories: + - SpellbookOffensive + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +- type: listing + id: SpellbookRodForm + name: spellbook-polymorph-rod-name + description: spellbook-polymorph-rod-desc + productAction: ActionPolymorphWizardRod + cost: + WizCoin: 3 + categories: + - SpellbookOffensive + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +# Defensive +- type: listing + id: SpellbookForceWall + name: spellbook-force-wall-name + description: spellbook-force-wall-desc + productAction: ActionForceWall + cost: + WizCoin: 3 + categories: + - SpellbookDefensive + +# Utility +- type: listing + id: SpellbookPolymorphSpider + name: spellbook-polymoprh-spider-name + description: spellbook-polymorph-spider-desc + productAction: ActionPolymorphWizardSpider + cost: + WizCoin: 2 + categories: + - SpellbookUtility + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +- type: listing + id: SpellbookBlink + name: spellbook-blink-name + description: spellbook-blink-desc + productAction: ActionBlink + cost: + WizCoin: 1 + categories: + - SpellbookUtility + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +- type: listing + id: SpellbookCharge + name: spellbook-charge-name + description: spellbook-charge-desc + productAction: ActionChargeSpell + cost: + WizCoin: 1 + categories: + - SpellbookUtility + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +# Equipment +- type: listing + id: SpellbookWandDoor + name: spellbook-wand-polymorph-door-name + description: spellbook-wand-polymorph-door-description + productEntity: WeaponWandPolymorphDoor + cost: + WizCoin: 3 + categories: + - SpellbookEquipment + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +- type: listing + id: SpellbookWandPolymorphCarp + name: spellbook-wand-polymorph-carp-name + description: spellbook-wand-polymorph-carp-description + productEntity: WeaponWandPolymorphCarp + cost: + WizCoin: 3 + categories: + - SpellbookEquipment + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +# Event +- type: listing + id: SpellbookEventSummonGhosts + name: spellbook-event-summon-ghosts-name + description: spellbook-event-summon-ghosts-description + productAction: ActionSummonGhosts + cost: + WizCoin: 0 + categories: + - SpellbookEvents + conditions: + - !type:ListingLimitedStockCondition + stock: 1 + +# Upgrades +- type: listing + id: SpellbookFireballUpgrade + productUpgradeId: SpellbookFireballUpgrade + name: spellbook-upgrade-fireball-name + description: spellbook-upgrade-fireball-description + icon: + sprite: Objects/Magic/magicactions.rsi + state: fireball + cost: + WizCoin: 2 + categories: + - SpellbookOffensive + conditions: + - !type:BuyBeforeCondition + whitelist: + - SpellbookFireball + # manual for now + - !type:ListingLimitedStockCondition + stock: 2 diff --git a/Resources/Prototypes/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Entities/Clothing/Head/hats.yml index a844fd724a..2d2b2a353e 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hats.yml @@ -390,7 +390,7 @@ - Snout - type: entity - parent: ClothingHeadBase + parent: ClothingHeadHatWizardBase id: ClothingHeadHatRedwizard name: red wizard hat description: Strange-looking red hat-wear that most certainly belongs to a real magic user. @@ -505,7 +505,7 @@ - type: entity - parent: ClothingHeadBase + parent: ClothingHeadHatWizardBase id: ClothingHeadHatVioletwizard name: violet wizard hat description: "Strange-looking violet hat-wear that most certainly belongs to a real magic user." @@ -534,6 +534,7 @@ name: witch hat description: A witch hat. components: + - type: WizardClothes #Yes this will count - type: Sprite sprite: Clothing/Head/Hats/witch.rsi - type: Clothing @@ -558,7 +559,14 @@ sprite: Clothing/Head/Hats/wizard_fake.rsi - type: entity + abstract: true parent: ClothingHeadBase + id: ClothingHeadHatWizardBase + components: + - type: WizardClothes + +- type: entity + parent: ClothingHeadHatWizardBase id: ClothingHeadHatWizard name: wizard hat description: Strange-looking blue hat-wear that most certainly belongs to a powerful magic user. diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml index fc02afe35f..112637cdd3 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml @@ -166,9 +166,16 @@ - type: Clothing sprite: Clothing/OuterClothing/Misc/santa.rsi +- type: entity + abstract: true + parent: ClothingOuterBase + id: ClothingOuterWizardBase + components: + - type: WizardClothes + # Is this wizard wearing a fanny pack??? - type: entity - parent: ClothingOuterBase + parent: ClothingOuterWizardBase id: ClothingOuterWizardViolet name: violet wizard robes description: A bizarre gem-encrusted violet robe that radiates magical energies. @@ -179,7 +186,7 @@ sprite: Clothing/OuterClothing/Misc/violetwizard.rsi - type: entity - parent: ClothingOuterBase + parent: ClothingOuterWizardBase id: ClothingOuterWizard name: wizard robes description: A bizarre gem-encrusted blue robe that radiates magical energies. @@ -190,7 +197,7 @@ sprite: Clothing/OuterClothing/Misc/wizard.rsi - type: entity - parent: ClothingOuterBase + parent: ClothingOuterWizardBase id: ClothingOuterWizardRed name: red wizard robes description: Strange-looking, red, hat-wear that most certainly belongs to a real magic user. diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 6523419821..a79d96065a 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2345,6 +2345,18 @@ bloodMaxVolume: 150 bloodReagent: Laughter +- type: entity + name: wizard spider + parent: MobGiantSpider + id: MobGiantSpiderWizard + description: This spider looks a little magical + suffix: Wizard + components: + - type: Accentless + removes: + - type: ReplacementAccent + accent: xeno #let this wizard speak + - type: entity name: possum parent: SimpleMobBase diff --git a/Resources/Prototypes/Entities/Objects/Magic/books.yml b/Resources/Prototypes/Entities/Objects/Magic/books.yml index dfb875f677..554c5214c1 100644 --- a/Resources/Prototypes/Entities/Objects/Magic/books.yml +++ b/Resources/Prototypes/Entities/Objects/Magic/books.yml @@ -9,7 +9,7 @@ layers: - state: paper_blood - state: cover_strong - color: "#645a5a" + color: "#645a5a" - state: decor_wingette_flat color: "#4d0303" - state: icon_pentagramm @@ -19,13 +19,58 @@ tags: - Spellbook +# For the Wizard Antag +# Do not add discounts or price inflation +- type: entity + id: WizardsGrimoire + name: wizards grimoire + suffix: Wizard + parent: BaseItem + components: + - type: Sprite + sprite: Objects/Misc/books.rsi + layers: + - state: paper_blood + - state: cover_strong + color: "#645a5a" + - state: decor_wingette_flat + color: "#4d0303" + - state: icon_pentagramm + color: "#f7e19f" + - type: UserInterface + interfaces: + enum.StoreUiKey.Key: + type: StoreBoundUserInterface + - type: ActivatableUI + key: enum.StoreUiKey.Key + - type: Store + refundAllowed: true + ownerOnly: true # get your own tome! + preset: StorePresetSpellbook + balance: + WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are + +# Not meant for wizard antag but meant for spawning, so people can't abuse refund if they were given a tome +- type: entity + id: WizardsGrimoireNoRefund + name: wizards grimoire + suffix: Wizard, No Refund + parent: WizardsGrimoire + components: + - type: Store + refundAllowed: false + ownerOnly: true # get your own tome! + preset: StorePresetSpellbook + balance: + WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are + - type: entity id: SpawnSpellbook name: spawn spellbook parent: BaseSpellbook components: - type: Spellbook - spells: + spellActions: ActionSpawnMagicarpSpell: -1 - type: entity @@ -48,7 +93,7 @@ - state: detail_rivets color: gold - type: Spellbook - spells: + spellActions: ActionForceWall: -1 - type: entity @@ -69,7 +114,7 @@ - state: detail_rivets color: gold - type: Spellbook - spells: + spellActions: ActionBlink: -1 - type: entity @@ -92,7 +137,7 @@ color: red - state: overlay_blood - type: Spellbook - spells: + spellActions: ActionSmite: -1 - type: entity @@ -114,7 +159,7 @@ - state: detail_bookmark color: "#98c495" - type: Spellbook - spells: + spellActions: ActionKnock: -1 - type: entity @@ -138,7 +183,7 @@ - state: icon_magic_fireball shader: unshaded - type: Spellbook - spells: + spellActions: ActionFireball: -1 - type: entity @@ -153,7 +198,7 @@ layers: - state: spell_default - type: Spellbook - spells: + spellActions: ActionFlashRune: -1 ActionExplosionRune: -1 ActionIgniteRune: -1 diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml index 2e73056e02..7af9981255 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml @@ -135,7 +135,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -262,7 +262,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -298,7 +298,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -419,7 +419,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -657,7 +657,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -693,7 +693,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -870,7 +870,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: @@ -1210,7 +1210,7 @@ name: force wall components: - type: TimedDespawn - lifetime: 20 + lifetime: 12 - type: Tag tags: - Wall @@ -1250,7 +1250,7 @@ - type: RCDDeconstructable cost: 6 delay: 8 - fx: EffectRCDDeconstruct8 + fx: EffectRCDDeconstruct8 - type: Destructible thresholds: - trigger: diff --git a/Resources/Prototypes/Magic/event_spells.yml b/Resources/Prototypes/Magic/event_spells.yml new file mode 100644 index 0000000000..e59e1b2db8 --- /dev/null +++ b/Resources/Prototypes/Magic/event_spells.yml @@ -0,0 +1,13 @@ +- type: entity + id: ActionSummonGhosts + name: Summon Ghosts + description: Makes all current ghosts permanently invisible + noSpawn: true + components: + - type: InstantAction + useDelay: 120 + itemIconStyle: BigAction + icon: + sprite: Mobs/Ghosts/ghost_human.rsi + state: icon + event: !type:ToggleGhostVisibilityToAllEvent diff --git a/Resources/Prototypes/Magic/knock_spell.yml b/Resources/Prototypes/Magic/knock_spell.yml index f00897d32c..e2c3dcfd4c 100644 --- a/Resources/Prototypes/Magic/knock_spell.yml +++ b/Resources/Prototypes/Magic/knock_spell.yml @@ -7,6 +7,8 @@ - type: InstantAction useDelay: 10 itemIconStyle: BigAction + sound: !type:SoundPathSpecifier + path: /Audio/Magic/knock.ogg icon: sprite: Objects/Magic/magicactions.rsi state: knock diff --git a/Resources/Prototypes/Magic/projectile_spells.yml b/Resources/Prototypes/Magic/projectile_spells.yml index 196472fe7b..b8db7557bb 100644 --- a/Resources/Prototypes/Magic/projectile_spells.yml +++ b/Resources/Prototypes/Magic/projectile_spells.yml @@ -4,10 +4,12 @@ description: Fires an explosive fireball towards the clicked location. noSpawn: true components: + - type: Magic - type: WorldTargetAction useDelay: 15 itemIconStyle: BigAction checkCanAccess: false + raiseOnUser: true range: 60 sound: !type:SoundPathSpecifier path: /Audio/Magic/fireball.ogg @@ -16,25 +18,25 @@ state: fireball event: !type:ProjectileSpellEvent prototype: ProjectileFireball - posData: !type:TargetCasterPos speech: action-speech-spell-fireball - type: ActionUpgrade effectedLevels: 2: ActionFireballII + 3: ActionFireballIII - type: entity id: ActionFireballII parent: ActionFireball name: Fireball II - description: Fire three explosive fireball towards the clicked location. + description: Fires a fireball, but faster! noSpawn: true components: - type: WorldTargetAction - useDelay: 5 - charges: 3 + useDelay: 10 renewCharges: true itemIconStyle: BigAction checkCanAccess: false + raiseOnUser: true range: 60 sound: !type:SoundPathSpecifier path: /Audio/Magic/fireball.ogg @@ -43,5 +45,27 @@ state: fireball event: !type:ProjectileSpellEvent prototype: ProjectileFireball - posData: !type:TargetCasterPos speech: action-speech-spell-fireball + +- type: entity + id: ActionFireballIII + parent: ActionFireball + name: Fireball III + description: The fastest fireball in the west! + noSpawn: true + components: + - type: WorldTargetAction + useDelay: 8 + renewCharges: true + itemIconStyle: BigAction + checkCanAccess: false + raiseOnUser: true + range: 60 + sound: !type:SoundPathSpecifier + path: /Audio/Magic/fireball.ogg + icon: + sprite: Objects/Magic/magicactions.rsi + state: fireball + event: !type:ProjectileSpellEvent + prototype: ProjectileFireball + speech: action-speech-spell-fireball diff --git a/Resources/Prototypes/Magic/teleport_spells.yml b/Resources/Prototypes/Magic/teleport_spells.yml index 30c83891ee..cc89cf8ee0 100644 --- a/Resources/Prototypes/Magic/teleport_spells.yml +++ b/Resources/Prototypes/Magic/teleport_spells.yml @@ -8,9 +8,11 @@ useDelay: 10 range: 16 # default examine-range. # ^ should probably add better validation that the clicked location is on the users screen somewhere, + sound: !type:SoundPathSpecifier + path: /Audio/Magic/blink.ogg itemIconStyle: BigAction checkCanAccess: false - repeat: true + repeat: false icon: sprite: Objects/Magic/magicactions.rsi state: blink diff --git a/Resources/Prototypes/Magic/utility_spells.yml b/Resources/Prototypes/Magic/utility_spells.yml new file mode 100644 index 0000000000..dccdda3789 --- /dev/null +++ b/Resources/Prototypes/Magic/utility_spells.yml @@ -0,0 +1,15 @@ +- type: entity + id: ActionChargeSpell + name: Charge + description: Adds a charge back to your wand + noSpawn: true + components: + - type: InstantAction + useDelay: 30 + itemIconStyle: BigAction + icon: + sprite: Objects/Weapons/Guns/Basic/wands.rsi + state: nothing + event: !type:ChargeSpellEvent + charge: 1 + speech: DI'RI CEL! diff --git a/Resources/Prototypes/Polymorphs/polymorph.yml b/Resources/Prototypes/Polymorphs/polymorph.yml index 582f69b744..a1a805c74f 100644 --- a/Resources/Prototypes/Polymorphs/polymorph.yml +++ b/Resources/Prototypes/Polymorphs/polymorph.yml @@ -175,3 +175,25 @@ revertOnDeath: true revertOnCrit: true duration: 20 + +# Polymorphs for Wizards polymorph self spell +- type: polymorph + id: WizardSpider + configuration: + entity: MobGiantSpiderWizard #Not angry so ghosts can't just take over the wizard + transferName: true + inventory: None + revertOnDeath: true + revertOnCrit: true + +- type: polymorph + id: WizardRod + configuration: + entity: ImmovableRodWizard #CLANG + transferName: true + transferDamage: false + inventory: None + duration: 1 + forced: true + revertOnCrit: false + revertOnDeath: false diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml index 6cf641061e..6bd9756c3e 100644 --- a/Resources/Prototypes/Store/categories.yml +++ b/Resources/Prototypes/Store/categories.yml @@ -7,6 +7,32 @@ id: Debug2 name: store-category-debug2 +#WIZARD +- type: storeCategory + id: SpellbookOffensive + name: store-caregory-spellbook-offensive + priority: 0 + +- type: storeCategory + id: SpellbookDefensive + name: store-caregory-spellbook-defensive + priority: 1 + +- type: storeCategory + id: SpellbookUtility + name: store-caregory-spellbook-utility + priority: 2 + +- type: storeCategory + id: SpellbookEquipment + name: store-caregory-spellbook-equipment + priority: 3 + +- type: storeCategory + id: SpellbookEvents + name: store-caregory-spellbook-events + priority: 4 + #uplink categoires - type: storeCategory id: UplinkWeaponry diff --git a/Resources/Prototypes/Store/currency.yml b/Resources/Prototypes/Store/currency.yml index 91039a75e6..b1cff06be2 100644 --- a/Resources/Prototypes/Store/currency.yml +++ b/Resources/Prototypes/Store/currency.yml @@ -1,7 +1,7 @@ - type: currency id: Telecrystal displayName: store-currency-display-telecrystal - cash: + cash: 1: Telecrystal1 canWithdraw: true @@ -10,7 +10,12 @@ displayName: store-currency-display-stolen-essence canWithdraw: false +- type: currency + id: WizCoin + displayName: store-currency-display-wizcoin + canWithdraw: false + #debug - type: currency id: DebugDollar - displayName: store-currency-display-debugdollar \ No newline at end of file + displayName: store-currency-display-debugdollar diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index 84aa7db544..166c29fe41 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -15,3 +15,15 @@ - UplinkPointless currencyWhitelist: - Telecrystal + +- type: storePreset + id: StorePresetSpellbook + storeName: Spellbook + categories: + - SpellbookOffensive #Fireball, Rod Form + - SpellbookDefensive #Magic Missile, Wall of Force + - SpellbookUtility #Body Swap, Lich, Teleport, Knock, Polymorph + - SpellbookEquipment #Battlemage Robes, Staff of Locker + - SpellbookEvents #Summon Weapons, Summon Ghosts + currencyWhitelist: + - WizCoin