using System; using System.Linq; using System.Numerics; using ACE.Database; using ACE.DatLoader; using ACE.DatLoader.FileTypes; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Factories; using ACE.Server.Managers; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.Physics; using ACE.Server.Physics.Extensions; namespace ACE.Server.WorldObjects { partial class Creature { public float ReloadMissileAmmo(ActionChain actionChain = null) { var weapon = GetEquippedMissileWeapon(); var ammo = GetEquippedAmmo(); if (weapon == null || ammo == null) return 0.0f; var newChain = actionChain == null; if (newChain) actionChain = new ActionChain(); var animLength = 0.0f; if (weapon.IsAmmoLauncher) { var animSpeed = GetAnimSpeed(); //Console.WriteLine($"AnimSpeed: {animSpeed}"); animLength = EnqueueMotionPersist(actionChain, MotionCommand.Reload, animSpeed); // start pulling out next arrow EnqueueMotionPersist(actionChain, MotionCommand.Ready); // finish reloading } // ensure ammo visibility for players actionChain.AddAction(this, () => { if (CombatMode != CombatMode.Missile) return; EnqueueActionBroadcast(p => p.TrackEquippedObject(this, ammo)); var delayChain = new ActionChain(); delayChain.AddDelaySeconds(0.001f); // ensuring this message gets sent after player broadcasts above... delayChain.AddAction(this, () => { EnqueueBroadcast(new GameMessageParentEvent(this, ammo, ACE.Entity.Enum.ParentLocation.RightHand, ACE.Entity.Enum.Placement.RightHandCombat)); }); delayChain.EnqueueChain(); }); if (newChain) actionChain.EnqueueChain(); var animLength2 = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.Reload, MotionCommand.Ready); //Console.WriteLine($"AnimLength: {animLength} + {animLength2}"); return animLength + animLength2; } public Vector3 GetDir2D(Vector3 source, Vector3 dest) { var diff = dest - source; diff.Z = 0; return Vector3.Normalize(diff); } /// /// Launches a projectile from player to target /// public WorldObject LaunchProjectile(WorldObject weapon, WorldObject ammo, WorldObject target, Vector3 origin, Quaternion orientation, Vector3 velocity) { var player = this as Player; if (!velocity.IsValid()) { if (player != null) player.SendWeenieError(WeenieError.YourAttackMisfired); return null; } var proj = WorldObjectFactory.CreateNewWorldObject(ammo.WeenieClassId); proj.ProjectileSource = this; proj.ProjectileTarget = target; proj.ProjectileLauncher = weapon; proj.ProjectileAmmo = ammo; proj.Location = new Position(Location); proj.Location.Pos = origin; proj.Location.Rotation = orientation; SetProjectilePhysicsState(proj, target, velocity); var success = LandblockManager.AddObject(proj); if (!success || proj.PhysicsObj == null) { if (!proj.HitMsg) { if (player != null) player.Session.Network.EnqueueSend(new GameMessageSystemChat("Your missile attack hit the environment.", ChatMessageType.Broadcast)); } proj.Destroy(); return null; } if (!IsProjectileVisible(proj)) { proj.OnCollideEnvironment(); proj.Destroy(); return null; } var pkStatus = player?.PlayerKillerStatus ?? PlayerKillerStatus.Creature; proj.EnqueueBroadcast(new GameMessagePublicUpdatePropertyInt(proj, PropertyInt.PlayerKillerStatus, (int)pkStatus)); proj.EnqueueBroadcast(new GameMessageScript(proj.Guid, PlayScript.Launch, 0f)); // detonate point-blank projectiles immediately /*var radsum = target.PhysicsObj.GetRadius() + proj.PhysicsObj.GetRadius(); var dist = Vector3.Distance(origin, dest); if (dist < radsum) { Console.WriteLine($"Point blank"); proj.OnCollideObject(target); }*/ return proj; } public const float ProjSpawnHeight = 0.8454f; /// /// Returns the origin to spawn the projectile in the attacker local space /// public Vector3 GetProjectileSpawnOrigin(uint projectileWcid, MotionCommand motion) { var attackerRadius = PhysicsObj.GetPhysicsRadius(); var projectileRadius = GetProjectileRadius(projectileWcid); //Console.WriteLine($"{Name} radius: {attackerRadius}"); //Console.WriteLine($"Projectile {projectileWcid} radius: {projectileRadius}"); var radsum = attackerRadius * 2.0f + projectileRadius * 2.0f + PhysicsGlobals.EPSILON; var origin = new Vector3(0, radsum, 0); // rotate by aim angle var angle = motion.GetAimAngle().ToRadians(); var zRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitX, angle); origin = Vector3.Transform(origin, zRotation); origin.Z += Height * ProjSpawnHeight; return origin; } /// /// Returns the cached physics radius for a projectile wcid /// private static float GetProjectileRadius(uint projectileWcid) { if (ProjectileRadiusCache.TryGetValue(projectileWcid, out var radius)) return radius; var weenie = DatabaseManager.World.GetCachedWeenie(projectileWcid); if (weenie == null) { log.Error($"Creature_Missile.GetProjectileRadius(): couldn't find projectile weenie {projectileWcid}"); return 0.0f; } if (!weenie.PropertiesDID.TryGetValue(PropertyDataId.Setup, out var setupId)) { log.Error($"Creature_Missile.GetProjectileRadius(): couldn't find SetupId for {weenie.WeenieClassId} - {weenie.ClassName}"); return 0.0f; } var setup = DatManager.PortalDat.ReadFromDat(setupId); if (!weenie.PropertiesFloat.TryGetValue(PropertyFloat.DefaultScale, out var scale)) scale = 1.0f; var result = (float)(setup.Spheres[0].Radius * scale); ProjectileRadiusCache.TryAdd(projectileWcid, result); return result; } // lowest value found in data / for starter bows public const float DefaultProjectileSpeed = 20.0f; public float GetProjectileSpeed() { var missileLauncher = GetEquippedMissileWeapon(); var maxVelocity = missileLauncher?.MaximumVelocity ?? DefaultProjectileSpeed; if (maxVelocity == 0.0f) { log.Warn($"{Name}.GetMissileSpeed() - {missileLauncher.Name} ({missileLauncher.Guid}) has speed 0"); maxVelocity = DefaultProjectileSpeed; } if (this is Player player && player.GetCharacterOption(CharacterOption.UseFastMissiles)) { maxVelocity *= PropertyManager.GetDouble("fast_missile_modifier").Item; } // hard cap in physics engine maxVelocity = Math.Min(maxVelocity, PhysicsGlobals.MaxVelocity); //Console.WriteLine($"MaxVelocity: {maxVelocity}"); return (float)maxVelocity; } public Vector3 GetAimVelocity(WorldObject target, float projectileSpeed) { var crossLandblock = Location.Landblock != target.Location.Landblock; // eye level -> target point var origin = crossLandblock ? Location.ToGlobal(false) : Location.Pos; origin.Z += Height * ProjSpawnHeight; var dest = crossLandblock ? target.Location.ToGlobal(false) : target.Location.Pos; dest.Z += target.Height / GetAimHeight(target); var dir = Vector3.Normalize(dest - origin); var velocity = GetProjectileVelocity(target, origin, dir, dest, projectileSpeed, out float time); return velocity; } public Vector3 CalculateProjectileVelocity(Vector3 localOrigin, WorldObject target, float projectileSpeed, out Vector3 origin, out Quaternion rotation) { var sourceLoc = PhysicsObj.Position.ACEPosition(); var targetLoc = target.PhysicsObj.Position.ACEPosition(); var crossLandblock = sourceLoc.Landblock != targetLoc.Landblock; var startPos = crossLandblock ? sourceLoc.ToGlobal(false) : sourceLoc.Pos; var endPos = crossLandblock ? targetLoc.ToGlobal(false) : targetLoc.Pos; var dir = Vector3.Normalize(endPos - startPos); var angle = Math.Atan2(-dir.X, dir.Y); rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)angle); origin = sourceLoc.Pos + Vector3.Transform(localOrigin, rotation); startPos += Vector3.Transform(localOrigin, rotation); endPos.Z += target.Height / GetAimHeight(target); var velocity = GetProjectileVelocity(target, startPos, dir, endPos, projectileSpeed, out float time); return velocity; } /// /// Updates the ammo count or destroys the ammo after launching the projectile. /// /// The equipped missile ammo object public virtual void UpdateAmmoAfterLaunch(WorldObject ammo) { // hide previously held ammo EnqueueBroadcast(new GameMessagePickupEvent(ammo)); // monsters have infinite ammo? /*if (ammo.StackSize == null || ammo.StackSize <= 1) { TryUnwieldObjectWithBroadcasting(ammo.Guid, out _, out _); ammo.Destroy(); } else { ammo.SetStackSize(ammo.StackSize - 1); EnqueueBroadcast(new GameMessageSetStackSize(ammo)); }*/ } /// /// Calculates the velocity to launch the projectile from origin to dest /// public Vector3 GetProjectileVelocity(WorldObject target, Vector3 origin, Vector3 dir, Vector3 dest, float speed, out float time, bool useGravity = true) { time = 0.0f; Vector3 s0; float t0; var gravity = useGravity ? -PhysicsGlobals.Gravity : 0.00001f; var targetVelocity = target.PhysicsObj.CachedVelocity; if (!targetVelocity.Equals(Vector3.Zero)) { if (this is Player player && !player.GetCharacterOption(CharacterOption.LeadMissileTargets)) { // fall through } else { // use movement quartic solver if (!PropertyManager.GetBool("trajectory_alt_solver").Item) { var numSolutions = Trajectory.solve_ballistic_arc(origin, speed, dest, targetVelocity, gravity, out s0, out _, out time); if (numSolutions > 0) return s0; } else return Trajectory2.CalculateTrajectory(origin, dest, targetVelocity, speed, useGravity); } } // use stationary solver if (!PropertyManager.GetBool("trajectory_alt_solver").Item) { Trajectory.solve_ballistic_arc(origin, speed, dest, gravity, out s0, out _, out t0, out _); time = t0; return s0; } else return Trajectory2.CalculateTrajectory(origin, dest, Vector3.Zero, speed, useGravity); } /// /// Sets the physics state for a launched projectile /// public void SetProjectilePhysicsState(WorldObject obj, WorldObject target, Vector3 velocity) { obj.InitPhysicsObj(); obj.ReportCollisions = true; obj.Missile = true; obj.AlignPath = true; obj.PathClipped = true; obj.Ethereal = false; obj.IgnoreCollisions = false; var pos = obj.Location.Pos; var rotation = obj.Location.Rotation; obj.PhysicsObj.Position.Frame.Origin = pos; obj.PhysicsObj.Position.Frame.Orientation = rotation; if (obj.HasMissileFlightPlacement) obj.Placement = ACE.Entity.Enum.Placement.MissileFlight; else obj.Placement = null; obj.CurrentMotionState = null; obj.PhysicsObj.Velocity = velocity; obj.PhysicsObj.ProjectileTarget = target.PhysicsObj; // Projectiles with RotationSpeed get omega values and "align path" turned off which // creates the nice swirling animation if ((obj.RotationSpeed ?? 0) != 0) { obj.AlignPath = false; obj.PhysicsObj.Omega = new Vector3((float)(Math.PI * 2 * obj.RotationSpeed), 0, 0); } obj.PhysicsObj.set_active(true); } public Sound GetLaunchMissileSound(WorldObject weapon) { switch (weapon.DefaultCombatStyle) { case CombatStyle.Bow: return Sound.BowRelease; case CombatStyle.Crossbow: return Sound.CrossbowRelease; default: return Sound.ThrownWeaponRelease1; } } public const float MetersToYards = 1.094f; // 1.09361 public const float MissileRangeCap = 85.0f / MetersToYards; // 85 yards = ~77.697 meters w/ ac formula public const float DefaultMaxVelocity = 20.0f; // ? public float GetMaxMissileRange() { var weapon = GetEquippedMissileWeapon(); var maxVelocity = weapon?.MaximumVelocity ?? DefaultMaxVelocity; var missileRange = (float)Math.Pow(maxVelocity, 2.0f) * 0.1020408163265306f; //var missileRange = (float)Math.Pow(maxVelocity, 2.0f) * 0.0682547266398198f; //var strengthMod = SkillFormula.GetAttributeMod((int)Strength.Current); //var maxRange = Math.Min(missileRange * strengthMod, MissileRangeCap); var maxRange = Math.Min(missileRange, MissileRangeCap); // any kind of other caps for monsters specifically? // throwing lugian rocks @ 85 yards seems a bit far... //Console.WriteLine($"{Name}.GetMaxMissileRange(): maxVelocity={maxVelocity}, strengthMod={strengthMod}, maxRange={maxRange}"); // for client display /*var maxRangeYards = maxRange * MetersToYards; if (maxRangeYards >= 10.0f) maxRangeYards -= maxRangeYards % 5.0f; else maxRangeYards = (float)Math.Ceiling(maxRangeYards); Console.WriteLine($"Max range: {maxRange} ({maxRangeYards} yds.)");*/ return maxRange; } public static MotionCommand GetAimLevel(Vector3 velocity) { // get z-angle? var zAngle = Vector3.Normalize(velocity).Z * 90.0f; var aimLevel = MotionCommand.AimLevel; if (zAngle >= 82.5f) aimLevel = MotionCommand.AimHigh90; else if (zAngle >= 67.5f) aimLevel = MotionCommand.AimHigh75; else if (zAngle >= 52.5f) aimLevel = MotionCommand.AimHigh60; else if (zAngle >= 37.5f) aimLevel = MotionCommand.AimHigh45; else if (zAngle >= 22.5f) aimLevel = MotionCommand.AimHigh30; else if (zAngle >= 7.5f) aimLevel = MotionCommand.AimHigh15; else if (zAngle > -7.5f) aimLevel = MotionCommand.AimLevel; else if (zAngle > -22.5f) aimLevel = MotionCommand.AimLow15; else if (zAngle > -37.5f) aimLevel = MotionCommand.AimLow30; else if (zAngle > -52.5f) aimLevel = MotionCommand.AimLow45; else if (zAngle > -67.5f) aimLevel = MotionCommand.AimLow60; else if (zAngle > -82.5f) aimLevel = MotionCommand.AimLow75; else aimLevel = MotionCommand.AimLow90; //Console.WriteLine($"Z Angle: {aimLevel.GetAimAngle()}"); return aimLevel; } } }