using System;
using System.Collections.Generic;
using ACE.Common;
using ACE.DatLoader;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Physics.Animation;
namespace ACE.Server.WorldObjects
{
///
/// Monster casting for magic spells
///
partial class Creature
{
private bool AiUsesMana
{
get => GetProperty(PropertyBool.AiUsesMana) ?? true; // default true?
set { if (!value) RemoveProperty(PropertyBool.AiUsesMana); else SetProperty(PropertyBool.AiUsesMana, value); }
}
///
/// Very specific monsters will be set to use the human casting animations,
/// ie. the windup and casting gestures from the spell
///
private bool AiUseHumanMagicAnimations
{
get => GetProperty(PropertyBool.AiUseHumanMagicAnimations) ?? false;
set { if (!value) RemoveProperty(PropertyBool.AiUseHumanMagicAnimations); else SetProperty(PropertyBool.AiUseHumanMagicAnimations, value); }
}
///
/// The amount of time a monster waits to cast a magic spell
/// defined as seconds from the start of the previous attack
/// the most common value in the db is 3s
/// some other common values include 2s and 1s, with some mobs having values up to 1m
///
private double? AiUseMagicDelay
{
get => GetProperty(PropertyFloat.AiUseMagicDelay);
set { if (!value.HasValue) RemoveProperty(PropertyFloat.AiUseMagicDelay); else SetProperty(PropertyFloat.AiUseMagicDelay, value.Value); }
}
///
/// Returns TRUE if monster has known spells
///
private bool HasKnownSpells => Biota.HasKnownSpell(BiotaDatabaseLock);
///
/// The next spell the monster will attempt to cast
///
private Spell CurrentSpell { get; set; }
private bool TryRollSpell()
{
CurrentSpell = null;
//Console.WriteLine($"{Name}.TryRollSpell(), probability={GetProbabilityAny()}");
// monster spellbooks have probabilities with base 2.0
// ie. a 5% chance would be 2.05 instead of 0.05
// much less common, some monsters will have spells with just base 2.0 probability
// there were probably other criteria used to select these spells (emote responses, monster ai responses)
// for now, 2.0 base just becomes a 2% chance
if (Biota.PropertiesSpellBook == null)
return false;
// We don't use thread safety here. Monster spell books aren't mutated cross-threads.
// This reduces memory consumption by not cloning the spell book every single TryRollSpell()
//foreach (var spell in Biota.CloneSpells(BiotaDatabaseLock)) // Thread-safe
foreach (var spell in Biota.PropertiesSpellBook) // Not thread-safe
{
var probability = spell.Value > 2.0f ? spell.Value - 2.0f : spell.Value / 100.0f;
var rng = ThreadSafeRandom.Next(0.0f, 1.0f);
if (rng < probability)
{
CurrentSpell = new Spell(spell.Key);
return true;
}
}
return false;
}
///
/// Returns the probability of this monster casting a spell for an attack
///
private float GetProbabilityAny()
{
var probabilities = new List();
foreach (var spell in Biota.GetKnownSpellsProbabilities(BiotaDatabaseLock))
{
var probability = spell > 2.0f ? spell - 2.0f : spell / 100.0f;
probabilities.Add(probability);
}
return Probability.GetProbabilityAny(probabilities);
}
///
/// Returns the maximum range for the current spell
///
private float GetSpellMaxRange()
{
var skill = GetMagicSkillForRangeCheck();
var maxRange = Math.Min(CurrentSpell.BaseRangeConstant + skill * CurrentSpell.BaseRangeMod, Player.MaxRadarRange_Outdoors);
if (maxRange == 0.0f)
maxRange = float.PositiveInfinity;
return maxRange;
}
private bool IsSelfCast()
{
if (CurrentAttack != CombatType.Magic)
return false;
return GetSpellMaxRange() == float.PositiveInfinity;
}
///
/// Performs the monster windup spell animation,
/// casts the spell, and returns to attack stance
///
private void MagicAttack()
{
var target = AttackTarget as Creature;
if (target == null || !target.IsAlive)
{
FindNextTarget();
return;
}
var spell = CurrentSpell;
// turn to?
if (AiUsesMana && !UseMana()) return;
// spell words
if (AiUseHumanMagicAnimations)
{
var spellWords = spell._spellBase.GetSpellWords(DatManager.PortalDat.SpellComponentsTable);
if (!string.IsNullOrWhiteSpace(spellWords))
EnqueueBroadcast(new GameMessageHearSpeech(spellWords, Name, Guid.Full, ChatMessageType.Spellcasting), LocalBroadcastRange);
}
var preCastTime = PreCastMotion(AttackTarget);
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(preCastTime);
actionChain.AddAction(this, () =>
{
if (IsDead || AttackTarget == null || target.IsDead)
return;
CastSpell(spell);
PostCastMotion();
});
actionChain.EnqueueChain();
var postCastTime = GetPostCastTime(spell);
var animTime = preCastTime + postCastTime;
//Console.WriteLine($"{Name}.MagicAttack(): preCastTime({preCastTime}), postCastTime({postCastTime})");
// slight variation here
PrevAttackTime = Timers.RunningTime + preCastTime;
var powerupTime = (float)(PowerupTime ?? 1.0f);
var postDelay = ThreadSafeRandom.Next(0.0f, powerupTime);
NextMoveTime = NextAttackTime = PrevAttackTime + postCastTime + postDelay;
}
private bool UseMana()
{
// do any monsters have mana conversion?
var target = GetSpellMaxRange() < float.PositiveInfinity ? AttackTarget : this;
var manaUsed = CalculateManaUsage(this, CurrentSpell, target);
if (manaUsed > Mana.Current)
return false;
Mana.Current -= manaUsed;
return true;
}
private const float PreCastSpeed = 2.0f;
private const float PostCastSpeed = 1.0f;
private const float PostCastSpeed_Ranged = 1.66f; // ??
///
/// Perform the first part of monster spell casting animation - spreading arms out
///
public float PreCastMotion(WorldObject target, bool fallback = false)
{
if (AiUseHumanMagicAnimations && !fallback)
return PreCastMotion_Human(target);
var motion = new Motion(this, MotionCommand.CastSpell, PreCastSpeed);
motion.MotionState.TurnSpeed = 2.25f;
//motion.HasTarget = true;
//motion.TargetGuid = target.Guid;
CurrentMotionState = motion;
EnqueueBroadcastMotion(motion);
return MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.CastSpell, PreCastSpeed);
}
///
/// For monsters with AiUseHumanMagicAnimations = true,
/// performs the windup gestures from the spell scarabs
///
/// The amount of time for the windup gestures to complete
private float PreCastMotion_Human(WorldObject target)
{
// todo: play each motion at the proper time,
// ensuring the monster is still alive at each step
CurrentSpell.Formula.GetMonsterFormula();
// FIXME: data
var castAnimTime = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, CurrentSpell.Formula.CastGesture, PreCastSpeed);
if (castAnimTime == 0)
return PreCastMotion(target, true);
var animTime = 0.0f;
foreach (var windupGesture in CurrentSpell.Formula.WindupGestures)
{
var motion = new Motion(this, windupGesture, PreCastSpeed);
motion.MotionState.TurnSpeed = 2.25f;
CurrentMotionState = motion;
EnqueueBroadcastMotion(motion);
animTime += MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, windupGesture, PreCastSpeed);
}
var castMotion = new Motion(this, CurrentSpell.Formula.CastGesture, PreCastSpeed);
castMotion.MotionState.TurnSpeed = 2.25f;
CurrentMotionState = castMotion;
EnqueueBroadcastMotion(castMotion);
animTime += castAnimTime;
return animTime;
}
///
/// Casts the current monster spell on target
///
public void CastSpell(Spell spell)
{
if (AttackTarget == null) return;
var targetSelf = spell.Flags.HasFlag(SpellFlags.SelfTargeted);
var untargeted = spell.NonComponentTargetType == ItemType.None;
var target = AttackTarget;
if (untargeted)
target = null;
else if (targetSelf)
target = this;
var caster = GetEquippedWand();
// handle self procs
if (spell.IsHarmful && target != this)
TryProcEquippedItems(this, this, true, caster);
// If the target is too far away, don't cast. This checks to see of this monster and the target are on separate landblock groups, and potentially separate threads.
// This also fixes cross-threading issues
if (target != null && (CurrentLandblock == null || target.CurrentLandblock == null || CurrentLandblock.CurrentLandblockGroup != target.CurrentLandblock.CurrentLandblockGroup))
return;
// try to resist spell, if applicable
if (TryResistSpell(target, spell))
{
TryHandleFactionMob(target);
return;
}
var targetCreature = target as Creature;
// TODO: see if this can be coalesced
switch (spell.School)
{
case MagicSchool.CreatureEnchantment:
HandleCastSpell(spell, target);
if (spell.IsHarmful)
{
// handle target procs
if (targetCreature != null && targetCreature != this)
TryProcEquippedItems(this, targetCreature, false, caster);
}
break;
case MagicSchool.ItemEnchantment:
TryCastItemEnchantment_WithRedirects(spell, target);
break;
case MagicSchool.LifeMagic:
HandleCastSpell(spell, target, null, caster);
if (spell.MetaSpellType != SpellType.LifeProjectile)
{
TryHandleFactionMob(target);
if (spell.IsHarmful)
{
// handle target procs
if (targetCreature != null && targetCreature != this)
TryProcEquippedItems(this, targetCreature, false, caster);
}
}
break;
case MagicSchool.WarMagic:
case MagicSchool.VoidMagic:
HandleCastSpell(spell, target, caster);
break;
}
}
///
/// Perform the animations after casting a spell,
/// ie. moving arms back in, returning to previous stance
///
public void PostCastMotion()
{
var animSpeed = IsRanged ? PostCastSpeed_Ranged : PostCastSpeed;
var motion = new Motion(this, MotionCommand.Ready, animSpeed);
motion.MotionState.TurnSpeed = 2.25f;
//motion.HasTarget = true;
//motion.TargetGuid = target.Guid;
CurrentMotionState = motion;
EnqueueBroadcastMotion(motion);
}
public float GetPostCastTime(Spell spell, bool fallback = false)
{
if (AiUseHumanMagicAnimations && !fallback)
return GetPostCastTime_Human(spell);
var animSpeed = IsRanged ? PostCastSpeed_Ranged : PostCastSpeed;
return MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.CastSpell, MotionCommand.Ready, animSpeed);
}
private float GetPostCastTime_Human(Spell spell)
{
var animSpeed = IsRanged ? PostCastSpeed_Ranged : PostCastSpeed;
var animTime = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, spell.Formula.CastGesture, MotionCommand.Ready, animSpeed);
// FIXME: data
if (animTime == 0.0f)
return GetPostCastTime(spell, true);
return animTime;
}
///
/// Returns the magic skill level used for spell range checks.
/// (initial points + points due to directly raising the skill)
///
///
private uint GetMagicSkillForRangeCheck()
{
var skill = GetCreatureSkill(CurrentSpell.School);
// verify this - should it be using base?
// seems like it could be off, player formula uses current + cap?
return skill.InitLevel + skill.Ranks;
}
public void TryHandleFactionMob(WorldObject target)
{
if (target == this || target is Player)
return;
var creatureTarget = target as Creature;
if (creatureTarget == null || !AllowFactionCombat(creatureTarget) && !PotentialFoe(creatureTarget))
return;
MonsterOnAttackMonster(creatureTarget);
}
///
/// Checks for AiUseHumanMagicAnimations and if true, sets CurrentSpell and sets combat mode to Magic
///
public void CheckForHumanPreCast(Spell spell)
{
if (AiUseHumanMagicAnimations)
{
CurrentSpell = new Spell(spell.Id);
SetCombatMode(CombatMode.Magic);
}
}
}
}