]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Magic Refactor + Wizard Grimoire (#22568)
authorkeronshb <54602815+keronshb@users.noreply.github.com>
Sat, 11 May 2024 23:06:49 +0000 (19:06 -0400)
committerGitHub <noreply@github.com>
Sat, 11 May 2024 23:06:49 +0000 (19:06 -0400)
* Brings over changes from the original magic refactor PR

* Adds Master Spellbook, spellbook categories, WizCoin currency, and locale

* Wiz€oin™

* Adds currency whitelist to Spellbook preset, grants contained actions on action added.

* Adds grant contained action and remove provided action.

* adds a way for actions to be upgraded to the store

* Adds Fireball 3 and fixes action upgrade logic so that it checks if the action can level or if the action can upgrade separately

* Fixes upgrade logic in ActionUpgradeSystem to allow for level ups without an actual upgrade. Fixed action upgrade logic in store system as well

* Removes current action entity from the bought entities list and adds new or old action entity

* Removes Current Entity

* Removes old comments, fixes TransferAllActionsWithNewAttached

* Removes TODO

* Removes Product Action Upgrade Event

* reverts changes to immovablerodrule

* Removes stale event reference

* fixes mind action grant logic

* reverts shared gun system change to projectile anomaly system

* forgor to remove the using

* Reverts unintended changes to action container

* Adds refund button to the store

* Refreshes store back to origin.

* Refund with correct currency

* Init refund

* Check for terminating and update interface

* Disables refund button

* Removes preset allow refund

* dont refund if map changed

* adds refunds to stores

* Adds method to check for starting map

* comments, datafields, some requested changes

* turns event into ref event

* Adds datafields

* Switches to entity terminating event

* Changes store entity to be nullable and checks if store is terminating to remove reference.

* Tryadd instead of containskey

* Adds a refund disable method, disables refund on bought ent container changes if not an action

* Removes duplicate refundcomp

* Removes unintended merges

* Removed another unintended change from merge

* removes extra using statement

* readds using statement

* might as well just remove both usings since it won't leave the PR

* Fixes Action upgrades from stores

* Changes to non obsolete method uses

* Shares spawn code between instant and world

* Adds action entity to action event, adds beforecastspellevent, adds spell requirements to magic component

* puts prereq check in spell methods, sets up template code for before cast event

* checks for required wizard clothes

* Networks Magic Comp and Wizard Clothes Comp. Renames MagicSpawnData to MagicInstantSpawnData.

* Removes posdata from projectiles

* Speech > RequiresSpeech

* Fixes ActionOnInteract

* checks for muted

* popup for missing reqs

* Validate click loc for blink spell

* Checks if doors are in range and not obstructed before opening

* Check ents by map coords

* Adds speak event

* Comments spellbooks

* Removes comments

* Unobsoletes smite spell

* Invert if

* Requirements loc

* Fixes spell reqs

* Inverts an if

* Comment updates

* Starts doafter work

* Removes doafter references

* Balances fireball upgrades to be more reasonable

* Enables refund on master spellbooks

* Spells to do

* update spellbook doafter

* knock toggles bolts

* Touch Spell comments

* Comments for pending spells

* more comments

* adds spider polymorph to spellbook

* TODOs for spells

* reorganizes spellbook categories and adds wands

* fixes spacing and adds limited conditions

* commented owner only for future store PR

* reenables owner only for the grimoire

* fixes grimoire sprite

* Adds wizard rod polymorph

* summon ghosts event

* Moves rod form to offensive category

* Adds charge spell and loc for rod polymorph

* Oops forgor the actual chages

* Item Recall comment

* Fixes UI

* removes extra field for wizard rod

* Cleanup

* New Condition (INCOMPLETE)

* Fix linter

* Fix linter (for real)

* fixed some descriptions

* adds regions to magic

* Adds a non-refund wizard grimoire, fixes blink to deselect after teleporting, reduces force wall despawn time to 12 seconds

* removes limited upgrade condition

---------

Co-authored-by: AJCM <AJCM@tutanota.com>
46 files changed:
Content.Client/Magic/MagicSystem.cs [new file with mode: 0644]
Content.Server/Actions/ActionOnInteractSystem.cs
Content.Server/Ghost/GhostSystem.cs
Content.Server/Magic/MagicSystem.cs
Content.Server/Store/Systems/StoreSystem.Refund.cs
Content.Server/Store/Systems/StoreSystem.Ui.cs
Content.Shared/Actions/ActionEvents.cs
Content.Shared/Actions/SharedActionsSystem.cs
Content.Shared/Ghost/GhostComponent.cs
Content.Shared/Magic/Components/MagicComponent.cs [new file with mode: 0644]
Content.Shared/Magic/Components/SpellbookComponent.cs [moved from Content.Server/Magic/Components/SpellbookComponent.cs with 60% similarity]
Content.Shared/Magic/Components/WizardClothesComponent.cs [new file with mode: 0644]
Content.Shared/Magic/Events/BeforeCastSpellEvent.cs [new file with mode: 0644]
Content.Shared/Magic/Events/ChargeSpellEvent.cs [new file with mode: 0644]
Content.Shared/Magic/Events/InstantSpawnSpellEvent.cs
Content.Shared/Magic/Events/KnockSpellEvent.cs
Content.Shared/Magic/Events/ProjectileSpellEvent.cs
Content.Shared/Magic/Events/SmiteSpellEvent.cs
Content.Shared/Magic/Events/SpeakSpellEvent.cs [new file with mode: 0644]
Content.Shared/Magic/Events/TeleportSpellEvent.cs
Content.Shared/Magic/Events/WorldSpawnSpellEvent.cs
Content.Shared/Magic/MagicInstantSpawnData.cs [new file with mode: 0644]
Content.Shared/Magic/MagicSpawnData.cs [deleted file]
Content.Shared/Magic/SharedMagicSystem.cs [new file with mode: 0644]
Content.Shared/Magic/SpellbookSystem.cs [new file with mode: 0644]
Content.Shared/Store/ListingPrototype.cs
Resources/Locale/en-US/magic/magic.ftl [new file with mode: 0644]
Resources/Locale/en-US/store/categories.ftl
Resources/Locale/en-US/store/currency.ftl
Resources/Locale/en-US/store/spellbook-catalog.ftl [new file with mode: 0644]
Resources/Prototypes/Actions/polymorph.yml
Resources/Prototypes/Catalog/spellbook_catalog.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Clothing/Head/hats.yml
Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Objects/Magic/books.yml
Resources/Prototypes/Entities/Structures/Walls/walls.yml
Resources/Prototypes/Magic/event_spells.yml [new file with mode: 0644]
Resources/Prototypes/Magic/knock_spell.yml
Resources/Prototypes/Magic/projectile_spells.yml
Resources/Prototypes/Magic/teleport_spells.yml
Resources/Prototypes/Magic/utility_spells.yml [new file with mode: 0644]
Resources/Prototypes/Polymorphs/polymorph.yml
Resources/Prototypes/Store/categories.yml
Resources/Prototypes/Store/currency.yml
Resources/Prototypes/Store/presets.yml

diff --git a/Content.Client/Magic/MagicSystem.cs b/Content.Client/Magic/MagicSystem.cs
new file mode 100644 (file)
index 0000000..03aa9eb
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Magic;
+
+namespace Content.Client.Magic;
+
+public sealed class MagicSystem : SharedMagicSystem;
index 657ab46d60ca8d8c8b95d8dca0caf320944912b2..b6eec0ce0f69ad769e185f67fa44a6f31a3c231d 100644 (file)
@@ -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<InstantActionComponent>(component.ActionEntities);
+        if (component.ActionEntities is not {} actionEnts)
+        {
+            if (!TryComp<ActionsContainerComponent>(uid,  out var actionsContainerComponent))
+                return;
+
+            actionEnts = actionsContainerComponent.Container.ContainedEntities.ToList();
+        }
+
+        var options = GetValidActions<InstantActionComponent>(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<ActionsContainerComponent>(uid,  out var actionsContainerComponent))
+                return;
+
+            actionEnts = actionsContainerComponent.Container.ContainedEntities.ToList();
+        }
+
         // First, try entity target actions
         if (args.Target != null)
         {
-            var entOptions = GetValidActions<EntityTargetActionComponent>(component.ActionEntities, args.CanReach);
+            var entOptions = GetValidActions<EntityTargetActionComponent>(actionEnts, args.CanReach);
             for (var i = entOptions.Count - 1; i >= 0; i--)
             {
                 var action = entOptions[i];
index ac519b4c2e508f7265d032cd1fc1bf79f036a248..b1fb67cce7b2fa6539b9a757a4cc2a12a4159341 100644 (file)
@@ -78,6 +78,7 @@ namespace Content.Server.Ghost
             SubscribeLocalEvent<GhostComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
 
             SubscribeLocalEvent<RoundEndTextAppendEvent>(_ => MakeVisible(true));
+            SubscribeLocalEvent<ToggleGhostVisibilityToAllEvent>(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);
+        }
+
         /// <summary>
         /// When the round ends, make all players able to see ghosts.
         /// </summary>
index f7250c01ba5c54231af64cfedbe3ab809981bb1e..2cf5136b42770fb68bad3793733eb35ebfec6650 100644 (file)
-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;
 
-/// <summary>
-/// Handles learning and using spells (actions)
-/// </summary>
-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<SpellbookComponent, MapInitEvent>(OnInit);
-        SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
-        SubscribeLocalEvent<SpellbookComponent, SpellbookDoAfterEvent>(OnDoAfter);
-
-        SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
-        SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
-        SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
-        SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
-        SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
-        SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
-        SubscribeLocalEvent<ChangeComponentsSpellEvent>(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)
-        {
-            BreakOnDamage = true,
-            BreakOnMove = true,
-            NeedHand = true //What, are you going to read with your eyes only??
-        };
-
-        _doAfter.TryStartDoAfter(doAfterEventArgs);
-    }
-
-    #region Spells
-
-    /// <summary>
-    /// Handles the instant action (i.e. on the caster) attempting to spawn an entity.
-    /// </summary>
-    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<PreventCollideComponent>(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<EntityCoordinates> GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data)
-    {
-        switch (data)
-        {
-            case TargetCasterPos:
-                return new List<EntityCoordinates>(1) {casterXform.Coordinates};
-            case TargetInFront:
-            {
-                // This is shit but you get the idea.
-                var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
-
-                if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
-                    return new List<EntityCoordinates>();
-
-                if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
-                    return new List<EntityCoordinates>();
-
-                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<EntityCoordinates>(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<EntityCoordinates>(3)
-                        {
-                            coords,
-                            coordsPlus,
-                            coordsMinus,
-                        };
-                    }
-                }
-
-                return new List<EntityCoordinates>();
-            }
-            default:
-                throw new ArgumentOutOfRangeException();
-        }
-    }
-
-    /// <summary>
-    /// Teleports the user to the clicked location
-    /// </summary>
-    /// <param name="args"></param>
-    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;
+        SubscribeLocalEvent<SpeakSpellEvent>(OnSpellSpoken);
     }
 
-    /// <summary>
-    /// Opens all doors within range
-    /// </summary>
-    /// <param name="args"></param>
-    private void OnKnockSpell(KnockSpellEvent args)
+    private void OnSpellSpoken(ref SpeakSpellEvent 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<DoorBoltComponent>(entity, out var bolts))
-                _doorSystem.SetBoltsDown((entity, bolts), false);
-
-            if (TryComp<DoorComponent>(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<BodyComponent>(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<BodyComponent>(part) && !HasComp<BrainComponent>(part))
-            {
-                QueueDel(part);
-            }
-        }
-    }
-
-    /// <summary>
-    /// Spawns entity prototypes from a list within range of click.
-    /// </summary>
-    /// <remarks>
-    /// It will offset mobs after the first mob based on the OffsetVector2 property supplied.
-    /// </remarks>
-    /// <param name="args"> The Spawn Spell Event args.</param>
-    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;
-    }
-
-    /// <summary>
-    /// Loops through a supplied list of entity prototypes and spawns them
-    /// </summary>
-    /// <remarks>
-    /// 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
-    /// </remarks>
-    /// <param name="entityEntries"> The list of Entities to spawn in</param>
-    /// <param name="entityCoords"> Map Coordinates where the entities will spawn</param>
-    /// <param name="lifetime"> Check to see if the entities should self delete</param>
-    /// <param name="offsetVector2"> A Vector2 offset that the entities will spawn in</param>
-    private void SpawnSpellHelper(List<EntitySpawnEntry> 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<TimedDespawnComponent>(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);
     }
 }
index d59ee75e3eaeefd11272212170d8f938f67ed180..5a8be4be2bbd758d1ae1e5a50ae5017965eb71da 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Server.Store.Components;
+using Content.Server.Store.Components;
 using Robust.Shared.Containers;
 
 namespace Content.Server.Store.Systems;
index fa363c54c1235efb45b629e863c816aa3c224c77..0a1a8d19f318ab24fcfbc36397e896f04ba63963 100644 (file)
@@ -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)
             {
index c6002d0d4ad370454400b3f760d41ce6077ebf65..6cc50bc21b4233e48a150aca21828a06f059f14a 100644 (file)
@@ -157,7 +157,7 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs
     public EntityUid Performer;
 
     /// <summary>
-    ///     The action that was performed.
+    ///     The action the event belongs to.
     /// </summary>
     public EntityUid Action;
 }
index 315d2725b2e268e57293ec2f96129494fcbd05b5..30687c932256018665d64810256f04c860932d86 100644 (file)
@@ -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;
 
index f7717e8d232ca46daad43687cbf82cf6687d3e9b..96e9b717b90084ad0ca1c659ce5ff58cf3bfdb80 100644 (file)
@@ -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 (file)
index 0000000..bcc1106
--- /dev/null
@@ -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
+    /// <summary>
+    ///     Does this spell require Wizard Robes & Hat?
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool RequiresClothes;
+
+    /// <summary>
+    ///     Does this spell require the user to speak?
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool RequiresSpeech;
+
+    // TODO: FreeHand - should check if toggleable action
+    //  Check which hand is free to toggle action in
+}
similarity index 60%
rename from Content.Server/Magic/Components/SpellbookComponent.cs
rename to Content.Shared/Magic/Components/SpellbookComponent.cs
index ebc3c880436323fd02730e30dd2e880b0dbdebd4..f1b307c245d89ade43a6bc727916656962e86323 100644 (file)
@@ -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;
 
 /// <summary>
-/// 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 <see cref="LearnPermanently"/> it will teach
+/// the performer the spells and wipe the book.
+/// Default behavior requires the book to be held in hand
 /// </summary>
-[RegisterComponent]
+[RegisterComponent, Access(typeof(SpellbookSystem))]
 public sealed partial class SpellbookComponent : Component
 {
     /// <summary>
@@ -18,18 +19,18 @@ public sealed partial class SpellbookComponent : Component
     /// <summary>
     /// The three fields below is just used for initialization.
     /// </summary>
-    [DataField("spells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, EntityPrototype>))]
+    [DataField]
     [ViewVariables(VVAccess.ReadWrite)]
-    public Dictionary<string, int> SpellActions = new();
+    public Dictionary<EntProtoId, int> SpellActions = new();
 
-    [DataField("learnTime")]
+    [DataField]
     [ViewVariables(VVAccess.ReadWrite)]
     public float LearnTime = .75f;
 
     /// <summary>
     ///  If true, the spell action stays even after the book is removed
     /// </summary>
-    [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 (file)
index 0000000..063cf56
--- /dev/null
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Magic.Components;
+
+/// <summary>
+/// The <see cref="SharedMagicSystem"/> checks this if a spell requires wizard clothes
+/// </summary>
+[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 (file)
index 0000000..afb5c1f
--- /dev/null
@@ -0,0 +1,12 @@
+namespace Content.Shared.Magic.Events;
+
+[ByRefEvent]
+public struct BeforeCastSpellEvent(EntityUid performer)
+{
+    /// <summary>
+    /// The Performer of the event, to check if they meet the requirements.
+    /// </summary>
+    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 (file)
index 0000000..8898761
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.Actions;
+
+namespace Content.Shared.Magic.Events;
+
+/// <summary>
+/// Adds provided Charge to the held wand
+/// </summary>
+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; }
+}
index ef8d6898623084208b4d762dcf5cdd433b640ece..1405b158271c0cbe72fd4f71c427c2f61b6fb575 100644 (file)
@@ -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
     /// <summary>
     /// What entity should be spawned.
     /// </summary>
-    [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    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; }
 
     /// <summary>
     /// Gets the targeted spawn positons; may lead to multiple entities being spawned.
     /// </summary>
-    [DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
+    [DataField]
+    public MagicInstantSpawnData PosData = new TargetCasterPos();
 }
index a3b0be55759bea2ded654a1d96129a491deaa09d..24a1700d21f1609bda34535223fe3b88d6348db9 100644 (file)
@@ -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
 {
     /// <summary>
     /// 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
     /// </summary>
-    [DataField("range")]
-    public float Range = 4f;
+    [DataField]
+    public float Range = 10f;
 
-    [DataField("knockSound")]
-    public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg");
-
-    /// <summary>
-    /// Volume control for the spell.
-    /// </summary>
-    [DataField("knockVolume")]
-    public float KnockVolume = 5f;
-
-    [DataField("speech")]
+    [DataField]
     public string? Speech { get; private set; }
 }
index 44966257699dcb7e7cf770e985b9d787571b7726..336ea03346b14552fa3425fa1945fb847a030575 100644 (file)
@@ -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
     /// <summary>
     /// What entity should be spawned.
     /// </summary>
-    [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string Prototype = default!;
+    [DataField(required: true)]
+    public EntProtoId Prototype;
 
-    /// <summary>
-    /// Gets the targeted spawn positions; may lead to multiple entities being spawned.
-    /// </summary>
-    [DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
-
-    [DataField("speech")]
+    [DataField]
     public string? Speech { get; private set; }
 }
index 08ec63c05e778c46b93ad6257d35c204b3686e14..74ca116ad59642402589ca11581c97cee587f79a 100644 (file)
@@ -4,12 +4,13 @@ namespace Content.Shared.Magic.Events;
 
 public sealed partial class SmiteSpellEvent : EntityTargetActionEvent, ISpeakSpell
 {
+    // TODO: Make part of gib method
     /// <summary>
-    ///     Should this smite delete all parts/mechanisms gibbed except for the brain?
+    /// Should this smite delete all parts/mechanisms gibbed except for the brain?
     /// </summary>
-    [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 (file)
index 0000000..1b3f7af
--- /dev/null
@@ -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;
+}
index b24f6ec72f75552b6c7fc6ac0ce41118caaa163b..525c1e510524dd928f21bbe7e84baba68087b176 100644 (file)
@@ -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
     /// <summary>
     /// Volume control for the spell.
     /// </summary>
-    [DataField("blinkVolume")]
+    [DataField]
     public float BlinkVolume = 5f;
 }
index 4355cab8421944f98a1997f13d231979a5693204..2f50c67b3e71038689a2685b754e7e9b872f46bd 100644 (file)
@@ -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
-
     /// <summary>
     /// The list of prototypes this spell will spawn
     /// </summary>
-    [DataField("prototypes")]
-    public List<EntitySpawnEntry> Contents = new();
+    [DataField]
+    public List<EntitySpawnEntry> Prototypes = new();
 
     // TODO: This offset is liable for deprecation.
+    // TODO: Target tile via code instead?
     /// <summary>
     /// 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.
     /// </summary>
-    [DataField("offset")]
+    [DataField]
     public Vector2 Offset;
 
     /// <summary>
     /// Lifetime to set for the entities to self delete
     /// </summary>
-    [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 (file)
index 0000000..5dcc145
--- /dev/null
@@ -0,0 +1,25 @@
+namespace Content.Shared.Magic;
+
+// TODO: If still needed, move to magic component
+[ImplicitDataDefinitionForInheritors]
+public abstract partial class MagicInstantSpawnData;
+
+/// <summary>
+/// Spawns underneath caster.
+/// </summary>
+public sealed partial class TargetCasterPos : MagicInstantSpawnData;
+
+/// <summary>
+/// Spawns 3 tiles wide in front of the caster.
+/// </summary>
+public sealed partial class TargetInFront : MagicInstantSpawnData
+{
+    [DataField]
+    public int Width = 3;
+}
+
+
+/// <summary>
+/// Spawns 1 tile in front of caster
+/// </summary>
+public sealed partial class TargetInFrontSingle : MagicInstantSpawnData;
diff --git a/Content.Shared/Magic/MagicSpawnData.cs b/Content.Shared/Magic/MagicSpawnData.cs
deleted file mode 100644 (file)
index cd96d4a..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Content.Shared.Magic;
-
-[ImplicitDataDefinitionForInheritors]
-public abstract partial class MagicSpawnData
-{
-
-}
-
-/// <summary>
-/// Spawns 1 at the caster's feet.
-/// </summary>
-public sealed partial class TargetCasterPos : MagicSpawnData {}
-
-/// <summary>
-/// Targets the 3 tiles in front of the caster.
-/// </summary>
-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 (file)
index 0000000..cc7a297
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Handles learning and using spells (actions)
+/// </summary>
+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<MagicComponent, BeforeCastSpellEvent>(OnBeforeCastSpell);
+
+        SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
+        SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
+        SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
+        SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
+        SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
+        SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
+        SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
+        SubscribeLocalEvent<ChargeSpellEvent>(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<MagicComponent> 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<WizardClothesComponent>(item);
+                else
+                    hasReqs = false;
+
+                if (!hasReqs)
+                    break;
+            }
+        }
+
+        if (comp.RequiresSpeech && HasComp<MutedComponent>(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
+    /// <summary>
+    /// Handles the instant action (i.e. on the caster) attempting to spawn an entity.
+    /// </summary>
+    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;
+    }
+
+        /// <summary>
+    ///     Gets spawn positions listed on <see cref="InstantSpawnSpellEvent"/>
+    /// </summary>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    private List<EntityCoordinates> GetInstantSpawnPositions(TransformComponent casterXform, MagicInstantSpawnData data)
+    {
+        switch (data)
+        {
+            case TargetCasterPos:
+                return new List<EntityCoordinates>(1) {casterXform.Coordinates};
+            case TargetInFrontSingle:
+            {
+                var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
+
+                if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
+                    return new List<EntityCoordinates>();
+                if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
+                    return new List<EntityCoordinates>();
+
+                var tileIndex = tileReference.Value.GridIndices;
+                return new List<EntityCoordinates>(1) { _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex) };
+            }
+            case TargetInFront:
+            {
+                var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
+
+                if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
+                    return new List<EntityCoordinates>();
+
+                if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
+                    return new List<EntityCoordinates>();
+
+                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<EntityCoordinates>(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<EntityCoordinates>(3)
+                        {
+                            coords,
+                            coordsPlus,
+                            coordsMinus,
+                        };
+                    }
+                }
+
+                return new List<EntityCoordinates>();
+            }
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
+    // End Instant Spawn Spells
+    #endregion
+    #region World Spawn Spells
+    /// <summary>
+    /// Spawns entities from a list within range of click.
+    /// </summary>
+    /// <remarks>
+    /// It will offset entities after the first entity based on the OffsetVector2.
+    /// </remarks>
+    /// <param name="args"> The Spawn Spell Event args.</param>
+    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;
+    }
+
+    /// <summary>
+    /// Loops through a supplied list of entity prototypes and spawns them
+    /// </summary>
+    /// <remarks>
+    /// 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
+    /// </remarks>
+    /// <param name="entityEntries"> The list of Entities to spawn in</param>
+    /// <param name="entityCoords"> Map Coordinates where the entities will spawn</param>
+    /// <param name="lifetime"> Check to see if the entities should self delete</param>
+    /// <param name="offsetVector2"> A Vector2 offset that the entities will spawn in</param>
+    private void WorldSpawnSpellHelper(List<EntitySpawnEntry> 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?
+    /// <summary>
+    /// Teleports the user to the clicked location
+    /// </summary>
+    /// <param name="args"></param>
+    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<TimedDespawnComponent>(ent);
+            comp.Lifetime = lifetime.Value;
+        }
+
+        if (preventCollide)
+        {
+            var comp = EnsureComp<PreventCollideComponent>(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<BodyComponent>(ev.Target, out var body))
+            return;
+
+        _body.GibBody(ev.Target, true, body);
+    }
+    // End Smite Spells
+    #endregion
+    #region Knock Spells
+    /// <summary>
+    /// Opens all doors and locks within range
+    /// </summary>
+    /// <param name="args"></param>
+    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<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown)
+                _door.SetBoltsDown((target, doorBoltComp), false, predicted: true);
+
+            if (TryComp<DoorComponent>(target, out var doorComp) && doorComp.State is not DoorState.Open)
+                _door.StartOpening(target);
+
+            if (TryComp<LockComponent>(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<HandsComponent>(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<BasicEntityAmmoProviderComponent>(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 (file)
index 0000000..84b2b23
--- /dev/null
@@ -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<SpellbookComponent, MapInitEvent>(OnInit, before: [typeof(SharedMagicSystem)]);
+        SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
+        SubscribeLocalEvent<SpellbookComponent, SpellbookDoAfterEvent>(OnDoAfter);
+    }
+
+    private void OnInit(Entity<SpellbookComponent> 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<SpellbookComponent> ent, ref UseInHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        AttemptLearn(ent, args);
+
+        args.Handled = true;
+    }
+
+    private void OnDoAfter<T>(Entity<SpellbookComponent> 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<ActionsContainerComponent>(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<SpellbookComponent> 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);
+    }
+}
index d3d2e13cdfdcc6f803546cdb265122f8c7b270fd..559c2a33bf5d9d243a7c0c38a3b2d4a56f0889b8 100644 (file)
@@ -75,14 +75,14 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
     public EntProtoId? ProductAction;
 
     /// <summary>
-    ///     The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
-    ///         upgrade or to use standalone as an upgrade
+    /// The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
+    /// upgrade or to use standalone as an upgrade
     /// </summary>
     [DataField]
-    public ProtoId<ListingPrototype>? ProductUpgradeID;
+    public ProtoId<ListingPrototype>? ProductUpgradeId;
 
     /// <summary>
-    ///     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
     /// </summary>
     [DataField]
     [NonSerialized]
@@ -161,7 +161,7 @@ public partial class ListingData : IEquatable<ListingData>, 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 (file)
index 0000000..4c8a5fc
--- /dev/null
@@ -0,0 +1 @@
+spell-requirements-failed = Missing requirements to cast this spell!
index 17247b84f4907f45b8445029e01c53eefe37b0a3..4ebeff3b23762923a8e46c6b5dfb3ae39813b851 100644 (file)
@@ -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
+
index ed28391531a8ccf18a7423ea24822739ab1190a0..ada70b5597a54dae869ae025e6c0eb799f7eb4c6 100644 (file)
@@ -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 (file)
index 0000000..457f029
--- /dev/null
@@ -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!
index 7472fc0062054914d1329b177108c84d237de2dd..445dc8d9f54f535ae7e4d114c6459b3591d6630b 100644 (file)
   - 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 (file)
index 0000000..38b95c3
--- /dev/null
@@ -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
index a844fd724a36b65fb20518a34e408c98dffa85ab..2d2b2a353ef39ae30ef8e74a4ea6c293a97f1473 100644 (file)
     - 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.
 
 
 - 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."
   name: witch hat
   description: A witch hat.
   components:
+  - type: WizardClothes #Yes this will count
   - type: Sprite
     sprite: Clothing/Head/Hats/witch.rsi
   - type: Clothing
     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.
index fc02afe35f173efd0cb0c6a9c21afe9e8cf4890e..112637cdd3d00f4cf5a268d72a99d637493beedb 100644 (file)
   - type: Clothing
     sprite: Clothing/OuterClothing/Misc/santa.rsi
 
-# Is this wizard wearing a fanny pack???
 - type: entity
+  abstract: true
   parent: ClothingOuterBase
+  id: ClothingOuterWizardBase
+  components:
+  - type: WizardClothes
+
+# Is this wizard wearing a fanny pack???
+- type: entity
+  parent: ClothingOuterWizardBase
   id: ClothingOuterWizardViolet
   name: violet wizard robes
   description: A bizarre gem-encrusted violet robe that radiates magical energies.
     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.
     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.
index 652341982187ddf98090f45f50b85fd09fa981ce..a79d96065a6dc67fec6ab2f8ce4bf5ba540cd65b 100644 (file)
       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
index dfb875f6771d71756953f811065f2e8c4b51fa75..554c5214c199900e4b5ab750bdddf43b8e71d3fc 100644 (file)
@@ -9,7 +9,7 @@
       layers:
       - state: paper_blood
       - state: cover_strong
-        color: "#645a5a" 
+        color: "#645a5a"
       - state: decor_wingette_flat
         color: "#4d0303"
       - state: icon_pentagramm
       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
       - state: detail_rivets
         color: gold
     - type: Spellbook
-      spells:
+      spellActions:
         ActionBlink: -1
 
 - type: entity
         color: red
       - state: overlay_blood
     - type: Spellbook
-      spells:
+      spellActions:
         ActionSmite: -1
 
 - type: entity
       - state: detail_bookmark
         color: "#98c495"
     - type: Spellbook
-      spells:
+      spellActions:
         ActionKnock: -1
 
 - type: entity
       - state: icon_magic_fireball
         shader: unshaded
     - type: Spellbook
-      spells:
+      spellActions:
         ActionFireball: -1
 
 - type: entity
     layers:
     - state: spell_default
   - type: Spellbook
-    spells:
+    spellActions:
       ActionFlashRune: -1
       ActionExplosionRune: -1
       ActionIgniteRune: -1
index 2e73056e02336b0c26050de28ea62a7073affc54..7af998125551afee4fb15101cc7bfa8201b093f0 100644 (file)
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8    
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8   
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8  
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8    
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8    
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8    
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: RCDDeconstructable
     cost: 6
     delay: 8
-    fx: EffectRCDDeconstruct8    
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   name: force wall
   components:
     - type: TimedDespawn
-      lifetime: 20
+      lifetime: 12
     - type: Tag
       tags:
         - Wall
   - 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 (file)
index 0000000..e59e1b2
--- /dev/null
@@ -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
index f00897d32c7bcb0f6ae5a6f7ec4c4c3b930b2d54..e2c3dcfd4c7eed087f23c03efba76738b192b8ec 100644 (file)
@@ -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
index 196472fe7b2f9d20c59802b74326c3a0e1303546..b8db7557bba6e382754b2333615a3007b5e63f30 100644 (file)
@@ -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
       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
       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
index 30c83891eeee147a7300bfe571410ab241a46e39..cc89cf8ee0d83edca304ee65ecdf5260f5948618 100644 (file)
@@ -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 (file)
index 0000000..dccdda3
--- /dev/null
@@ -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!
index 582f69b744ed3541468a1965b6fc80a27b763a1a..a1a805c74fcdb27570f2370c8e60c524e2908c9a 100644 (file)
     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
index 6cf641061e9ec39fa64dba9681dce09013d64695..6bd9756c3e94a467cc209c27326460bf8f18d33c 100644 (file)
@@ -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
index 91039a75e6a2e0056a359a91145340fd796a2219..b1cff06be2d65c548d8c91656f2ac095bd0ec5a5 100644 (file)
@@ -1,7 +1,7 @@
 - type: currency
   id: Telecrystal
   displayName: store-currency-display-telecrystal
-  cash: 
+  cash:
     1: Telecrystal1
   canWithdraw: true
 
   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
index 84aa7db5441cbb2f56e03f2028b648cbe15974b8..166c29fe4165a17aae7b8df1d81464b0627cd604 100644 (file)
   - 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