using System;
using System.Numerics;
using ACE.Entity.Enum;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Physics.Animation;
namespace ACE.Server.WorldObjects
{
partial class Creature
{
///
/// The delay between missile attacks (todo: find actual value)
///
public const float MissileDelay = 1.0f;
///
/// Returns TRUE if monster has physical ranged attacks
///
public new bool IsRanged => GetEquippedMissileWeapon() != null;
///
/// Starts a monster missile attack
///
public void RangeAttack()
{
var target = AttackTarget as Creature;
if (target == null || !target.IsAlive)
{
FindNextTarget();
return;
}
var weapon = GetEquippedMissileWeapon();
var ammo = GetEquippedAmmo();
if (weapon == null || weapon.IsAmmoLauncher && ammo == null) return;
// simulate accuracy bar / allow client rotate to fully complete
var actionChain = new ActionChain();
//IsTurning = true;
//actionChain.AddDelaySeconds(0.5f);
// do missile attack
actionChain.AddAction(this, LaunchMissile);
actionChain.EnqueueChain();
}
///
/// Launches a missile attack from monster to target
///
public void LaunchMissile()
{
//IsTurning = false;
var weapon = GetEquippedMissileWeapon();
if (weapon == null || AttackTarget == null) return;
var ammo = weapon.IsAmmoLauncher ? GetEquippedAmmo() : weapon;
if (ammo == null) return;
var launcher = GetEquippedMissileLauncher();
/*if (!IsDirectVisible(AttackTarget))
{
// ensure direct line of sight
//NextAttackTime = Timers.RunningTime + 1.0f;
SwitchToMeleeAttack();
return;
}*/
if (SwitchWeaponsPending)
{
NextAttackTime = Timers.RunningTime + 1.0f;
return;
}
// should this be called each launch?
AttackHeight = ChooseAttackHeight();
var dist = GetDistanceToTarget();
//Console.WriteLine("RangeAttack: " + dist);
if (DebugMove)
Console.WriteLine($"[{Timers.RunningTime}] - {Name} ({Guid}) - LaunchMissile");
var projectileSpeed = GetProjectileSpeed();
// get z-angle for aim motion
var aimVelocity = GetAimVelocity(AttackTarget, projectileSpeed);
var aimLevel = GetAimLevel(aimVelocity);
// calculate projectile spawn pos and velocity
var localOrigin = GetProjectileSpawnOrigin(ammo.WeenieClassId, aimLevel);
var velocity = CalculateProjectileVelocity(localOrigin, AttackTarget, projectileSpeed, out Vector3 origin, out Quaternion orientation);
//Console.WriteLine($"Velocity: {velocity}");
// launch animation
var actionChain = new ActionChain();
var launchTime = EnqueueMotion(actionChain, aimLevel);
//Console.WriteLine("LaunchTime: " + launchTime);
// launch projectile
actionChain.AddAction(this, () =>
{
if (IsDead) return;
// handle self-procs
TryProcEquippedItems(this, this, true, weapon);
var sound = GetLaunchMissileSound(weapon);
EnqueueBroadcast(new GameMessageSound(Guid, sound, 1.0f));
// TODO: monster stamina usage
if (AttackTarget != null)
{
var projectile = LaunchProjectile(launcher, ammo, AttackTarget, origin, orientation, velocity);
UpdateAmmoAfterLaunch(ammo);
}
});
// will ammo be depleted?
/*if (ammo.StackSize == null || ammo.StackSize <= 1)
{
// compare monsters: lugianmontokrenegade / sclavusse / zombielichtowerarcher
actionChain.EnqueueChain();
NextMoveTime = NextAttackTime = Timers.RunningTime + launchTime + MissileDelay;
return;
}*/
// reload animation
var animSpeed = GetAnimSpeed();
var reloadTime = EnqueueMotion(actionChain, MotionCommand.Reload, animSpeed);
//Console.WriteLine("ReloadTime: " + reloadTime);
// reset for next projectile
EnqueueMotion(actionChain, MotionCommand.Ready);
var linkAnim = reloadTime > 0 ? MotionCommand.Reload : aimLevel;
var linkTime = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, linkAnim, MotionCommand.Ready);
if (weapon.IsThrownWeapon)
{
if (reloadTime > 0)
{
actionChain.EnqueueChain();
actionChain = new ActionChain();
}
actionChain.AddDelaySeconds(linkTime);
}
//log.Info($"{Name}.Reload time: launchTime({launchTime}) + reloadTime({reloadTime}) + linkTime({linkTime})");
actionChain.AddAction(this, () => EnqueueBroadcast(new GameMessageParentEvent(this, ammo,
ACE.Entity.Enum.ParentLocation.RightHand, ACE.Entity.Enum.Placement.RightHandCombat)));
actionChain.EnqueueChain();
PrevAttackTime = Timers.RunningTime;
var timeOffset = launchTime + reloadTime + linkTime;
NextMoveTime = NextAttackTime = PrevAttackTime + timeOffset + MissileDelay;
}
///
/// Returns missile base damage from a monster attack
///
public BaseDamageMod GetMissileDamage()
{
// FIXME: use actual projectile, instead of currently equipped ammo
var ammo = GetMissileAmmo();
return ammo.GetDamageMod(this);
}
// reset between targets?
public int MonsterProjectile_OnCollideEnvironment_Counter;
public void MonsterProjectile_OnCollideEnvironment()
{
//Console.WriteLine($"{Name}.MonsterProjectile_OnCollideEnvironment()");
MonsterProjectile_OnCollideEnvironment_Counter++;
// chance of switching to melee, or static counter in retail?
/*var rng = ThreadSafeRandom.Next(1, 3);
if (rng == 3)
SwitchToMeleeAttack();*/
if (MonsterProjectile_OnCollideEnvironment_Counter >= 3)
TrySwitchToMeleeAttack();
}
public bool SwitchWeaponsPending;
public void TrySwitchToMeleeAttack()
{
// 24139 - Invisible Assailant never switches to melee?
if (AiAllowedCombatStyle == CombatStyle.StubbornMissile || Visibility) return;
SwitchWeaponsPending = true;
if (NextMoveTime > Timers.RunningTime)
{
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(NextMoveTime - Timers.RunningTime);
actionChain.AddAction(this, () => SwitchToMeleeAttack());
actionChain.EnqueueChain();
}
else
SwitchToMeleeAttack();
}
public void SwitchToMeleeAttack()
{
if (IsDead) return;
var weapon = GetEquippedMissileWeapon();
var ammo = GetEquippedAmmo();
if (weapon == null && ammo == null)
return;
var actionChain = new ActionChain();
EnqueueMotion_Force(actionChain, MotionStance.NonCombat, MotionCommand.Ready, (MotionCommand)CurrentMotionState.Stance);
EnqueueMotion_Force(actionChain, MotionStance.HandCombat, MotionCommand.Ready, MotionCommand.NonCombat);
actionChain.AddAction(this, () =>
{
if (IsDead) return;
// actually destroys the missile weapon + ammo here,
// to ensure they can't be re-selected from inventory
if (weapon != null)
{
TryUnwieldObjectWithBroadcasting(weapon.Guid, out _, out _);
weapon.Destroy();
}
if (ammo != null)
{
TryUnwieldObjectWithBroadcasting(ammo.Guid, out _, out _);
ammo.Destroy();
}
EquipInventoryItems(true);
var innerChain = new ActionChain();
EnqueueMotion_Force(innerChain, MotionStance.NonCombat, MotionCommand.Ready, (MotionCommand)CurrentMotionState.Stance);
innerChain.AddAction(this, () =>
{
if (IsDead) return;
//DoAttackStance();
// inlined DoAttackStance() / slightly modified -- do not rely on SetCombatMode() for stance swapping time in 1 action,
// as it doesn't support that anymore
var newStanceTime = SetCombatMode(CombatMode.Melee);
NextMoveTime = NextAttackTime = Timers.RunningTime + newStanceTime;
PrevAttackTime = NextMoveTime - (AiUseMagicDelay ?? 3.0f);
PhysicsObj.StartTimer();
// end inline
ResetAttack();
SwitchWeaponsPending = false;
// this is an unfortunate hack to fix the following scenario:
// since this function can be called at any point in time now,
// including when LaunchMissile -> EnqueueMotion is in the middle of an action queue,
// CurrentMotionState.Stance can get reset to the previous combat stance if that happens
var newStance = CurrentMotionState.Stance;
var swapChain = new ActionChain();
swapChain.AddDelaySeconds(2.0f);
swapChain.AddAction(this, () => CurrentMotionState.Stance = newStance);
swapChain.EnqueueChain();
});
innerChain.EnqueueChain();
});
actionChain.EnqueueChain();
}
}
}