Magic Refactor + Wizard Grimoire (#22568)
* 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>
This commit is contained in:
5
Content.Client/Magic/MagicSystem.cs
Normal file
5
Content.Client/Magic/MagicSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Magic;
|
||||
|
||||
namespace Content.Client.Magic;
|
||||
|
||||
public sealed class MagicSystem : SharedMagicSystem;
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,407 +1,22 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Server.Magic.Components;
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Coordinates.Helpers;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Magic;
|
||||
using Content.Shared.Magic.Events;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Spawners;
|
||||
|
||||
namespace Content.Server.Magic;
|
||||
|
||||
/// <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);
|
||||
SubscribeLocalEvent<SpeakSpellEvent>(OnSpellSpoken);
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, SpellbookComponent component, DoAfterEvent args)
|
||||
private void OnSpellSpoken(ref SpeakSpellEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
if (!component.LearnPermanently)
|
||||
{
|
||||
_actionsSystem.GrantActions(args.Args.User, component.Spells, uid);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (id, charges) in component.SpellActions)
|
||||
{
|
||||
// TOOD store spells entity ids on some sort of innate magic user component or something like that.
|
||||
EntityUid? actionId = null;
|
||||
if (_actionsSystem.AddAction(args.Args.User, ref actionId, id))
|
||||
_actionsSystem.SetCharges(actionId, charges < 0 ? null : charges);
|
||||
}
|
||||
|
||||
component.SpellActions.Clear();
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, SpellbookComponent component, MapInitEvent args)
|
||||
{
|
||||
if (component.LearnPermanently)
|
||||
return;
|
||||
|
||||
foreach (var (id, charges) in component.SpellActions)
|
||||
{
|
||||
var spell = _actionContainer.AddAction(uid, id);
|
||||
if (spell == null)
|
||||
continue;
|
||||
|
||||
_actionsSystem.SetCharges(spell, charges < 0 ? null : charges);
|
||||
component.Spells.Add(spell.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUse(EntityUid uid, SpellbookComponent component, UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
AttemptLearn(uid, component, args);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void AttemptLearn(EntityUid uid, SpellbookComponent component, UseInHandEvent args)
|
||||
{
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.LearnTime, new SpellbookDoAfterEvent(), uid, target: uid)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true //What, are you going to read with your eyes only??
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterEventArgs);
|
||||
}
|
||||
|
||||
#region Spells
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens all doors within range
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
private void OnKnockSpell(KnockSpellEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
Speak(args);
|
||||
|
||||
//Get the position of the player
|
||||
var transform = Transform(args.Performer);
|
||||
var coords = transform.Coordinates;
|
||||
|
||||
_audio.PlayPvs(args.KnockSound, args.Performer, AudioParams.Default.WithVolume(args.KnockVolume));
|
||||
|
||||
//Look for doors and don't open them if they're already open.
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(coords, args.Range))
|
||||
{
|
||||
if (TryComp<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Store.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Store.Systems;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
39
Content.Shared/Magic/Components/MagicComponent.cs
Normal file
39
Content.Shared/Magic/Components/MagicComponent.cs
Normal file
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
10
Content.Shared/Magic/Components/WizardClothesComponent.cs
Normal file
10
Content.Shared/Magic/Components/WizardClothesComponent.cs
Normal file
@@ -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;
|
||||
12
Content.Shared/Magic/Events/BeforeCastSpellEvent.cs
Normal file
12
Content.Shared/Magic/Events/BeforeCastSpellEvent.cs
Normal file
@@ -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;
|
||||
}
|
||||
18
Content.Shared/Magic/Events/ChargeSpellEvent.cs
Normal file
18
Content.Shared/Magic/Events/ChargeSpellEvent.cs
Normal file
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
8
Content.Shared/Magic/Events/SpeakSpellEvent.cs
Normal file
8
Content.Shared/Magic/Events/SpeakSpellEvent.cs
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
25
Content.Shared/Magic/MagicInstantSpawnData.cs
Normal file
25
Content.Shared/Magic/MagicInstantSpawnData.cs
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
519
Content.Shared/Magic/SharedMagicSystem.cs
Normal file
519
Content.Shared/Magic/SharedMagicSystem.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
96
Content.Shared/Magic/SpellbookSystem.cs
Normal file
96
Content.Shared/Magic/SpellbookSystem.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
1
Resources/Locale/en-US/magic/magic.ftl
Normal file
1
Resources/Locale/en-US/magic/magic.ftl
Normal file
@@ -0,0 +1 @@
|
||||
spell-requirements-failed = Missing requirements to cast this spell!
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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™
|
||||
|
||||
35
Resources/Locale/en-US/store/spellbook-catalog.ftl
Normal file
35
Resources/Locale/en-US/store/spellbook-catalog.ftl
Normal file
@@ -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!
|
||||
@@ -14,3 +14,33 @@
|
||||
- type: InstantAction
|
||||
event: !type:PolymorphActionEvent
|
||||
itemIconStyle: NoItem
|
||||
|
||||
- type: entity
|
||||
id: ActionPolymorphWizardSpider
|
||||
name: Spider Polymorph
|
||||
description: Polymorphs you into a Spider.
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: InstantAction
|
||||
useDelay: 60
|
||||
event: !type:PolymorphActionEvent
|
||||
protoId: WizardSpider
|
||||
itemIconStyle: NoItem
|
||||
icon:
|
||||
sprite: Mobs/Animals/spider.rsi
|
||||
state: tarantula
|
||||
|
||||
- type: entity
|
||||
id: ActionPolymorphWizardRod
|
||||
name: Rod Form
|
||||
description: CLANG!
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: InstantAction
|
||||
useDelay: 60
|
||||
event: !type:PolymorphActionEvent
|
||||
protoId: WizardRod
|
||||
itemIconStyle: NoItem
|
||||
icon:
|
||||
sprite: Objects/Fun/immovable_rod.rsi
|
||||
state: icon
|
||||
|
||||
140
Resources/Prototypes/Catalog/spellbook_catalog.yml
Normal file
140
Resources/Prototypes/Catalog/spellbook_catalog.yml
Normal file
@@ -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
|
||||
@@ -390,7 +390,7 @@
|
||||
- Snout
|
||||
|
||||
- type: entity
|
||||
parent: ClothingHeadBase
|
||||
parent: ClothingHeadHatWizardBase
|
||||
id: ClothingHeadHatRedwizard
|
||||
name: red wizard hat
|
||||
description: Strange-looking red hat-wear that most certainly belongs to a real magic user.
|
||||
@@ -505,7 +505,7 @@
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: ClothingHeadBase
|
||||
parent: ClothingHeadHatWizardBase
|
||||
id: ClothingHeadHatVioletwizard
|
||||
name: violet wizard hat
|
||||
description: "Strange-looking violet hat-wear that most certainly belongs to a real magic user."
|
||||
@@ -534,6 +534,7 @@
|
||||
name: witch hat
|
||||
description: A witch hat.
|
||||
components:
|
||||
- type: WizardClothes #Yes this will count
|
||||
- type: Sprite
|
||||
sprite: Clothing/Head/Hats/witch.rsi
|
||||
- type: Clothing
|
||||
@@ -558,7 +559,14 @@
|
||||
sprite: Clothing/Head/Hats/wizard_fake.rsi
|
||||
|
||||
- type: entity
|
||||
abstract: true
|
||||
parent: ClothingHeadBase
|
||||
id: ClothingHeadHatWizardBase
|
||||
components:
|
||||
- type: WizardClothes
|
||||
|
||||
- type: entity
|
||||
parent: ClothingHeadHatWizardBase
|
||||
id: ClothingHeadHatWizard
|
||||
name: wizard hat
|
||||
description: Strange-looking blue hat-wear that most certainly belongs to a powerful magic user.
|
||||
|
||||
@@ -166,9 +166,16 @@
|
||||
- type: Clothing
|
||||
sprite: Clothing/OuterClothing/Misc/santa.rsi
|
||||
|
||||
- type: entity
|
||||
abstract: true
|
||||
parent: ClothingOuterBase
|
||||
id: ClothingOuterWizardBase
|
||||
components:
|
||||
- type: WizardClothes
|
||||
|
||||
# Is this wizard wearing a fanny pack???
|
||||
- type: entity
|
||||
parent: ClothingOuterBase
|
||||
parent: ClothingOuterWizardBase
|
||||
id: ClothingOuterWizardViolet
|
||||
name: violet wizard robes
|
||||
description: A bizarre gem-encrusted violet robe that radiates magical energies.
|
||||
@@ -179,7 +186,7 @@
|
||||
sprite: Clothing/OuterClothing/Misc/violetwizard.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingOuterBase
|
||||
parent: ClothingOuterWizardBase
|
||||
id: ClothingOuterWizard
|
||||
name: wizard robes
|
||||
description: A bizarre gem-encrusted blue robe that radiates magical energies.
|
||||
@@ -190,7 +197,7 @@
|
||||
sprite: Clothing/OuterClothing/Misc/wizard.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingOuterBase
|
||||
parent: ClothingOuterWizardBase
|
||||
id: ClothingOuterWizardRed
|
||||
name: red wizard robes
|
||||
description: Strange-looking, red, hat-wear that most certainly belongs to a real magic user.
|
||||
|
||||
@@ -2345,6 +2345,18 @@
|
||||
bloodMaxVolume: 150
|
||||
bloodReagent: Laughter
|
||||
|
||||
- type: entity
|
||||
name: wizard spider
|
||||
parent: MobGiantSpider
|
||||
id: MobGiantSpiderWizard
|
||||
description: This spider looks a little magical
|
||||
suffix: Wizard
|
||||
components:
|
||||
- type: Accentless
|
||||
removes:
|
||||
- type: ReplacementAccent
|
||||
accent: xeno #let this wizard speak
|
||||
|
||||
- type: entity
|
||||
name: possum
|
||||
parent: SimpleMobBase
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
layers:
|
||||
- state: paper_blood
|
||||
- state: cover_strong
|
||||
color: "#645a5a"
|
||||
color: "#645a5a"
|
||||
- state: decor_wingette_flat
|
||||
color: "#4d0303"
|
||||
- state: icon_pentagramm
|
||||
@@ -19,13 +19,58 @@
|
||||
tags:
|
||||
- Spellbook
|
||||
|
||||
# For the Wizard Antag
|
||||
# Do not add discounts or price inflation
|
||||
- type: entity
|
||||
id: WizardsGrimoire
|
||||
name: wizards grimoire
|
||||
suffix: Wizard
|
||||
parent: BaseItem
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Misc/books.rsi
|
||||
layers:
|
||||
- state: paper_blood
|
||||
- state: cover_strong
|
||||
color: "#645a5a"
|
||||
- state: decor_wingette_flat
|
||||
color: "#4d0303"
|
||||
- state: icon_pentagramm
|
||||
color: "#f7e19f"
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.StoreUiKey.Key:
|
||||
type: StoreBoundUserInterface
|
||||
- type: ActivatableUI
|
||||
key: enum.StoreUiKey.Key
|
||||
- type: Store
|
||||
refundAllowed: true
|
||||
ownerOnly: true # get your own tome!
|
||||
preset: StorePresetSpellbook
|
||||
balance:
|
||||
WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are
|
||||
|
||||
# Not meant for wizard antag but meant for spawning, so people can't abuse refund if they were given a tome
|
||||
- type: entity
|
||||
id: WizardsGrimoireNoRefund
|
||||
name: wizards grimoire
|
||||
suffix: Wizard, No Refund
|
||||
parent: WizardsGrimoire
|
||||
components:
|
||||
- type: Store
|
||||
refundAllowed: false
|
||||
ownerOnly: true # get your own tome!
|
||||
preset: StorePresetSpellbook
|
||||
balance:
|
||||
WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are
|
||||
|
||||
- type: entity
|
||||
id: SpawnSpellbook
|
||||
name: spawn spellbook
|
||||
parent: BaseSpellbook
|
||||
components:
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionSpawnMagicarpSpell: -1
|
||||
|
||||
- type: entity
|
||||
@@ -48,7 +93,7 @@
|
||||
- state: detail_rivets
|
||||
color: gold
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionForceWall: -1
|
||||
|
||||
- type: entity
|
||||
@@ -69,7 +114,7 @@
|
||||
- state: detail_rivets
|
||||
color: gold
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionBlink: -1
|
||||
|
||||
- type: entity
|
||||
@@ -92,7 +137,7 @@
|
||||
color: red
|
||||
- state: overlay_blood
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionSmite: -1
|
||||
|
||||
- type: entity
|
||||
@@ -114,7 +159,7 @@
|
||||
- state: detail_bookmark
|
||||
color: "#98c495"
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionKnock: -1
|
||||
|
||||
- type: entity
|
||||
@@ -138,7 +183,7 @@
|
||||
- state: icon_magic_fireball
|
||||
shader: unshaded
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionFireball: -1
|
||||
|
||||
- type: entity
|
||||
@@ -153,7 +198,7 @@
|
||||
layers:
|
||||
- state: spell_default
|
||||
- type: Spellbook
|
||||
spells:
|
||||
spellActions:
|
||||
ActionFlashRune: -1
|
||||
ActionExplosionRune: -1
|
||||
ActionIgniteRune: -1
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -262,7 +262,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -298,7 +298,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -419,7 +419,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -657,7 +657,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -693,7 +693,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -870,7 +870,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
@@ -1210,7 +1210,7 @@
|
||||
name: force wall
|
||||
components:
|
||||
- type: TimedDespawn
|
||||
lifetime: 20
|
||||
lifetime: 12
|
||||
- type: Tag
|
||||
tags:
|
||||
- Wall
|
||||
@@ -1250,7 +1250,7 @@
|
||||
- type: RCDDeconstructable
|
||||
cost: 6
|
||||
delay: 8
|
||||
fx: EffectRCDDeconstruct8
|
||||
fx: EffectRCDDeconstruct8
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
|
||||
13
Resources/Prototypes/Magic/event_spells.yml
Normal file
13
Resources/Prototypes/Magic/event_spells.yml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
description: Fires an explosive fireball towards the clicked location.
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: Magic
|
||||
- type: WorldTargetAction
|
||||
useDelay: 15
|
||||
itemIconStyle: BigAction
|
||||
checkCanAccess: false
|
||||
raiseOnUser: true
|
||||
range: 60
|
||||
sound: !type:SoundPathSpecifier
|
||||
path: /Audio/Magic/fireball.ogg
|
||||
@@ -16,25 +18,25 @@
|
||||
state: fireball
|
||||
event: !type:ProjectileSpellEvent
|
||||
prototype: ProjectileFireball
|
||||
posData: !type:TargetCasterPos
|
||||
speech: action-speech-spell-fireball
|
||||
- type: ActionUpgrade
|
||||
effectedLevels:
|
||||
2: ActionFireballII
|
||||
3: ActionFireballIII
|
||||
|
||||
- type: entity
|
||||
id: ActionFireballII
|
||||
parent: ActionFireball
|
||||
name: Fireball II
|
||||
description: Fire three explosive fireball towards the clicked location.
|
||||
description: Fires a fireball, but faster!
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: WorldTargetAction
|
||||
useDelay: 5
|
||||
charges: 3
|
||||
useDelay: 10
|
||||
renewCharges: true
|
||||
itemIconStyle: BigAction
|
||||
checkCanAccess: false
|
||||
raiseOnUser: true
|
||||
range: 60
|
||||
sound: !type:SoundPathSpecifier
|
||||
path: /Audio/Magic/fireball.ogg
|
||||
@@ -43,5 +45,27 @@
|
||||
state: fireball
|
||||
event: !type:ProjectileSpellEvent
|
||||
prototype: ProjectileFireball
|
||||
posData: !type:TargetCasterPos
|
||||
speech: action-speech-spell-fireball
|
||||
|
||||
- type: entity
|
||||
id: ActionFireballIII
|
||||
parent: ActionFireball
|
||||
name: Fireball III
|
||||
description: The fastest fireball in the west!
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: WorldTargetAction
|
||||
useDelay: 8
|
||||
renewCharges: true
|
||||
itemIconStyle: BigAction
|
||||
checkCanAccess: false
|
||||
raiseOnUser: true
|
||||
range: 60
|
||||
sound: !type:SoundPathSpecifier
|
||||
path: /Audio/Magic/fireball.ogg
|
||||
icon:
|
||||
sprite: Objects/Magic/magicactions.rsi
|
||||
state: fireball
|
||||
event: !type:ProjectileSpellEvent
|
||||
prototype: ProjectileFireball
|
||||
speech: action-speech-spell-fireball
|
||||
|
||||
@@ -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
|
||||
|
||||
15
Resources/Prototypes/Magic/utility_spells.yml
Normal file
15
Resources/Prototypes/Magic/utility_spells.yml
Normal file
@@ -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!
|
||||
@@ -175,3 +175,25 @@
|
||||
revertOnDeath: true
|
||||
revertOnCrit: true
|
||||
duration: 20
|
||||
|
||||
# Polymorphs for Wizards polymorph self spell
|
||||
- type: polymorph
|
||||
id: WizardSpider
|
||||
configuration:
|
||||
entity: MobGiantSpiderWizard #Not angry so ghosts can't just take over the wizard
|
||||
transferName: true
|
||||
inventory: None
|
||||
revertOnDeath: true
|
||||
revertOnCrit: true
|
||||
|
||||
- type: polymorph
|
||||
id: WizardRod
|
||||
configuration:
|
||||
entity: ImmovableRodWizard #CLANG
|
||||
transferName: true
|
||||
transferDamage: false
|
||||
inventory: None
|
||||
duration: 1
|
||||
forced: true
|
||||
revertOnCrit: false
|
||||
revertOnDeath: false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: currency
|
||||
id: Telecrystal
|
||||
displayName: store-currency-display-telecrystal
|
||||
cash:
|
||||
cash:
|
||||
1: Telecrystal1
|
||||
canWithdraw: true
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
displayName: store-currency-display-stolen-essence
|
||||
canWithdraw: false
|
||||
|
||||
- type: currency
|
||||
id: WizCoin
|
||||
displayName: store-currency-display-wizcoin
|
||||
canWithdraw: false
|
||||
|
||||
#debug
|
||||
- type: currency
|
||||
id: DebugDollar
|
||||
displayName: store-currency-display-debugdollar
|
||||
displayName: store-currency-display-debugdollar
|
||||
|
||||
@@ -15,3 +15,15 @@
|
||||
- UplinkPointless
|
||||
currencyWhitelist:
|
||||
- Telecrystal
|
||||
|
||||
- type: storePreset
|
||||
id: StorePresetSpellbook
|
||||
storeName: Spellbook
|
||||
categories:
|
||||
- SpellbookOffensive #Fireball, Rod Form
|
||||
- SpellbookDefensive #Magic Missile, Wall of Force
|
||||
- SpellbookUtility #Body Swap, Lich, Teleport, Knock, Polymorph
|
||||
- SpellbookEquipment #Battlemage Robes, Staff of Locker
|
||||
- SpellbookEvents #Summon Weapons, Summon Ghosts
|
||||
currencyWhitelist:
|
||||
- WizCoin
|
||||
|
||||
Reference in New Issue
Block a user