ace/Source/ACE.Server/WorldObjects/SpellProjectile.cs

1004 lines
44 KiB
C#

using System;
using System.Numerics;
using ACE.Common;
using ACE.Entity;
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.Managers;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.WorldObjects.Entity;
namespace ACE.Server.WorldObjects
{
public class SpellProjectile : WorldObject
{
public Spell Spell;
public ProjectileSpellType SpellType { get; set; }
public Position SpawnPos { get; set; }
public float DistanceToTarget { get; set; }
public uint LifeProjectileDamage { get; set; }
public SpellProjectileInfo Info { get; set; }
/// <summary>
/// Only set to true when this spell was launched by using the built-in spell on a caster
/// </summary>
public bool IsWeaponSpell { get; set; }
/// <summary>
/// If a spell projectile is from a proc source,
/// make sure there is no attempt to re-proc again when the spell projectile hits
/// </summary>
public bool FromProc { get; set; }
public int DebugVelocity;
/// <summary>
/// A new biota be created taking all of its values from weenie.
/// </summary>
public SpellProjectile(Weenie weenie, ObjectGuid guid) : base(weenie, guid)
{
SetEphemeralValues();
}
/// <summary>
/// Restore a WorldObject from the database.
/// </summary>
public SpellProjectile(Biota biota) : base(biota)
{
SetEphemeralValues();
}
private void SetEphemeralValues()
{
// Override weenie description defaults
ValidLocations = null;
DefaultScriptId = null;
}
/// <summary>
/// Perfroms additional set up of the spell projectile based on the spell id or its derived type.
/// </summary>
public void Setup(Spell spell, ProjectileSpellType spellType)
{
Spell = spell;
SpellType = spellType;
InitPhysicsObj();
// Runtime changes to default state
ReportCollisions = true;
Missile = true;
AlignPath = true;
PathClipped = true;
IgnoreCollisions = false;
// FIXME: use data here
if (!Spell.Name.Equals("Rolling Death"))
Ethereal = false;
if (SpellType == ProjectileSpellType.Bolt || SpellType == ProjectileSpellType.Streak
|| SpellType == ProjectileSpellType.Arc || SpellType == ProjectileSpellType.Volley || SpellType == ProjectileSpellType.Blast
|| WeenieClassId == 7276 || WeenieClassId == 7277 || WeenieClassId == 7279 || WeenieClassId == 7280)
{
DefaultScriptId = (uint)PlayScript.ProjectileCollision;
DefaultScriptIntensity = 1.0f;
}
// Some wall spells don't have scripted collisions
if (WeenieClassId == 7278 || WeenieClassId == 7281 || WeenieClassId == 7282 || WeenieClassId == 23144)
{
ScriptedCollision = false;
}
AllowEdgeSlide = false;
// No need to send an ObjScale of 1.0f over the wire since that is the default value
if (ObjScale == 1.0f)
ObjScale = null;
if (SpellType == ProjectileSpellType.Ring)
{
if (spell.Id == 3818)
{
DefaultScriptId = (uint)PlayScript.Explode;
DefaultScriptIntensity = 1.0f;
ScriptedCollision = true;
}
else
{
ScriptedCollision = false;
}
}
// Projectiles with RotationSpeed get omega values and "align path" turned off which
// creates the nice swirling animation
if ((RotationSpeed ?? 0) != 0)
{
AlignPath = false;
PhysicsObj.Omega = new Vector3((float)(Math.PI * 2 * RotationSpeed), 0, 0);
}
}
public static ProjectileSpellType GetProjectileSpellType(uint spellID)
{
var spell = new Spell(spellID);
if (spell.Wcid == 0)
return ProjectileSpellType.Undef;
if (spell.NumProjectiles == 1)
{
if (spell.Category >= SpellCategory.AcidStreak && spell.Category <= SpellCategory.SlashingStreak ||
spell.Category == SpellCategory.NetherStreak || spell.Category == SpellCategory.Fireworks)
return ProjectileSpellType.Streak;
else if (spell.NonTracking)
return ProjectileSpellType.Arc;
else
return ProjectileSpellType.Bolt;
}
if (spell.Category >= SpellCategory.AcidRing && spell.Category <= SpellCategory.SlashingRing || spell.SpreadAngle == 360)
return ProjectileSpellType.Ring;
if (spell.Category >= SpellCategory.AcidBurst && spell.Category <= SpellCategory.SlashingBurst ||
spell.Category == SpellCategory.NetherDamageOverTimeRaising3)
return ProjectileSpellType.Blast;
// 1481 - Flaming Missile Volley
if (spell.Category >= SpellCategory.AcidVolley && spell.Category <= SpellCategory.BladeVolley || spell.Name.Contains("Volley"))
return ProjectileSpellType.Volley;
if (spell.Category >= SpellCategory.AcidWall && spell.Category <= SpellCategory.SlashingWall)
return ProjectileSpellType.Wall;
if (spell.Category >= SpellCategory.AcidStrike && spell.Category <= SpellCategory.SlashingStrike)
return ProjectileSpellType.Strike;
return ProjectileSpellType.Undef;
}
public float GetProjectileScriptIntensity(ProjectileSpellType spellType)
{
if (spellType == ProjectileSpellType.Wall)
{
return 0.4f;
}
if (spellType == ProjectileSpellType.Ring)
{
if (Spell.Level == 6 || Spell.Id == 3818)
return 0.4f;
if (Spell.Level == 7)
return 1.0f;
}
// Bolt, Blast, Volley, Streak and Arc all seem to use this scale
// TODO: should this be based on spell level, or power of first scarab?
// ie. can this use Spell.Formula.ScarabScale?
switch (Spell.Level)
{
case 1:
return 0f;
case 2:
return 0.2f;
case 3:
return 0.4f;
case 4:
return 0.6f;
case 5:
return 0.8f;
case 6:
case 7:
case 8:
return 1.0f;
default:
return 0f;
}
}
public bool WorldEntryCollision { get; set; }
public void ProjectileImpact()
{
//Console.WriteLine($"{Name}.ProjectileImpact()");
ReportCollisions = false;
Ethereal = true;
IgnoreCollisions = true;
NoDraw = true;
Cloaked = true;
LightsStatus = false;
PhysicsObj.set_active(false);
if (PhysicsObj.entering_world)
{
// this path should only happen if spell_projectile_ethereal = false
EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.Launch, GetProjectileScriptIntensity(SpellType)));
WorldEntryCollision = true;
}
EnqueueBroadcast(new GameMessageSetState(this, PhysicsObj.State));
EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.Explode, GetProjectileScriptIntensity(SpellType)));
// this should only be needed for spell_projectile_ethereal = true,
// however it can also fix a display issue on client in default mode,
// where GameMessageSetState updates projectile to ethereal before it has actually collided on client,
// causing a 'ghost' projectile to continue to sail through the target
PhysicsObj.Velocity = Vector3.Zero;
EnqueueBroadcast(new GameMessageVectorUpdate(this));
ActionChain selfDestructChain = new ActionChain();
selfDestructChain.AddDelaySeconds(5.0);
selfDestructChain.AddAction(this, () => Destroy());
selfDestructChain.EnqueueChain();
}
/// <summary>
/// Handles collision with scenery or other static objects that would block a projectile from reaching its target,
/// in which case the projectile should be removed with no further processing.
/// </summary>
public override void OnCollideEnvironment()
{
//Console.WriteLine($"{Name}.OnCollideEnvironment()");
if (Info != null && ProjectileSource is Player player && player.DebugSpell)
{
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{Name}.OnCollideEnvironment()", ChatMessageType.Broadcast));
player.Session.Network.EnqueueSend(new GameMessageSystemChat(Info.ToString(), ChatMessageType.Broadcast));
}
ProjectileImpact();
}
public override void OnCollideObject(WorldObject target)
{
//Console.WriteLine($"{Name}.OnCollideObject({target.Name})");
var player = ProjectileSource as Player;
if (Info != null && player != null && player.DebugSpell)
{
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{Name}.OnCollideObject({target?.Name} ({target?.Guid}))", ChatMessageType.Broadcast));
player.Session.Network.EnqueueSend(new GameMessageSystemChat(Info.ToString(), ChatMessageType.Broadcast));
}
ProjectileImpact();
// ensure valid creature target
var creatureTarget = target as Creature;
if (creatureTarget == null || target == ProjectileSource)
return;
if (player != null)
player.LastHitSpellProjectile = Spell;
// ensure caster can damage target
var sourceCreature = ProjectileSource as Creature;
if (sourceCreature != null && !sourceCreature.CanDamage(creatureTarget))
return;
// if player target, ensure matching PK status
var targetPlayer = creatureTarget as Player;
var pkError = ProjectileSource?.CheckPKStatusVsTarget(creatureTarget, Spell);
if (pkError != null)
{
if (player != null)
player.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(player.Session, pkError[0], creatureTarget.Name));
if (targetPlayer != null)
targetPlayer.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(targetPlayer.Session, pkError[1], ProjectileSource.Name));
return;
}
var critical = false;
var critDefended = false;
var overpower = false;
var damage = CalculateDamage(ProjectileSource, creatureTarget, ref critical, ref critDefended, ref overpower);
if (damage != null)
{
if (Spell.MetaSpellType == ACE.Entity.Enum.SpellType.EnchantmentProjectile)
{
// handle EnchantmentProjectile successfully landing on target
ProjectileSource.CreateEnchantment(creatureTarget, ProjectileSource, ProjectileLauncher, Spell, false, FromProc);
}
else
{
DamageTarget(creatureTarget, damage.Value, critical, critDefended, overpower);
}
// if this SpellProjectile has a TargetEffect, play it on successful hit
DoSpellEffects(Spell, ProjectileSource, creatureTarget, true);
if (player != null)
Proficiency.OnSuccessUse(player, player.GetCreatureSkill(Spell.School), Spell.PowerMod);
// handle target procs
// note that for untargeted multi-projectile spells,
// ProjectileTarget will be null here, so procs will not apply
// TODO: instead of ProjectileLauncher is Caster, perhaps a SpellProjectile.CanProc bool that defaults to true,
// but is set to false if the source of a spell is from a proc, to prevent multi procs?
if (sourceCreature != null && ProjectileTarget != null && !FromProc)
{
// TODO figure out why cross-landblock group operations are happening here. We shouldn't need this code Mag-nus 2021-02-09
bool threadSafe = true;
if (LandblockManager.CurrentlyTickingLandblockGroupsMultiThreaded)
{
// Ok... if we got here, we're likely in the parallel landblock physics processing.
if (sourceCreature.CurrentLandblock == null || creatureTarget.CurrentLandblock == null || sourceCreature.CurrentLandblock.CurrentLandblockGroup != creatureTarget.CurrentLandblock.CurrentLandblockGroup)
threadSafe = false;
}
if (threadSafe)
// This can result in spell projectiles being added to either sourceCreature or creatureTargets landblock.
sourceCreature.TryProcEquippedItems(sourceCreature, creatureTarget, false, ProjectileLauncher);
else
{
// sourceCreature and creatureTarget are now in different landblock groups.
// What has likely happened is that sourceCreature sent a projectile toward creatureTarget. Before impact, sourceCreature was teleported away.
// To perform this fully thread safe, we would enqueue the work onto worldManager.
// WorldManager.EnqueueAction(new ActionEventDelegate(() => sourceCreature.TryProcEquippedItems(creatureTarget, false)));
// But, to keep it simple, we will just ignore it and not bother with TryProcEquippedItems for this particular impact.
}
}
}
// also called on resist
if (player != null && targetPlayer == null)
player.OnAttackMonster(creatureTarget);
if (player == null && targetPlayer == null)
{
// check for faction combat
if (sourceCreature != null && creatureTarget != null && (sourceCreature.AllowFactionCombat(creatureTarget) || sourceCreature.PotentialFoe(creatureTarget)))
sourceCreature.MonsterOnAttackMonster(creatureTarget);
}
}
/// <summary>
/// Calculates the damage for a spell projectile
/// Used by war magic, void magic, and life magic projectiles
/// </summary>
public float? CalculateDamage(WorldObject source, Creature target, ref bool criticalHit, ref bool critDefended, ref bool overpower)
{
var sourcePlayer = source as Player;
var targetPlayer = target as Player;
if (source == null || !target.IsAlive || targetPlayer != null && targetPlayer.Invincible)
return null;
// check lifestone protection
if (targetPlayer != null && targetPlayer.UnderLifestoneProtection)
{
if (sourcePlayer != null)
sourcePlayer.Session.Network.EnqueueSend(new GameMessageSystemChat($"The Lifestone's magic protects {targetPlayer.Name} from the attack!", ChatMessageType.Magic));
targetPlayer.HandleLifestoneProtection();
return null;
}
var critDamageBonus = 0.0f;
var weaponCritDamageMod = 1.0f;
var weaponResistanceMod = 1.0f;
var resistanceMod = 1.0f;
// life magic
var lifeMagicDamage = 0.0f;
// war/void magic
var baseDamage = 0;
var skillBonus = 0.0f;
var finalDamage = 0.0f;
var resistanceType = Creature.GetResistanceType(Spell.DamageType);
var sourceCreature = source as Creature;
if (sourceCreature?.Overpower != null)
overpower = Creature.GetOverpower(sourceCreature, target);
var weapon = ProjectileLauncher;
var resistSource = IsWeaponSpell ? weapon : source;
var resisted = source.TryResistSpell(target, Spell, resistSource, true);
if (resisted && !overpower)
return null;
CreatureSkill attackSkill = null;
if (sourceCreature != null)
attackSkill = sourceCreature.GetCreatureSkill(Spell.School);
// critical hit
var criticalChance = GetWeaponMagicCritFrequency(weapon, sourceCreature, attackSkill, target);
if (ThreadSafeRandom.Next(0.0f, 1.0f) < criticalChance)
{
if (targetPlayer != null && targetPlayer.AugmentationCriticalDefense > 0)
{
var criticalDefenseMod = sourcePlayer != null ? 0.05f : 0.25f;
var criticalDefenseChance = targetPlayer.AugmentationCriticalDefense * criticalDefenseMod;
if (criticalDefenseChance > ThreadSafeRandom.Next(0.0f, 1.0f))
critDefended = true;
}
if (!critDefended)
criticalHit = true;
}
var absorbMod = GetAbsorbMod(target);
bool isPVP = sourcePlayer != null && targetPlayer != null;
//http://acpedia.org/wiki/Announcements_-_2014/01_-_Forces_of_Nature - Aegis is 72% effective in PvP
if (isPVP && (target.CombatMode == CombatMode.Melee || target.CombatMode == CombatMode.Missile))
{
absorbMod = 1 - absorbMod;
absorbMod *= 0.72f;
absorbMod = 1 - absorbMod;
}
if (isPVP && Spell.IsHarmful)
Player.UpdatePKTimers(sourcePlayer, targetPlayer);
var elementalDamageMod = GetCasterElementalDamageModifier(weapon, sourceCreature, target, Spell.DamageType);
// Possible 2x + damage bonus for the slayer property
var slayerMod = GetWeaponCreatureSlayerModifier(weapon, sourceCreature, target);
// life magic projectiles: ie., martyr's hecatomb
if (Spell.MetaSpellType == ACE.Entity.Enum.SpellType.LifeProjectile)
{
lifeMagicDamage = LifeProjectileDamage * Spell.DamageRatio;
// could life magic projectiles crit?
// if so, did they use the same 1.5x formula as war magic, instead of 2.0x?
if (criticalHit)
{
// verify: CriticalMultiplier only applied to the additional crit damage,
// whereas CD/CDR applied to the total damage (base damage + additional crit damage)
weaponCritDamageMod = GetWeaponCritDamageMod(weapon, sourceCreature, attackSkill, target);
critDamageBonus = lifeMagicDamage * 0.5f * weaponCritDamageMod;
}
weaponResistanceMod = GetWeaponResistanceModifier(weapon, sourceCreature, attackSkill, Spell.DamageType);
// if attacker/weapon has IgnoreMagicResist directly, do not transfer to spell projectile
// only pass if SpellProjectile has it directly, such as 2637 - Invoking Aun Tanua
resistanceMod = (float)Math.Max(0.0f, target.GetResistanceMod(resistanceType, this, null, weaponResistanceMod));
finalDamage = (lifeMagicDamage + critDamageBonus) * elementalDamageMod * slayerMod * resistanceMod * absorbMod;
}
// war/void magic projectiles
else
{
if (criticalHit)
{
// Original:
// http://acpedia.org/wiki/Announcements_-_2002/08_-_Atonement#Letter_to_the_Players
// Critical Strikes: In addition to the skill-based damage bonus, each projectile spell has a 2% chance of causing a critical hit on the target and doing increased damage.
// A magical critical hit is similar in some respects to melee critical hits (although the damage calculation is handled differently).
// While a melee critical hit automatically does twice the maximum damage of the weapon, a magical critical hit will do an additional half the minimum damage of the spell.
// For instance, a magical critical hit from a level 7 spell, which does 110-180 points of damage, would add an additional 55 points of damage to the spell.
// Later updated for PvE only:
// http://acpedia.org/wiki/Announcements_-_2004/07_-_Treaties_in_Stone#Letter_to_the_Players
// Currently when a War Magic spell scores a critical hit, it adds a multiple of the base damage of the spell to a normal damage roll.
// Starting in July, War Magic critical hits will instead add a multiple of the maximum damage of the spell.
// No more crits that do less damage than non-crits!
if (isPVP) // PvP: 50% of the MIN damage added to normal damage roll
critDamageBonus = Spell.MinDamage * 0.5f;
else // PvE: 50% of the MAX damage added to normal damage roll
critDamageBonus = Spell.MaxDamage * 0.5f;
// verify: CriticalMultiplier only applied to the additional crit damage,
// whereas CD/CDR applied to the total damage (base damage + additional crit damage)
weaponCritDamageMod = GetWeaponCritDamageMod(weapon, sourceCreature, attackSkill, target);
critDamageBonus *= weaponCritDamageMod;
}
/* War Magic skill-based damage bonus
* http://acpedia.org/wiki/Announcements_-_2002/08_-_Atonement#Letter_to_the_Players
*/
if (sourcePlayer != null)
{
var magicSkill = sourcePlayer.GetCreatureSkill(Spell.School).Current;
if (magicSkill > Spell.Power)
{
var percentageBonus = (magicSkill - Spell.Power) / 1000.0f;
skillBonus = Spell.MinDamage * percentageBonus;
}
}
baseDamage = ThreadSafeRandom.Next(Spell.MinDamage, Spell.MaxDamage);
weaponResistanceMod = GetWeaponResistanceModifier(weapon, sourceCreature, attackSkill, Spell.DamageType);
// if attacker/weapon has IgnoreMagicResist directly, do not transfer to spell projectile
// only pass if SpellProjectile has it directly, such as 2637 - Invoking Aun Tanua
resistanceMod = (float)Math.Max(0.0f, target.GetResistanceMod(resistanceType, this, null, weaponResistanceMod));
if (sourcePlayer != null && targetPlayer != null && Spell.DamageType == DamageType.Nether)
{
// for direct damage from void spells in pvp,
// apply void_pvp_modifier *on top of* the player's natural resistance to nether
// this supposedly brings the direct damage from void spells in pvp closer to retail
resistanceMod *= (float)PropertyManager.GetDouble("void_pvp_modifier").Item;
}
finalDamage = baseDamage + critDamageBonus + skillBonus;
finalDamage *= elementalDamageMod * slayerMod * resistanceMod * absorbMod;
}
// show debug info
if (sourceCreature != null && sourceCreature.DebugDamage.HasFlag(Creature.DebugDamageType.Attacker))
{
ShowInfo(sourceCreature, Spell, attackSkill, criticalChance, criticalHit, critDefended, overpower, weaponCritDamageMod, skillBonus, baseDamage, critDamageBonus, elementalDamageMod, slayerMod, weaponResistanceMod, resistanceMod, absorbMod, LifeProjectileDamage, lifeMagicDamage, finalDamage);
}
if (target.DebugDamage.HasFlag(Creature.DebugDamageType.Defender))
{
ShowInfo(target, Spell, attackSkill, criticalChance, criticalHit, critDefended, overpower, weaponCritDamageMod, skillBonus, baseDamage, critDamageBonus, elementalDamageMod, slayerMod, weaponResistanceMod, resistanceMod, absorbMod, LifeProjectileDamage, lifeMagicDamage, finalDamage);
}
return finalDamage;
}
public float GetAbsorbMod(Creature target)
{
switch (target.CombatMode)
{
case CombatMode.Melee:
// does target have shield equipped?
var shield = target.GetEquippedShield();
if (shield != null && shield.GetAbsorbMagicDamage() != null)
return GetShieldMod(target, shield);
break;
case CombatMode.Missile:
var missileLauncherOrShield = target.GetEquippedMissileLauncher() ?? target.GetEquippedShield();
if (missileLauncherOrShield != null && missileLauncherOrShield.GetAbsorbMagicDamage() != null)
return AbsorbMagic(target, missileLauncherOrShield);
break;
case CombatMode.Magic:
var caster = target.GetEquippedWand();
if (caster != null && caster.GetAbsorbMagicDamage() != null)
return AbsorbMagic(target, caster);
break;
}
return 1.0f;
}
/// <summary>
/// Calculates the amount of damage a shield absorbs from magic projectile
/// </summary>
public float GetShieldMod(Creature target, WorldObject shield)
{
// is spell projectile in front of creature target,
// within shield effectiveness area?
var effectiveAngle = 180.0f;
var angle = target.GetAngle(this);
if (Math.Abs(angle) > effectiveAngle / 2.0f)
return 1.0f;
// https://asheron.fandom.com/wiki/Shield
// The formula to determine magic absorption for shields is:
// Reduction Percent = (cap * specMod * baseSkill * 0.003f) - (cap * specMod * 0.3f)
// Cap = Maximum reduction
// SpecMod = 1.0 for spec, 0.8 for trained
// BaseSkill = 100 to 433 (above 433 base shield you always achieve the maximum %)
var shieldSkill = target.GetCreatureSkill(Skill.Shield);
// ensure trained?
if (shieldSkill.AdvancementClass < SkillAdvancementClass.Trained || shieldSkill.Base < 100)
return 1.0f;
var baseSkill = Math.Min(shieldSkill.Base, 433);
var specMod = shieldSkill.AdvancementClass == SkillAdvancementClass.Specialized ? 1.0f : 0.8f;
var cap = (float)(shield.GetAbsorbMagicDamage() ?? 0.0f);
// speced, 100 skill = 0%
// trained, 100 skill = 0%
// speced, 200 skill = 30%
// trained, 200 skill = 24%
// speced, 300 skill = 60%
// trained, 300 skill = 48%
// speced, 433 skill = 100%
// trained, 433 skill = 80%
var reduction = (cap * specMod * baseSkill * 0.003f) - (cap * specMod * 0.3f);
var shieldMod = Math.Min(1.0f, 1.0f - reduction);
return shieldMod;
}
/// <summary>
/// Calculates the damage reduction modifier for bows and casters
/// with 'Magic Absorbing' property
/// </summary>
public float AbsorbMagic(Creature target, WorldObject item)
{
// https://asheron.fandom.com/wiki/Category:Magic_Absorbing
// Tomes and Bows
// The formula to determine magic absorption for Tomes and the Fetish of the Dark Idols:
// - For a 25% maximum item: (magic absorbing %) = 25 - (0.1 * (319 - base magic defense))
// - For a 10% maximum item: (magic absorbing %) = 10 - (0.04 * (319 - base magic defense))
// wiki currently has what is likely a typo for the 10% formula,
// where it has a factor of 0.4 instead of 0.04
// with 0.4, the 10% items would not start to become effective until base magic defense 294
// with 0.04, both formulas start to become effective at base magic defense 69
// using an equivalent formula that produces the correct results for 10% and 25%,
// and also produces the correct results for any %
var absorbMagicDamage = item.GetAbsorbMagicDamage();
if (absorbMagicDamage == null)
return 1.0f;
var maxPercent = absorbMagicDamage.Value;
var baseCap = 319;
var magicDefBase = target.GetCreatureSkill(Skill.MagicDefense).Base;
var diff = Math.Max(0, baseCap - magicDefBase);
var percent = maxPercent - maxPercent * diff * 0.004f;
return Math.Min(1.0f, 1.0f - (float)percent);
}
/// <summary>
/// Called for a spell projectile to damage its target
/// </summary>
public void DamageTarget(Creature target, float damage, bool critical, bool critDefended, bool overpower)
{
var targetPlayer = target as Player;
if (targetPlayer != null && targetPlayer.Invincible || target.IsDead)
return;
var sourceCreature = ProjectileSource as Creature;
var sourcePlayer = ProjectileSource as Player;
var pkBattle = sourcePlayer != null && targetPlayer != null;
var amount = 0u;
var percent = 0.0f;
var damageRatingMod = 1.0f;
var heritageMod = 1.0f;
var sneakAttackMod = 1.0f;
var critDamageRatingMod = 1.0f;
var pkDamageRatingMod = 1.0f;
var damageResistRatingMod = 1.0f;
var critDamageResistRatingMod = 1.0f;
var pkDamageResistRatingMod = 1.0f;
WorldObject equippedCloak = null;
// handle life projectiles for stamina / mana
if (Spell.Category == SpellCategory.StaminaLowering)
{
percent = damage / target.Stamina.MaxValue;
amount = (uint)-target.UpdateVitalDelta(target.Stamina, (int)-Math.Round(damage));
}
else if (Spell.Category == SpellCategory.ManaLowering)
{
percent = damage / target.Mana.MaxValue;
amount = (uint)-target.UpdateVitalDelta(target.Mana, (int)-Math.Round(damage));
}
else
{
// for possibly applying sneak attack to magic projectiles,
// only do this for health-damaging projectiles?
if (sourcePlayer != null)
{
// TODO: use target direction vs. projectile position, instead of player position
// could sneak attack be applied to void DoTs?
sneakAttackMod = sourcePlayer.GetSneakAttackMod(target);
//Console.WriteLine("Magic sneak attack: + sneakAttackMod);
heritageMod = sourcePlayer.GetHeritageBonus(sourcePlayer.GetEquippedWand()) ? 1.05f : 1.0f;
}
var damageRating = sourceCreature?.GetDamageRating() ?? 0;
damageRatingMod = Creature.AdditiveCombine(Creature.GetPositiveRatingMod(damageRating), heritageMod, sneakAttackMod);
damageResistRatingMod = target.GetDamageResistRatingMod(CombatType.Magic);
if (critical)
{
critDamageRatingMod = Creature.GetPositiveRatingMod(sourceCreature?.GetCritDamageRating() ?? 0);
critDamageResistRatingMod = Creature.GetNegativeRatingMod(target.GetCritDamageResistRating());
damageRatingMod = Creature.AdditiveCombine(damageRatingMod, critDamageRatingMod);
damageResistRatingMod = Creature.AdditiveCombine(damageResistRatingMod, critDamageResistRatingMod);
}
if (pkBattle)
{
pkDamageRatingMod = Creature.GetPositiveRatingMod(sourceCreature?.GetPKDamageRating() ?? 0);
pkDamageResistRatingMod = Creature.GetNegativeRatingMod(target.GetPKDamageResistRating());
damageRatingMod = Creature.AdditiveCombine(damageRatingMod, pkDamageRatingMod);
damageResistRatingMod = Creature.AdditiveCombine(damageResistRatingMod, pkDamageResistRatingMod);
}
damage *= damageRatingMod * damageResistRatingMod;
percent = damage / target.Health.MaxValue;
//Console.WriteLine($"Damage rating: " + Creature.ModToRating(damageRatingMod));
equippedCloak = target.EquippedCloak;
if (equippedCloak != null && Cloak.HasDamageProc(equippedCloak) && Cloak.RollProc(equippedCloak, percent))
{
var reducedDamage = Cloak.GetReducedAmount(ProjectileSource, damage);
Cloak.ShowMessage(target, ProjectileSource, damage, reducedDamage);
damage = reducedDamage;
percent = damage / target.Health.MaxValue;
}
amount = (uint)-target.UpdateVitalDelta(target.Health, (int)-Math.Round(damage));
target.DamageHistory.Add(ProjectileSource, Spell.DamageType, amount);
//if (targetPlayer != null && targetPlayer.Fellowship != null)
//targetPlayer.Fellowship.OnVitalUpdate(targetPlayer);
}
amount = (uint)Math.Round(damage); // full amount for debugging
// show debug info
if (sourceCreature != null && sourceCreature.DebugDamage.HasFlag(Creature.DebugDamageType.Attacker))
{
ShowInfo(sourceCreature, heritageMod, sneakAttackMod, damageRatingMod, damageResistRatingMod, critDamageRatingMod, critDamageResistRatingMod, pkDamageRatingMod, pkDamageResistRatingMod, damage);
}
if (target.DebugDamage.HasFlag(Creature.DebugDamageType.Defender))
{
ShowInfo(target, heritageMod, sneakAttackMod, damageRatingMod, damageResistRatingMod, critDamageRatingMod, critDamageResistRatingMod, pkDamageRatingMod, pkDamageResistRatingMod, damage);
}
if (target.IsAlive)
{
string verb = null, plural = null;
Strings.GetAttackVerb(Spell.DamageType, percent, ref verb, ref plural);
var type = Spell.DamageType.GetName().ToLower();
var critMsg = critical ? "Critical hit! " : "";
var sneakMsg = sneakAttackMod > 1.0f ? "Sneak Attack! " : "";
var overpowerMsg = overpower ? "Overpower! " : "";
var nonHealth = Spell.Category == SpellCategory.StaminaLowering || Spell.Category == SpellCategory.ManaLowering;
if (sourcePlayer != null)
{
var critProt = critDefended ? " Your critical hit was avoided with their augmentation!" : "";
var attackerMsg = $"{critMsg}{overpowerMsg}{sneakMsg}You {verb} {target.Name} for {amount} points with {Spell.Name}.{critProt}";
// could these crit / sneak attack?
if (nonHealth)
{
var vital = Spell.Category == SpellCategory.StaminaLowering ? "stamina" : "mana";
attackerMsg = $"With {Spell.Name} you drain {amount} points of {vital} from {target.Name}.";
}
if (!sourcePlayer.SquelchManager.Squelches.Contains(target, ChatMessageType.Magic))
sourcePlayer.Session.Network.EnqueueSend(new GameMessageSystemChat(attackerMsg, ChatMessageType.Magic));
}
if (targetPlayer != null)
{
var critProt = critDefended ? " Your augmentation allows you to avoid a critical hit!" : "";
var defenderMsg = $"{critMsg}{overpowerMsg}{sneakMsg}{ProjectileSource.Name} {plural} you for {amount} points with {Spell.Name}.{critProt}";
if (nonHealth)
{
var vital = Spell.Category == SpellCategory.StaminaLowering ? "stamina" : "mana";
defenderMsg = $"{ProjectileSource.Name} casts {Spell.Name} and drains {amount} points of your {vital}.";
}
if (!targetPlayer.SquelchManager.Squelches.Contains(ProjectileSource, ChatMessageType.Magic))
targetPlayer.Session.Network.EnqueueSend(new GameMessageSystemChat(defenderMsg, ChatMessageType.Magic));
if (sourceCreature != null)
targetPlayer.SetCurrentAttacker(sourceCreature);
}
if (!nonHealth)
{
if (equippedCloak != null && Cloak.HasProcSpell(equippedCloak))
Cloak.TryProcSpell(target, ProjectileSource, equippedCloak, percent);
target.EmoteManager.OnDamage(sourcePlayer);
if (critical)
target.EmoteManager.OnReceiveCritical(sourcePlayer);
}
}
else
{
var lastDamager = ProjectileSource != null ? new DamageHistoryInfo(ProjectileSource) : null;
target.OnDeath(lastDamager, Spell.DamageType, critical);
target.Die();
}
}
/// <summary>
/// Sets the physics state for a launched projectile
/// </summary>
public void SetProjectilePhysicsState(WorldObject target, bool useGravity)
{
if (useGravity)
GravityStatus = true;
CurrentMotionState = null;
Placement = null;
// TODO: Physics description timestamps (sequence numbers) don't seem to be getting updated
//Console.WriteLine("SpellProjectile PhysicsState: " + PhysicsObj.State);
var pos = Location.Pos;
var rotation = Location.Rotation;
PhysicsObj.Position.Frame.Origin = pos;
PhysicsObj.Position.Frame.Orientation = rotation;
var velocity = Velocity;
//velocity = Vector3.Transform(velocity, Matrix4x4.Transpose(Matrix4x4.CreateFromQuaternion(rotation)));
PhysicsObj.Velocity = velocity;
if (target != null)
PhysicsObj.ProjectileTarget = target.PhysicsObj;
PhysicsObj.set_active(true);
}
public static void ShowInfo(Creature observed, Spell spell, CreatureSkill skill, float criticalChance, bool criticalHit, bool critDefended, bool overpower, float weaponCritDamageMod,
float magicSkillBonus, int baseDamage, float critDamageBonus, float elementalDamageMod, float slayerMod,
float weaponResistanceMod, float resistanceMod, float absorbMod,
float lifeProjectileDamage, float lifeMagicDamage, float finalDamage)
{
var observer = PlayerManager.GetOnlinePlayer(observed.DebugDamageTarget);
if (observer == null)
{
observed.DebugDamage = Creature.DebugDamageType.None;
return;
}
var info = $"Skill: {skill.Skill.ToSentence()}\n";
info += $"CriticalChance: {criticalChance}\n";
info += $"CriticalHit: {criticalHit}\n";
if (critDefended)
info += $"CriticalDefended: {critDefended}\n";
info += $"Overpower: {overpower}\n";
if (spell.MetaSpellType == ACE.Entity.Enum.SpellType.LifeProjectile)
{
// life magic projectile
info += $"LifeProjectileDamage: {lifeProjectileDamage}\n";
info += $"DamageRatio: {spell.DamageRatio}\n";
info += $"LifeMagicDamage: {lifeMagicDamage}\n";
}
else
{
// war/void projectile
var difficulty = Math.Min(spell.Power, 350);
info += $"Difficulty: {difficulty}\n";
if (magicSkillBonus != 0.0f)
info += $"SkillBonus: {magicSkillBonus}\n";
info += $"BaseDamageRange: {spell.MinDamage} - {spell.MaxDamage}\n";
info += $"BaseDamage: {baseDamage}\n";
info += $"DamageType: {spell.DamageType}\n";
}
if (weaponCritDamageMod != 1.0f)
info += $"WeaponCritDamageMod: {weaponCritDamageMod}\n";
if (critDamageBonus != 0)
info += $"CritDamageBonus: {critDamageBonus}\n";
if (elementalDamageMod != 1.0f)
info += $"ElementalDamageMod: {elementalDamageMod}\n";
if (slayerMod != 1.0f)
info += $"SlayerMod: {slayerMod}\n";
if (weaponResistanceMod != 1.0f)
info += $"WeaponResistanceMod: {weaponResistanceMod}\n";
if (resistanceMod != 1.0f)
info += $"ResistanceMod: {resistanceMod}\n";
if (absorbMod != 1.0f)
info += $"AbsorbMod: {absorbMod}\n";
//observer.Session.Network.EnqueueSend(new GameMessageSystemChat(info, ChatMessageType.Broadcast));
observer.DebugDamageBuffer += info;
}
public static void ShowInfo(Creature observed, float heritageMod, float sneakAttackMod, float damageRatingMod, float damageResistRatingMod,
float critDamageRatingMod, float critDamageResistRatingMod, float pkDamageRatingMod, float pkDamageResistRatingMod, float damage)
{
var observer = PlayerManager.GetOnlinePlayer(observed.DebugDamageTarget);
if (observer == null)
{
observed.DebugDamage = Creature.DebugDamageType.None;
return;
}
var info = "";
if (heritageMod != 1.0f)
info += $"HeritageMod: {heritageMod}\n";
if (sneakAttackMod != 1.0f)
info += $"SneakAttackMod: {sneakAttackMod}\n";
if (critDamageRatingMod != 1.0f)
info += $"CritDamageRatingMod: {critDamageRatingMod}\n";
if (pkDamageRatingMod != 1.0f)
info += $"PkDamageRatingMod: {pkDamageRatingMod}\n";
if (damageRatingMod != 1.0f)
info += $"DamageRatingMod: {damageRatingMod}\n";
if (critDamageResistRatingMod != 1.0f)
info += $"CritDamageResistRatingMod: {critDamageResistRatingMod}\n";
if (pkDamageResistRatingMod != 1.0f)
info += $"PkDamageResistRatingMod: {pkDamageResistRatingMod}\n";
if (damageResistRatingMod != 1.0f)
info += $"DamageResistRatingMod: {damageResistRatingMod}\n";
info += $"Final damage: {damage}";
observer.Session.Network.EnqueueSend(new GameMessageSystemChat(observer.DebugDamageBuffer + info, ChatMessageType.Broadcast));
observer.DebugDamageBuffer = null;
}
}
}