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.Actions;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
@@ -38,10 +39,18 @@ public sealed class ActionOnInteractSystem : EntitySystem
|
|||||||
|
|
||||||
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
|
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
|
||||||
{
|
{
|
||||||
if (args.Handled || component.ActionEntities == null)
|
if (args.Handled)
|
||||||
return;
|
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)
|
if (options.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -58,13 +67,21 @@ public sealed class ActionOnInteractSystem : EntitySystem
|
|||||||
|
|
||||||
private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, AfterInteractEvent args)
|
private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, AfterInteractEvent args)
|
||||||
{
|
{
|
||||||
if (args.Handled || component.ActionEntities == null)
|
if (args.Handled)
|
||||||
return;
|
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
|
// First, try entity target actions
|
||||||
if (args.Target != null)
|
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--)
|
for (var i = entOptions.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var action = entOptions[i];
|
var action = entOptions[i];
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ namespace Content.Server.Ghost
|
|||||||
SubscribeLocalEvent<GhostComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
|
SubscribeLocalEvent<GhostComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
|
||||||
|
|
||||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(_ => MakeVisible(true));
|
SubscribeLocalEvent<RoundEndTextAppendEvent>(_ => MakeVisible(true));
|
||||||
|
SubscribeLocalEvent<ToggleGhostVisibilityToAllEvent>(OnToggleGhostVisibilityToAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGhostHearingAction(EntityUid uid, GhostComponent component, ToggleGhostHearingActionEvent args)
|
private void OnGhostHearingAction(EntityUid uid, GhostComponent component, ToggleGhostHearingActionEvent args)
|
||||||
@@ -363,6 +364,15 @@ namespace Content.Server.Ghost
|
|||||||
args.Cancelled = true;
|
args.Cancelled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnToggleGhostVisibilityToAll(ToggleGhostVisibilityToAllEvent ev)
|
||||||
|
{
|
||||||
|
if (ev.Handled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.Handled = true;
|
||||||
|
MakeVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When the round ends, make all players able to see ghosts.
|
/// When the round ends, make all players able to see ghosts.
|
||||||
/// </summary>
|
/// </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.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;
|
||||||
using Content.Shared.Magic.Events;
|
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;
|
namespace Content.Server.Magic;
|
||||||
|
|
||||||
/// <summary>
|
public sealed class MagicSystem : SharedMagicSystem
|
||||||
/// Handles learning and using spells (actions)
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MagicSystem : EntitySystem
|
|
||||||
{
|
{
|
||||||
[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 ChatSystem _chat = default!;
|
||||||
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<SpellbookComponent, MapInitEvent>(OnInit);
|
SubscribeLocalEvent<SpeakSpellEvent>(OnSpellSpoken);
|
||||||
SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
|
|
||||||
SubscribeLocalEvent<SpellbookComponent, SpellbookDoAfterEvent>(OnDoAfter);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
|
|
||||||
SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
|
|
||||||
SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
|
|
||||||
SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
|
|
||||||
SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
|
|
||||||
SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
|
|
||||||
SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDoAfter(EntityUid uid, SpellbookComponent component, DoAfterEvent args)
|
private void OnSpellSpoken(ref SpeakSpellEvent args)
|
||||||
{
|
{
|
||||||
if (args.Handled || args.Cancelled)
|
_chat.TrySendInGameICMessage(args.Performer, Loc.GetString(args.Speech), InGameICChatType.Speak, false);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Server.Store.Components;
|
using Content.Server.Store.Components;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
|
|
||||||
namespace Content.Server.Store.Systems;
|
namespace Content.Server.Store.Systems;
|
||||||
|
|||||||
@@ -215,11 +215,11 @@ public sealed partial class StoreSystem
|
|||||||
{
|
{
|
||||||
HandleRefundComp(uid, component, actionId.Value);
|
HandleRefundComp(uid, component, actionId.Value);
|
||||||
|
|
||||||
if (listing.ProductUpgradeID != null)
|
if (listing.ProductUpgradeId != null)
|
||||||
{
|
{
|
||||||
foreach (var upgradeListing in component.Listings)
|
foreach (var upgradeListing in component.Listings)
|
||||||
{
|
{
|
||||||
if (upgradeListing.ID == listing.ProductUpgradeID)
|
if (upgradeListing.ID == listing.ProductUpgradeId)
|
||||||
{
|
{
|
||||||
upgradeListing.ProductActionEntity = actionId.Value;
|
upgradeListing.ProductActionEntity = actionId.Value;
|
||||||
break;
|
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)
|
if (listing.ProductActionEntity != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs
|
|||||||
public EntityUid Performer;
|
public EntityUid Performer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The action that was performed.
|
/// The action the event belongs to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EntityUid Action;
|
public EntityUid Action;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -569,13 +569,12 @@ public abstract class SharedActionsSystem : EntitySystem
|
|||||||
handled = actionEvent.Handled;
|
handled = actionEvent.Handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
_audio.PlayPredicted(action.Sound, performer,predicted ? performer : null);
|
|
||||||
handled |= action.Sound != null;
|
|
||||||
|
|
||||||
if (!handled)
|
if (!handled)
|
||||||
return; // no interaction occurred.
|
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;
|
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 ToggleGhostHearingActionEvent : InstantActionEvent { }
|
||||||
|
|
||||||
|
public sealed partial class ToggleGhostVisibilityToAllEvent : InstantActionEvent { }
|
||||||
|
|
||||||
public sealed partial class BooActionEvent : 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.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
|
||||||
|
|
||||||
namespace Content.Server.Magic.Components;
|
namespace Content.Shared.Magic.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent, Access(typeof(SpellbookSystem))]
|
||||||
public sealed partial class SpellbookComponent : Component
|
public sealed partial class SpellbookComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -18,18 +19,18 @@ public sealed partial class SpellbookComponent : Component
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The three fields below is just used for initialization.
|
/// The three fields below is just used for initialization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("spells", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, EntityPrototype>))]
|
[DataField]
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public Dictionary<string, int> SpellActions = new();
|
public Dictionary<EntProtoId, int> SpellActions = new();
|
||||||
|
|
||||||
[DataField("learnTime")]
|
[DataField]
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public float LearnTime = .75f;
|
public float LearnTime = .75f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If true, the spell action stays even after the book is removed
|
/// If true, the spell action stays even after the book is removed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("learnPermanently")]
|
[DataField]
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool LearnPermanently;
|
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 Content.Shared.Actions;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Shared.Magic.Events;
|
namespace Content.Shared.Magic.Events;
|
||||||
|
|
||||||
@@ -9,17 +8,18 @@ public sealed partial class InstantSpawnSpellEvent : InstantActionEvent, ISpeakS
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What entity should be spawned.
|
/// What entity should be spawned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField(required: true)]
|
||||||
public string Prototype = default!;
|
public EntProtoId Prototype;
|
||||||
|
|
||||||
[DataField("preventCollide")]
|
[DataField]
|
||||||
public bool PreventCollideWithCaster = true;
|
public bool PreventCollideWithCaster = true;
|
||||||
|
|
||||||
[DataField("speech")]
|
[DataField]
|
||||||
public string? Speech { get; private set; }
|
public string? Speech { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the targeted spawn positons; may lead to multiple entities being spawned.
|
/// Gets the targeted spawn positons; may lead to multiple entities being spawned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
|
[DataField]
|
||||||
|
public MagicInstantSpawnData PosData = new TargetCasterPos();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Robust.Shared.Audio;
|
|
||||||
|
|
||||||
namespace Content.Shared.Magic.Events;
|
namespace Content.Shared.Magic.Events;
|
||||||
|
|
||||||
@@ -7,20 +6,12 @@ public sealed partial class KnockSpellEvent : InstantActionEvent, ISpeakSpell
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The range this spell opens doors in
|
/// 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>
|
/// </summary>
|
||||||
[DataField("range")]
|
[DataField]
|
||||||
public float Range = 4f;
|
public float Range = 10f;
|
||||||
|
|
||||||
[DataField("knockSound")]
|
[DataField]
|
||||||
public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Volume control for the spell.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("knockVolume")]
|
|
||||||
public float KnockVolume = 5f;
|
|
||||||
|
|
||||||
[DataField("speech")]
|
|
||||||
public string? Speech { get; private set; }
|
public string? Speech { get; private set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Shared.Magic.Events;
|
namespace Content.Shared.Magic.Events;
|
||||||
|
|
||||||
@@ -9,14 +8,9 @@ public sealed partial class ProjectileSpellEvent : WorldTargetActionEvent, ISpea
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What entity should be spawned.
|
/// What entity should be spawned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField(required: true)]
|
||||||
public string Prototype = default!;
|
public EntProtoId Prototype;
|
||||||
|
|
||||||
/// <summary>
|
[DataField]
|
||||||
/// Gets the targeted spawn positions; may lead to multiple entities being spawned.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
|
|
||||||
|
|
||||||
[DataField("speech")]
|
|
||||||
public string? Speech { get; private set; }
|
public string? Speech { get; private set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ namespace Content.Shared.Magic.Events;
|
|||||||
|
|
||||||
public sealed partial class SmiteSpellEvent : EntityTargetActionEvent, ISpeakSpell
|
public sealed partial class SmiteSpellEvent : EntityTargetActionEvent, ISpeakSpell
|
||||||
{
|
{
|
||||||
|
// TODO: Make part of gib method
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[DataField("deleteNonBrainParts")]
|
[DataField]
|
||||||
public bool DeleteNonBrainParts = true;
|
public bool DeleteNonBrainParts = true;
|
||||||
|
|
||||||
[DataField("speech")]
|
[DataField]
|
||||||
public string? Speech { get; private set; }
|
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 Content.Shared.Actions;
|
||||||
using Robust.Shared.Audio;
|
|
||||||
|
|
||||||
namespace Content.Shared.Magic.Events;
|
namespace Content.Shared.Magic.Events;
|
||||||
|
|
||||||
|
// TODO: Can probably just be an entity or something
|
||||||
public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakSpell
|
public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakSpell
|
||||||
{
|
{
|
||||||
[DataField("blinkSound")]
|
[DataField]
|
||||||
public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg");
|
|
||||||
|
|
||||||
[DataField("speech")]
|
|
||||||
public string? Speech { get; private set; }
|
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>
|
/// <summary>
|
||||||
/// Volume control for the spell.
|
/// Volume control for the spell.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("blinkVolume")]
|
[DataField]
|
||||||
public float BlinkVolume = 5f;
|
public float BlinkVolume = 5f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,31 @@ using Content.Shared.Storage;
|
|||||||
|
|
||||||
namespace Content.Shared.Magic.Events;
|
namespace Content.Shared.Magic.Events;
|
||||||
|
|
||||||
|
// TODO: This class needs combining with InstantSpawnSpellEvent
|
||||||
|
|
||||||
public sealed partial class WorldSpawnSpellEvent : WorldTargetActionEvent, ISpeakSpell
|
public sealed partial class WorldSpawnSpellEvent : WorldTargetActionEvent, ISpeakSpell
|
||||||
{
|
{
|
||||||
// TODO:This class needs combining with InstantSpawnSpellEvent
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of prototypes this spell will spawn
|
/// The list of prototypes this spell will spawn
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("prototypes")]
|
[DataField]
|
||||||
public List<EntitySpawnEntry> Contents = new();
|
public List<EntitySpawnEntry> Prototypes = new();
|
||||||
|
|
||||||
// TODO: This offset is liable for deprecation.
|
// TODO: This offset is liable for deprecation.
|
||||||
|
// TODO: Target tile via code instead?
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The offset the prototypes will spawn in on relative to the one prior.
|
/// 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.
|
/// Set to 0,0 to have them spawn on the same tile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("offset")]
|
[DataField]
|
||||||
public Vector2 Offset;
|
public Vector2 Offset;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lifetime to set for the entities to self delete
|
/// Lifetime to set for the entities to self delete
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("lifetime")] public float? Lifetime;
|
[DataField]
|
||||||
|
public float? Lifetime;
|
||||||
|
|
||||||
[DataField("speech")]
|
[DataField]
|
||||||
public string? Speech { get; private set; }
|
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;
|
public EntProtoId? ProductAction;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
|
/// 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
|
/// upgrade or to use standalone as an upgrade
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public ProtoId<ListingPrototype>? ProductUpgradeID;
|
public ProtoId<ListingPrototype>? ProductUpgradeId;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
[NonSerialized]
|
[NonSerialized]
|
||||||
@@ -161,7 +161,7 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
|
|||||||
Priority = Priority,
|
Priority = Priority,
|
||||||
ProductEntity = ProductEntity,
|
ProductEntity = ProductEntity,
|
||||||
ProductAction = ProductAction,
|
ProductAction = ProductAction,
|
||||||
ProductUpgradeID = ProductUpgradeID,
|
ProductUpgradeId = ProductUpgradeId,
|
||||||
ProductActionEntity = ProductActionEntity,
|
ProductActionEntity = ProductActionEntity,
|
||||||
ProductEvent = ProductEvent,
|
ProductEvent = ProductEvent,
|
||||||
PurchaseAmount = PurchaseAmount,
|
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
|
# Revenant
|
||||||
store-category-abilities = Abilities
|
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-telecrystal = TC
|
||||||
store-currency-display-stolen-essence = Stolen Essence
|
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
|
- type: InstantAction
|
||||||
event: !type:PolymorphActionEvent
|
event: !type:PolymorphActionEvent
|
||||||
itemIconStyle: NoItem
|
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
|
- Snout
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingHeadBase
|
parent: ClothingHeadHatWizardBase
|
||||||
id: ClothingHeadHatRedwizard
|
id: ClothingHeadHatRedwizard
|
||||||
name: red wizard hat
|
name: red wizard hat
|
||||||
description: Strange-looking red hat-wear that most certainly belongs to a real magic user.
|
description: Strange-looking red hat-wear that most certainly belongs to a real magic user.
|
||||||
@@ -505,7 +505,7 @@
|
|||||||
|
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingHeadBase
|
parent: ClothingHeadHatWizardBase
|
||||||
id: ClothingHeadHatVioletwizard
|
id: ClothingHeadHatVioletwizard
|
||||||
name: violet wizard hat
|
name: violet wizard hat
|
||||||
description: "Strange-looking violet hat-wear that most certainly belongs to a real magic user."
|
description: "Strange-looking violet hat-wear that most certainly belongs to a real magic user."
|
||||||
@@ -534,6 +534,7 @@
|
|||||||
name: witch hat
|
name: witch hat
|
||||||
description: A witch hat.
|
description: A witch hat.
|
||||||
components:
|
components:
|
||||||
|
- type: WizardClothes #Yes this will count
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Clothing/Head/Hats/witch.rsi
|
sprite: Clothing/Head/Hats/witch.rsi
|
||||||
- type: Clothing
|
- type: Clothing
|
||||||
@@ -558,7 +559,14 @@
|
|||||||
sprite: Clothing/Head/Hats/wizard_fake.rsi
|
sprite: Clothing/Head/Hats/wizard_fake.rsi
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
|
abstract: true
|
||||||
parent: ClothingHeadBase
|
parent: ClothingHeadBase
|
||||||
|
id: ClothingHeadHatWizardBase
|
||||||
|
components:
|
||||||
|
- type: WizardClothes
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
parent: ClothingHeadHatWizardBase
|
||||||
id: ClothingHeadHatWizard
|
id: ClothingHeadHatWizard
|
||||||
name: wizard hat
|
name: wizard hat
|
||||||
description: Strange-looking blue hat-wear that most certainly belongs to a powerful magic user.
|
description: Strange-looking blue hat-wear that most certainly belongs to a powerful magic user.
|
||||||
|
|||||||
@@ -166,9 +166,16 @@
|
|||||||
- type: Clothing
|
- type: Clothing
|
||||||
sprite: Clothing/OuterClothing/Misc/santa.rsi
|
sprite: Clothing/OuterClothing/Misc/santa.rsi
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
abstract: true
|
||||||
|
parent: ClothingOuterBase
|
||||||
|
id: ClothingOuterWizardBase
|
||||||
|
components:
|
||||||
|
- type: WizardClothes
|
||||||
|
|
||||||
# Is this wizard wearing a fanny pack???
|
# Is this wizard wearing a fanny pack???
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingOuterBase
|
parent: ClothingOuterWizardBase
|
||||||
id: ClothingOuterWizardViolet
|
id: ClothingOuterWizardViolet
|
||||||
name: violet wizard robes
|
name: violet wizard robes
|
||||||
description: A bizarre gem-encrusted violet robe that radiates magical energies.
|
description: A bizarre gem-encrusted violet robe that radiates magical energies.
|
||||||
@@ -179,7 +186,7 @@
|
|||||||
sprite: Clothing/OuterClothing/Misc/violetwizard.rsi
|
sprite: Clothing/OuterClothing/Misc/violetwizard.rsi
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingOuterBase
|
parent: ClothingOuterWizardBase
|
||||||
id: ClothingOuterWizard
|
id: ClothingOuterWizard
|
||||||
name: wizard robes
|
name: wizard robes
|
||||||
description: A bizarre gem-encrusted blue robe that radiates magical energies.
|
description: A bizarre gem-encrusted blue robe that radiates magical energies.
|
||||||
@@ -190,7 +197,7 @@
|
|||||||
sprite: Clothing/OuterClothing/Misc/wizard.rsi
|
sprite: Clothing/OuterClothing/Misc/wizard.rsi
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: ClothingOuterBase
|
parent: ClothingOuterWizardBase
|
||||||
id: ClothingOuterWizardRed
|
id: ClothingOuterWizardRed
|
||||||
name: red wizard robes
|
name: red wizard robes
|
||||||
description: Strange-looking, red, hat-wear that most certainly belongs to a real magic user.
|
description: Strange-looking, red, hat-wear that most certainly belongs to a real magic user.
|
||||||
|
|||||||
@@ -2345,6 +2345,18 @@
|
|||||||
bloodMaxVolume: 150
|
bloodMaxVolume: 150
|
||||||
bloodReagent: Laughter
|
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
|
- type: entity
|
||||||
name: possum
|
name: possum
|
||||||
parent: SimpleMobBase
|
parent: SimpleMobBase
|
||||||
|
|||||||
@@ -19,13 +19,58 @@
|
|||||||
tags:
|
tags:
|
||||||
- Spellbook
|
- 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
|
- type: entity
|
||||||
id: SpawnSpellbook
|
id: SpawnSpellbook
|
||||||
name: spawn spellbook
|
name: spawn spellbook
|
||||||
parent: BaseSpellbook
|
parent: BaseSpellbook
|
||||||
components:
|
components:
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionSpawnMagicarpSpell: -1
|
ActionSpawnMagicarpSpell: -1
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -48,7 +93,7 @@
|
|||||||
- state: detail_rivets
|
- state: detail_rivets
|
||||||
color: gold
|
color: gold
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionForceWall: -1
|
ActionForceWall: -1
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -69,7 +114,7 @@
|
|||||||
- state: detail_rivets
|
- state: detail_rivets
|
||||||
color: gold
|
color: gold
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionBlink: -1
|
ActionBlink: -1
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -92,7 +137,7 @@
|
|||||||
color: red
|
color: red
|
||||||
- state: overlay_blood
|
- state: overlay_blood
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionSmite: -1
|
ActionSmite: -1
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -114,7 +159,7 @@
|
|||||||
- state: detail_bookmark
|
- state: detail_bookmark
|
||||||
color: "#98c495"
|
color: "#98c495"
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionKnock: -1
|
ActionKnock: -1
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -138,7 +183,7 @@
|
|||||||
- state: icon_magic_fireball
|
- state: icon_magic_fireball
|
||||||
shader: unshaded
|
shader: unshaded
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionFireball: -1
|
ActionFireball: -1
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -153,7 +198,7 @@
|
|||||||
layers:
|
layers:
|
||||||
- state: spell_default
|
- state: spell_default
|
||||||
- type: Spellbook
|
- type: Spellbook
|
||||||
spells:
|
spellActions:
|
||||||
ActionFlashRune: -1
|
ActionFlashRune: -1
|
||||||
ActionExplosionRune: -1
|
ActionExplosionRune: -1
|
||||||
ActionIgniteRune: -1
|
ActionIgniteRune: -1
|
||||||
|
|||||||
@@ -1210,7 +1210,7 @@
|
|||||||
name: force wall
|
name: force wall
|
||||||
components:
|
components:
|
||||||
- type: TimedDespawn
|
- type: TimedDespawn
|
||||||
lifetime: 20
|
lifetime: 12
|
||||||
- type: Tag
|
- type: Tag
|
||||||
tags:
|
tags:
|
||||||
- Wall
|
- Wall
|
||||||
|
|||||||
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
|
- type: InstantAction
|
||||||
useDelay: 10
|
useDelay: 10
|
||||||
itemIconStyle: BigAction
|
itemIconStyle: BigAction
|
||||||
|
sound: !type:SoundPathSpecifier
|
||||||
|
path: /Audio/Magic/knock.ogg
|
||||||
icon:
|
icon:
|
||||||
sprite: Objects/Magic/magicactions.rsi
|
sprite: Objects/Magic/magicactions.rsi
|
||||||
state: knock
|
state: knock
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
description: Fires an explosive fireball towards the clicked location.
|
description: Fires an explosive fireball towards the clicked location.
|
||||||
noSpawn: true
|
noSpawn: true
|
||||||
components:
|
components:
|
||||||
|
- type: Magic
|
||||||
- type: WorldTargetAction
|
- type: WorldTargetAction
|
||||||
useDelay: 15
|
useDelay: 15
|
||||||
itemIconStyle: BigAction
|
itemIconStyle: BigAction
|
||||||
checkCanAccess: false
|
checkCanAccess: false
|
||||||
|
raiseOnUser: true
|
||||||
range: 60
|
range: 60
|
||||||
sound: !type:SoundPathSpecifier
|
sound: !type:SoundPathSpecifier
|
||||||
path: /Audio/Magic/fireball.ogg
|
path: /Audio/Magic/fireball.ogg
|
||||||
@@ -16,25 +18,25 @@
|
|||||||
state: fireball
|
state: fireball
|
||||||
event: !type:ProjectileSpellEvent
|
event: !type:ProjectileSpellEvent
|
||||||
prototype: ProjectileFireball
|
prototype: ProjectileFireball
|
||||||
posData: !type:TargetCasterPos
|
|
||||||
speech: action-speech-spell-fireball
|
speech: action-speech-spell-fireball
|
||||||
- type: ActionUpgrade
|
- type: ActionUpgrade
|
||||||
effectedLevels:
|
effectedLevels:
|
||||||
2: ActionFireballII
|
2: ActionFireballII
|
||||||
|
3: ActionFireballIII
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: ActionFireballII
|
id: ActionFireballII
|
||||||
parent: ActionFireball
|
parent: ActionFireball
|
||||||
name: Fireball II
|
name: Fireball II
|
||||||
description: Fire three explosive fireball towards the clicked location.
|
description: Fires a fireball, but faster!
|
||||||
noSpawn: true
|
noSpawn: true
|
||||||
components:
|
components:
|
||||||
- type: WorldTargetAction
|
- type: WorldTargetAction
|
||||||
useDelay: 5
|
useDelay: 10
|
||||||
charges: 3
|
|
||||||
renewCharges: true
|
renewCharges: true
|
||||||
itemIconStyle: BigAction
|
itemIconStyle: BigAction
|
||||||
checkCanAccess: false
|
checkCanAccess: false
|
||||||
|
raiseOnUser: true
|
||||||
range: 60
|
range: 60
|
||||||
sound: !type:SoundPathSpecifier
|
sound: !type:SoundPathSpecifier
|
||||||
path: /Audio/Magic/fireball.ogg
|
path: /Audio/Magic/fireball.ogg
|
||||||
@@ -43,5 +45,27 @@
|
|||||||
state: fireball
|
state: fireball
|
||||||
event: !type:ProjectileSpellEvent
|
event: !type:ProjectileSpellEvent
|
||||||
prototype: ProjectileFireball
|
prototype: ProjectileFireball
|
||||||
posData: !type:TargetCasterPos
|
|
||||||
speech: action-speech-spell-fireball
|
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
|
useDelay: 10
|
||||||
range: 16 # default examine-range.
|
range: 16 # default examine-range.
|
||||||
# ^ should probably add better validation that the clicked location is on the users screen somewhere,
|
# ^ 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
|
itemIconStyle: BigAction
|
||||||
checkCanAccess: false
|
checkCanAccess: false
|
||||||
repeat: true
|
repeat: false
|
||||||
icon:
|
icon:
|
||||||
sprite: Objects/Magic/magicactions.rsi
|
sprite: Objects/Magic/magicactions.rsi
|
||||||
state: blink
|
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
|
revertOnDeath: true
|
||||||
revertOnCrit: true
|
revertOnCrit: true
|
||||||
duration: 20
|
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
|
id: Debug2
|
||||||
name: store-category-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
|
#uplink categoires
|
||||||
- type: storeCategory
|
- type: storeCategory
|
||||||
id: UplinkWeaponry
|
id: UplinkWeaponry
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
displayName: store-currency-display-stolen-essence
|
displayName: store-currency-display-stolen-essence
|
||||||
canWithdraw: false
|
canWithdraw: false
|
||||||
|
|
||||||
|
- type: currency
|
||||||
|
id: WizCoin
|
||||||
|
displayName: store-currency-display-wizcoin
|
||||||
|
canWithdraw: false
|
||||||
|
|
||||||
#debug
|
#debug
|
||||||
- type: currency
|
- type: currency
|
||||||
id: DebugDollar
|
id: DebugDollar
|
||||||
|
|||||||
@@ -15,3 +15,15 @@
|
|||||||
- UplinkPointless
|
- UplinkPointless
|
||||||
currencyWhitelist:
|
currencyWhitelist:
|
||||||
- Telecrystal
|
- 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