Simple Magic Spellbook System (#7823)

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
keronshb
2022-05-29 02:29:10 -04:00
committed by GitHub
parent fb29fd5ecb
commit 11f729d024
52 changed files with 1120 additions and 1 deletions

View File

@@ -0,0 +1,4 @@
namespace Content.Server.Explosion.Components;
[RegisterComponent]
public sealed class ActiveTriggerOnTimedCollideComponent : Component { }

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers on click.
/// </summary>
[RegisterComponent]
public sealed class TriggerOnActivateComponent : Component { }

View File

@@ -0,0 +1,18 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers when the entity is overlapped for the specified duration.
/// </summary>
[RegisterComponent]
public sealed class TriggerOnTimedCollideComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("threshold")]
public float Threshold;
/// <summary>
/// A collection of entities that are colliding with this, and their own unique accumulator.
/// </summary>
[ViewVariables]
public readonly Dictionary<EntityUid, float> Colliding = new();
}

View File

@@ -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<TriggerOnTimedCollideComponent, StartCollideEvent>(OnTimerCollide);
SubscribeLocalEvent<TriggerOnTimedCollideComponent, EndCollideEvent>(OnTimerEndCollide);
SubscribeLocalEvent<TriggerOnTimedCollideComponent, ComponentRemove>(OnComponentRemove);
}
private void OnTimerCollide(EntityUid uid, TriggerOnTimedCollideComponent component, StartCollideEvent args)
{
//Ensures the entity trigger will have an active component
EnsureComp<ActiveTriggerOnTimedCollideComponent>(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<ActiveTriggerOnTimedCollideComponent>(uid))
RemComp<ActiveTriggerOnTimedCollideComponent>(uid);
}
private void OnComponentRemove(EntityUid uid, TriggerOnTimedCollideComponent component, ComponentRemove args)
{
if (HasComp<ActiveTriggerOnTimedCollideComponent>(uid))
RemComp<ActiveTriggerOnTimedCollideComponent>(uid);
}
private void UpdateTimedCollide(float frameTime)
{
foreach (var (activeTrigger, triggerOnTimedCollide) in EntityQuery<ActiveTriggerOnTimedCollideComponent, TriggerOnTimedCollideComponent>())
{
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;
}
}
}
}
}

View File

@@ -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<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
SubscribeLocalEvent<ExplodeOnTriggerComponent, TriggerEvent>(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)

View File

@@ -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;
/// <summary>
/// Spellbooks for having an entity learn spells as long as they've read the book and it's in their hand.
/// </summary>
[RegisterComponent]
public sealed class SpellbookComponent : Component
{
/// <summary>
/// List of spells that this book has. This is a combination of the WorldSpells, EntitySpells, and InstantSpells.
/// </summary>
[ViewVariables]
public readonly List<ActionType> Spells = new();
/// <summary>
/// The three fields below is just used for initialization.
/// </summary>
[DataField("worldSpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, WorldTargetActionPrototype>))]
public readonly Dictionary<string, int> WorldSpells = new();
[DataField("entitySpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, EntityTargetActionPrototype>))]
public readonly Dictionary<string, int> EntitySpells = new();
[DataField("instantSpells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, InstantActionPrototype>))]
public readonly Dictionary<string, int> InstantSpells = new();
[ViewVariables]
[DataField("learnTime")]
public float LearnTime = .75f;
public CancellationTokenSource? CancelToken;
}

View File

@@ -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
{
/// <summary>
/// What entity should be spawned.
/// </summary>
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = default!;
[ViewVariables, DataField("preventCollide")]
public bool PreventCollideWithCaster = true;
/// <summary>
/// Gets the targeted spawn positons; may lead to multiple entities being spawned.
/// </summary>
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
}
[ImplicitDataDefinitionForInheritors]
public abstract class MagicSpawnData
{
}
/// <summary>
/// Spawns 1 at the caster's feet.
/// </summary>
public sealed class TargetCasterPos : MagicSpawnData {}
/// <summary>
/// Targets the 3 tiles in front of the caster.
/// </summary>
public sealed class TargetInFront : MagicSpawnData
{
[DataField("width")]
public int Width = 3;
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Actions;
using Content.Shared.Sound;
namespace Content.Server.Magic.Events;
public sealed class KnockSpellEvent : InstantActionEvent
{
/// <summary>
/// The range this spell opens doors in
/// 4f is the default
/// </summary>
[DataField("range")]
public float Range = 4f;
[DataField("knockSound")]
public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg");
/// <summary>
/// Volume control for the spell.
/// -6f is default because the base soundfile is really loud
/// </summary>
[DataField("knockVolume")]
public float KnockVolume = -6f;
}

View File

@@ -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");
}

View File

@@ -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
/// <summary>
/// The list of prototypes this spell will spawn
/// </summary>
[DataField("prototypes")]
public List<EntitySpawnEntry> Contents = new();
// TODO: This offset is liable for deprecation.
/// <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")]
public Vector2 Offset;
/// <summary>
/// Lifetime to set for the entities to self delete
/// </summary>
[DataField("lifetime")] public float? Lifetime;
}

View File

@@ -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;
/// <summary>
/// Handles learning and using spells (actions)
/// </summary>
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<SpellbookComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<SpellbookComponent, LearnDoAfterComplete>(OnLearnComplete);
SubscribeLocalEvent<SpellbookComponent, LearnDoAfterCancel>(OnLearnCancel);
SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
SubscribeLocalEvent<WorldSpawnSpellEvent>(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<WorldTargetActionPrototype>(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<InstantActionPrototype>(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<EntityTargetActionPrototype>(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
/// <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;
}
}
args.Handled = true;
}
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 (!_mapManager.TryGetGrid(casterXform.GridID, 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.MapId) return;
transform.WorldPosition = args.Target.Position;
transform.AttachToGridOrMap();
SoundSystem.Play(Filter.Pvs(args.Target), args.BlinkSound.GetSound());
args.Handled = true;
}
/// <summary>
/// Opens all doors within range
/// </summary>
/// <param name="args"></param>
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<AirlockComponent>(entity, out var airlock))
airlock.BoltsDown = false;
if (TryComp<DoorComponent>(entity, out var doorComp) && doorComp.State is not DoorState.Open)
_doorSystem.StartOpening(doorComp.Owner);
}
args.Handled = true;
}
/// <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);
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="mapCoords"> 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, 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<TimedDespawnComponent>(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
}

View File

@@ -0,0 +1,15 @@
namespace Content.Server.Spawners.Components;
/// <summary>
/// Put this component on something you would like to despawn after a certain amount of time
/// </summary>
[RegisterComponent]
public sealed class TimedDespawnComponent : Component
{
/// <summary>
/// How long the entity will exist before despawning
/// </summary>
[ViewVariables]
[DataField("lifetime")]
public float Lifetime = 5f;
}

View File

@@ -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<TimedDespawnComponent>())
{
entity.Lifetime -= frameTime;
if (entity.Lifetime <= 0)
EntityManager.QueueDeleteEntity(entity.Owner);
}
}
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Physics;
/// <summary>
/// Use this to allow a specific UID to prevent collides
/// </summary>
[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;
}
}

View File

@@ -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<PreventCollideComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<PreventCollideComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<PreventCollideComponent, PreventCollideEvent>(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();
}
}

View File

@@ -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";

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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"
}
]
}