diff --git a/Content.Server/Explosion/Components/ActiveTriggerOnTimedCollideComponent.cs b/Content.Server/Explosion/Components/ActiveTriggerOnTimedCollideComponent.cs new file mode 100644 index 0000000000..51530d1b22 --- /dev/null +++ b/Content.Server/Explosion/Components/ActiveTriggerOnTimedCollideComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Server.Explosion.Components; + +[RegisterComponent] +public sealed class ActiveTriggerOnTimedCollideComponent : Component { } diff --git a/Content.Server/Explosion/Components/TriggerOnActivateComponent.cs b/Content.Server/Explosion/Components/TriggerOnActivateComponent.cs new file mode 100644 index 0000000000..549ab62f8f --- /dev/null +++ b/Content.Server/Explosion/Components/TriggerOnActivateComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Explosion.Components; + +/// +/// Triggers on click. +/// +[RegisterComponent] +public sealed class TriggerOnActivateComponent : Component { } diff --git a/Content.Server/Explosion/Components/TriggerOnTimedCollideComponent.cs b/Content.Server/Explosion/Components/TriggerOnTimedCollideComponent.cs new file mode 100644 index 0000000000..892ce33e49 --- /dev/null +++ b/Content.Server/Explosion/Components/TriggerOnTimedCollideComponent.cs @@ -0,0 +1,18 @@ +namespace Content.Server.Explosion.Components; + +/// +/// Triggers when the entity is overlapped for the specified duration. +/// +[RegisterComponent] +public sealed class TriggerOnTimedCollideComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + [DataField("threshold")] + public float Threshold; + + /// + /// A collection of entities that are colliding with this, and their own unique accumulator. + /// + [ViewVariables] + public readonly Dictionary Colliding = new(); +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs new file mode 100644 index 0000000000..efa39946ee --- /dev/null +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.TimedCollide.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Content.Server.Explosion.Components; +using Content.Server.Explosion.EntitySystems; +using Robust.Shared.Physics.Dynamics; + +namespace Content.Server.Explosion.EntitySystems; + +public sealed partial class TriggerSystem +{ + private void InitializeTimedCollide() + { + SubscribeLocalEvent(OnTimerCollide); + SubscribeLocalEvent(OnTimerEndCollide); + SubscribeLocalEvent(OnComponentRemove); + } + + private void OnTimerCollide(EntityUid uid, TriggerOnTimedCollideComponent component, StartCollideEvent args) + { + //Ensures the entity trigger will have an active component + EnsureComp(uid); + var otherUID = args.OtherFixture.Body.Owner; + component.Colliding.Add(otherUID, 0); + } + + private void OnTimerEndCollide(EntityUid uid, TriggerOnTimedCollideComponent component, EndCollideEvent args) + { + var otherUID = args.OtherFixture.Body.Owner; + component.Colliding.Remove(otherUID); + + if (component.Colliding.Count == 0 && HasComp(uid)) + RemComp(uid); + } + + private void OnComponentRemove(EntityUid uid, TriggerOnTimedCollideComponent component, ComponentRemove args) + { + if (HasComp(uid)) + RemComp(uid); + } + + private void UpdateTimedCollide(float frameTime) + { + foreach (var (activeTrigger, triggerOnTimedCollide) in EntityQuery()) + { + foreach (var (collidingEntity, collidingTimer) in triggerOnTimedCollide.Colliding) + { + triggerOnTimedCollide.Colliding[collidingEntity] += frameTime; + if (collidingTimer > triggerOnTimedCollide.Threshold) + { + RaiseLocalEvent(activeTrigger.Owner, new TriggerEvent(activeTrigger.Owner, collidingEntity)); + triggerOnTimedCollide.Colliding[collidingEntity] -= triggerOnTimedCollide.Threshold; + } + } + } + } +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index dafc127ac7..0d04cf2bb5 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -2,6 +2,8 @@ using Content.Server.Administration.Logs; using Content.Server.Explosion.Components; using Content.Server.Flash; using Content.Server.Flash.Components; +using Content.Server.Sticky.Events; +using Content.Shared.Actions; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Physics; @@ -10,6 +12,7 @@ using Robust.Shared.Player; using Content.Shared.Sound; using Content.Shared.Trigger; using Content.Shared.Database; +using Content.Shared.Interaction; namespace Content.Server.Explosion.EntitySystems { @@ -44,8 +47,10 @@ namespace Content.Server.Explosion.EntitySystems InitializeProximity(); InitializeOnUse(); InitializeSignal(); + InitializeTimedCollide(); SubscribeLocalEvent(OnTriggerCollide); + SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(HandleDeleteTrigger); SubscribeLocalEvent(HandleExplodeTrigger); @@ -76,6 +81,10 @@ namespace Content.Server.Explosion.EntitySystems Trigger(component.Owner); } + private void OnActivate(EntityUid uid, TriggerOnActivateComponent component, ActivateInWorldEvent args) + { + Trigger(component.Owner, args.User); + } public void Trigger(EntityUid trigger, EntityUid? user = null) { @@ -124,6 +133,7 @@ namespace Content.Server.Explosion.EntitySystems UpdateProximity(frameTime); UpdateTimer(frameTime); + UpdateTimedCollide(frameTime); } private void UpdateTimer(float frameTime) diff --git a/Content.Server/Magic/Components/SpellbookComponent.cs b/Content.Server/Magic/Components/SpellbookComponent.cs new file mode 100644 index 0000000000..34a9cd2bba --- /dev/null +++ b/Content.Server/Magic/Components/SpellbookComponent.cs @@ -0,0 +1,36 @@ +using System.Threading; +using Content.Shared.Actions.ActionTypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; + +namespace Content.Server.Magic; + +/// +/// Spellbooks for having an entity learn spells as long as they've read the book and it's in their hand. +/// +[RegisterComponent] +public sealed class SpellbookComponent : Component +{ + /// + /// List of spells that this book has. This is a combination of the WorldSpells, EntitySpells, and InstantSpells. + /// + [ViewVariables] + public readonly List Spells = new(); + + /// + /// The three fields below is just used for initialization. + /// + [DataField("worldSpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + public readonly Dictionary WorldSpells = new(); + + [DataField("entitySpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + public readonly Dictionary EntitySpells = new(); + + [DataField("instantSpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + public readonly Dictionary InstantSpells = new(); + + [ViewVariables] + [DataField("learnTime")] + public float LearnTime = .75f; + + public CancellationTokenSource? CancelToken; +} diff --git a/Content.Server/Magic/Events/InstantSpawnSpellEvent.cs b/Content.Server/Magic/Events/InstantSpawnSpellEvent.cs new file mode 100644 index 0000000000..5b81b29c6f --- /dev/null +++ b/Content.Server/Magic/Events/InstantSpawnSpellEvent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Magic.Events; + +public sealed class InstantSpawnSpellEvent : InstantActionEvent +{ + /// + /// What entity should be spawned. + /// + [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Prototype = default!; + + [ViewVariables, DataField("preventCollide")] + public bool PreventCollideWithCaster = true; + + /// + /// Gets the targeted spawn positons; may lead to multiple entities being spawned. + /// + [DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos(); +} + +[ImplicitDataDefinitionForInheritors] +public abstract class MagicSpawnData +{ + +} + +/// +/// Spawns 1 at the caster's feet. +/// +public sealed class TargetCasterPos : MagicSpawnData {} + +/// +/// Targets the 3 tiles in front of the caster. +/// +public sealed class TargetInFront : MagicSpawnData +{ + [DataField("width")] + public int Width = 3; +} diff --git a/Content.Server/Magic/Events/KnockSpellEvent.cs b/Content.Server/Magic/Events/KnockSpellEvent.cs new file mode 100644 index 0000000000..cc7d3fb9ed --- /dev/null +++ b/Content.Server/Magic/Events/KnockSpellEvent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Actions; +using Content.Shared.Sound; + +namespace Content.Server.Magic.Events; + +public sealed class KnockSpellEvent : InstantActionEvent +{ + /// + /// The range this spell opens doors in + /// 4f is the default + /// + [DataField("range")] + public float Range = 4f; + + [DataField("knockSound")] + public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg"); + + /// + /// Volume control for the spell. + /// -6f is default because the base soundfile is really loud + /// + [DataField("knockVolume")] + public float KnockVolume = -6f; +} diff --git a/Content.Server/Magic/Events/TeleportSpellEvent.cs b/Content.Server/Magic/Events/TeleportSpellEvent.cs new file mode 100644 index 0000000000..a0dc573377 --- /dev/null +++ b/Content.Server/Magic/Events/TeleportSpellEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.Actions; +using Content.Shared.Sound; + +namespace Content.Server.Magic.Events; + +public sealed class TeleportSpellEvent : WorldTargetActionEvent +{ + [DataField("blinkSound")] + public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg"); +} diff --git a/Content.Server/Magic/Events/WorldSpawnSpellEvent.cs b/Content.Server/Magic/Events/WorldSpawnSpellEvent.cs new file mode 100644 index 0000000000..533d9bfcdb --- /dev/null +++ b/Content.Server/Magic/Events/WorldSpawnSpellEvent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Actions; +using Content.Shared.Storage; + +namespace Content.Server.Magic.Events; + +public sealed class WorldSpawnSpellEvent : WorldTargetActionEvent +{ + // TODO:This class needs combining with InstantSpawnSpellEvent + + /// + /// The list of prototypes this spell will spawn + /// + [DataField("prototypes")] + public List Contents = new(); + + // TODO: This offset is liable for deprecation. + /// + /// 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")] + public Vector2 Offset; + + /// + /// Lifetime to set for the entities to self delete + /// + [DataField("lifetime")] public float? Lifetime; +} + diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs new file mode 100644 index 0000000000..53aecb98a5 --- /dev/null +++ b/Content.Server/Magic/MagicSystem.cs @@ -0,0 +1,317 @@ +using System.Threading; +using Content.Server.Coordinates.Helpers; +using Content.Server.Decals; +using Content.Server.DoAfter; +using Content.Server.Doors.Components; +using Content.Server.Magic.Events; +using Content.Server.Spawners.Components; +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; +using Content.Shared.Interaction.Events; +using Content.Shared.Maps; +using Content.Shared.Physics; +using Content.Shared.Storage; +using Robust.Shared.Audio; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Magic; + +/// +/// Handles learning and using spells (actions) +/// +public sealed class MagicSystem : EntitySystem +{ + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedDoorSystem _doorSystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfter = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnUse); + SubscribeLocalEvent(OnLearnComplete); + SubscribeLocalEvent(OnLearnCancel); + + SubscribeLocalEvent(OnInstantSpawn); + SubscribeLocalEvent(OnTeleportSpell); + SubscribeLocalEvent(OnKnockSpell); + SubscribeLocalEvent(OnWorldSpawn); + + } + + private void OnInit(EntityUid uid, SpellbookComponent component, ComponentInit args) + { + //Negative charges means the spell can be used without it running out. + foreach (var (id, charges) in component.WorldSpells) + { + var spell = new WorldTargetAction(_prototypeManager.Index(id)); + _actionsSystem.SetCharges(spell, charges < 0 ? null : charges); + component.Spells.Add(spell); + } + + foreach (var (id, charges) in component.InstantSpells) + { + var spell = new InstantAction(_prototypeManager.Index(id)); + _actionsSystem.SetCharges(spell, charges < 0 ? null : charges); + component.Spells.Add(spell); + } + + foreach (var (id, charges) in component.EntitySpells) + { + var spell = new EntityTargetAction(_prototypeManager.Index(id)); + _actionsSystem.SetCharges(spell, charges < 0 ? null : charges); + component.Spells.Add(spell); + } + } + + 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) + { + if (component.CancelToken != null) return; + + component.CancelToken = new CancellationTokenSource(); + + var doAfterEventArgs = new DoAfterEventArgs(args.User, component.LearnTime, component.CancelToken.Token, uid) + { + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnStun = true, + NeedHand = true, //What, are you going to read with your eyes only?? + TargetFinishedEvent = new LearnDoAfterComplete(args.User), + TargetCancelledEvent = new LearnDoAfterCancel(), + }; + + _doAfter.DoAfter(doAfterEventArgs); + } + + private void OnLearnComplete(EntityUid uid, SpellbookComponent component, LearnDoAfterComplete ev) + { + component.CancelToken = null; + _actionsSystem.AddActions(ev.User, component.Spells, uid); + } + + private void OnLearnCancel(EntityUid uid, SpellbookComponent component, LearnDoAfterCancel args) + { + component.CancelToken = null; + } + + #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; + } + } + + args.Handled = true; + } + + private List GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data) + { + switch (data) + { + case TargetCasterPos: + return new List(1) {casterXform.Coordinates}; + case TargetInFront: + { + // This is shit but you get the idea. + var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized); + + if (!_mapManager.TryGetGrid(casterXform.GridID, 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.MapId) return; + + transform.WorldPosition = args.Target.Position; + transform.AttachToGridOrMap(); + SoundSystem.Play(Filter.Pvs(args.Target), args.BlinkSound.GetSound()); + args.Handled = true; + } + + /// + /// Opens all doors within range + /// + /// + private void OnKnockSpell(KnockSpellEvent args) + { + if (args.Handled) + return; + + //Get the position of the player + var transform = Transform(args.Performer); + var coords = transform.Coordinates; + + SoundSystem.Play(Filter.Pvs(coords), args.KnockSound.GetSound(), 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 airlock)) + airlock.BoltsDown = false; + + if (TryComp(entity, out var doorComp) && doorComp.State is not DoorState.Open) + _doorSystem.StartOpening(doorComp.Owner); + } + + args.Handled = true; + } + + /// + /// 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); + + 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, MapCoordinates mapCoords, float? lifetime, Vector2 offsetVector2) + { + var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random); + + var offsetCoords = mapCoords; + 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 + + #region DoAfterClasses + + private sealed class LearnDoAfterComplete : EntityEventArgs + { + public readonly EntityUid User; + + public LearnDoAfterComplete(EntityUid uid) + { + User = uid; + } + } + + private sealed class LearnDoAfterCancel : EntityEventArgs { } + + #endregion +} diff --git a/Content.Server/Spawners/Components/TimedDespawnComponent.cs b/Content.Server/Spawners/Components/TimedDespawnComponent.cs new file mode 100644 index 0000000000..b5f6e9568c --- /dev/null +++ b/Content.Server/Spawners/Components/TimedDespawnComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Server.Spawners.Components; + +/// +/// Put this component on something you would like to despawn after a certain amount of time +/// +[RegisterComponent] +public sealed class TimedDespawnComponent : Component +{ + /// + /// How long the entity will exist before despawning + /// + [ViewVariables] + [DataField("lifetime")] + public float Lifetime = 5f; +} diff --git a/Content.Server/Spawners/EntitySystems/TimedDespawnSystem.cs b/Content.Server/Spawners/EntitySystems/TimedDespawnSystem.cs new file mode 100644 index 0000000000..172d57b436 --- /dev/null +++ b/Content.Server/Spawners/EntitySystems/TimedDespawnSystem.cs @@ -0,0 +1,19 @@ +using Content.Server.Spawners.Components; + +namespace Content.Server.Spawners.EntitySystems; + +public sealed class TimedDespawnSystem : EntitySystem +{ + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var entity in EntityQuery()) + { + entity.Lifetime -= frameTime; + + if (entity.Lifetime <= 0) + EntityManager.QueueDeleteEntity(entity.Owner); + } + } +} diff --git a/Content.Shared/Physics/PreventCollideComponent.cs b/Content.Shared/Physics/PreventCollideComponent.cs new file mode 100644 index 0000000000..6f11929caa --- /dev/null +++ b/Content.Shared/Physics/PreventCollideComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Physics; + +/// +/// Use this to allow a specific UID to prevent collides +/// +[RegisterComponent, NetworkedComponent] +public sealed class PreventCollideComponent : Component +{ + public EntityUid Uid; +} + +[Serializable, NetSerializable] +public sealed class PreventCollideComponentState : ComponentState +{ + public EntityUid Uid; + + public PreventCollideComponentState(PreventCollideComponent component) + { + Uid = component.Uid; + } +} diff --git a/Content.Shared/Physics/SharedPreventCollideSystem.cs b/Content.Shared/Physics/SharedPreventCollideSystem.cs new file mode 100644 index 0000000000..eb1491a997 --- /dev/null +++ b/Content.Shared/Physics/SharedPreventCollideSystem.cs @@ -0,0 +1,38 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Physics.Dynamics; + +namespace Content.Shared.Physics; + +public sealed class SharedPreventCollideSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnPreventCollide); + } + + private void OnGetState(EntityUid uid, PreventCollideComponent component, ref ComponentGetState args) + { + args.State = new PreventCollideComponentState(component); + } + + private void OnHandleState(EntityUid uid, PreventCollideComponent component, ref ComponentHandleState args) + { + if (args.Current is not PreventCollideComponentState state) + return; + + component.Uid = state.Uid; + } + + private void OnPreventCollide(EntityUid uid, PreventCollideComponent component, PreventCollideEvent args) + { + var otherUid = args.BodyB.Owner; + + if (component.Uid == otherUid) + args.Cancel(); + } + +} diff --git a/Content.Shared/Sound/SoundSpecifier.cs b/Content.Shared/Sound/SoundSpecifier.cs index adea9d2f57..1c89180532 100644 --- a/Content.Shared/Sound/SoundSpecifier.cs +++ b/Content.Shared/Sound/SoundSpecifier.cs @@ -3,13 +3,14 @@ using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Utility; namespace Content.Shared.Sound { - [ImplicitDataDefinitionForInheritors] + [ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] public abstract class SoundSpecifier { [ViewVariables(VVAccess.ReadWrite), DataField("params")] @@ -19,6 +20,7 @@ namespace Content.Shared.Sound public abstract string GetSound(IRobustRandom? rand = null, IPrototypeManager? proto = null); } + [Serializable, NetSerializable] public sealed class SoundPathSpecifier : SoundSpecifier { public const string Node = "path"; @@ -47,6 +49,7 @@ namespace Content.Shared.Sound } } + [Serializable, NetSerializable] public sealed class SoundCollectionSpecifier : SoundSpecifier { public const string Node = "collection"; diff --git a/Resources/Audio/Magic/blink.ogg b/Resources/Audio/Magic/blink.ogg new file mode 100644 index 0000000000..58b7cc8562 Binary files /dev/null and b/Resources/Audio/Magic/blink.ogg differ diff --git a/Resources/Audio/Magic/forcewall.ogg b/Resources/Audio/Magic/forcewall.ogg new file mode 100644 index 0000000000..4fbca3f42b Binary files /dev/null and b/Resources/Audio/Magic/forcewall.ogg differ diff --git a/Resources/Audio/Magic/knock.ogg b/Resources/Audio/Magic/knock.ogg new file mode 100644 index 0000000000..fe2f54f97a Binary files /dev/null and b/Resources/Audio/Magic/knock.ogg differ diff --git a/Resources/Audio/Magic/licenses.txt b/Resources/Audio/Magic/licenses.txt new file mode 100644 index 0000000000..d6cf718f20 --- /dev/null +++ b/Resources/Audio/Magic/licenses.txt @@ -0,0 +1,5 @@ +https://github.com/Citadel-Station-13/Citadel-Station-13/blob/master/sound/magic/ForceWall.ogg +https://github.com/Citadel-Station-13/Citadel-Station-13/blob/master/sound/magic/blink.ogg +https://github.com/Citadel-Station-13/Citadel-Station-13/blob/master/sound/magic/Knock.ogg + +copyright: CC BY-SA 3.0 \ No newline at end of file diff --git a/Resources/Locale/en-US/magic/spells-actions.ftl b/Resources/Locale/en-US/magic/spells-actions.ftl new file mode 100644 index 0000000000..2c956b9664 --- /dev/null +++ b/Resources/Locale/en-US/magic/spells-actions.ftl @@ -0,0 +1,17 @@ +action-name-spell-rune-flash = Flash Rune +action-description-spell-rune-flash = Summons a rune that flashes if used. + +action-name-spell-forcewall = Forcewall +action-description-spell-forcewall = Creates a magical barrier. +action-speech-spell-forcewall = TARCOL MINTI ZHERI + +action-name-spell-knock = Knock +action-description-spell-knock = This spell opens nearby doors. +action-speech-spell-knock = AULIE OXIN FIERA + +action-name-spell-blink = Blink +action-description-spell-blink = Teleport to the clicked location. + +action-name-spell-summon-magicarp = Summon Magicarp +action-description-spell-summon-magicarp = This spell summons three Magi-Carp to your aid! May or may not turn on user. +action-speech-spell-summon-magicarp = AIE KHUSE EU diff --git a/Resources/Prototypes/Entities/Objects/Magic/books.yml b/Resources/Prototypes/Entities/Objects/Magic/books.yml new file mode 100644 index 0000000000..2a939f0942 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Magic/books.yml @@ -0,0 +1,65 @@ +- type: entity + id: BaseSpellbook + name: spellbook + parent: BaseItem + abstract: true + components: + - type: Sprite + netsync: false + sprite: Objects/Misc/books.rsi + layers: + - state: book_demonomicon + - type: Spellbook + +- type: entity + id: SpawnSpellbook + name: spawn spellbook + parent: BaseSpellbook + components: + - type: Spellbook + instantSpells: + FlashRune: -1 + worldSpells: + SpawnMagicarpSpell: -1 + +- type: entity + id: ForceWallSpellbook + name: force wall spellbook + parent: BaseSpellbook + components: + - type: Sprite + netsync: false + sprite: Objects/Magic/spellbooks.rsi + layers: + - state: bookforcewall + - type: Spellbook + instantSpells: + ForceWall: -1 + +- type: entity + id: BlinkBook + name: blink spellbook + parent: BaseSpellbook + components: + - type: Sprite + netsync: false + sprite: Objects/Magic/spellbooks.rsi + layers: + - state: spellbook + - type: Spellbook + worldSpells: + Blink: -1 + +- type: entity + id: KnockSpellbook + name: knock spellbook + parent: BaseSpellbook + components: + - type: Sprite + netsync: false + sprite: Objects/Magic/spellbooks.rsi + layers: + - state: bookknock + - type: Spellbook + instantSpells: + Knock: -1 diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml index 5c5fa19a2b..efb0286889 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml @@ -714,3 +714,31 @@ layer: - GlassLayer - type: Airtight + +- type: entity + id: WallForce + name: Force Wall + components: + - type: TimedDespawn + lifetime: 20 + - type: Tag + tags: + - Wall + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + mask: + - FullTileMask + layer: + - WallLayer + - type: Airtight + - type: Sprite + sprite: Structures/Magic/forcewall.rsi + state: forcewall + - type: Icon + sprite: Structures/Magic/forcewall.rsi + state: forcewall diff --git a/Resources/Prototypes/Magic/Fixtures/runes.yml b/Resources/Prototypes/Magic/Fixtures/runes.yml new file mode 100644 index 0000000000..b4aaa7753e --- /dev/null +++ b/Resources/Prototypes/Magic/Fixtures/runes.yml @@ -0,0 +1,141 @@ +- type: entity + id: BaseRune + name: "rune" + abstract: true + placement: + mode: SnapgridCenter + components: + - type: Clickable + - type: Sprite + sprite: Structures/Magic/Cult/rune.rsi + netsync: false + layers: + - state: cult2 + color: '#FF00FF' + +- type: entity + parent: BaseRune + id: CollideRune + name: "collision rune" + abstract: true + components: + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.4,-0.4,0.4,0.4" + hard: false + id: rune + mask: + - ItemMask + layer: + - SlipLayer + - type: Physics + +- type: entity + parent: CollideRune + id: ActivateRune + name: "activation rune" + abstract: true + components: + - type: TriggerOnActivate + +- type: entity + parent: CollideRune + id: CollideTimerRune + name: "collision timed rune" + abstract: true + components: + - type: TriggerOnTimedCollide + threshold: 5 + +- type: entity + parent: CollideRune + id: ExplosionRune + name: "explosion rune" + components: + - type: TriggerOnCollide + fixtureID: rune + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Cryo + totalIntensity: 20.0 + intensitySlope: 5 + maxIntensity: 4 + - type: Sprite + sprite: Structures/Magic/Cult/trap.rsi + layers: + - state: trap + color: '#FF770055' + +- type: entity + parent: CollideRune + id: StunRune + name: "stun rune" + components: + - type: TriggerOnCollide + fixtureID: rune + - type: DeleteOnTrigger + - type: StunOnCollide + stunAmount: 5 + knockdownAmount: 3 + - type: Sprite + sprite: Structures/Magic/Cult/trap.rsi + layers: + - state: trap + color: '#FFFF0055' + +- type: entity + parent: CollideRune + id: IgniteRune + name: "ignite rune" + components: + - type: TriggerOnCollide + fixtureID: rune + - type: DeleteOnTrigger + - type: IgniteOnCollide + fireStacks: 10 + - type: Sprite + sprite: Structures/Magic/Cult/trap.rsi + layers: + - state: trap + color: '#FF000055' + +- type: entity + parent: CollideTimerRune + id: ExplosionTimedRune + name: "explosion timed rune" + components: + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Cryo + totalIntensity: 20.0 + intensitySlope: 5 + maxIntensity: 4 + +- type: entity + parent: ActivateRune + id: ExplosionActivateRune + name: "explosion activated rune" + components: + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Cryo + totalIntensity: 20.0 + intensitySlope: 5 + maxIntensity: 4 + +- type: entity + parent: ActivateRune + id: FlashRune + name: "flash rune" + components: + - type: FlashOnTrigger + - type: DeleteOnTrigger + +- type: entity + parent: CollideTimerRune + id: FlashRuneTimer + name: "flash timed rune" + components: + - type: FlashOnTrigger diff --git a/Resources/Prototypes/Magic/forcewall_spells.yml b/Resources/Prototypes/Magic/forcewall_spells.yml new file mode 100644 index 0000000000..ab5c038ba9 --- /dev/null +++ b/Resources/Prototypes/Magic/forcewall_spells.yml @@ -0,0 +1,15 @@ +- type: instantAction + id: ForceWall + name: action-name-spell-forcewall + description: action-description-spell-forcewall + useDelay: 10 + speech: action-speech-spell-forcewall + itemIconStyle: BigAction + sound: !type:SoundPathSpecifier + path: /Audio/Magic/forcewall.ogg + icon: + sprite: Objects/Magic/magicactions.rsi + state: shield + serverEvent: !type:InstantSpawnSpellEvent + prototype: WallForce + posData: !type:TargetInFront diff --git a/Resources/Prototypes/Magic/knock_spell.yml b/Resources/Prototypes/Magic/knock_spell.yml new file mode 100644 index 0000000000..3cfade9290 --- /dev/null +++ b/Resources/Prototypes/Magic/knock_spell.yml @@ -0,0 +1,11 @@ +- type: instantAction + id: Knock + name: action-name-spell-knock + description: action-description-spell-knock + useDelay: 10 + speech: action-speech-spell-knock + itemIconStyle: BigAction + icon: + sprite: Objects/Magic/magicactions.rsi + state: knock + serverEvent: !type:KnockSpellEvent diff --git a/Resources/Prototypes/Magic/rune_spells.yml b/Resources/Prototypes/Magic/rune_spells.yml new file mode 100644 index 0000000000..35d74c7007 --- /dev/null +++ b/Resources/Prototypes/Magic/rune_spells.yml @@ -0,0 +1,11 @@ +- type: instantAction + id: FlashRune + name: action-name-spell-rune-flash + description: action-description-spell-rune-flash + useDelay: 10 + itemIconStyle: BigAction + icon: + sprite: Objects/Magic/magicactions.rsi + state: spell_default + serverEvent: !type:InstantSpawnSpellEvent + prototype: FlashRune diff --git a/Resources/Prototypes/Magic/spawn_spells.yml b/Resources/Prototypes/Magic/spawn_spells.yml new file mode 100644 index 0000000000..98aa3639a6 --- /dev/null +++ b/Resources/Prototypes/Magic/spawn_spells.yml @@ -0,0 +1,16 @@ +- type: worldTargetAction + id: SpawnMagicarpSpell + name: action-name-spell-summon-magicarp + description: action-description-spell-summon-magicarp + useDelay: 10 + range: 4 + speech: action-speech-spell-summon-magicarp + itemIconStyle: BigAction + icon: + sprite: Objects/Magic/magicactions.rsi + state: spell_default + serverEvent: !type:WorldSpawnSpellEvent + prototypes: + - id: MobCarpMagic + amount: 3 + offsetVector2: 0, 1 diff --git a/Resources/Prototypes/Magic/teleport_spells.yml b/Resources/Prototypes/Magic/teleport_spells.yml new file mode 100644 index 0000000000..230ae024b7 --- /dev/null +++ b/Resources/Prototypes/Magic/teleport_spells.yml @@ -0,0 +1,14 @@ +- type: worldTargetAction + id: Blink + name: action-name-spell-blink + description: action-description-spell-blink + useDelay: 10 + range: 16 # default examine-range. + # ^ should probably add better validation that the clicked location is on the users screen somewhere, + itemIconStyle: BigAction + checkCanAccess: false + repeat: true + icon: + sprite: Objects/Magic/magicactions.rsi + state: blink + serverEvent: !type:TeleportSpellEvent diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/blink.png b/Resources/Textures/Objects/Magic/magicactions.rsi/blink.png new file mode 100644 index 0000000000..1e5f6fdd4c Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/blink.png differ diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/knock.png b/Resources/Textures/Objects/Magic/magicactions.rsi/knock.png new file mode 100644 index 0000000000..00caca7b03 Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/knock.png differ diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json b/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json new file mode 100644 index 0000000000..e8acf68246 --- /dev/null +++ b/Resources/Textures/Objects/Magic/magicactions.rsi/meta.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/78db6bd5c2b2b3d1f5cd8fd75be3a39d5d929943", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "spell_default" + }, + { + "name": "shield" + }, + { + "name": "knock" + }, + { + "name": "blink" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/shield.png b/Resources/Textures/Objects/Magic/magicactions.rsi/shield.png new file mode 100644 index 0000000000..8d1562b489 Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/shield.png differ diff --git a/Resources/Textures/Objects/Magic/magicactions.rsi/spell_default.png b/Resources/Textures/Objects/Magic/magicactions.rsi/spell_default.png new file mode 100644 index 0000000000..e7780ff2ca Binary files /dev/null and b/Resources/Textures/Objects/Magic/magicactions.rsi/spell_default.png differ diff --git a/Resources/Textures/Objects/Magic/spellbooks.rsi/bookfireball.png b/Resources/Textures/Objects/Magic/spellbooks.rsi/bookfireball.png new file mode 100644 index 0000000000..ed18010a72 Binary files /dev/null and b/Resources/Textures/Objects/Magic/spellbooks.rsi/bookfireball.png differ diff --git a/Resources/Textures/Objects/Magic/spellbooks.rsi/bookforcewall.png b/Resources/Textures/Objects/Magic/spellbooks.rsi/bookforcewall.png new file mode 100644 index 0000000000..3f35598953 Binary files /dev/null and b/Resources/Textures/Objects/Magic/spellbooks.rsi/bookforcewall.png differ diff --git a/Resources/Textures/Objects/Magic/spellbooks.rsi/bookknock.png b/Resources/Textures/Objects/Magic/spellbooks.rsi/bookknock.png new file mode 100644 index 0000000000..9f5d68af7d Binary files /dev/null and b/Resources/Textures/Objects/Magic/spellbooks.rsi/bookknock.png differ diff --git a/Resources/Textures/Objects/Magic/spellbooks.rsi/meta.json b/Resources/Textures/Objects/Magic/spellbooks.rsi/meta.json new file mode 100644 index 0000000000..f9f1703f24 --- /dev/null +++ b/Resources/Textures/Objects/Magic/spellbooks.rsi/meta.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/f3e328af032f0ba0234b866c24ccb0003e1a4993", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "bookfireball", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "bookforcewall", + "delays": [ + [ + 0.1, + 0.1 + ] + ] + }, + { + "name": "bookknock", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "spellbook" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Magic/spellbooks.rsi/spellbook.png b/Resources/Textures/Objects/Magic/spellbooks.rsi/spellbook.png new file mode 100644 index 0000000000..d24f198f94 Binary files /dev/null and b/Resources/Textures/Objects/Magic/spellbooks.rsi/spellbook.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult1.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult1.png new file mode 100644 index 0000000000..a0bd42ac96 Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult1.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult2.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult2.png new file mode 100644 index 0000000000..89a090114a Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult2.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult3.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult3.png new file mode 100644 index 0000000000..b3c443c359 Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult3.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult4.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult4.png new file mode 100644 index 0000000000..27d2c2ea7d Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult4.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult5.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult5.png new file mode 100644 index 0000000000..7e1da8e108 Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult5.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult6.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult6.png new file mode 100644 index 0000000000..9ddc70fa7d Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult6.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult7.png b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult7.png new file mode 100644 index 0000000000..f970fb6572 Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/rune.rsi/cult7.png differ diff --git a/Resources/Textures/Structures/Magic/Cult/rune.rsi/meta.json b/Resources/Textures/Structures/Magic/Cult/rune.rsi/meta.json new file mode 100644 index 0000000000..182c0b5afc --- /dev/null +++ b/Resources/Textures/Structures/Magic/Cult/rune.rsi/meta.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/f7c09077d2fb8a11fdc11e5e780f8e337f60ef85", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "cult1" + }, + { + "name": "cult2" + }, + { + "name": "cult3" + }, + { + "name": "cult4" + }, + { + "name": "cult5" + }, + { + "name": "cult6" + }, + { + "name": "cult7" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Magic/Cult/trap.rsi/meta.json b/Resources/Textures/Structures/Magic/Cult/trap.rsi/meta.json new file mode 100644 index 0000000000..fb18bcc7bc --- /dev/null +++ b/Resources/Textures/Structures/Magic/Cult/trap.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/95db5084abc411e77b5d994b473f4456ac72139a", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "trap" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Magic/Cult/trap.rsi/trap.png b/Resources/Textures/Structures/Magic/Cult/trap.rsi/trap.png new file mode 100644 index 0000000000..a522ea67fa Binary files /dev/null and b/Resources/Textures/Structures/Magic/Cult/trap.rsi/trap.png differ diff --git a/Resources/Textures/Structures/Magic/forcewall.rsi/forcewall.png b/Resources/Textures/Structures/Magic/forcewall.rsi/forcewall.png new file mode 100644 index 0000000000..fc5b2a6863 Binary files /dev/null and b/Resources/Textures/Structures/Magic/forcewall.rsi/forcewall.png differ diff --git a/Resources/Textures/Structures/Magic/forcewall.rsi/forcewallspawn.png b/Resources/Textures/Structures/Magic/forcewall.rsi/forcewallspawn.png new file mode 100644 index 0000000000..2a3fac583d Binary files /dev/null and b/Resources/Textures/Structures/Magic/forcewall.rsi/forcewallspawn.png differ diff --git a/Resources/Textures/Structures/Magic/forcewall.rsi/meta.json b/Resources/Textures/Structures/Magic/forcewall.rsi/meta.json new file mode 100644 index 0000000000..56a718f9e4 --- /dev/null +++ b/Resources/Textures/Structures/Magic/forcewall.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/475a7d2499f6c29f31488799902b7cf0f7f495b6", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "forcewallspawn", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.3, + 0.1 + ] + ] + }, + { + "name": "forcewall" + } + ] +} \ No newline at end of file