Merge pull request #304 from KayenEQ/Development

Implemented archery projectiles to do damage on impact.
This commit is contained in:
Michael Cook (mackal) 2014-12-01 16:30:26 -05:00
commit a59cdc2c89
11 changed files with 455 additions and 227 deletions

View File

@ -1,10 +1,15 @@
EQEMu Changelog (Started on Sept 24, 2003 15:50)
-------------------------------------------------------
== 11/28/2014 ==
Trevius: Fixed a zone crash related to numhits for spells.
Trevius: Fixed a query related to group leaders logging in.
Trevius (Natedog): Fixed a world crash related to attempting to join an adventure with Mercenaries.
== 11/27/2014 ==
Kayen: Projectiles (ie Arrows) fired from archery will now do damage upon impact instead of instantly (consistent w/ live).
Optional SQL: utils/sql/git/optional/2014_11_27_ProjectileDmgOnImpact.sql
== 11/25/2014 ==
Trevius: Spells that modify model size are now limited to 2 size adjustments from the base size.
Trevius: Fix to prevent Mercenaries from being set as Group Leader.

View File

@ -419,6 +419,7 @@ RULE_INT ( Combat, ArcheryBonusChance, 50)
RULE_INT ( Combat, BerserkerFrenzyStart, 35)
RULE_INT ( Combat, BerserkerFrenzyEnd, 45)
RULE_BOOL ( Combat, OneProcPerWeapon, true) //If enabled, One proc per weapon per round
RULE_BOOL ( Combat, ProjectileDmgOnImpact, true) //If enabled, projectiles (ie arrows) will hit on impact, instead of instantly.
RULE_CATEGORY_END()
RULE_CATEGORY( NPC )

View File

@ -0,0 +1 @@
INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Combat:ProjectileDmgOnImpact', 'true', 'If enabled, projectiles (ie arrows) will hit on impact, instead of instantly.');

View File

@ -565,8 +565,7 @@ bool Client::Process() {
viral_timer_counter = 0;
}
if(projectile_timer.Check())
SpellProjectileEffect();
ProjectileAttack();
if(spellbonuses.GravityEffect == 1) {
if(gravity_timer.Check())

View File

@ -459,6 +459,24 @@ struct Shielders_Struct {
uint16 shielder_bonus;
};
typedef struct
{
uint16 increment;
uint16 hit_increment;
uint16 target_id;
int32 wpn_dmg;
float origin_x;
float origin_y;
float origin_z;
float tlast_x;
float tlast_y;
uint32 ranged_id;
uint32 ammo_id;
int ammo_slot;
uint8 skill;
float speed_mod;
} tProjatk;
//eventually turn this into a typedef and
//make DoAnim take it instead of int, to enforce its use.
enum { //type arguments to DoAnim

View File

@ -279,13 +279,24 @@ Mob::Mob(const char* in_name,
casting_spell_inventory_slot = 0;
target = 0;
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_spell_id[i] = 0; }
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_target_id[i] = 0; }
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_increment[i] = 0; }
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_x[i] = 0; }
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_y[i] = 0; }
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { projectile_z[i] = 0; }
projectile_timer.Disable();
ActiveProjectileATK = false;
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++)
{
ProjectileAtk[i].increment = 0;
ProjectileAtk[i].hit_increment = 0;
ProjectileAtk[i].target_id = 0;
ProjectileAtk[i].wpn_dmg = 0;
ProjectileAtk[i].origin_x = 0.0f;
ProjectileAtk[i].origin_y = 0.0f;
ProjectileAtk[i].origin_z = 0.0f;
ProjectileAtk[i].tlast_x = 0.0f;
ProjectileAtk[i].tlast_y = 0.0f;
ProjectileAtk[i].ranged_id = 0;
ProjectileAtk[i].ammo_id = 0;
ProjectileAtk[i].ammo_slot = 0;
ProjectileAtk[i].skill = 0;
ProjectileAtk[i].speed_mod = 0.0f;
}
memset(&itembonuses, 0, sizeof(StatBonuses));
memset(&spellbonuses, 0, sizeof(StatBonuses));
@ -4235,49 +4246,6 @@ bool Mob::TryReflectSpell(uint32 spell_id)
return false;
}
void Mob::SpellProjectileEffect()
{
bool time_disable = false;
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) {
if (projectile_increment[i] == 0){
continue;
}
Mob* target = entity_list.GetMobID(projectile_target_id[i]);
float dist = 0;
if (target)
dist = target->CalculateDistance(projectile_x[i], projectile_y[i], projectile_z[i]);
int increment_end = 0;
increment_end = static_cast<int>(dist / 10) - 1; //This pretty accurately determines end time for speed for 1.5 and timer of 250 ms
if (increment_end <= projectile_increment[i]){
if (target && IsValidSpell(projectile_spell_id[i]))
SpellOnTarget(projectile_spell_id[i], target, false, true, spells[projectile_spell_id[i]].ResistDiff, true);
projectile_spell_id[i] = 0;
projectile_target_id[i] = 0;
projectile_x[i] = 0, projectile_y[i] = 0, projectile_z[i] = 0;
projectile_increment[i] = 0;
time_disable = true;
}
else {
projectile_increment[i]++;
time_disable = false;
}
}
if (time_disable)
projectile_timer.Disable();
}
void Mob::DoGravityEffect()
{
Mob *caster = nullptr;

View File

@ -237,8 +237,7 @@ public:
uint16 CastingSpellID() const { return casting_spell_id; }
bool DoCastingChecks();
bool TryDispel(uint8 caster_level, uint8 buff_level, int level_modifier);
void SpellProjectileEffect();
bool TrySpellProjectile(Mob* spell_target, uint16 spell_id);
bool TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed = 1.5f);
void ResourceTap(int32 damage, uint16 spell_id);
void TryTriggerThreshHold(int32 damage, int effect_id, Mob* attacker);
bool CheckSpellCategory(uint16 spell_id, int category_id, int effect_id);
@ -729,9 +728,13 @@ public:
int32 ReduceAllDamage(int32 damage);
virtual void DoSpecialAttackDamage(Mob *who, SkillUseTypes skill, int32 max_damage, int32 min_damage = 1, int32 hate_override = -1, int ReuseTime = 10, bool HitChance=false, bool CanAvoid=true);
virtual void DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const Item_Struct* item=nullptr, uint16 weapon_damage=0, int16 chance_mod=0,int16 focus=0, int ReuseTime=0);
virtual void DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const Item_Struct* AmmoItem=nullptr, uint16 weapon_damage=0, int16 chance_mod=0,int16 focus=0, int ReuseTime=0, uint32 range_id=0, int AmmoSlot=0, float speed = 4.0f);
virtual void DoMeleeSkillAttackDmg(Mob* other, uint16 weapon_damage, SkillUseTypes skillinuse, int16 chance_mod=0, int16 focus=0, bool CanRiposte=false, int ReuseTime=0);
virtual void DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const ItemInst* Ammo=nullptr, uint16 weapon_damage=0, int16 chance_mod=0, int16 focus=0, int ReuseTime=0);
virtual void DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon=nullptr, const ItemInst* Ammo=nullptr, uint16 weapon_damage=0, int16 chance_mod=0, int16 focus=0, int ReuseTime=0, uint32 range_id=0, uint32 ammo_id=0, const Item_Struct *AmmoItem=nullptr, int AmmoSlot=0, float speed= 4.0f);
bool TryProjectileAttack(Mob* other, const Item_Struct *item, SkillUseTypes skillInUse, uint16 weapon_dmg, const ItemInst* RangeWeapon, const ItemInst* Ammo, int AmmoSlot, float speed);
void ProjectileAttack();
inline bool HasProjectileAttack() const { return ActiveProjectileATK; }
inline void SetProjectileAttack(bool value) { ActiveProjectileATK = value; }
bool CanDoSpecialAttack(Mob *other);
bool Flurry(ExtraAttackOptions *opts);
bool Rampage(ExtraAttackOptions *opts);
@ -847,7 +850,7 @@ public:
// HP Event
inline int GetNextHPEvent() const { return nexthpevent; }
void SetNextHPEvent( int hpevent );
void SendItemAnimation(Mob *to, const Item_Struct *item, SkillUseTypes skillInUse);
void SendItemAnimation(Mob *to, const Item_Struct *item, SkillUseTypes skillInUse, float velocity= 4.0);
inline int& GetNextIncHPEvent() { return nextinchpevent; }
void SetNextIncHPEvent( int inchpevent );
@ -1097,11 +1100,8 @@ protected:
uint8 bardsong_slot;
uint32 bardsong_target_id;
Timer projectile_timer;
uint32 projectile_spell_id[MAX_SPELL_PROJECTILE];
uint16 projectile_target_id[MAX_SPELL_PROJECTILE];
uint8 projectile_increment[MAX_SPELL_PROJECTILE];
float projectile_x[MAX_SPELL_PROJECTILE], projectile_y[MAX_SPELL_PROJECTILE], projectile_z[MAX_SPELL_PROJECTILE];
bool ActiveProjectileATK;
tProjatk ProjectileAtk[MAX_SPELL_PROJECTILE];
float rewind_x;
float rewind_y;

View File

@ -597,7 +597,6 @@ void Mob::AI_ShutDown() {
tic_timer.Disable();
mana_timer.Disable();
spellend_timer.Disable();
projectile_timer.Disable();
rewind_timer.Disable();
bindwound_timer.Disable();
stunned_timer.Disable();

View File

@ -670,8 +670,7 @@ bool NPC::Process()
viral_timer_counter = 0;
}
if(projectile_timer.Check())
SpellProjectileEffect();
ProjectileAttack();
if(spellbonuses.GravityEffect == 1) {
if(gravity_timer.Check())

View File

@ -198,7 +198,7 @@ void Client::OPCombatAbility(const EQApplicationPacket *app) {
SetAttackTimer();
ThrowingAttack(GetTarget());
if (CheckDoubleRangedAttack())
RangedAttack(GetTarget(), true);
ThrowingAttack(GetTarget(), true);
return;
}
//ranged attack (archery)
@ -596,7 +596,6 @@ void Mob::RogueBackstab(Mob* other, bool min_damage, int ReuseTime)
for (int i = 0; i < EmuConstants::ITEM_COMMON_SIZE; ++i)
{
ItemInst *aug = wpn->GetAugment(i);
if(aug)
{
backstab_dmg += aug->GetItem()->BackstabDmg;
}
@ -788,8 +787,8 @@ void Client::RangedAttack(Mob* other, bool CanDoubleAttack) {
return;
}
SendItemAnimation(GetTarget(), AmmoItem, SkillArchery);
DoArcheryAttackDmg(GetTarget(), RangeWeapon, Ammo);
//Shoots projectile and/or applies the archery damage
DoArcheryAttackDmg(GetTarget(), RangeWeapon, Ammo,0,0,0,0,0,0, AmmoItem, ammo_slot);
//EndlessQuiver AA base1 = 100% Chance to avoid consumption arrow.
int ChanceAvoidConsume = aabonuses.ConsumeProjectile + itembonuses.ConsumeProjectile + spellbonuses.ConsumeProjectile;
@ -805,147 +804,204 @@ void Client::RangedAttack(Mob* other, bool CanDoubleAttack) {
CommonBreakInvisible();
}
void Mob::DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon, const ItemInst* Ammo, uint16 weapon_damage, int16 chance_mod, int16 focus, int ReuseTime) {
if (!CanDoSpecialAttack(other))
void Mob::DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon, const ItemInst* Ammo, uint16 weapon_damage, int16 chance_mod, int16 focus, int ReuseTime,
uint32 range_id, uint32 ammo_id, const Item_Struct *AmmoItem, int AmmoSlot, float speed) {
if ((other == nullptr ||
((IsClient() && CastToClient()->dead) ||
(other->IsClient() && other->CastToClient()->dead)) ||
HasDied() ||
(!IsAttackAllowed(other)) ||
(other->GetInvul() ||
other->GetSpecialAbility(IMMUNE_MELEE))))
{
return;
}
if (!other->CheckHitChance(this, SkillArchery, MainPrimary, chance_mod)) {
const ItemInst* _RangeWeapon = nullptr;
const ItemInst* _Ammo = nullptr;
const Item_Struct* ammo_lost = nullptr;
/*
If LaunchProjectile is false this function will do archery damage on target,
otherwise it will shoot the projectile at the target, once the projectile hits target
this function is then run again to do the damage portion
*/
bool LaunchProjectile = false;
bool ProjectileMiss = false;
if (RuleB(Combat, ProjectileDmgOnImpact)){
if (AmmoItem)
LaunchProjectile = true;
else{
/*
Item sync check on projectile landing.
Weapon damage is already calculated so this only affects procs!
Ammo proc check will use database to find proc if you used up your last ammo.
If you change range item mid projectile flight, you loose your chance to proc from bow (Deal with it!).
*/
if (!RangeWeapon && !Ammo && range_id && ammo_id){
if (weapon_damage == 0)
ProjectileMiss = true; //This indicates that MISS was originally calculated.
if (IsClient()){
_RangeWeapon = CastToClient()->m_inv[MainRange];
if (_RangeWeapon && !_RangeWeapon->GetItem() && _RangeWeapon->GetItem()->ID == range_id)
RangeWeapon = _RangeWeapon;
_Ammo = CastToClient()->m_inv[AmmoSlot];
if (_Ammo && _Ammo->GetItem() && _Ammo->GetItem()->ID == ammo_id)
Ammo = _Ammo;
else
ammo_lost = database.GetItem(ammo_id);
}
}
}
}
else if (AmmoItem)
SendItemAnimation(other, AmmoItem, SkillArchery);
if (ProjectileMiss || !other->CheckHitChance(this, SkillArchery, MainPrimary, chance_mod)) {
mlog(COMBAT__RANGED, "Ranged attack missed %s.", other->GetName());
other->Damage(this, 0, SPELL_UNKNOWN, SkillArchery);
if (LaunchProjectile){
TryProjectileAttack(other, AmmoItem, SkillArchery, 0, RangeWeapon, Ammo, AmmoSlot, speed);
return;
}
else
other->Damage(this, 0, SPELL_UNKNOWN, SkillArchery);
} else {
mlog(COMBAT__RANGED, "Ranged attack hit %s.", other->GetName());
bool HeadShot = false;
uint32 HeadShot_Dmg = TryHeadShot(other, SkillArchery);
if (HeadShot_Dmg)
HeadShot = true;
bool HeadShot = false;
uint32 HeadShot_Dmg = TryHeadShot(other, SkillArchery);
if (HeadShot_Dmg)
HeadShot = true;
int32 hate = 0;
int32 TotalDmg = 0;
int16 WDmg = 0;
int16 ADmg = 0;
if (!weapon_damage){
WDmg = GetWeaponDamage(other, RangeWeapon);
ADmg = GetWeaponDamage(other, Ammo);
}
else
WDmg = weapon_damage;
int32 TotalDmg = 0;
int16 WDmg = 0;
int16 ADmg = 0;
if (!weapon_damage){
WDmg = GetWeaponDamage(other, RangeWeapon);
ADmg = GetWeaponDamage(other, Ammo);
}
else
WDmg = weapon_damage;
if (LaunchProjectile){//1: Shoot the Projectile once we calculate weapon damage.
TryProjectileAttack(other, AmmoItem, SkillArchery, WDmg, RangeWeapon, Ammo, AmmoSlot, speed);
return;
}
if (focus) //From FcBaseEffects
WDmg += WDmg*focus/100;
if (focus) //From FcBaseEffects
WDmg += WDmg*focus/100;
if((WDmg > 0) || (ADmg > 0)) {
if(WDmg < 0)
WDmg = 0;
if(ADmg < 0)
ADmg = 0;
uint32 MaxDmg = (RuleR(Combat, ArcheryBaseDamageBonus)*(WDmg+ADmg)*GetDamageTable(SkillArchery)) / 100;
int32 hate = ((WDmg+ADmg));
if (HeadShot)
MaxDmg = HeadShot_Dmg;
uint16 bonusArcheryDamageModifier = aabonuses.ArcheryDamageModifier + itembonuses.ArcheryDamageModifier + spellbonuses.ArcheryDamageModifier;
MaxDmg += MaxDmg*bonusArcheryDamageModifier / 100;
mlog(COMBAT__RANGED, "Bow DMG %d, Arrow DMG %d, Max Damage %d.", WDmg, ADmg, MaxDmg);
bool dobonus = false;
if(GetClass() == RANGER && GetLevel() > 50)
{
int bonuschance = RuleI(Combat, ArcheryBonusChance);
bonuschance = mod_archery_bonus_chance(bonuschance, RangeWeapon);
if( !RuleB(Combat, UseArcheryBonusRoll) || (MakeRandomInt(1, 100) < bonuschance) )
{
if(RuleB(Combat, ArcheryBonusRequiresStationary))
{
if(other->IsNPC() && !other->IsMoving() && !other->IsRooted())
{
dobonus = true;
}
}
else
{
dobonus = true;
}
}
if(dobonus)
{
MaxDmg *= 2;
hate *= 2;
MaxDmg = mod_archery_bonus_damage(MaxDmg, RangeWeapon);
mlog(COMBAT__RANGED, "Ranger. Double damage success roll, doubling damage to %d", MaxDmg);
Message_StringID(MT_CritMelee, BOW_DOUBLE_DAMAGE);
}
}
if (MaxDmg == 0)
MaxDmg = 1;
if(RuleB(Combat, UseIntervalAC))
TotalDmg = MaxDmg;
else
TotalDmg = MakeRandomInt(1, MaxDmg);
int minDmg = 1;
if(GetLevel() > 25){
//twice, for ammo and weapon
TotalDmg += (2*((GetLevel()-25)/3));
minDmg += (2*((GetLevel()-25)/3));
minDmg += minDmg * GetMeleeMinDamageMod_SE(SkillArchery) / 100;
hate += (2*((GetLevel()-25)/3));
}
if (!HeadShot)
other->AvoidDamage(this, TotalDmg, false);
other->MeleeMitigation(this, TotalDmg, minDmg);
if(TotalDmg > 0)
{
ApplyMeleeDamageBonus(SkillArchery, TotalDmg);
TotalDmg += other->GetFcDamageAmtIncoming(this, 0, true, SkillArchery);
TotalDmg += (itembonuses.HeroicDEX / 10) + (TotalDmg * other->GetSkillDmgTaken(SkillArchery) / 100) + GetSkillDmgAmt(SkillArchery);
TotalDmg = mod_archery_damage(TotalDmg, dobonus, RangeWeapon);
TryCriticalHit(other, SkillArchery, TotalDmg);
other->AddToHateList(this, hate, 0, false);
CheckNumHitsRemaining(NUMHIT_OutgoingHitSuccess);
}
}
else
TotalDmg = -5;
if((WDmg > 0) || (ADmg > 0)) {
if(WDmg < 0)
WDmg = 0;
if(ADmg < 0)
ADmg = 0;
uint32 MaxDmg = (RuleR(Combat, ArcheryBaseDamageBonus)*(WDmg+ADmg)*GetDamageTable(SkillArchery)) / 100;
hate = ((WDmg+ADmg));
if (HeadShot)
entity_list.MessageClose_StringID(this, false, 200, MT_CritMelee, FATAL_BOW_SHOT, GetName());
other->Damage(this, TotalDmg, SPELL_UNKNOWN, SkillArchery);
if (TotalDmg > 0 && HasSkillProcSuccess() && GetTarget() && other && !other->HasDied()){
if (ReuseTime)
TrySkillProc(other, SkillArchery, ReuseTime);
else
TrySkillProc(other, SkillArchery, 0, true, MainRange);
MaxDmg = HeadShot_Dmg;
uint16 bonusArcheryDamageModifier = aabonuses.ArcheryDamageModifier + itembonuses.ArcheryDamageModifier + spellbonuses.ArcheryDamageModifier;
MaxDmg += MaxDmg*bonusArcheryDamageModifier / 100;
mlog(COMBAT__RANGED, "Bow DMG %d, Arrow DMG %d, Max Damage %d.", WDmg, ADmg, MaxDmg);
bool dobonus = false;
if(GetClass() == RANGER && GetLevel() > 50){
int bonuschance = RuleI(Combat, ArcheryBonusChance);
bonuschance = mod_archery_bonus_chance(bonuschance, RangeWeapon);
if( !RuleB(Combat, UseArcheryBonusRoll) || (MakeRandomInt(1, 100) < bonuschance)){
if(RuleB(Combat, ArcheryBonusRequiresStationary)){
if(other->IsNPC() && !other->IsMoving() && !other->IsRooted())
dobonus = true;
}
else
dobonus = true;
}
if(dobonus){
MaxDmg *= 2;
hate *= 2;
MaxDmg = mod_archery_bonus_damage(MaxDmg, RangeWeapon);
mlog(COMBAT__RANGED, "Ranger. Double damage success roll, doubling damage to %d", MaxDmg);
Message_StringID(MT_CritMelee, BOW_DOUBLE_DAMAGE);
}
}
if (MaxDmg == 0)
MaxDmg = 1;
if(RuleB(Combat, UseIntervalAC))
TotalDmg = MaxDmg;
else
TotalDmg = MakeRandomInt(1, MaxDmg);
int minDmg = 1;
if(GetLevel() > 25){
//twice, for ammo and weapon
TotalDmg += (2*((GetLevel()-25)/3));
minDmg += (2*((GetLevel()-25)/3));
minDmg += minDmg * GetMeleeMinDamageMod_SE(SkillArchery) / 100;
hate += (2*((GetLevel()-25)/3));
}
if (!HeadShot)
other->AvoidDamage(this, TotalDmg, false);
other->MeleeMitigation(this, TotalDmg, minDmg);
if(TotalDmg > 0){
CommonOutgoingHitSuccess(other, TotalDmg, SkillArchery);
TotalDmg = mod_archery_damage(TotalDmg, dobonus, RangeWeapon);
}
}
else
TotalDmg = -5;
if (HeadShot)
entity_list.MessageClose_StringID(this, false, 200, MT_CritMelee, FATAL_BOW_SHOT, GetName());
other->AddToHateList(this, hate, 0, false);
other->Damage(this, TotalDmg, SPELL_UNKNOWN, SkillArchery);
//Skill Proc Success
if (TotalDmg > 0 && HasSkillProcSuccess() && other && !other->HasDied()){
if (ReuseTime)
TrySkillProc(other, SkillArchery, ReuseTime);
else
TrySkillProc(other, SkillArchery, 0, true, MainRange);
}
}
//try proc on hits and misses
if((RangeWeapon != nullptr) && GetTarget() && other && !other->HasDied()){
if (LaunchProjectile)
return;//Shouldn't reach this point, but just in case.
//Weapon Proc
if(!RangeWeapon && other && !other->HasDied())
TryWeaponProc(RangeWeapon, other, MainRange);
}
//Arrow procs because why not?
if((Ammo != NULL) && GetTarget() && other && !other->HasDied())
{
TryWeaponProc(Ammo, other, MainRange);
}
//Ammo Proc
if (ammo_lost)
TryWeaponProc(nullptr, ammo_lost, other, MainRange);
else if(Ammo && other && !other->HasDied())
TryWeaponProc(Ammo, other, MainRange);
if (HasSkillProcs() && GetTarget() && other && !other->HasDied()){
//Skill Proc
if (HasSkillProcs() && other && !other->HasDied()){
if (ReuseTime)
TrySkillProc(other, SkillArchery, ReuseTime);
else
@ -953,6 +1009,121 @@ void Mob::DoArcheryAttackDmg(Mob* other, const ItemInst* RangeWeapon, const Item
}
}
bool Mob::TryProjectileAttack(Mob* other, const Item_Struct *item, SkillUseTypes skillInUse, uint16 weapon_dmg, const ItemInst* RangeWeapon, const ItemInst* Ammo, int AmmoSlot, float speed){
if (!other)
return false;
int slot = -1;
//Make sure there is an avialable slot.
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) {
if (ProjectileAtk[i].target_id == 0){
slot = i;
break;
}
}
if (slot < 0)
return false;
float speed_mod = speed * 0.45f;
float distance = other->CalculateDistance(GetX(), GetY(), GetZ());
float hit = 60.0f + (distance / speed_mod); //Calcuation: 60 = Animation Lag, 1.8 = Speed modifier for speed of (4)
ProjectileAtk[slot].increment = 1;
ProjectileAtk[slot].hit_increment = static_cast<uint16>(hit); //This projected hit time if target does NOT MOVE
ProjectileAtk[slot].target_id = other->GetID();
ProjectileAtk[slot].wpn_dmg = weapon_dmg;
ProjectileAtk[slot].origin_x = GetX();
ProjectileAtk[slot].origin_y = GetY();
ProjectileAtk[slot].origin_z = GetZ();
if (RangeWeapon && RangeWeapon->GetItem())
ProjectileAtk[slot].ranged_id = RangeWeapon->GetItem()->ID;
if (Ammo && Ammo->GetItem())
ProjectileAtk[slot].ammo_id = Ammo->GetItem()->ID;
ProjectileAtk[slot].ammo_slot = 0;
ProjectileAtk[slot].skill = skillInUse;
ProjectileAtk[slot].speed_mod = speed_mod;
SetProjectileAttack(true);
if(item)
SendItemAnimation(other, item, skillInUse, speed);
else if (IsNPC())
ProjectileAnimation(other, 0,false,speed,0,0,0,CastToNPC()->GetAmmoIDfile(),skillInUse);
return true;
}
void Mob::ProjectileAttack()
{
if (!HasProjectileAttack())
return;;
Mob* target = nullptr;
bool disable = true;
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) {
if (ProjectileAtk[i].increment == 0){
continue;
}
disable = false;
Mob* target = entity_list.GetMobID(ProjectileAtk[i].target_id);
if (target && target->IsMoving()){ //Only recalculate hit increment if target moving
//Due to frequency that we need to check increment the targets position variables may not be updated even if moving. Do a simple check before calculating distance.
if (ProjectileAtk[i].tlast_x != target->GetX() || ProjectileAtk[i].tlast_y != target->GetY()){
ProjectileAtk[i].tlast_x = target->GetX();
ProjectileAtk[i].tlast_y = target->GetY();
float distance = target->CalculateDistance(ProjectileAtk[i].origin_x, ProjectileAtk[i].origin_y, ProjectileAtk[i].origin_z);
float hit = 60.0f + (distance / ProjectileAtk[i].speed_mod); //Calcuation: 60 = Animation Lag, 1.8 = Speed modifier for speed of (4)
ProjectileAtk[i].hit_increment = static_cast<uint16>(hit);
}
}
if (ProjectileAtk[i].hit_increment <= ProjectileAtk[i].increment){
if (target){
if (ProjectileAtk[i].skill == SkillArchery)
DoArcheryAttackDmg(target, nullptr, nullptr,ProjectileAtk[i].wpn_dmg,0,0,0,ProjectileAtk[i].ranged_id, ProjectileAtk[i].ammo_id, nullptr, ProjectileAtk[i].ammo_slot);
else if (ProjectileAtk[i].skill == SkillThrowing)
DoThrowingAttackDmg(target, nullptr, nullptr,ProjectileAtk[i].wpn_dmg,0,0,0, ProjectileAtk[i].ranged_id, ProjectileAtk[i].ammo_slot);
else if (ProjectileAtk[i].skill == SkillConjuration && IsValidSpell(ProjectileAtk[i].wpn_dmg))
SpellOnTarget(ProjectileAtk[i].wpn_dmg, target, false, true, spells[ProjectileAtk[i].wpn_dmg].ResistDiff, true);
}
ProjectileAtk[i].increment = 0;
ProjectileAtk[i].target_id = 0;
ProjectileAtk[i].wpn_dmg = 0;
ProjectileAtk[i].origin_x = 0.0f;
ProjectileAtk[i].origin_y = 0.0f;
ProjectileAtk[i].origin_z = 0.0f;
ProjectileAtk[i].tlast_x = 0.0f;
ProjectileAtk[i].tlast_y = 0.0f;
ProjectileAtk[i].ranged_id = 0;
ProjectileAtk[i].ammo_id = 0;
ProjectileAtk[i].ammo_slot = 0;
ProjectileAtk[i].skill = 0;
ProjectileAtk[i].speed_mod = 0.0f;
}
else {
ProjectileAtk[i].increment++;
}
}
if (disable)
SetProjectileAttack(false);
}
void NPC::RangedAttack(Mob* other)
{
@ -1180,8 +1351,6 @@ void Client::ThrowingAttack(Mob* other, bool CanDoubleAttack) { //old was 51
(GetAppearance() == eaDead)){
return;
}
//send item animation, also does the throw animation
SendItemAnimation(GetTarget(), item, SkillThrowing);
DoThrowingAttackDmg(GetTarget(), RangeWeapon, item);
@ -1191,21 +1360,78 @@ void Client::ThrowingAttack(Mob* other, bool CanDoubleAttack) { //old was 51
CommonBreakInvisible();
}
void Mob::DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon, const Item_Struct* item, uint16 weapon_damage, int16 chance_mod,int16 focus, int ReuseTime)
void Mob::DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon, const Item_Struct* AmmoItem, uint16 weapon_damage, int16 chance_mod,int16 focus, int ReuseTime, uint32 range_id, int AmmoSlot, float speed)
{
if (!CanDoSpecialAttack(other))
if ((other == nullptr ||
((IsClient() && CastToClient()->dead) ||
(other->IsClient() && other->CastToClient()->dead)) ||
HasDied() ||
(!IsAttackAllowed(other)) ||
(other->GetInvul() ||
other->GetSpecialAbility(IMMUNE_MELEE))))
{
return;
}
if (!other->CheckHitChance(this, SkillThrowing, MainPrimary, chance_mod)){
const ItemInst* _RangeWeapon = nullptr;
const Item_Struct* ammo_lost = nullptr;
/*
If LaunchProjectile is false this function will do archery damage on target,
otherwise it will shoot the projectile at the target, once the projectile hits target
this function is then run again to do the damage portion
*/
bool LaunchProjectile = false;
bool ProjectileMiss = false;
if (RuleB(Combat, ProjectileDmgOnImpact)){
if (AmmoItem)
LaunchProjectile = true;
else{
if (!RangeWeapon && range_id){
if (weapon_damage == 0)
ProjectileMiss = true; //This indicates that MISS was originally calculated.
if (IsClient()){
_RangeWeapon = CastToClient()->m_inv[AmmoSlot];
if (_RangeWeapon && _RangeWeapon->GetItem() && _RangeWeapon->GetItem()->ID != range_id)
RangeWeapon = _RangeWeapon;
else
ammo_lost = database.GetItem(range_id);
}
}
}
}
else if (AmmoItem)
SendItemAnimation(other, AmmoItem, SkillThrowing);
if (ProjectileMiss || !other->CheckHitChance(this, SkillThrowing, MainPrimary, chance_mod)){
mlog(COMBAT__RANGED, "Ranged attack missed %s.", other->GetName());
other->Damage(this, 0, SPELL_UNKNOWN, SkillThrowing);
if (LaunchProjectile){
TryProjectileAttack(other, AmmoItem, SkillThrowing, 0, RangeWeapon, nullptr, AmmoSlot, speed);
return;
}
else
other->Damage(this, 0, SPELL_UNKNOWN, SkillThrowing);
} else {
mlog(COMBAT__RANGED, "Throwing attack hit %s.", other->GetName());
int16 WDmg = 0;
if (!weapon_damage && item != nullptr)
WDmg = GetWeaponDamage(other, item);
if (!weapon_damage){
if (IsClient() && RangeWeapon)
WDmg = GetWeaponDamage(other, RangeWeapon);
else if (AmmoItem)
WDmg = GetWeaponDamage(other, AmmoItem);
if (LaunchProjectile){
TryProjectileAttack(other, AmmoItem, SkillThrowing, WDmg, RangeWeapon, nullptr, AmmoSlot, speed);
return;
}
}
else
WDmg = weapon_damage;
@ -1242,7 +1468,7 @@ void Mob::DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon, const Ite
other->AddToHateList(this, 2*WDmg, 0, false);
other->Damage(this, TotalDmg, SPELL_UNKNOWN, SkillThrowing);
if (TotalDmg > 0 && HasSkillProcSuccess() && GetTarget() && other && !other->HasDied()){
if (TotalDmg > 0 && HasSkillProcSuccess() && other && !other->HasDied()){
if (ReuseTime)
TrySkillProc(other, SkillThrowing, ReuseTime);
else
@ -1250,19 +1476,24 @@ void Mob::DoThrowingAttackDmg(Mob* other, const ItemInst* RangeWeapon, const Ite
}
}
if((RangeWeapon != nullptr) && GetTarget() && other && (other->GetHP() > -10))
if (LaunchProjectile)
return;
//Throwing item Proc
if (ammo_lost)
TryWeaponProc(nullptr, ammo_lost, other, MainRange);
else if(RangeWeapon && other && !other->HasDied())
TryWeaponProc(RangeWeapon, other, MainRange);
if (HasSkillProcs() && GetTarget() && other && !other->HasDied()){
if (HasSkillProcs() && other && !other->HasDied()){
if (ReuseTime)
TrySkillProc(other, SkillThrowing, ReuseTime);
else
TrySkillProc(other, SkillThrowing, 0, false, MainRange);
}
}
void Mob::SendItemAnimation(Mob *to, const Item_Struct *item, SkillUseTypes skillInUse) {
void Mob::SendItemAnimation(Mob *to, const Item_Struct *item, SkillUseTypes skillInUse, float velocity) {
EQApplicationPacket *outapp = new EQApplicationPacket(OP_SomeItemPacketMaybe, sizeof(Arrow_Struct));
Arrow_Struct *as = (Arrow_Struct *) outapp->pBuffer;
as->type = 1;
@ -1290,7 +1521,7 @@ void Mob::SendItemAnimation(Mob *to, const Item_Struct *item, SkillUseTypes skil
Arc causes the object to form an arc in motion. A value too high will
*/
as->velocity = 4.0;
as->velocity = velocity;
//these angle and tilt used together seem to make the arrow/knife throw as straight as I can make it

View File

@ -6400,17 +6400,16 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama
return false;
}
bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id){
bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed){
/*For mage 'Bolt' line and other various spells.
-This is mostly accurate for how the modern clients handle this effect.
-It was changed at some point to use an actual projectile as done here (opposed to a particle effect in classic)
-The projectile graphic appears to be that of 'Ball of Sunlight' ID 80648 and will be visible to anyone in SoF+
-There is no LOS check to prevent a bolt from being cast. If you don't have LOS your bolt simply goes into whatever barrier
and you lose your mana. If there is LOS the bolt will lock onto your target and the damage is applied when it hits the target.
-If your target moves the bolt moves with it in any direction or angle (consistent with other projectiles).
-The way this is written once a bolt is cast a timer checks the distance from the initial cast to the target repeatedly
and calculates at what predicted time the bolt should hit that target in client_process (therefore accounting for any target movement).
-The way this is written once a bolt is cast a the distance from the initial cast to the target repeatedly
check and if target is moving recalculates at what predicted time the bolt should hit that target in client_process
When bolt hits its predicted point the damage is then done to target.
Note: Projectile speed of 1 takes 3 seconds to go 100 distance units. Calculations are based on this constant.
Live Bolt speed: Projectile speed of X takes 5 seconds to go 300 distance units.
@ -6422,31 +6421,41 @@ bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id){
return false;
uint8 anim = spells[spell_id].CastingAnim;
int bolt_id = -1;
int slot = -1;
//Make sure there is an avialable bolt to be cast.
for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) {
if (projectile_spell_id[i] == 0){
bolt_id = i;
if (ProjectileAtk[i].target_id == 0){
slot = i;
break;
}
}
if (bolt_id < 0)
if (slot < 0)
return false;
if (CheckLosFN(spell_target)) {
projectile_spell_id[bolt_id] = spell_id;
projectile_target_id[bolt_id] = spell_target->GetID();
projectile_x[bolt_id] = GetX(), projectile_y[bolt_id] = GetY(), projectile_z[bolt_id] = GetZ();
projectile_increment[bolt_id] = 1;
projectile_timer.Start(250);
float speed_mod = speed * 0.45f; //Constant for adjusting speeds to match calculated impact time.
float distance = spell_target->CalculateDistance(GetX(), GetY(), GetZ());
float hit = 60.0f + (distance / speed_mod);
ProjectileAtk[slot].increment = 1;
ProjectileAtk[slot].hit_increment = static_cast<uint16>(hit); //This projected hit time if target does NOT MOVE
ProjectileAtk[slot].target_id = spell_target->GetID();
ProjectileAtk[slot].wpn_dmg = spell_id; //Store spell_id in weapon damage field
ProjectileAtk[slot].origin_x = GetX();
ProjectileAtk[slot].origin_y = GetY();
ProjectileAtk[slot].origin_z = GetZ();
ProjectileAtk[slot].skill = SkillConjuration;
ProjectileAtk[slot].speed_mod = speed_mod;
SetProjectileAttack(true);
}
//This will use the correct graphic as defined in the player_1 field of spells_new table. Found in UF+ spell files.
if (RuleB(Spells, UseLiveSpellProjectileGFX)) {
ProjectileAnimation(spell_target,0, false, 1.5,0,0,0, spells[spell_id].player_1);
ProjectileAnimation(spell_target,0, false, speed,0,0,0, spells[spell_id].player_1);
}
//This allows limited support for server using older spell files that do not contain data for bolt graphics.
@ -6456,19 +6465,17 @@ bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id){
if (IsClient()){
if (CastToClient()->GetClientVersionBit() <= 4) //Titanium needs alternate graphic.
ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_Titanium)), false, 1.5);
ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_Titanium)), false, speed);
else
ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_SOF)), false, 1.5);
ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_SOF)), false, speed);
}
else
ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_NPC)), false, 1.5);
ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_NPC)), false, speed);
}
//Default to an arrow if not using a mage bolt (Use up to date spell file and enable above rules for best results)
else
ProjectileAnimation(spell_target,0, 1, 1.5);
ProjectileAnimation(spell_target,0, 1, speed);
}
if (spells[spell_id].CastingAnim == 64)