Files
Monolith/Content.Shared/Magic/SharedMagicSystem.cs
T
Redrover1760 f2f81e6d27 Engine Update Arc (#1441)
* Increase benchmark BuildTimeout (#37029)

* Increase benchmark BuildTimeout

* Add RaiseEvent benchmark

* a

* More benchmarks

* It just gets optimised away

* IComponent event

* cleanup bench

* Update b2dynamictree (#30630)

* v249.0.0

* Make MappingDataNode use string keys (#36111)

* MappingDataNode string keys

* a

* poke

* v254.0.0

* fixes

* chore: automatically update REUSE headers

* v255.0.0

* Predict virtual hands and co (#36617)

These are the easy ones anything else gets slightly spicier.

* chore: automatically update REUSE headers

* Update submodule to 254.1.0 (#36711)

* Update submodule to 254.1.0

* API update

* chore: automatically update REUSE headers

* v256.0.0

* v257.0.0

* Add AssertMultiple to ContrabandTest (#35662)

* add AssertMultiple to ContrabandTest

* do the same for magazine visuals test

* :trollface:

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>

* Fix MagazineVisualsSpritesExist for engine PR (#36761)

* chore: automatically update REUSE headers

* aaaa

* I'm silly

* aaaaaaaaaaaaaaaaaa

* test

* bawk

* chore: automatically update REUSE headers

* v258.0.0

* v259.0.0

* Content changes for SetTiles change (#37229)

* Content changes for SetTiles change

* Retest with new engine changes

* Derp

* Update for new engine PR changes

* chore: automatically update REUSE headers

* v260.2.0

* CompFactory updates (#37559)

* aaa

* chore: automatically update REUSE headers

* more removals

* chore: automatically update REUSE headers

* another gone

* component factory begoneth

* chore: automatically update REUSE headers

* odd fix

* Cleanup BiomeSystem.Commands (#36084)

* _mapManager.GetMapEntityId -> _mapSystem.GetMapOrInvalid

* _mapManager.MapExists -> _mapSystem.MapExists

* Unused using

* manager fixes

* chore: automatically update REUSE headers

* wizden shuttle collision

* Add sun shadows (planet lighting stage 2) (#35145)

* Implements a Dynamic Lighting System on maps.

* Edit: the night should be a little bit brighter and blue now.

* Major edit: everything must be done on the client side now, with certain datafield replicated.
Changes were outlined in the salvage to accommodate the new lighting system.

* Edit: The offset is now serverside, this makes the time accurate in all situations.

* Removing ununsed import

* Minor tweaks

* Tweak in time precision

* Minor tweak + Unused import removed

* Edit: apparently RealTime is better for what I'm looking for

* Fix: Now the time is calculated correctly.

* Minor tweaks

* Adds condition for when the light should be updated

* Add planet lighting

* she

* close-ish

* c

* bittersweat

* Fixes

* Revert "Merge branch '22719' into 2024-09-29-planet-lighting"

This reverts commit 9f2785bb16aee47d794aa3eed8ae15004f97fc35, reversing
changes made to 19649c07a5fb625423e08fc18d91c9cb101daa86.

* Europa and day-night

* weh

* rooves working

* Clean

* Remove Europa

* Fixes

* fix

* Update

* Fix caves

* Update for engine

* Add sun shadows (planet lighting v2)

For now mostly targeting walls and having the shadows change over time. Got the basic proof-of-concept working just needs a hell of a lot of polish.

* Documentation

* a

* Fixes

* Move blur to an overlay

* Slughands

* Fixes

* Apply RoofOverlay per-grid not per-map

* Fix light render scales

* sangas

* Juice it a bit

* Better angle

* Fixes

* Add color support

* Rounding bandaid

* Wehs

* Better

* Remember I forgot to do this when writing docs

---------

Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com>

* chore: automatically update REUSE headers

* Optimize parallax VRAM usage (#37180)

* Disable parallax texture preloading

Many parallax layers are specific to a single map and will likely never be loaded for the duration of the game. Save VRAM by not loading them always.

Requires engine master

* Put generated parallax identifier in texture name

Makes it show up properly in debugging tools

* Don't load generated parallaxes multiple times

Many parallax prototypes re-use the same generated parallax configs. These generated parallaxes were being loaded multiple times at once, which was a massive waste of VRAM.

We now move these into a separate cache for deduplication. I had to write a lot of logic to handle loading cancellation and ref counting. Yay.

Also fixes some spaghetti with the previous parallax loading system: cancellation didn't work properly, give proper names to generated texture names, etc.

This saves like 100+ MB of VRAM.

* chore: automatically update REUSE headers

* Fixes server error spam from bad wizden code

* better name

* formatting

* fixes it but better

* comments

* oh god

* oh god

* Sloth's subfloor vismask adventure (#35347)

* Add a subfloor vismask

Significantly cuts down on sent entity count.

* More optimisations

* Fix command

* Fixes

* namespace cleanup

* Review

* Vismasks

* Content update

* Bandaid

* awewa

* Revert these

* reh

* Update Content.Shared/SubFloor/TrayScannerComponent.cs

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Fix TrayScannerUser (#35959)

* Fix TrayScannerUser

* Ignore

* Update Content.Shared/SubFloor/TrayScannerUserComponent.cs

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Fix gas mixers and pneumatic valves being hidden by floor tiles (#36428)

fix gas mixers and pneumatics valves being hidden by floor tiles

* Remove unused PowerConsumer from grilles (#36077)

This caused tons of completely wasted processing in the power system. Incredible.

* Remove update from DeviceLinkSystem (#37152)

The tick updates were purely used to decrease the invoke counter once per tick. Now instead we just calculate the effective counter value with some trivial math on the tick number. This completely removes the need for an update function.

The relative tick is not stored to map files. If we really need this, we can add a TickOffsetSerializer (similar to TimeOffsetSerializer), but I doubt it matters.

* Use approx NPC steering checks (#36110)

Much faster lookup and shouldn't affect final result.

* Fix the sensor monitoring console forcing a GC every 3 seconds (#38146)

* Optimize sensor monitoring window graph drawing

* Add shared static Vector2 pool for all GraphView instances

* Address requested changes

* remove lock

* chore: automatically update REUSE headers

* whoops

* Predict pipe anchoring and unanchroing (#35977)

* yay prediction

* Added cool popups

* bruh

* this is better!

* Fix tile-events returns (#37502)

* Fix tile-events returns

Should really be continues.

* More

* More optimisations

---------

Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: monolith8319 <195513600+monolith8319@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Dinner <180707738+DinnerCalzone@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
2025-07-19 01:23:16 -04:00

546 lines
20 KiB
C#

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.Mind;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Speech.Muting;
using Content.Shared.Storage;
using Content.Shared.Stunnable;
using Content.Shared.Tag;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Spawners;
namespace Content.Shared.Magic;
// TODO: Move BeforeCast & Prerequirements (like Wizard clothes) to action comp
// Alt idea - make it its own comp and split, like the Charge PR
// TODO: Move speech to actionComp or again, its own ECS
// TODO: Use the MagicComp just for pure backend things like spawning patterns?
/// <summary>
/// Handles learning and using spells (actions)
/// </summary>
public abstract class SharedMagicSystem : EntitySystem
{
[Dependency] private readonly ISerializationManager _seriMan = 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!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedStunSystem _stun = 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);
SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell);
SubscribeLocalEvent<MindSwapSpellEvent>(OnMindSwapSpell);
SubscribeLocalEvent<VoidApplauseSpellEvent>(OnVoidApplause);
}
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;
if (ev.DoSpeech)
Speak(ev);
RemoveComponents(ev.Target, ev.ToRemove);
AddComponents(ev.Target, ev.ToAdd);
}
// 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 != _transform.GetMapId(args.Target) || !_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;
}
public virtual void OnVoidApplause(VoidApplauseSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
return;
ev.Handled = true;
Speak(ev);
_transform.SwapPositions(ev.Performer, ev.Target);
}
// 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;
}
}
private void AddComponents(EntityUid target, ComponentRegistry comps)
{
foreach (var (name, data) in comps)
{
if (HasComp(target, data.Component.GetType()))
continue;
var component = (Component)Factory.GetComponent(name);
var temp = (object)component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(target, (Component)temp!);
}
}
private void RemoveComponents(EntityUid target, HashSet<string> comps)
{
foreach (var toRemove in comps)
{
if (Factory.TryGetRegistration(toRemove, out var registration))
RemComp(target, registration.Type);
}
}
// End Spell Helpers
#endregion
#region Touch 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 Touch 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
#region Global Spells
// TODO: Change this into a "StartRuleAction" when actions with multiple events are supported
protected virtual void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev)
{
if (!_net.IsServer || ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || ev.Spawns is not { } spawns)
return;
ev.Handled = true;
Speak(ev);
var allHumans = _mind.GetAliveHumans();
foreach (var human in allHumans)
{
if (!human.Comp.OwnedEntity.HasValue)
continue;
var ent = human.Comp.OwnedEntity.Value;
if (_tag.HasTag(ent, "InvalidForGlobalSpawnSpell"))
continue;
var mapCoords = _transform.GetMapCoordinates(ent);
foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random))
{
var spawned = Spawn(spawn, mapCoords);
_hands.PickupOrDrop(ent, spawned);
}
}
_audio.PlayGlobal(ev.Sound, ev.Performer);
}
#endregion
#region Mindswap Spells
private void OnMindSwapSpell(MindSwapSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
return;
ev.Handled = true;
Speak(ev);
// Need performer mind, but target mind is unnecessary, such as taking over a NPC
// Need to get target mind before putting performer mind into their body if they have one
// Thus, assign bool before first transfer, then check afterwards
if (!_mind.TryGetMind(ev.Performer, out var perMind, out var perMindComp))
return;
var tarHasMind = _mind.TryGetMind(ev.Target, out var tarMind, out var tarMindComp);
_mind.TransferTo(perMind, ev.Target);
if (tarHasMind)
{
_mind.TransferTo(tarMind, ev.Performer);
}
_stun.TryParalyze(ev.Target, ev.TargetStunDuration, true);
_stun.TryParalyze(ev.Performer, ev.PerformerStunDuration, true);
}
#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);
}
}