ace/Source/ACE.Server/WorldObjects/Player_Magic.cs
gmriggs 3dbcd775f4
fix Kiss of the Grave and some other fellowship spells (#4425)
* fix Kiss of the Grave and some other fellowship spells

* revise comment
2026-03-26 01:24:47 -04:00

1438 lines
53 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using ACE.Common;
using ACE.DatLoader;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Managers;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Physics;
namespace ACE.Server.WorldObjects
{
partial class Player
{
// TODO: get rid of this, only used for determining if TurnTo is required
public enum TargetCategory
{
Undef,
WorldObject,
Wielded,
Inventory,
Self,
Fellowship
}
public MagicState MagicState;
/// <summary>
/// The last spell projectile launched by this player
/// to successfully collided with a target
/// </summary>
public Spell LastHitSpellProjectile;
/// <summary>
/// Limiter for switching between war and void magic
/// </summary>
public double LastSuccessCast_Time;
public MagicSchool LastSuccessCast_School;
public bool DebugSpell { get; set; }
public string DebugDamageBuffer { get; set; }
public RecordCast RecordCast { get; set; }
/// <summary>
/// Returns the magic skill associated with the magic school
/// for the last collided spell projectile
/// </summary>
public Skill GetCurrentMagicSkill()
{
if (LastHitSpellProjectile == null)
return Skill.WarMagic; // this should never happen, but just in case
switch (LastHitSpellProjectile.School)
{
case MagicSchool.WarMagic:
default:
return Skill.WarMagic;
case MagicSchool.LifeMagic:
return Skill.LifeMagic;
case MagicSchool.CreatureEnchantment:
return Skill.CreatureEnchantment;
case MagicSchool.ItemEnchantment:
return Skill.ItemEnchantment;
case MagicSchool.VoidMagic:
return Skill.VoidMagic;
}
}
/// <summary>
/// Handles player targeted casting message
/// </summary>
/// <param name="builtInSpell">If TRUE, casting a built-in spell from a weapon</param>
public void HandleActionCastTargetedSpell(uint targetGuid, uint spellId, WorldObject casterItem = null)
{
//Console.WriteLine($"{Name}.HandleActionCastTargetedSpell({targetGuid:X8}, {spellId}, {builtInSpell})");
if (CombatMode != CombatMode.Magic)
{
log.Warn($"{Name}.HandleActionCastTargetedSpell({targetGuid:X8}, {spellId}, {casterItem?.Name}) - CombatMode mismatch {CombatMode}, LastCombatMode: {LastCombatMode}");
if (LastCombatMode == CombatMode.Magic)
CombatMode = CombatMode.Magic;
else
{
SendUseDoneEvent();
return;
}
}
if (FastTick && PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.CurrentStyle != (uint)MotionStance.Magic)
{
log.Warn($"{Name} CombatMode: {CombatMode}, CurrentMotionState: {CurrentMotionState.Stance}.{CurrentMotionState.MotionState.ForwardCommand}, Physics: {(MotionStance)PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.CurrentStyle}.{(MotionCommand)PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.ForwardCommand}");
ApplyPhysicsMotion(new Motion(MotionStance.Magic));
SendUseDoneEvent(WeenieError.YoureTooBusy);
return;
}
if (IsJumping)
{
SendUseDoneEvent(WeenieError.YouCantDoThatWhileInTheAir);
return;
}
if (PKLogout)
{
SendUseDoneEvent(WeenieError.YouHaveBeenInPKBattleTooRecently);
return;
}
if (IsBusy && MagicState.CanQueue)
{
MagicState.CastQueue = new CastQueue(CastQueueType.Targeted, targetGuid, spellId, casterItem);
MagicState.CanQueue = false;
return;
}
if (!VerifyBusy())
return;
// verify spell is contained in player's spellbook,
// or in the weapon's spellbook in the case of built-in spells
if (!VerifySpell(spellId, casterItem))
{
SendUseDoneEvent(WeenieError.MagicInvalidSpellType);
return;
}
var targetCategory = GetTargetCategory(targetGuid, spellId, out var target);
if (target == null || target.Teleporting)
{
SendUseDoneEvent(WeenieError.TargetNotAcquired);
return;
}
MagicState.OnCastStart();
MagicState.SetWindupParams(targetGuid, spellId, casterItem);
StartPos = new Physics.Common.Position(PhysicsObj.Position);
if (RecordCast.Enabled)
RecordCast.OnCastTargetedSpell(new Spell(spellId), target);
if (targetCategory != TargetCategory.WorldObject && targetCategory != TargetCategory.Wielded)
{
if (!CreatePlayerSpell(target, targetCategory, spellId, casterItem))
MagicState.OnCastDone();
return;
}
// start turning
if (!FastTick)
{
var rotateTarget = target;
if (rotateTarget.WielderId != null)
rotateTarget = CurrentLandblock?.GetObject(rotateTarget.WielderId.Value);
var rotateTime = Rotate(rotateTarget);
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(rotateTime);
actionChain.AddAction(this, () =>
{
// ensure target still exists
targetCategory = GetTargetCategory(targetGuid, spellId, out target);
if (target == null)
{
SendUseDoneEvent(WeenieError.TargetNotAcquired);
MagicState.OnCastDone();
return;
}
if (!CreatePlayerSpell(target, targetCategory, spellId, casterItem))
MagicState.OnCastDone();
});
actionChain.EnqueueChain();
}
else
TurnTo_Magic(target);
}
public void DoWindup(WindupParams windupParams, bool checkAngle)
{
//Console.WriteLine($"{Name}.DoWindup()");
// ensure target still exists
var targetCategory = GetTargetCategory(windupParams.TargetGuid, windupParams.SpellId, out var target);
if (target == null)
{
SendUseDoneEvent(WeenieError.TargetNotAcquired);
MagicState.OnCastDone();
return;
}
if (!checkAngle || IsWithinAngle(target))
{
if (!CreatePlayerSpell(target, targetCategory, windupParams.SpellId, windupParams.CasterItem))
MagicState.OnCastDone();
}
else
{
// restart turn if required
if (PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.TurnCommand == 0)
TurnTo_Magic(target);
else
MagicState.PendingTurnRelease = true;
}
}
public TargetCategory GetTargetCategory(uint targetGuid, uint spellId, out WorldObject target)
{
// fellowship spell
var spell = new Spell(spellId);
if (spell.IsFellowshipSpell)
{
target = this;
return TargetCategory.Fellowship;
}
// direct landblock object
target = CurrentLandblock?.GetObject(targetGuid);
if (target != null)
return targetGuid == Guid.Full ? TargetCategory.Self : TargetCategory.WorldObject;
// self-wielded
target = GetEquippedItem(targetGuid);
if (target != null)
return TargetCategory.Inventory;
// inventory item
target = GetInventoryItem(targetGuid);
if (target != null)
return TargetCategory.Inventory;
// other selectable wielded
target = CurrentLandblock?.GetWieldedObject(targetGuid, true);
if (target != null)
return TargetCategory.Wielded;
// known trade objects
var tradePartner = GetKnownTradeObj(new ObjectGuid(targetGuid));
if (tradePartner != null)
{
target = tradePartner.GetEquippedItem(targetGuid);
if (target != null)
return TargetCategory.Wielded;
target = tradePartner.GetInventoryItem(targetGuid);
if (target != null)
return TargetCategory.Inventory;
}
return TargetCategory.Undef;
}
/// <summary>
/// Handles player untargeted casting message
/// </summary>
public void HandleActionMagicCastUnTargetedSpell(uint spellId)
{
//Console.WriteLine($"{Name}.HandleActionCastUnTargetedSpell({spellId})");
if (CombatMode != CombatMode.Magic)
{
log.Warn($"{Name}.HandleActionMagicCastUnTargetedSpell({spellId}) - CombatMode mismatch {CombatMode}, LastCombatMode {LastCombatMode}");
if (LastCombatMode == CombatMode.Magic)
CombatMode = CombatMode.Magic;
else
{
SendUseDoneEvent();
return;
}
}
if (FastTick && PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.CurrentStyle != (uint)MotionStance.Magic)
{
log.Warn($"{Name} CombatMode: {CombatMode}, CurrentMotionState: {CurrentMotionState.Stance}.{CurrentMotionState.MotionState.ForwardCommand}, Physics: {(MotionStance)PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.CurrentStyle}.{(MotionCommand)PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.ForwardCommand}");
ApplyPhysicsMotion(new Motion(MotionStance.Magic));
SendUseDoneEvent(WeenieError.YoureTooBusy);
return;
}
if (IsJumping)
{
SendUseDoneEvent(WeenieError.YouCantDoThatWhileInTheAir);
return;
}
if (PKLogout)
{
SendUseDoneEvent(WeenieError.YouHaveBeenInPKBattleTooRecently);
return;
}
if (IsBusy && MagicState.CanQueue)
{
MagicState.CastQueue = new CastQueue(CastQueueType.Untargeted, 0, spellId, null);
MagicState.CanQueue = false;
return;
}
if (!VerifyBusy())
return;
// verify spell is contained in player's spellbook,
// or in the weapon's spellbook in the case of built-in spells
if (!VerifySpell(spellId))
return;
if (RecordCast.Enabled)
RecordCast.OnCastUntargetedSpell(new Spell(spellId));
MagicState.OnCastStart();
StartPos = new Physics.Common.Position(PhysicsObj.Position);
if (!CreatePlayerSpell(spellId))
MagicState.OnCastDone();
}
/// <summary>
/// Verifies spell is contained in player's spellbook,
/// or in the weapon's spellbook in the case of built-in spells
/// </summary>
/// <param name="builtInSpell">If TRUE, casting a built-in spell from a weapon</param>
public bool VerifySpell(uint spellId, WorldObject casterItem = null)
{
if (casterItem != null)
return IsWeaponSpell(spellId, casterItem);
else
return SpellIsKnown(spellId);
// send error message?
}
/// <summary>
/// Returns TRUE if the currently equipped casting implement
/// has a built-in spell
/// </summary>
public bool IsWeaponSpell(uint spellId, WorldObject casterItem)
{
var caster = GetEquippedWand();
if (casterItem != null)
caster = casterItem;
if (caster == null || caster.SpellDID == null)
return false;
return caster.SpellDID == spellId;
}
public enum CastingPreCheckStatus
{
CastFailed,
InvalidPKStatus,
Success
}
public static float Windup_MaxMove = 6.0f;
public static float Windup_MaxMoveSq = Windup_MaxMove * Windup_MaxMove;
public bool VerifyBusy()
{
if (IsBusy || Teleporting || suicideInProgress)
{
SendUseDoneEvent(WeenieError.YoureTooBusy);
return false;
}
return true;
}
public Spell ValidateSpell(uint spellId, bool isWeaponSpell = false)
{
var spell = new Spell(spellId);
if (spell.NotFound)
{
if (spell._spellBase == null)
{
Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, $"SpellId {spell.Id} Invalid."));
SendUseDoneEvent(WeenieError.None);
}
else
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"{spell.Name} spell not implemented, yet!", ChatMessageType.System));
SendUseDoneEvent(WeenieError.MagicInvalidSpellType);
}
return null;
}
if (!isWeaponSpell && !HasComponentsForSpell(spell))
{
SendUseDoneEvent(WeenieError.YouDontHaveAllTheComponents);
return null;
}
return spell;
}
public bool VerifySpellTarget(Spell spell, WorldObject target)
{
if (IsInvalidTarget(spell, target))
{
Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, $"{spell.Name} cannot be cast on {target.Name}."));
SendUseDoneEvent(WeenieError.None);
return false;
}
return true;
}
/// <summary>
/// Determines whether the target for the spell being cast is invalid
/// </summary>
protected bool IsInvalidTarget(Spell spell, WorldObject target)
{
var targetPlayer = target as Player;
var targetCreature = target as Creature;
// ensure target is enchantable
if (!target.IsEnchantable) return true;
// Self targeted spells should have a target of self
if (spell.Flags.HasFlag(SpellFlags.SelfTargeted) && target != this)
return true;
// Invalidate non Item Enchantment spells cast against non Creatures or Players
if (spell.School != MagicSchool.ItemEnchantment && targetCreature == null)
return true;
// Invalidate beneficial spells against Creature/Non-player targets
if (targetCreature != null && targetPlayer == null && spell.IsBeneficial)
return true;
// check item spells
if (targetCreature == null && target.WielderId != null)
{
var parent = CurrentLandblock.GetObject(target.WielderId.Value) as Player;
// Invalidate beneficial spells against monster wielded items
if (parent == null && spell.IsBeneficial)
return true;
// Invalidate harmful spells against player wielded items, depending on pk status
if (parent != null && spell.IsHarmful && CheckPKStatusVsTarget(parent, spell) != null)
return true;
}
// verify target type for item enchantment
if (spell.School == MagicSchool.ItemEnchantment && !VerifyNonComponentTargetType(spell, target))
{
if (spell.DispelSchool != MagicSchool.ItemEnchantment || !PropertyManager.GetBool("item_dispel").Item)
return true;
}
// brittlemail / lure / other negative item spells cannot be cast with player as target
// TODO: by end of retail, players couldn't cast any negative spells on themselves
// this feature is currently in ace for dev testing...
if (target == this && spell.IsNegativeRedirectable)
return true;
if (targetCreature != null && targetCreature != this && spell.NonComponentTargetType == ItemType.Creature && !CanDamage(targetCreature))
return true;
return false;
}
public bool VerifySpellRange(WorldObject target, TargetCategory targetCategory, Spell spell, WorldObject casterItem, uint magicSkill)
{
if (targetCategory != TargetCategory.WorldObject && targetCategory != TargetCategory.Wielded || target.Guid == Guid)
return true;
var targetLoc = target;
if (targetLoc.WielderId != null)
targetLoc = CurrentLandblock?.GetObject(targetLoc.WielderId.Value);
float distanceTo = Location.Distance2D(targetLoc.Location);
if (casterItem == null)
{
// use init + ranks, same as acclient DetermineSpellRange -> InqSkillLevel
// this is much lower than base, and omits things like attribute formula + base augs + enlightenment
var playerSkill = GetCreatureSkill(spell.School);
magicSkill = playerSkill.InitLevel + playerSkill.Ranks;
}
var maxRange = Math.Min(spell.BaseRangeConstant + magicSkill * spell.BaseRangeMod, MaxRadarRange_Outdoors);
if (distanceTo > maxRange)
{
SendUseDoneEvent(WeenieError.MissileOutOfRange);
return false;
}
// bootstrapping this function for indoor/outdoor check, since it is called both before and after windup
if (spell.Flags.HasFlag(SpellFlags.NotIndoor))
{
if (Location.Indoors || target != null && target.Location.Indoors)
{
SendUseDoneEvent(WeenieError.YourSpellCannotBeCastInside);
return false;
}
}
if (spell.Flags.HasFlag(SpellFlags.NotOutdoor))
{
if (!Location.Indoors || target != null && !target.Location.Indoors)
{
SendUseDoneEvent(WeenieError.YourSpellCannotBeCastOutside);
return false;
}
}
return true;
}
public CastingPreCheckStatus GetCastingPreCheckStatus(Spell spell, uint magicSkill, bool isWeaponSpell)
{
var difficulty = spell.Power;
var castingPreCheckStatus = CastingPreCheckStatus.CastFailed;
if (magicSkill > 0 && magicSkill >= (int)difficulty - 50)
{
var chance = SkillCheck.GetMagicSkillChance((int)magicSkill, (int)difficulty);
var rng = ThreadSafeRandom.Next(0.0f, 1.0f);
if (chance > rng)
castingPreCheckStatus = CastingPreCheckStatus.Success;
}
// build-in spells never fizzle
if (isWeaponSpell)
castingPreCheckStatus = CastingPreCheckStatus.Success;
// limit casting time between war and void
if (spell.School == MagicSchool.VoidMagic && LastSuccessCast_School == MagicSchool.WarMagic ||
spell.School == MagicSchool.WarMagic && LastSuccessCast_School == MagicSchool.VoidMagic)
{
// roll each time?
var timeLimit = ThreadSafeRandom.Next(3.0f, 5.0f);
if (Time.GetUnixTime() - LastSuccessCast_Time < timeLimit)
{
var curType = spell.School == MagicSchool.WarMagic ? "War" : "Void";
var prevType = LastSuccessCast_School == MagicSchool.VoidMagic ? "Nether" : "Elemental";
Session.Network.EnqueueSend(new GameMessageSystemChat($"The {prevType} energies permeating your blood cause this {curType} magic to fail.", ChatMessageType.Magic));
castingPreCheckStatus = CastingPreCheckStatus.CastFailed;
}
}
return castingPreCheckStatus;
}
public bool CalculateManaUsage(CastingPreCheckStatus castingPreCheckStatus, Spell spell, WorldObject target, WorldObject casterItem, out uint manaUsed)
{
manaUsed = 0;
if (castingPreCheckStatus == CastingPreCheckStatus.Success)
manaUsed = CalculateManaUsage(this, spell, target);
else if (castingPreCheckStatus == CastingPreCheckStatus.CastFailed)
manaUsed = 5; // todo: verify with retail
var currentMana = Mana.Current;
if (casterItem != null)
{
//var caster = GetEquippedWand();
currentMana = (uint)(casterItem.ItemCurMana ?? 0);
}
if (manaUsed > currentMana)
{
SendUseDoneEvent(WeenieError.YouDontHaveEnoughManaToCast);
return false;
}
Proficiency.OnSuccessUse(this, GetCreatureSkill(Skill.ManaConversion), spell.PowerMod);
return true;
}
public void DoSpellWords(Spell spell, bool isWeaponSpell)
{
spell.Formula.GetPlayerFormula(this);
var spellWords = spell._spellBase.GetSpellWords(DatManager.PortalDat.SpellComponentsTable);
if (!string.IsNullOrWhiteSpace(spellWords) && !isWeaponSpell)
EnqueueBroadcast(new GameMessageHearSpeech(spellWords, GetNameWithSuffix(), Guid.Full, ChatMessageType.Spellcasting), LocalBroadcastRange);
OnTalk(spellWords);
}
public static float CastSpeed = 2.0f; // from retail pcaps, player animation speed for windup / first half of cast gesture
public void DoWindupGestures(Spell spell, bool isWeaponSpell, ActionChain castChain)
{
if (spell.Flags.HasFlag(SpellFlags.FastCast) || isWeaponSpell)
return;
if (FastTick)
{
castChain.AddAction(this, () =>
{
PhysicsObj.StopCompletely(false);
MagicState.TurnStarted = false;
MagicState.IsTurning = false;
});
}
var windupTime = 0.0f;
foreach (var windupGesture in spell.Formula.WindupGestures)
{
if (RecordCast.Enabled)
{
castChain.AddAction(this, () =>
{
var animLength = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, windupGesture, CastSpeed);
RecordCast.Log($"Windup Gesture: {windupGesture}, Windup Time: {animLength}");
});
}
// don't mess with CurrentMotionState here?
if (!FastTick)
windupTime = EnqueueMotionMagic(castChain, windupGesture, CastSpeed);
/*Console.WriteLine($"{spell.Name}");
Console.WriteLine($"Windup Gesture: " + windupGesture);
Console.WriteLine($"Windup time: " + windupTime);
Console.WriteLine("-------");*/
}
if (FastTick)
windupTime = EnqueueMotionAction(castChain, spell.Formula.WindupGestures, CastSpeed, MotionStance.Magic, checkCasting: true);
}
public void DoCastGesture(Spell spell, WorldObject casterItem, ActionChain castChain)
{
MagicState.CastGesture = spell.Formula.CastGesture;
if (casterItem != null)
{
//var caster = GetEquippedWand();
if (casterItem.UseUserAnimation != 0)
MagicState.CastGesture = casterItem.UseUserAnimation;
}
if (RecordCast.Enabled)
{
castChain.AddAction(this, () =>
{
var animLength = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MagicState.CastGesture, CastSpeed);
RecordCast.Log($"Cast Gesture: {MagicState.CastGesture}, Cast Time: {animLength}");
});
}
castChain.AddAction(this, () =>
{
if (!MagicState.IsCasting) return;
MagicState.CastGestureStartTime = DateTime.UtcNow;
if (FastTick)
PhysicsObj.StopCompletely(false);
});
if (MagicState.CastGesture == MotionCommand.Invalid)
MagicState.CastGesture = MotionCommand.Ready;
var castTime = 0.0f;
if (FastTick)
castTime = EnqueueMotion(castChain, MagicState.CastGesture, CastSpeed, true, null, true);
else
castTime = EnqueueMotionMagic(castChain, MagicState.CastGesture, CastSpeed);
//Console.WriteLine($"Cast Gesture: " + MagicState.CastGesture);
//Console.WriteLine($"Cast time: " + castTime);
}
// 20 from MoveToManager threshold?
public const float MaxAngle = 5;
public void DoCastSpell(MagicState _state, bool checkAngle = true)
{
//Console.WriteLine("DoCastSpell");
if (!MagicState.IsCasting)
return;
var state = _state?.CastSpellParams;
if (state == null)
{
log.Warn($"{Name}.DoCastSpell(): null state detected");
log.Warn(_state);
// send UseDone?
SendUseDoneEvent(WeenieError.BadCast);
return;
}
DoCastSpell(state.Spell, state.CasterItem, state.MagicSkill, state.ManaUsed, state.Target, state.Status, checkAngle);
}
public bool IsWithinAngle(WorldObject target)
{
// TODO: investigate this more, difference for GetAngle() between ACE and ac physics engine
var angle = 0.0f;
if (target != this)
{
if (target.CurrentLandblock == null)
{
FindObject(target.Guid.Full, SearchLocations.Everywhere, out _, out var rootOwner, out _);
if (rootOwner == null)
log.Error($"{Name}.IsWithinAngle({target.Name} ({target.Guid})) - couldn't find rootOwner");
else if (rootOwner != this)
angle = GetAngle(rootOwner);
}
else
angle = GetAngle(target);
}
//Console.WriteLine($"Angle: " + angle);
var maxAngle = PropertyManager.GetDouble("spellcast_max_angle").Item;
if (RecordCast.Enabled)
RecordCast.Log($"DoCastSpell(angle={angle} vs. {maxAngle})");
return angle <= maxAngle;
}
public void DoCastSpell(Spell spell, WorldObject casterItem, uint magicSkill, uint manaUsed, WorldObject target, CastingPreCheckStatus castingPreCheckStatus, bool checkAngle = true)
{
if (target != null)
{
// verify target still exists
var targetCategory = GetTargetCategory(target.Guid.Full, spell.Id, out target);
if (target == null)
{
SendWeenieError(WeenieError.TargetNotAcquired);
FinishCast();
return;
}
// do second rotate, if applicable
// TODO: investigate this more, difference for GetAngle() between ACE and ac physics engine
if (checkAngle && !IsWithinAngle(target))
{
if (!FastTick)
{
var rotateTime = Rotate(target);
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(rotateTime);
actionChain.AddAction(this, () => DoCastSpell(spell, casterItem, magicSkill, manaUsed, target, castingPreCheckStatus, false));
actionChain.EnqueueChain();
}
else
{
if (PhysicsObj.MovementManager.MotionInterpreter.InterpretedState.TurnCommand == 0)
TurnTo_Magic(target);
else
MagicState.PendingTurnRelease = true;
}
return;
}
// verify spell range
if (!VerifySpellRange(target, targetCategory, spell, casterItem, magicSkill))
{
FinishCast();
return;
}
}
if (IsDead)
{
FinishCast();
return;
}
DoCastSpell_Inner(spell, casterItem, manaUsed, target, castingPreCheckStatus);
}
public WorldObject TurnTarget;
public void TurnTo_Magic(WorldObject target)
{
//Console.WriteLine($"{Name}.TurnTo_Magic()");
TurnTarget = target;
MagicState.TurnStarted = true;
MagicState.IsTurning = true;
if (FastTick)
{
if (PropertyManager.GetDouble("spellcast_max_angle").Item > 5.0f && IsWithinAngle(target))
{
// emulate current gdle TurnTo - doesn't match retail, but some players may prefer this
OnMoveComplete_Magic(WeenieError.None);
return;
}
// verify cast radius before every automatic TurnTo after windup
if (!VerifyCastRadius())
return;
var stopCompletely = !MagicState.CastMotionDone;
//var stopCompletely = true;
CreateTurnToChain2(target, null, null, stopCompletely, MagicState.AlwaysTurn);
MagicState.AlwaysTurn = false;
}
}
public Physics.Common.Position StartPos { get; set; }
public void DoCastSpell_Inner(Spell spell, WorldObject casterItem, uint manaUsed, WorldObject target, CastingPreCheckStatus castingPreCheckStatus, bool finishCast = true)
{
if (RecordCast.Enabled)
RecordCast.Log($"DoCastSpell_Inner()");
if (MagicState.CastMeter)
{
var gestureTime = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MagicState.CastGesture, CastSpeed);
var castTime = DateTime.UtcNow - MagicState.CastGestureStartTime;
var efficiency = 1.0f - (float)castTime.TotalSeconds / gestureTime;
var msg = $"Cast efficiency: {efficiency * 100}%";
Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Broadcast));
}
// consume mana
var caster = casterItem ?? GetEquippedWand(); // TODO: persist this from the beginning, since this is done with delay
var isWeaponSpell = casterItem != null;
var itemCaster = isWeaponSpell ? caster : null;
if (!isWeaponSpell)
UpdateVitalDelta(Mana, -(int)manaUsed);
else
{
if (itemCaster != null)
itemCaster.ItemCurMana -= (int)manaUsed;
else
castingPreCheckStatus = CastingPreCheckStatus.CastFailed;
}
// consume spell components
if (!isWeaponSpell)
TryBurnComponents(spell);
// check windup move distance cap
var dist = StartPos.Distance(PhysicsObj.Position);
// only PKs affected by these caps?
if (dist > Windup_MaxMove && PlayerKillerStatus != PlayerKillerStatus.NPK)
{
//player.Session.Network.EnqueueSend(new GameEventWeenieError(player.Session, WeenieError.YouHaveMovedTooFar));
Session.Network.EnqueueSend(new GameMessageSystemChat("Your movement disrupted spell casting!", ChatMessageType.Magic));
EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.Fizzle, 0.5f));
if (finishCast)
FinishCast();
return;
}
var pk_error = CheckPKStatusVsTarget(target, spell);
if (pk_error != null)
castingPreCheckStatus = CastingPreCheckStatus.InvalidPKStatus;
switch (castingPreCheckStatus)
{
case CastingPreCheckStatus.Success:
if (!spell.IsFellowshipSpell)
CreatePlayerSpell(target, spell, isWeaponSpell);
else
{
var fellows = GetFellowshipTargets();
foreach (var fellow in fellows)
CreatePlayerSpell(fellow, spell, isWeaponSpell);
}
// handle self procs
if (spell.IsHarmful && target != this)
TryProcEquippedItems(this, this, true, caster);
break;
case CastingPreCheckStatus.InvalidPKStatus:
if (spell.NumProjectiles > 0)
HandleCastSpell(spell, target, itemCaster, caster, isWeaponSpell);
break;
default:
EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.Fizzle, 0.5f));
SendWeenieError(WeenieError.YourSpellFizzled);
break;
}
if (pk_error != null && spell.NumProjectiles == 0)
{
Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(Session, pk_error[0], target.Name));
if (target is Player targetPlayer)
targetPlayer.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(targetPlayer.Session, pk_error[1], Name));
}
if (finishCast)
FinishCast();
}
public void FinishCast()
{
var hasWindupGestures = MagicState.CastSpellParams?.HasWindupGestures ?? true;
var castGesture = MagicState.CastGesture;
if (FastTick)
castGesture = hasWindupGestures ? CurrentMotionState.MotionState.ForwardCommand : MagicState.CastGesture;
var selfTarget = !hasWindupGestures && MagicState.CastSpellParams.Target == this;
MagicState.OnCastDone();
IsBusy = true;
var queue = PropertyManager.GetBool("spellcast_recoil_queue").Item;
if (queue)
MagicState.CanQueue = true;
if (FastTick)
{
var fastbuff = selfTarget && PropertyManager.GetBool("fastbuff").Item;
// return to magic ready stance
var actionChain = new ActionChain();
EnqueueMotion(actionChain, MotionCommand.Ready, 1.0f, true, castGesture, false, fastbuff);
actionChain.AddAction(this, () =>
{
IsBusy = false;
SendUseDoneEvent();
if (queue)
HandleCastQueue();
//Console.WriteLine("====================================");
});
actionChain.EnqueueChain();
}
else
{
// temporarily old version:
// return to magic combat stance
var returnStance = new Motion(MotionStance.Magic, MotionCommand.Ready, 1.0f);
EnqueueBroadcastMotion(returnStance);
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(1.0f); // TODO: get actual recoil timing
actionChain.AddAction(this, () => {
IsBusy = false;
SendUseDoneEvent();
if (queue)
HandleCastQueue();
});
actionChain.EnqueueChain();
}
}
/// <summary>
/// Method used for handling player targeted spell casts
/// </summary>
/// <param name="builtInSpell">If TRUE, casting a built-in spell from a weapon</param>
public bool CreatePlayerSpell(WorldObject target, TargetCategory targetCategory, uint spellId, WorldObject casterItem)
{
var creatureTarget = target as Creature;
var spell = ValidateSpell(spellId, casterItem != null);
if (spell == null)
return false;
if (!VerifySpellTarget(spell, target))
return false;
// if casting implement has spell built in,
// use spellcraft from the item, instead of player's magic skill?
var caster = casterItem ?? GetEquippedWand();
var isWeaponSpell = casterItem != null && IsWeaponSpell(spell.Id, casterItem);
// Grab player's skill level in the spell's Magic School
var magicSkill = GetCreatureSkill(spell.School).Current;
if (isWeaponSpell && caster.ItemSpellcraft != null)
magicSkill = (uint)caster.ItemSpellcraft;
// verify spell range
if (!VerifySpellRange(target, targetCategory, spell, casterItem, magicSkill))
return false;
// get casting pre-check status
var castingPreCheckStatus = GetCastingPreCheckStatus(spell, magicSkill, isWeaponSpell);
// calculate mana usage
if (!CalculateManaUsage(castingPreCheckStatus, spell, target, casterItem, out var manaUsed))
return false;
// spell words
DoSpellWords(spell, isWeaponSpell);
var spellChain = new ActionChain();
//StartPos = new Physics.Common.Position(PhysicsObj.Position);
// do wind-up gestures: fastcast has no windup (creature enchantments)
DoWindupGestures(spell, isWeaponSpell, spellChain);
// cast spell
DoCastGesture(spell, casterItem, spellChain);
MagicState.SetCastParams(spell, casterItem, magicSkill, manaUsed, target, castingPreCheckStatus);
if (!FastTick)
spellChain.AddAction(this, () => DoCastSpell(MagicState));
spellChain.EnqueueChain();
return true;
}
public List<Player> GetFellowshipTargets()
{
if (Fellowship != null)
return Fellowship.GetFellowshipMembers().Values.ToList();
else
return new List<Player>() { this };
}
private void CreatePlayerSpell(WorldObject target, Spell spell, bool isWeaponSpell)
{
var targetCreature = target as Creature;
var targetPlayer = target as Player;
LastSuccessCast_School = spell.School;
LastSuccessCast_Time = Time.GetUnixTime();
var caster = GetEquippedWand();
var itemCaster = isWeaponSpell ? caster : null;
// verify after windup, still consumes mana
if (spell.MetaSpellType == SpellType.Dispel && !VerifyDispelPKStatus(this, target))
return;
switch (spell.School)
{
case MagicSchool.ItemEnchantment:
TryCastItemEnchantment_WithRedirects(spell, target, itemCaster);
// use target resistance?
Proficiency.OnSuccessUse(this, GetCreatureSkill(Skill.ItemEnchantment), spell.PowerMod);
if (spell.IsHarmful)
{
var playerRedirect = targetPlayer;
if (playerRedirect == null && target?.WielderId != null)
playerRedirect = CurrentLandblock?.GetObject(target.WielderId.Value) as Player;
if (playerRedirect != null)
UpdatePKTimers(this, playerRedirect);
}
break;
default:
if (!spell.IsProjectile)
{
if (targetPlayer == null)
OnAttackMonster(targetCreature);
if (TryResistSpell(target, spell, itemCaster))
break;
if (targetCreature != null && targetCreature.NonProjectileMagicImmune)
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You fail to affect {targetCreature.Name} with {spell.Name}", ChatMessageType.Magic));
break;
}
}
HandleCastSpell(spell, target, itemCaster, caster, isWeaponSpell);
if (!spell.IsProjectile)
{
if (spell.IsHarmful)
{
if (targetCreature != null)
Proficiency.OnSuccessUse(this, GetCreatureSkill(spell.School), targetCreature.GetCreatureSkill(Skill.MagicDefense).Current);
// handle target procs
if (targetCreature != null && targetCreature != this)
TryProcEquippedItems(this, targetCreature, false, caster);
if (targetPlayer != null)
UpdatePKTimers(this, targetPlayer);
}
else
Proficiency.OnSuccessUse(this, GetCreatureSkill(spell.School), spell.PowerMod);
}
break;
}
}
/// <summary>
/// Method used for handling player untargeted spell casts
/// </summary>
public bool CreatePlayerSpell(uint spellId)
{
var spell = ValidateSpell(spellId);
if (spell == null)
return false;
// get player's current magic skill
var magicSkill = GetCreatureSkill(spell.School).Current;
var castingPreCheckStatus = GetCastingPreCheckStatus(spell, magicSkill, false);
// calculate mana usage
if (!CalculateManaUsage(castingPreCheckStatus, spell, null, null, out var manaUsed))
return false;
// begin spellcasting
DoSpellWords(spell, false);
var spellChain = new ActionChain();
//StartPos = new Physics.Common.Position(PhysicsObj.Position);
// do wind-up gestures: fastcast has no windup (creature enchantments)
DoWindupGestures(spell, false, spellChain);
// do cast gesture
DoCastGesture(spell, null, spellChain);
// cast untargeted spell
MagicState.SetCastParams(spell, null, magicSkill, manaUsed, null, castingPreCheckStatus);
if (!FastTick)
spellChain.AddAction(this, () => DoCastSpell(MagicState));
spellChain.EnqueueChain();
return true;
}
public void TryBurnComponents(Spell spell)
{
if (SafeSpellComponents || PropertyManager.GetBool("safe_spell_comps").Item)
return;
var burned = spell.TryBurnComponents(this);
if (burned.Count == 0) return;
// decrement components
for (var i = burned.Count - 1; i >= 0; i--)
{
var component = burned[i];
if (!SpellFormula.SpellComponentsTable.SpellComponents.TryGetValue(component, out var spellComponent))
{
log.Error($"{Name}.TryBurnComponents(): Couldn't find SpellComponent {component}");
continue;
}
var wcid = Spell.GetComponentWCID(component);
if (wcid == 0) continue;
var item = GetInventoryItemsOfWCID(wcid).FirstOrDefault();
if (item == null)
{
if (SpellComponentsRequired && PropertyManager.GetBool("require_spell_comps").Item)
log.Warn($"{Name}.TryBurnComponents({spellComponent.Name}): not found in inventory");
else
burned.RemoveAt(i);
continue;
}
TryConsumeFromInventoryWithNetworking(item, 1);
}
if (burned.Count == 0)
return;
// send message to player
var msg = Spell.GetConsumeString(burned);
Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Magic));
}
/// <summary>
/// Returns TRUE if the player has the required number of components to cast spell
/// </summary>
public bool HasComponentsForSpell(Spell spell)
{
spell.Formula.GetPlayerFormula(this);
if (!SpellComponentsRequired || !PropertyManager.GetBool("require_spell_comps").Item)
return true;
var requiredComps = spell.Formula.GetRequiredComps();
foreach (var kvp in requiredComps)
{
var wcid = kvp.Key;
var required = kvp.Value;
var available = GetNumInventoryItemsOfWCID(wcid);
if (required > available)
return false;
}
return true;
}
public void HandleMotionDone_Magic(uint motionID, bool success)
{
//Console.WriteLine($"HandleMotionDone_Magic({(MotionCommand)motionID}, {success})");
if (!FastTick || !MagicState.IsCasting) return;
if (motionID == (uint)MagicState.CastGesture)
{
if (RecordCast.Enabled)
RecordCast.Log($"{Name}.HandleMotionDone_Magic({(MotionCommand)motionID}, {success}) - cast gesture done");
MagicState.CastMotionDone = true;
var actionChain = new ActionChain();
actionChain.AddDelayForOneTick();
actionChain.AddAction(this, () =>
{
if (!MagicState.IsCasting)
return;
MagicState.AlwaysTurn = true;
DoCastSpell(MagicState);
});
actionChain.EnqueueChain();
}
}
public void OnMoveComplete_Magic(WeenieError status)
{
//Console.WriteLine($"OnMoveComplete_Magic({status})");
if (!FastTick || !MagicState.IsCasting || !MagicState.TurnStarted)
return;
// this occurs after the player is done turning
// before the windup, or after the first half of the cast motion
// either completed or cancelled
if (RecordCast.Enabled)
RecordCast.Log($"{Name}.OnMoveComplete_Magic({status}) - DoCastSpell");
MagicState.IsTurning = false;
var checkAngle = status != WeenieError.None;
var actionChain = new ActionChain();
actionChain.AddDelayForOneTick();
actionChain.AddAction(this, () =>
{
if (!MagicState.IsCasting) return;
if (!MagicState.CastMotionDone)
DoWindup(MagicState.WindupParams, checkAngle);
else
DoCastSpell(MagicState, checkAngle);
});
actionChain.EnqueueChain();
}
public void FailCast(bool tryFizzle = true)
{
var parms = MagicState.CastSpellParams;
var werror = WeenieError.None;
if (parms != null && tryFizzle)
{
DoCastSpell_Inner(parms.Spell, parms.CasterItem, parms.ManaUsed, parms.Target, CastingPreCheckStatus.CastFailed, false);
werror = WeenieError.YourSpellFizzled;
}
SendUseDoneEvent(werror);
MagicState.OnCastDone();
}
public void OnTurnRelease()
{
MagicState.PendingTurnRelease = false;
if (!MagicState.CastMotionDone)
DoWindup(MagicState.WindupParams, true);
else
DoCastSpell(MagicState, true);
}
public bool VerifyCastRadius()
{
if (MagicState.CastGestureStartTime != DateTime.MinValue)
{
var dist = StartPos.Distance(PhysicsObj.Position);
if (dist > Windup_MaxMove && PlayerKillerStatus != PlayerKillerStatus.NPK)
{
FailCast();
return false;
}
}
return true;
}
public void CheckTurn()
{
// verify cast radius while manually moving after windup
if (!VerifyCastRadius())
return;
if (TurnTarget != null && IsWithinAngle(TurnTarget))
{
if (MagicState.PendingTurnRelease)
OnTurnRelease();
else
PhysicsObj.StopCompletely(false);
}
}
public void HandleCastQueue()
{
MagicState.CanQueue = false;
if (MagicState.CastQueue != null)
{
if (MagicState.CastQueue.Type == CastQueueType.Targeted)
HandleActionCastTargetedSpell(MagicState.CastQueue.TargetGuid, MagicState.CastQueue.SpellId, MagicState.CastQueue.CasterItem);
else
HandleActionMagicCastUnTargetedSpell(MagicState.CastQueue.SpellId);
}
}
public static bool VerifyNonComponentTargetType(Spell spell, WorldObject target)
{
// untargeted spell projectiles
if (target == null)
return spell.NonComponentTargetType == ItemType.None;
switch (spell.NonComponentTargetType)
{
case ItemType.Creature:
return target is Creature;
// banes / lures
case ItemType.Vestements:
//return target is Clothing || target.IsShield;
return target is Creature || target is Clothing || target.IsShield;
case ItemType.Weapon:
//return target is MeleeWeapon || target is MissileLauncher;
return target is Creature || target is MeleeWeapon || target is MissileLauncher;
case ItemType.Caster:
//return target is Caster;
return target is Creature || target is Caster;
case ItemType.WeaponOrCaster:
//return target is MeleeWeapon || target is MissileLauncher || target is Caster;
return target is Creature || target is MeleeWeapon || target is MissileLauncher || target is Caster;
case ItemType.Portal:
if (spell.MetaSpellType == SpellType.PortalRecall || spell.MetaSpellType == SpellType.PortalSummon)
return target is Creature;
else
return target is Portal;
case ItemType.LockableMagicTarget:
return target is Door || target is Chest;
// Essence Lull?
case ItemType.Item:
return !(target is Creature);
case ItemType.LifeStone:
return target is Lifestone;
}
log.Error($"VerifyNonComponentTargetType({spell.Id} - {spell.Name}, {target.Name}) - unexpected NonComponentTargetType {spell.NonComponentTargetType}");
return false;
}
/// <summary>
/// Sends a chat message with respect to SquelchManager
/// </summary>
public void SendChatMessage(WorldObject source, string msg, ChatMessageType msgType)
{
if (!SquelchManager.Squelches.Contains(source, msgType))
Session.Network.EnqueueSend(new GameMessageSystemChat(msg, msgType));
}
}
}