using System;
using System.Numerics;
using ACE.Entity.Enum;
using ACE.Server.Entity.Actions;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Physics.Animation;
namespace ACE.Server.WorldObjects
{
partial class Player
{
private float _accuracyLevel;
public float AccuracyLevel
{
get => IsExhausted ? 0.0f : _accuracyLevel;
set => _accuracyLevel = value;
}
public Creature MissileTarget;
public PowerAccuracy GetAccuracyRange()
{
if (AccuracyLevel < 0.33f)
return PowerAccuracy.Low;
else if (AccuracyLevel < 0.66f)
return PowerAccuracy.Medium;
else
return PowerAccuracy.High;
}
///
/// Called by network packet handler 0xA - GameActionTargetedMissileAttack
///
/// The target guid
/// The attack height 1-3
/// The 0-1 accuracy bar level
public void HandleActionTargetedMissileAttack(uint targetGuid, uint attackHeight, float accuracyLevel)
{
//log.Info($"-");
if (CombatMode != CombatMode.Missile)
{
log.Warn($"{Name}.HandleActionTargetedMissileAttack({targetGuid:X8}, {attackHeight}, {accuracyLevel}) - CombatMode mismatch {CombatMode}, LastCombatMode: {LastCombatMode}");
if (LastCombatMode == CombatMode.Missile)
CombatMode = CombatMode.Missile;
else
{
OnAttackDone();
return;
}
}
if (IsBusy || Teleporting || suicideInProgress)
{
SendWeenieError(WeenieError.YoureTooBusy);
OnAttackDone();
return;
}
if (IsJumping)
{
SendWeenieError(WeenieError.YouCantDoThatWhileInTheAir);
OnAttackDone();
return;
}
if (PKLogout)
{
SendWeenieError(WeenieError.YouHaveBeenInPKBattleTooRecently);
OnAttackDone();
return;
}
var weapon = GetEquippedMissileWeapon();
var ammo = GetEquippedAmmo();
// sanity check
accuracyLevel = Math.Clamp(accuracyLevel, 0.0f, 1.0f);
if (weapon == null || weapon.IsAmmoLauncher && ammo == null)
{
OnAttackDone();
return;
}
AttackHeight = (AttackHeight)attackHeight;
AttackQueue.Add(accuracyLevel);
if (MissileTarget == null)
AccuracyLevel = accuracyLevel; // verify
// get world object of target guid
var target = CurrentLandblock?.GetObject(targetGuid) as Creature;
if (target == null || target.Teleporting)
{
//log.Warn($"{Name}.HandleActionTargetedMissileAttack({targetGuid:X8}, {AttackHeight}, {accuracyLevel}) - couldn't find creature target guid");
OnAttackDone();
return;
}
if (Attacking || MissileTarget != null && MissileTarget.IsAlive)
return;
if (!CanDamage(target))
{
SendTransientError($"You cannot attack {target.Name}");
OnAttackDone();
return;
}
//log.Info($"{Name}.HandleActionTargetedMissileAttack({targetGuid:X8}, {attackHeight}, {accuracyLevel})");
AttackTarget = target;
MissileTarget = target;
var attackSequence = ++AttackSequence;
// record stance here and pass it along
// accounts for odd client behavior with swapping bows during repeat attacks
var stance = CurrentMotionState.Stance;
// turn if required
var rotateTime = Rotate(target);
var actionChain = new ActionChain();
var delayTime = rotateTime;
if (NextRefillTime > DateTime.UtcNow.AddSeconds(delayTime))
delayTime = (float)(NextRefillTime - DateTime.UtcNow).TotalSeconds;
actionChain.AddDelaySeconds(delayTime);
// do missile attack
actionChain.AddAction(this, () => LaunchMissile(target, attackSequence, stance));
actionChain.EnqueueChain();
}
///
/// Launches a missile attack from player to target
///
public void LaunchMissile(WorldObject target, int attackSequence, MotionStance stance, bool subsequent = false)
{
if (AttackSequence != attackSequence)
return;
var weapon = GetEquippedMissileWeapon();
if (weapon == null || CombatMode == CombatMode.NonCombat)
{
OnAttackDone();
return;
}
var ammo = weapon.IsAmmoLauncher ? GetEquippedAmmo() : weapon;
if (ammo == null)
{
OnAttackDone();
return;
}
var launcher = GetEquippedMissileLauncher();
var creature = target as Creature;
if (!IsAlive || IsBusy || MissileTarget == null || creature == null || !creature.IsAlive || suicideInProgress)
{
OnAttackDone();
return;
}
if (!TargetInRange(target))
{
// this must also be sent to actually display the transient message
SendWeenieError(WeenieError.MissileOutOfRange);
// this prevents the accuracy bar from refilling when 'repeat attacks' is enabled
OnAttackDone();
return;
}
var actionChain = new ActionChain();
if (subsequent && !IsFacing(target))
{
var rotateTime = Rotate(target);
actionChain.AddDelaySeconds(rotateTime);
}
// launch animation
// point of no return beyond this point -- cannot be cancelled
actionChain.AddAction(this, () => Attacking = true);
if (subsequent)
{
// client shows hourglass, until attack done is received
// retail only did this for subsequent attacks w/ repeat attacks on
Session.Network.EnqueueSend(new GameEventCombatCommenceAttack(Session));
}
var projectileSpeed = GetProjectileSpeed();
// get z-angle for aim motion
var aimVelocity = GetAimVelocity(target, projectileSpeed);
var aimLevel = GetAimLevel(aimVelocity);
// calculate projectile spawn pos and velocity
var localOrigin = GetProjectileSpawnOrigin(ammo.WeenieClassId, aimLevel);
var velocity = CalculateProjectileVelocity(localOrigin, target, projectileSpeed, out Vector3 origin, out Quaternion orientation);
//Console.WriteLine($"Velocity: {velocity}");
if (velocity == Vector3.Zero)
{
// pre-check succeeded, but actual velocity calculation failed
SendWeenieError(WeenieError.MissileOutOfRange);
// this prevents the accuracy bar from refilling when 'repeat attacks' is enabled
Attacking = false;
OnAttackDone();
return;
}
var launchTime = EnqueueMotionPersist(actionChain, aimLevel);
// launch projectile
actionChain.AddAction(this, () =>
{
// handle self-procs
TryProcEquippedItems(this, this, true, weapon);
var sound = GetLaunchMissileSound(weapon);
EnqueueBroadcast(new GameMessageSound(Guid, sound, 1.0f));
// stamina usage
// TODO: ensure enough stamina for attack
// TODO: verify formulas - double/triple cost for bow/xbow?
var staminaCost = GetAttackStamina(GetAccuracyRange());
UpdateVitalDelta(Stamina, -staminaCost);
var projectile = LaunchProjectile(launcher, ammo, target, origin, orientation, velocity);
UpdateAmmoAfterLaunch(ammo);
});
// ammo remaining?
if (!ammo.UnlimitedUse && (ammo.StackSize == null || ammo.StackSize <= 1))
{
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, "You are out of ammunition!"));
SetCombatMode(CombatMode.NonCombat);
Attacking = false;
OnAttackDone();
});
actionChain.EnqueueChain();
return;
}
// reload animation
var animSpeed = GetAnimSpeed();
var reloadTime = EnqueueMotionPersist(actionChain, stance, MotionCommand.Reload, animSpeed);
// reset for next projectile
EnqueueMotionPersist(actionChain, stance, MotionCommand.Ready);
var linkTime = MotionTable.GetAnimationLength(MotionTableId, stance, MotionCommand.Reload, MotionCommand.Ready);
//var cycleTime = MotionTable.GetCycleLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.Ready);
actionChain.AddAction(this, () =>
{
if (CombatMode == CombatMode.Missile)
EnqueueBroadcast(new GameMessageParentEvent(this, ammo, ACE.Entity.Enum.ParentLocation.RightHand, ACE.Entity.Enum.Placement.RightHandCombat));
});
actionChain.AddDelaySeconds(linkTime);
actionChain.AddAction(this, () =>
{
Attacking = false;
if (creature.IsAlive && GetCharacterOption(CharacterOption.AutoRepeatAttacks) && !IsBusy && !AttackCancelled)
{
// client starts refilling accuracy bar
Session.Network.EnqueueSend(new GameEventAttackDone(Session));
AccuracyLevel = AttackQueue.Fetch();
// can be cancelled, but cannot be pre-empted with another attack
var nextAttack = new ActionChain();
var nextRefillTime = AccuracyLevel;
NextRefillTime = DateTime.UtcNow.AddSeconds(nextRefillTime);
nextAttack.AddDelaySeconds(nextRefillTime);
// perform next attack
nextAttack.AddAction(this, () => { LaunchMissile(target, attackSequence, stance, true); });
nextAttack.EnqueueChain();
}
else
OnAttackDone();
});
actionChain.EnqueueChain();
if (UnderLifestoneProtection)
LifestoneProtectionDispel();
}
// TODO: the damage pipeline currently uses the creature ammo instead of the projectile
// for calculating damage. when the last arrow is launched, the player ammo will be null
// give projectiles an owner, and have the damage pipeline take the actual damage source object
// (ie. the arrow-in-flight, or a melee weapon)
public override float GetAimHeight(WorldObject target)
{
switch (AttackHeight.Value)
{
case ACE.Entity.Enum.AttackHeight.High: return 1.0f;
case ACE.Entity.Enum.AttackHeight.Medium: return 2.0f;
//case AttackHeight.Low: return target.Height;
case ACE.Entity.Enum.AttackHeight.Low: return 3.0f;
}
return 2.0f;
}
public override void UpdateAmmoAfterLaunch(WorldObject ammo)
{
//if (ammo.UnlimitedUse)
// return;
// hide previously held ammo
EnqueueBroadcast(new GameMessagePickupEvent(ammo));
if (ammo.UnlimitedUse)
return;
if (ammo.StackSize == null || ammo.StackSize <= 1)
TryDequipObjectWithNetworking(ammo.Guid, out _, DequipObjectAction.ConsumeItem);
else
TryConsumeFromInventoryWithNetworking(ammo, 1);
}
public bool TargetInRange(WorldObject target)
{
// 2d or 3d distance?
var dist = Location.DistanceTo(target.Location);
var maxRange = GetMaxMissileRange();
return dist <= maxRange;
}
}
}