diff --git a/.drone.yml b/.drone.yml index 749ab62c5..6b9e0425b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,4 +13,4 @@ steps: image: akkadius/eqemu-server:latest commands: - sudo chown eqemu:eqemu /drone/src/ * -R - - git submodule init && git submodule update && mkdir -p build && cd build && cmake -DEQEMU_BUILD_LOGIN=ON -DEQEMU_BUILD_BOTS=ON -DEQEMU_BUILD_LUA=ON -G 'Unix Makefiles' .. && make -j$((`nproc`-4)) \ No newline at end of file + - git submodule init && git submodule update && mkdir -p build && cd build && cmake -DEQEMU_BUILD_LOGIN=ON -DEQEMU_ENABLE_BOTS=ON -DEQEMU_BUILD_LUA=ON -G 'Unix Makefiles' .. && make -j$((`nproc`-4)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 526d68099..351aca1a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,6 +131,47 @@ OPTION(EQEMU_BUILD_TESTS "Build utility tests." OFF) OPTION(EQEMU_BUILD_CLIENT_FILES "Build Client Import/Export Data Programs." ON) OPTION(EQEMU_PREFER_LUA "Build with normal Lua even if LuaJIT is found." OFF) +#PRNG options +OPTION(EQEMU_ADDITIVE_LFIB_PRNG "Use Additive LFib for PRNG." OFF) +MARK_AS_ADVANCED(EQEMU_ADDITIVE_LFIB_PRNG) +OPTION(EQEMU_BIASED_INT_DIST "Use biased int dist instead of uniform." OFF) +MARK_AS_ADVANCED(EQEMU_BIASED_INT_DIST) +SET(EQEMU_CUSTOM_PRNG_ENGINE "" CACHE STRING "Custom random engine. (ex. std::default_random_engine)") +MARK_AS_ADVANCED(EQEMU_CUSTOM_PRNG_ENGINE) + +IF(CMAKE_COMPILER_IS_GNUCXX) + OPTION(EQEMU_SFMT19937 "Use GCC's extention for SIMD Fast MT19937." OFF) + MARK_AS_ADVANCED(EQEMU_SFMT19937) +ENDIF() + +IF(EQEMU_ADDITIVE_LFIB_PRNG) + ADD_DEFINITIONS(-DUSE_ADDITIVE_LFIB_PRNG) + IF(EQEMU_SFMT19937) + MESSAGE(STATUS "SFMT19937 and ADDITITVE_LFIB_PRNG both set, SFMT19937 ignored.") + SET(EQEMU_SFMT19937 OFF) + ENDIF() + IF(NOT EQEMU_CUSTOM_PRNG_ENGINE STREQUAL "") + MESSAGE(STATUS "CUSTOM_PRNG_ENGINE and ADDITITVE_LFIB_PRNG both set, CUSTOM_PRNG_ENGINE ignored.") + SET(EQEMU_CUSTOM_PRNG_ENGINE "") + ENDIF() +ENDIF() + +IF(EQEMU_SFMT19937) + ADD_DEFINITIONS(-DUSE_SFMT19937) + IF(NOT EQEMU_CUSTOM_PRNG_ENGINE STREQUAL "") + MESSAGE(STATUS "CUSTOM_PRNG_ENGINE and SFMT19937 both set, CUSTOM_PRNG_ENGINE ignored.") + SET(EQEMU_CUSTOM_PRNG_ENGINE "") + ENDIF() +ENDIF() + +IF(NOT EQEMU_CUSTOM_PRNG_ENGINE STREQUAL "") + ADD_DEFINITIONS(-DUSE_CUSTOM_PRNG_ENGINE=${EQEMU_CUSTOM_PRNG_ENGINE}) +ENDIF() + +IF(EQEMU_BIASED_INT_DIST) + ADD_DEFINITIONS(-DBIASED_INT_DIST) +ENDIF() + IF(EQEMU_COMMANDS_LOGGING) ADD_DEFINITIONS(-DCOMMANDS_LOGGING) ENDIF(EQEMU_COMMANDS_LOGGING) @@ -198,6 +239,9 @@ ELSEIF(OpenSSL_FOUND) SET(TLS_LIBRARY_LIBS ${OPENSSL_LIBRARIES}) SET(TLS_LIBRARY_INCLUDE ${OPENSSL_INCLUDE_DIR}) ADD_DEFINITIONS(-DEQEMU_USE_OPENSSL) + IF(${OPENSSL_VERSION} VERSION_GREATER_EQUAL "1.1.1") + ADD_DEFINITIONS(-DCPPHTTPLIB_OPENSSL_SUPPORT) + ENDIF() ELSEIF(MBEDTLS_FOUND) SET(TLS_LIBRARY_TYPE " mbedTLS") SET(TLS_LIBRARY_ENABLED ON) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 084ac9c4f..47edd22e8 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -451,6 +451,7 @@ SET(repositories ) SET(common_headers + additive_lagged_fibonacci_engine.h any.h base_packet.h base_data.h diff --git a/common/additive_lagged_fibonacci_engine.h b/common/additive_lagged_fibonacci_engine.h new file mode 100644 index 000000000..b396b4436 --- /dev/null +++ b/common/additive_lagged_fibonacci_engine.h @@ -0,0 +1,147 @@ +/* EQEMu: Everquest Server Emulator + Copyright (C) 2001-2021 EQEMu Development Team (http://eqemulator.net) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY except by those people which sell it, which + are required to give you total support for your newly bought product; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#pragma once + +#include +#include +#include +#include +#include + +/* + * This is an additive lagged fibonacci generator as seen in The Art of Computer Programming, Vol. 2 + * This should roughly match the implementation that EQ's client uses and be compatible with our Random class + * + * EQ's rand looks like it was from an example implementation that as posted on pscode.com + * + * You might also want to consider defining BIASED_INT_DIST as well to more closely match EQ + */ + +namespace EQ { + template + class additive_lagged_fibonacci_engine { + static_assert(std::is_unsigned::value, "result_type must be an unsigned integral type"); + static_assert(0u < j && j < k, "0 < j < k"); + static_assert(0u < w && w <= std::numeric_limits::digits, + "template argument substituting w out of bounds"); + + public: + using result_type = UIntType; + static constexpr size_t word_size = w; + static constexpr size_t short_lag = j; + static constexpr size_t long_lag = k; + static constexpr result_type default_seed = 19780503u; // default for subtract_with_carry_engine + + additive_lagged_fibonacci_engine() : additive_lagged_fibonacci_engine(default_seed) {} + + explicit additive_lagged_fibonacci_engine(result_type sd) { seed(sd); } + + void seed(result_type seed = default_seed) + { + state1 = long_lag - long_lag; + state2 = long_lag - short_lag; + state[0] = static_cast(seed) & ((1u << word_size) - 1); + state[1] = 1; + for (int i = 2; i < long_lag; ++i) + state[i] = (state[i - 1] + state[i - 2]) & ((1u << word_size) - 1); + return; + } + // TODO: seed via seed_seq + + static constexpr result_type min() { return 0; } + static constexpr result_type max() { return ((1u << word_size) - 1) >> 6; } + + void discard(unsigned long long z) { + for (; z != 0ULL; --z) + (*this)(); + } + + result_type operator()() { + result_type rand = (state[state1] + state[state2]) & ((1u << word_size) - 1); + state[state1] = rand; + if (++state1 == long_lag) + state1 = 0; + if (++state2 == long_lag) + state2 = 0; + + return rand >> 6; + } + + private: + result_type state1; + result_type state2; + result_type state[long_lag]; + + public: + template + friend bool operator==(const additive_lagged_fibonacci_engine &x, + const additive_lagged_fibonacci_engine &y) + { + return std::equal(x.state, x.state + long_lag, y.state) && x.state1 == y.state1 && + x.state2 == y.state2; + } + + template + friend bool operator!=(const additive_lagged_fibonacci_engine &x, + const additive_lagged_fibonacci_engine &y) + { return !(x == y); } + + template + friend std::basic_ostream & + operator<<(std::basic_istream &os, additive_lagged_fibonacci_engine &x) + { + using ios_base = typename std::basic_istream::ios_base; + + const typename ios_base::fmtflags flags = os.flags(); + const CharT fill = os.fill(); + const CharT space = os.widen(' '); + os.flags(ios_base::dec | ios_base::fixed | ios_base::left); + os.fill(space); + + for (size_t i = 0; i < long_lag; ++i) + os << x.state[i] << space; + os << x.state1 << space << x.state2; + + os.flags(flags); + os.fill(fill); + return os; + } + + template + friend std::basic_istream & + operator>>(std::basic_istream &is, additive_lagged_fibonacci_engine &x) + { + using ios_base = typename std::basic_istream::ios_base; + + const typename ios_base::fmtflags flags = is.flags(); + is.flags(ios_base::dec | ios_base::skipws); + + for (size_t i = 0; i < long_lag; ++i) + is >> x.state[i]; + is >> x.state1; + is >> x.state2; + + is.flags(flags); + return is; + } + }; + + using EQRand = additive_lagged_fibonacci_engine; +}; + diff --git a/common/random.h b/common/random.h index 203755efd..6d351121f 100644 --- a/common/random.h +++ b/common/random.h @@ -1,5 +1,5 @@ /* EQEMu: Everquest Server Emulator - Copyright (C) 2001-2014 EQEMu Development Team (http://eqemulator.net) + Copyright (C) 2001-2021 EQEMu Development Team (http://eqemulator.net) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -25,10 +25,24 @@ #include #include -/* This uses mt19937 seeded with the std::random_device - * The idea is to have this be included as a member of another class - * so mocking out for testing is easier - * If you need to reseed random.Reseed() +#ifdef USE_SFMT19937 +// only GCC supports, so lets turn it off +#ifndef __GNUC__ +#undef USE_SFMT19937 +#endif +#ifdef __clang__ +#undef USE_SFMT19937 +#endif +#endif + +#ifdef USE_ADDITIVE_LFIB_PRNG +#include "additive_lagged_fibonacci_engine.h" +#elif defined(USE_SFMT19937) +#include +#endif + +/* This uses mt19937 (or other compatible engine) seeded with the std::random_device. The idea is to have this be + * included as a member of another class so mocking out for testing is easier If you need to reseed random.Reseed() * Eventually this should be derived from an abstract base class */ @@ -40,7 +54,12 @@ namespace EQ { { if (low > high) std::swap(low, high); +// EQ uses biased int distribution, so I guess we can support it :P +#ifdef BIASED_INT_DIST + return low + m_gen() % (high - low + 1); +#else return int_dist(m_gen, int_param_t(low, high)); // [low, high] +#endif } // AKA old MakeRandomFloat @@ -98,11 +117,24 @@ namespace EQ { } private: +#ifndef BIASED_INT_DIST typedef std::uniform_int_distribution::param_type int_param_t; - typedef std::uniform_real_distribution::param_type real_param_t; - std::mt19937 m_gen; std::uniform_int_distribution int_dist; +#endif + typedef std::uniform_real_distribution::param_type real_param_t; std::uniform_real_distribution real_dist; +// define USE_CUSTOM_PRNG_ENGINE to any supported random engine like: +// #define USE_CUSTOM_PRNG_ENGINE std::default_random_engine +#ifdef USE_ADDITIVE_LFIB_PRNG + EQRand +#elif defined(USE_SFMT19937) + __gnu_cxx::sfmt19937 +#elif defined(USE_CUSTOM_PRNG_ENGINE) + USE_CUSTOM_PRNG_ENGINE +#else + std::mt19937 +#endif + m_gen; }; } diff --git a/common/spdat.h b/common/spdat.h index 03fed1d1f..eca404473 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -609,7 +609,7 @@ typedef enum { #define SE_TradeSkillMastery 263 // implemented - lets you raise more than one tradeskill above master. #define SE_HastenedAASkill 264 // implemented #define SE_MasteryofPast 265 // implemented[AA] - Spells less than effect values level can not be fizzled -#define SE_ExtraAttackChance 266 // implemented - increase chance to score an extra attack with a 2-Handed Weapon. +#define SE_ExtraAttackChance 266 // implemented, @OffBonus, gives your double attacks a percent chance to perform an extra attack with 2-handed primary weapon, base: chance, limit: amt attacks, max: none #define SE_AddPetCommand 267 // implemented - sets command base2 to base1 #define SE_ReduceTradeskillFail 268 // implemented - reduces chance to fail with given tradeskill by a percent chance #define SE_MaxBindWound 269 // implemented[AA] - Increase max HP you can bind wound. @@ -825,11 +825,16 @@ typedef enum { #define SE_Ff_Value_Min 479 // implemented, @Ff, Minimum base value of a spell that can be focused, base: spells to be focused base1 value #define SE_Ff_Value_Max 480 // implemented, @Ff, Max base value of a spell that can be focused, base: spells to be focused base1 value #define SE_Fc_Cast_Spell_On_Land 481 // implemented, @Fc, On Target, cast spell if hit by spell, base: chance pct, limit: spellid -//#define SE_Skill_Base_Damage_Mod 482 // #define SE_Fc_Spell_Damage_Pct_IncomingPC 483 // implemented, @Fc, On Target, spell damage taken mod pct, base: min pct, limit: max pct #define SE_Fc_Spell_Damage_Amt_IncomingPC 484 // implemented, @Fc, On Target, damage taken flat amt, base: amt #define SE_Ff_CasterClass 485 // implemented, @Ff, Caster of spell on target with a focus effect that is checked by incoming spells must be specified class(es). base1: class(es), Note: Set multiple classes same as would for items #define SE_Ff_Same_Caster 486 // implemented, @Ff, Caster of spell on target with a focus effect that is checked by incoming spells, base1: 0=Must be different caster 1=Must be same caster +#define SE_Fc_Cast_Spell_On_Land 481 // Implemented - [FOCUS] Spells cast on target with this Focus Effect will have chance to cause a Spell to be cast if limits met. +#define SE_Skill_Base_Damage_Mod 482 // implemented, @OffBonus, modify base melee damage by percent, base: pct, limit: skill(-1=ALL), max: none +#define SE_Fc_Spell_Damage_Pct_IncomingPC 483 // Implemented - [FOCUS] modifies incoming spell damage by percent +#define SE_Fc_Spell_Damage_Amt_IncomingPC 484 // Implemented - [FOCUS] modifies incoming spell damage by flat amount. Typically adds damage to incoming spells. +#define SE_Ff_CasterClass 485 // Implemented - [FOCUS LIMIT] Caster of spell on target with a focus effect that is checked by incoming spells must be specified class. +#define SE_Ff_Same_Caster 486 // Implemented - [FOCUS LIMIT] Caster of spell on target with a focus effect that is checked by incoming spells 0=Must be different caster 1=Must be same caster //#define SE_Extend_Tradeskill_Cap 487 // //#define SE_Defender_Melee_Force_Pct_PC 488 // //#define SE_Worn_Endurance_Regen_Cap 489 // @@ -841,8 +846,8 @@ typedef enum { #define SE_Ff_DurationMax 495 // implemented, @Ff, Max duration of spell that can be focused, base: tics #define SE_Critical_Melee_Damage_Mod_Max 496 // implemented - increase or decrease by percent critical damage (not stackable) //#define SE_Ff_FocusCastProcNoBypass 497 // -//#define SE_AddExtraAttackPct_1h_Primary 498 // -//#define SE_AddExtraAttackPct_1h_Secondary 499 // +#define SE_AddExtraAttackPct_1h_Primary 498 // implemented, @OffBonus, gives your double attacks a percent chance to perform an extra attack with 1-handed primary weapon, base: chance, limit: amt attacks, max: none +#define SE_AddExtraAttackPct_1h_Secondary 499 //implemented, @OffBonus, gives your double attacks a percent chance to perform an extra attack with 1-handed secondary weapon, base: chance, limit: amt attacks, max: none #define SE_Fc_CastTimeMod2 500 // implemented, @Fc, On Caster, cast time mod pct, base: pct, Note: Can reduce to instant cast #define SE_Fc_CastTimeAmt 501 // implemented, @Fc, On Caster, cast time mod flat amt, base: milliseconds, Note: Can reduce to instant cast #define SE_Fearstun 502 // implemented - Stun with a max level limit. Normal stun restrictions don't apply. diff --git a/zone/attack.cpp b/zone/attack.cpp index d09271c55..aa6c30bd4 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -5483,13 +5483,40 @@ void Client::DoAttackRounds(Mob *target, int hand, bool IsFromSpell) CheckIncreaseSkill(EQ::skills::SkillDoubleAttack, target, -10); if (CheckDoubleAttack()) { Attack(target, hand, false, false, IsFromSpell); - - // Modern AA description: Increases your chance of ... performing one additional hit with a 2-handed weapon when double attacking by 2%. + if (hand == EQ::invslot::slotPrimary) { - auto extraattackchance = aabonuses.ExtraAttackChance + spellbonuses.ExtraAttackChance + - itembonuses.ExtraAttackChance; - if (extraattackchance && HasTwoHanderEquipped() && zone->random.Roll(extraattackchance)) - Attack(target, hand, false, false, IsFromSpell); + + if (HasTwoHanderEquipped()) { + auto extraattackchance = aabonuses.ExtraAttackChance[0] + spellbonuses.ExtraAttackChance[0] + + itembonuses.ExtraAttackChance[0]; + if (extraattackchance && zone->random.Roll(extraattackchance)) { + auto extraattackamt = std::max({ aabonuses.ExtraAttackChance[1], spellbonuses.ExtraAttackChance[1], itembonuses.ExtraAttackChance[1] }); + for (int i = 0; i < extraattackamt; i++) { + Attack(target, hand, false, false, IsFromSpell); + } + } + } + else { + auto extraattackchance_primary = aabonuses.ExtraAttackChancePrimary[0] + spellbonuses.ExtraAttackChancePrimary[0] + + itembonuses.ExtraAttackChancePrimary[0]; + if (extraattackchance_primary && zone->random.Roll(extraattackchance_primary)) { + auto extraattackamt_primary = std::max({ aabonuses.ExtraAttackChancePrimary[1], spellbonuses.ExtraAttackChancePrimary[1], itembonuses.ExtraAttackChancePrimary[1] }); + for (int i = 0; i < extraattackamt_primary; i++) { + Attack(target, hand, false, false, IsFromSpell); + } + } + } + } + + if (hand == EQ::invslot::slotSecondary) { + auto extraattackchance_secondary = aabonuses.ExtraAttackChanceSecondary[0] + spellbonuses.ExtraAttackChanceSecondary[0] + + itembonuses.ExtraAttackChanceSecondary[0]; + if (extraattackchance_secondary && zone->random.Roll(extraattackchance_secondary)) { + auto extraattackamt_secondary = std::max({ aabonuses.ExtraAttackChanceSecondary[1], spellbonuses.ExtraAttackChanceSecondary[1], itembonuses.ExtraAttackChanceSecondary[1] }); + for (int i = 0; i < extraattackamt_secondary; i++) { + Attack(target, hand, false, false, IsFromSpell); + } + } } // you can only triple from the main hand diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 47a951c69..dab01419a 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -860,9 +860,6 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) case SE_MaxBindWound: newbon->MaxBindWound += base1; break; - case SE_ExtraAttackChance: - newbon->ExtraAttackChance += base1; - break; case SE_SeeInvis: newbon->SeeInvis = base1; break; @@ -984,7 +981,6 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) case SE_BlockBehind: newbon->BlockBehind += base1; break; - case SE_StrikeThrough: case SE_StrikeThrough2: newbon->StrikeThrough += base1; @@ -1177,6 +1173,17 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) break; } + case SE_Skill_Base_Damage_Mod: { + // Bad data or unsupported new skill + if (base2 > EQ::skills::HIGHEST_SKILL) + break; + if (base2 == ALL_SKILLS) + newbon->DamageModifier3[EQ::skills::HIGHEST_SKILL + 1] += base1; + else + newbon->DamageModifier3[base2] += base1; + break; + } + case SE_SlayUndead: { if (newbon->SlayUndead[1] < base1) newbon->SlayUndead[0] = base1; // Rate @@ -1561,6 +1568,34 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon) newbon->Pet_Add_Atk += base1; break; + case SE_ExtraAttackChance: + { + if (newbon->ExtraAttackChance[0] < base1) { + newbon->ExtraAttackChance[0] = base1; + newbon->ExtraAttackChance[1] = base2 ? base2 : 1; + } + break; + } + + case SE_AddExtraAttackPct_1h_Primary: + { + if (newbon->ExtraAttackChancePrimary[0] < base1) { + newbon->ExtraAttackChancePrimary[0] = base1; + newbon->ExtraAttackChancePrimary[1] = base2 ? base2 : 1; + } + break; + } + + case SE_AddExtraAttackPct_1h_Secondary: + { + + if (newbon->ExtraAttackChanceSecondary[0] < base1) { + newbon->ExtraAttackChanceSecondary[0] = base1; + newbon->ExtraAttackChanceSecondary[1] = base2 ? base2 : 1; + } + break; + } + // to do case SE_PetDiscipline: break; @@ -1715,7 +1750,9 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne case SE_ChangeFrenzyRad: { - // redundant to have level check here + if (max != 0 && GetLevel() > max) + break; + if(new_bonus->AggroRange == -1 || effect_value < new_bonus->AggroRange) { new_bonus->AggroRange = static_cast(effect_value); @@ -1725,6 +1762,8 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne case SE_Harmony: { + if (max != 0 && GetLevel() > max) + break; // Harmony effect as buff - kinda tricky // harmony could stack with a lull spell, which has better aggro range // take the one with less range in any case @@ -2323,6 +2362,19 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne break; } + case SE_Skill_Base_Damage_Mod: + { + // Bad data or unsupported new skill + if (base2 > EQ::skills::HIGHEST_SKILL) + break; + int skill = base2 == ALL_SKILLS ? EQ::skills::HIGHEST_SKILL + 1 : base2; + if (effect_value < 0 && new_bonus->DamageModifier3[skill] > effect_value) + new_bonus->DamageModifier3[skill] = effect_value; + else if (effect_value > 0 && new_bonus->DamageModifier3[skill] < effect_value) + new_bonus->DamageModifier3[skill] = effect_value; + break; + } + case SE_MinDamageModifier: { // Bad data or unsupported new skill @@ -2358,8 +2410,45 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne } case SE_ExtraAttackChance: - new_bonus->ExtraAttackChance += effect_value; + { + if (AdditiveWornBonus) { + new_bonus->ExtraAttackChance[0] += effect_value; + new_bonus->ExtraAttackChance[1] = base2 ? base2 : 1; + } + if (new_bonus->ExtraAttackChance[0] < effect_value) { + new_bonus->ExtraAttackChance[0] = effect_value; + new_bonus->ExtraAttackChance[1] = base2 ? base2 : 1; + } break; + } + + case SE_AddExtraAttackPct_1h_Primary: + { + if (AdditiveWornBonus) { + new_bonus->ExtraAttackChancePrimary[0] += effect_value; + new_bonus->ExtraAttackChancePrimary[1] = base2 ? base2 : 1; + } + + if (new_bonus->ExtraAttackChancePrimary[0] < effect_value) { + new_bonus->ExtraAttackChancePrimary[0] = effect_value; + new_bonus->ExtraAttackChancePrimary[1] = base2 ? base2 : 1; + } + break; + } + + case SE_AddExtraAttackPct_1h_Secondary: + { + if (AdditiveWornBonus) { + new_bonus->ExtraAttackChanceSecondary[0] += effect_value; + new_bonus->ExtraAttackChanceSecondary[1] = base2 ? base2 : 1; + } + + if (new_bonus->ExtraAttackChanceSecondary[0] < effect_value) { + new_bonus->ExtraAttackChanceSecondary[0] = effect_value; + new_bonus->ExtraAttackChanceSecondary[1] = base2 ? base2 : 1; + } + break; + } case SE_PercentXPIncrease: { @@ -4252,6 +4341,18 @@ void Mob::NegateSpellsBonuses(uint16 spell_id) break; } + case SE_Skill_Base_Damage_Mod: + { + for (int e = 0; e < EQ::skills::HIGHEST_SKILL + 1; e++) + { + spellbonuses.DamageModifier3[e] = effect_value; + aabonuses.DamageModifier3[e] = effect_value; + itembonuses.DamageModifier3[e] = effect_value; + } + break; + } + + case SE_MinDamageModifier: { for (int e = 0; e < EQ::skills::HIGHEST_SKILL + 1; e++) @@ -4276,9 +4377,21 @@ void Mob::NegateSpellsBonuses(uint16 spell_id) break; case SE_ExtraAttackChance: - spellbonuses.ExtraAttackChance = effect_value; - aabonuses.ExtraAttackChance = effect_value; - itembonuses.ExtraAttackChance = effect_value; + spellbonuses.ExtraAttackChance[0] = effect_value; + aabonuses.ExtraAttackChance[0] = effect_value; + itembonuses.ExtraAttackChance[0] = effect_value; + break; + + case SE_AddExtraAttackPct_1h_Primary: + spellbonuses.ExtraAttackChancePrimary[0] = effect_value; + aabonuses.ExtraAttackChancePrimary[0] = effect_value; + itembonuses.ExtraAttackChancePrimary[0] = effect_value; + break; + + case SE_AddExtraAttackPct_1h_Secondary: + spellbonuses.ExtraAttackChanceSecondary[0] = effect_value; + aabonuses.ExtraAttackChanceSecondary[0] = effect_value; + itembonuses.ExtraAttackChanceSecondary[0] = effect_value; break; case SE_PercentXPIncrease: diff --git a/zone/bot.cpp b/zone/bot.cpp index 757d77b9a..0cb352a18 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -3272,7 +3272,9 @@ void Bot::AI_Process() } TEST_COMBATANTS(); - int32 ExtraAttackChanceBonus = (spellbonuses.ExtraAttackChance + itembonuses.ExtraAttackChance + aabonuses.ExtraAttackChance); + auto ExtraAttackChanceBonus = + (spellbonuses.ExtraAttackChance[0] + itembonuses.ExtraAttackChance[0] + + aabonuses.ExtraAttackChance[0]); if (ExtraAttackChanceBonus) { if (p_item && p_item->GetItem()->IsType2HWeapon()) { diff --git a/zone/client.cpp b/zone/client.cpp index eb93f136f..ea2ea6fdd 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -10130,7 +10130,6 @@ void Client::SetAFK(uint8 afk_flag) { safe_delete(outapp); } - void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { uint32 zone_id = ZoneID(zone_short_name); std::string current_instance_type = str_tolower(instance_type); @@ -10241,3 +10240,8 @@ void Client::RemoveItem(uint32 item_id, uint32 quantity) } } } + +void Client::SetGMStatus(int newStatus) { + if (this->Admin() != newStatus) + database.UpdateGMStatus(this->AccountID(), newStatus); +} diff --git a/zone/client.h b/zone/client.h index 74e2eed54..0e73370c3 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1523,6 +1523,7 @@ public: void LoadAccountFlags(); void SetAccountFlag(std::string flag, std::string val); std::string GetAccountFlag(std::string flag); + void SetGMStatus(int newStatus); float GetDamageMultiplier(EQ::skills::SkillType how_long_has_this_been_missing); void Consume(const EQ::ItemData *item, uint8 type, int16 slot, bool auto_consume); void PlayMP3(const char* fname); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 92b3080c7..41d66705c 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -12997,7 +12997,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app) int16 charges = 0; if (item->Stackable || tmpmer_used) charges = mp->quantity; - else if ( item->MaxCharges > 1) + else if ( item->MaxCharges >= 1) charges = item->MaxCharges; EQ::ItemInstance* inst = database.CreateItem(item, charges); diff --git a/zone/command.cpp b/zone/command.cpp index d786e505c..a02da9ab1 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -75,8 +75,6 @@ #include "mob_movement_manager.h" #include "npc_scale_manager.h" #include "../common/content/world_content_service.h" - -#define CPPHTTPLIB_OPENSSL_SUPPORT #include "../common/http/httplib.h" extern QueryServ* QServ; diff --git a/zone/common.h b/zone/common.h index 1a05e7629..2ce4b9177 100644 --- a/zone/common.h +++ b/zone/common.h @@ -445,10 +445,13 @@ struct StatBonuses { int32 HitChanceEffect[EQ::skills::HIGHEST_SKILL + 2]; //Spell effect Chance to Hit, straight percent increase int32 DamageModifier[EQ::skills::HIGHEST_SKILL + 2]; //i int32 DamageModifier2[EQ::skills::HIGHEST_SKILL + 2]; //i + int32 DamageModifier3[EQ::skills::HIGHEST_SKILL + 2]; //i int32 MinDamageModifier[EQ::skills::HIGHEST_SKILL + 2]; //i int32 ProcChance; // ProcChance/10 == % increase i = CombatEffects int32 ProcChanceSPA; // ProcChance from spell effects - int32 ExtraAttackChance; + int32 ExtraAttackChance[2]; // base chance(w/ 2H weapon)=0, amt of extra attacks=1 + int32 ExtraAttackChancePrimary[2]; // base chance=0, , amt of extra attacks=1 + int32 ExtraAttackChanceSecondary[2]; // base chance=0, , amt of extra attacks=1 int32 DoTShielding; int32 DivineSaveChance[2]; // Second Chance (base1 = chance, base2 = spell on trigger) uint32 DeathSave[4]; // Death Pact [0](value = 1 partial 2 = full) [1]=slot [2]=LvLimit [3]=HealAmt diff --git a/zone/lua_client.cpp b/zone/lua_client.cpp index 233f40622..29699aeff 100644 --- a/zone/lua_client.cpp +++ b/zone/lua_client.cpp @@ -2158,6 +2158,11 @@ void Lua_Client::RemoveItem(uint32 item_id, uint32 quantity) { return self->RemoveItem(item_id, quantity); } +void Lua_Client::SetGMStatus(uint32 newStatus) { + Lua_Safe_Call_Void(); + return self->SetGMStatus(newStatus); +} + luabind::scope lua_register_client() { return luabind::class_("Client") .def(luabind::constructor<>()) @@ -2525,7 +2530,8 @@ luabind::scope lua_register_client() { .def("SendToInstance", (void(Lua_Client::*)(std::string,std::string,uint32,float,float,float,float,std::string,uint32))&Lua_Client::SendToInstance) .def("CountItem", (int(Lua_Client::*)(uint32))&Lua_Client::CountItem) .def("RemoveItem", (void(Lua_Client::*)(uint32))&Lua_Client::RemoveItem) - .def("RemoveItem", (void(Lua_Client::*)(uint32,uint32))&Lua_Client::RemoveItem); + .def("RemoveItem", (void(Lua_Client::*)(uint32,uint32))&Lua_Client::RemoveItem) + .def("SetGMStatus", (void(Lua_Client::*)(int32))& Lua_Client::SetGMStatus); } luabind::scope lua_register_inventory_where() { diff --git a/zone/lua_client.h b/zone/lua_client.h index 51da4b9df..1d44640e7 100644 --- a/zone/lua_client.h +++ b/zone/lua_client.h @@ -370,6 +370,7 @@ public: int CountItem(uint32 item_id); void RemoveItem(uint32 item_id); void RemoveItem(uint32 item_id, uint32 quantity); + void SetGMStatus(uint32 newStatus); void SetPrimaryWeaponOrnamentation(uint32 model_id); void SetSecondaryWeaponOrnamentation(uint32 model_id); diff --git a/zone/lua_spell.cpp b/zone/lua_spell.cpp index 18353efaa..c7730efff 100644 --- a/zone/lua_spell.cpp +++ b/zone/lua_spell.cpp @@ -474,6 +474,11 @@ int Lua_Spell::GetDamageShieldType() { return self->DamageShieldType; } +int Lua_Spell::GetRank() { + Lua_Safe_Call_Int(); + return self->rank; +} + luabind::scope lua_register_spell() { return luabind::class_("Spell") .def(luabind::constructor<>()) @@ -561,7 +566,8 @@ luabind::scope lua_register_spell() { .def("MaxDist", &Lua_Spell::GetMaxDist) .def("MaxDistMod", &Lua_Spell::GetMaxDistMod) .def("MinRange", &Lua_Spell::GetMinRange) - .def("DamageShieldType", &Lua_Spell::GetDamageShieldType); + .def("DamageShieldType", &Lua_Spell::GetDamageShieldType) + .def("Rank", &Lua_Spell::GetRank); } #endif diff --git a/zone/lua_spell.h b/zone/lua_spell.h index bfa5a19ed..e2def8a6a 100644 --- a/zone/lua_spell.h +++ b/zone/lua_spell.h @@ -107,6 +107,7 @@ public: float GetMaxDistMod(); float GetMinRange(); int GetDamageShieldType(); + int GetRank(); }; #endif diff --git a/zone/lua_stat_bonuses.cpp b/zone/lua_stat_bonuses.cpp index b6561199b..c007e09ca 100644 --- a/zone/lua_stat_bonuses.cpp +++ b/zone/lua_stat_bonuses.cpp @@ -572,7 +572,7 @@ int32 Lua_StatBonuses::GetProcChanceSPA() const { int32 Lua_StatBonuses::GetExtraAttackChance() const { Lua_Safe_Call_Int(); - return self->ExtraAttackChance; + return self->ExtraAttackChance[0]; } int32 Lua_StatBonuses::GetDoTShielding() const { diff --git a/zone/merc.cpp b/zone/merc.cpp index 3b8631edd..62b02c8a0 100644 --- a/zone/merc.cpp +++ b/zone/merc.cpp @@ -1624,7 +1624,7 @@ void Merc::AI_Process() { } } - int16 ExtraAttackChanceBonus = spellbonuses.ExtraAttackChance + itembonuses.ExtraAttackChance + aabonuses.ExtraAttackChance; + int16 ExtraAttackChanceBonus = spellbonuses.ExtraAttackChance[0] + itembonuses.ExtraAttackChance[0] + aabonuses.ExtraAttackChance[0]; if (GetTarget() && ExtraAttackChanceBonus) { if(zone->random.Roll(ExtraAttackChanceBonus)) diff --git a/zone/mob.cpp b/zone/mob.cpp index f3926a197..5156fbf6c 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -3708,9 +3708,6 @@ void Mob::TryTwincast(Mob *caster, Mob *target, uint32 spell_id) if(!IsValidSpell(spell_id)) return; - if (IsEffectInSpell(spell_id, SE_TwinCastBlocker)) - return; - if(IsClient()) { int32 focus = CastToClient()->GetFocusEffect(focusTwincast, spell_id); @@ -4862,6 +4859,9 @@ int16 Mob::GetMeleeDamageMod_SE(uint16 skill) dmg_mod += itembonuses.DamageModifier2[EQ::skills::HIGHEST_SKILL + 1] + spellbonuses.DamageModifier2[EQ::skills::HIGHEST_SKILL + 1] + aabonuses.DamageModifier2[EQ::skills::HIGHEST_SKILL + 1] + itembonuses.DamageModifier2[skill] + spellbonuses.DamageModifier2[skill] + aabonuses.DamageModifier2[skill]; + dmg_mod += itembonuses.DamageModifier3[EQ::skills::HIGHEST_SKILL + 1] + spellbonuses.DamageModifier3[EQ::skills::HIGHEST_SKILL + 1] + aabonuses.DamageModifier3[EQ::skills::HIGHEST_SKILL + 1] + + itembonuses.DamageModifier3[skill] + spellbonuses.DamageModifier3[skill] + aabonuses.DamageModifier3[skill]; + if(dmg_mod < -100) dmg_mod = -100; diff --git a/zone/mob.h b/zone/mob.h index 4e051d76f..ee3801833 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -837,6 +837,7 @@ public: inline int16 GetSpellPowerDistanceMod() const { return SpellPowerDistanceMod; }; inline void SetSpellPowerDistanceMod(int16 value) { SpellPowerDistanceMod = value; }; int32 GetSpellStat(uint32 spell_id, const char *identifier, uint8 slot = 0); + bool HarmonySpellLevelCheck(int32 spell_id, Mob* target = nullptr); void CastSpellOnLand(Mob* caster, uint32 spell_id); void FocusProcLimitProcess(); diff --git a/zone/perl_client.cpp b/zone/perl_client.cpp index 36c0ee361..1fc989bf7 100644 --- a/zone/perl_client.cpp +++ b/zone/perl_client.cpp @@ -3413,6 +3413,21 @@ XS(XS_Client_ReadBook) { XSRETURN_EMPTY; } +XS(XS_Client_SetGMStatus); /* prototype to pass -Wmissing-prototypes */ +XS(XS_Client_SetGMStatus) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: Client::SetGMStatus(THIS, int newStatus)"); // @categories Script Utility + { + Client *THIS; + int newStatus = (int)SvIV(ST(1)); + VALIDATE_THIS_IS_CLIENT; + THIS->SetGMStatus(newStatus); + THIS->UpdateAdmin(true); + } + XSRETURN_EMPTY; +} + XS(XS_Client_UpdateGroupAAs); /* prototype to pass -Wmissing-prototypes */ XS(XS_Client_UpdateGroupAAs) { dXSARGS; @@ -5706,6 +5721,7 @@ XS(boot_Client) { newXSproto(strcpy(buf, "UntrainDisc"), XS_Client_UntrainDisc, file, "$$;$"); newXSproto(strcpy(buf, "UntrainDiscAll"), XS_Client_UntrainDiscAll, file, "$;$"); newXSproto(strcpy(buf, "UpdateAdmin"), XS_Client_UpdateAdmin, file, "$;$"); + newXSproto(strcpy(buf, "SetGMStatus"), XS_Client_SetGMStatus, file, "$$"); newXSproto(strcpy(buf, "UpdateGroupAAs"), XS_Client_UpdateGroupAAs, file, "$$$"); newXSproto(strcpy(buf, "UpdateLDoNPoints"), XS_Client_UpdateLDoNPoints, file, "$$$"); newXSproto(strcpy(buf, "UpdateTaskActivity"), XS_Client_UpdateTaskActivity, file, "$$$$;$"); diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index be3408c81..6c49948d5 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -929,11 +929,31 @@ bool Mob::TryProjectileAttack(Mob *other, const EQ::ItemData *item, EQ::skills:: if (slot < 0) return false; - float speed_mod = speed; - + float distance_mod = 0.0f; float distance = other->CalculateDistance(GetX(), GetY(), GetZ()); - float hit = - 1200.0f + (10 * distance / speed_mod); // Calcuation: 60 = Animation Lag, 1.8 = Speed modifier for speed of (4) + + /* + New Distance Mod constant (7/25/21 update), modifier is needed to adjust slower speeds to have correct impact times at short distances. + We use archery 4.0 speed as a baseline for the forumla. At speed 1.5 at 50 pct distance mod is needed, where as speed 4.0 there is no modifer. + Therefore, we derive out our modifer as follows. distance_mod = (speed - 4) * ((50 - 0)/(1.5-4)). The ratio there is -20.0f. distance_mod = (speed - 4) * -20.0f + For distances >125 we use different modifier, this was all meticulously tested by eye to get the best possible outcome for projectile impact times. Not perfect though. + */ + + if (distance <= 125.0f) { + if (speed != 4.0f) { //Standard functions will always be 4.0f for archery. + distance_mod = (speed - 4.0f) * -20.0f; + distance += distance * distance_mod / 100.0f; + } + } + else if (distance > 125.0f && distance <= 200.0f) + distance = 3.14f * (distance / 2.0f); //Get distance of arc to better reflect projectile path length + + else if (distance > 200.0f) { + distance = distance * 1.30f; //Add 30% to base distance if over 200 range to tighten up hit timing. + distance = 3.14f * (distance / 2.0f); //Get distance of arc to better reflect projectile path length + } + + float hit = 1200.0f + (10 * distance / speed); ProjectileAtk[slot].increment = 1; ProjectileAtk[slot].hit_increment = static_cast(hit); // This projected hit time if target does NOT MOVE @@ -951,14 +971,12 @@ bool Mob::TryProjectileAttack(Mob *other, const EQ::ItemData *item, EQ::skills:: ProjectileAtk[slot].ammo_slot = 0; ProjectileAtk[slot].skill = skillInUse; - ProjectileAtk[slot].speed_mod = speed_mod; + ProjectileAtk[slot].speed_mod = speed; 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; } @@ -978,23 +996,40 @@ void Mob::ProjectileAttack() 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 (target && target->IsMoving()) { + /* + Only recalculate hit increment if target is 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 = 1200.0f + (10 * distance / ProjectileAtk[i].speed_mod); // Calcuation: 60 = - // Animation Lag, 1.8 = - // Speed modifier for speed - // of (4) + + //Recalculate from the original location the projectile was fired in relation to the current targets location. + float distance = target->CalculateDistance(ProjectileAtk[i].origin_x, ProjectileAtk[i].origin_y, ProjectileAtk[i].origin_z); + float distance_mod = 0.0f; + + if (distance <= 125.0f) { + distance_mod = (ProjectileAtk[i].speed_mod - 4.0f) * -20.0f; + distance += distance * distance_mod / 100.0f; + } + else if (distance > 125.0f && distance <= 200.0f) + distance = 3.14f * (distance / 2.0f); //Get distance of arc to better reflect projectile path length + + else if (distance > 200.0f) { + distance = distance * 1.30f; //Add 30% to base distance if over 200 range to tighten up hit timing. + distance = 3.14f * (distance / 2.0f); //Get distance of arc to better reflect projectile path length + } + + float hit = 1200.0f + (10 * distance / ProjectileAtk[i].speed_mod); + ProjectileAtk[i].hit_increment = static_cast(hit); } } - // We hit I guess? + // Check if we hit. if (ProjectileAtk[i].hit_increment <= ProjectileAtk[i].increment) { if (target) { if (IsNPC()) { @@ -1496,7 +1531,7 @@ void Mob::ProjectileAnimation(Mob* to, int item_id, bool IsArrow, float speed, f speed = 4.0; } if(!angle) { - angle = CalculateHeadingToTarget(to->GetX(), to->GetY()) * 2; + angle = CalculateHeadingToTarget(to->GetX(), to->GetY()); } if(!tilt) { tilt = 125; diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index ab534acc8..683d7f7ba 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -3226,6 +3226,10 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial, int level_ove case SE_Ff_ReuseTimeMax: case SE_Ff_Value_Min: case SE_Ff_Value_Max: + case SE_AddExtraAttackPct_1h_Primary: + case SE_AddExtraAttackPct_1h_Secondary: + case SE_Skill_Base_Damage_Mod: + { break; } @@ -4890,7 +4894,7 @@ int16 Client::CalcAAFocus(focusType type, const AA::Rank &rank, uint16 spell_id) break; case SE_FcTwincast: - if (type == focusTwincast) + if (type == focusTwincast && !IsEffectInSpell(spell_id, SE_TwinCastBlocker)) value = base1; break; @@ -5531,7 +5535,7 @@ int16 Mob::CalcFocusEffect(focusType type, uint16 focus_id, uint16 spell_id, boo break; case SE_FcTwincast: - if (type == focusTwincast) + if (type == focusTwincast && !IsEffectInSpell(spell_id, SE_TwinCastBlocker)) value = focus_spell.base[i]; break; @@ -7144,7 +7148,7 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama return false; } -bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed){ +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. @@ -7169,7 +7173,7 @@ bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed){ //Make sure there is an avialable bolt to be cast. for (int i = 0; i < MAX_SPELL_PROJECTILE; i++) { - if (ProjectileAtk[i].target_id == 0){ + if (ProjectileAtk[i].target_id == 0) { slot = i; break; } @@ -7178,11 +7182,35 @@ bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed){ if (slot < 0) return false; + float arc = 0.0f; + float distance_mod = 0.0f; + if (CheckLosFN(spell_target)) { - float speed_mod = speed; //Constant for adjusting speeds to match calculated impact time. float distance = spell_target->CalculateDistance(GetX(), GetY(), GetZ()); - float hit = 1200.0f + (10 * distance / speed_mod); + + /* + New Distance Mod constant (7/25/21 update), modifier is needed to adjust slower speeds to have correct impact times at short distances. + We use archery 4.0 speed as a baseline for the forumla. At speed 1.5 at 50 pct distance mod is needed, where as speed 4.0 there is no modifer. + Therefore, we derive out our modifer as follows. distance_mod = (speed - 4) * ((50 - 0)/(1.5-4)). The ratio there is -20.0f. distance_mod = (speed - 4) * -20.0f + For distances >125 we use different modifier, this was all meticulously tested by eye to get the best possible outcome for projectile impact times. Not perfect though. + */ + + if (distance <= 125.0f) { + distance_mod = (speed - 4.0f) * -20.0f; + distance += distance * distance_mod / 100.0f; + } + else if (distance > 125.0f && distance <= 200.0f) + distance = 3.14f * (distance / 2.0f); //Get distance of arc to better reflect projectile path length + + else if (distance > 200.0f) { + arc = 50.0f - ((distance - 200.0f) * 0.266f); //Arc angle gets drastically larger if >200 distance, lets lower it down gradually for better effect. + arc = std::max(arc, 20.0f); //No lower than 20 arc + distance = distance * 1.30f; //Add 30% to base distance if over 200 range to tighten up hit timing. + distance = 3.14f * (distance / 2.0f); //Get distance of arc to better reflect projectile path length + } + + float hit = 1200.0f + (10 * distance / speed); ProjectileAtk[slot].increment = 1; ProjectileAtk[slot].hit_increment = static_cast(hit); //This projected hit time if target does NOT MOVE @@ -7192,34 +7220,33 @@ bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id, float speed){ ProjectileAtk[slot].origin_y = GetY(); ProjectileAtk[slot].origin_z = GetZ(); ProjectileAtk[slot].skill = EQ::skills::SkillConjuration; - ProjectileAtk[slot].speed_mod = speed_mod; + ProjectileAtk[slot].speed_mod = speed; 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, speed,0,0,0, spells[spell_id].player_1); + ProjectileAnimation(spell_target, 0, false, speed, 0.0f, 0.0f, arc, spells[spell_id].player_1); } - //This allows limited support for server using older spell files that do not contain data for bolt graphics. else { //Only use fire graphic for fire spells. if (spells[spell_id].resisttype == RESIST_FIRE) { - if (IsClient()){ + if (IsClient()) { if (CastToClient()->ClientVersionBit() <= 4) //Titanium needs alternate graphic. - ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_Titanium)), false, speed); + ProjectileAnimation(spell_target, (RuleI(Spells, FRProjectileItem_Titanium)), false, speed, 0.0f, 0.0f, arc); else - ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_SOF)), false, speed); - } + ProjectileAnimation(spell_target, (RuleI(Spells, FRProjectileItem_SOF)), false, speed, 0.0f, 0.0f, arc); + } else - ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_NPC)), false, speed); + ProjectileAnimation(spell_target, (RuleI(Spells, FRProjectileItem_NPC)), false, speed, 0.0f, 0.0f, arc); } //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, speed); + ProjectileAnimation(spell_target, 0, 1, speed, 0.0f, 0.0f, arc); } if (spells[spell_id].CastingAnim == 64) @@ -7564,3 +7591,21 @@ void Client::BreakFeignDeathWhenCastOn(bool IsResisted) MessageString(Chat::SpellFailure,FD_CAST_ON); } } + +bool Mob::HarmonySpellLevelCheck(int32 spell_id, Mob *target) +{ + //'this' = caster of spell + if (!target) { + return false; + } + + for (int i = 0; i < EFFECT_COUNT; i++) { + // not important to check limit on SE_Lull as it doesnt have one and if the other components won't land, then SE_Lull wont either + if (spells[spell_id].effectid[i] == SE_ChangeFrenzyRad || spells[spell_id].effectid[i] == SE_Harmony) { + if ((spells[spell_id].max[i] != 0 && target->GetLevel() > spells[spell_id].max[i]) || target->GetSpecialAbility(IMMUNE_PACIFY)) { + return false; + } + } + } + return true; +} diff --git a/zone/spells.cpp b/zone/spells.cpp index 207d263ef..62634243d 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -224,17 +224,9 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, if (spellbonuses.NegateIfCombat) BuffFadeByEffect(SE_NegateIfCombat); - if(IsClient() && GetTarget() && IsHarmonySpell(spell_id)) - { - for(int i = 0; i < EFFECT_COUNT; i++) { - // not important to check limit on SE_Lull as it doesnt have one and if the other components won't land, then SE_Lull wont either - if (spells[spell_id].effectid[i] == SE_ChangeFrenzyRad || spells[spell_id].effectid[i] == SE_Harmony) { - if((spells[spell_id].max[i] != 0 && GetTarget()->GetLevel() > spells[spell_id].max[i]) || GetTarget()->GetSpecialAbility(IMMUNE_PACIFY)) { - InterruptSpell(CANNOT_AFFECT_NPC, 0x121, spell_id); - return(false); - } - } - } + if (IsClient() && IsHarmonySpell(spell_id) && !HarmonySpellLevelCheck(spell_id, entity_list.GetMobID(target_id))) { + InterruptSpell(SPELL_NO_EFFECT, 0x121, spell_id); + return false; } if (HasActiveSong() && IsBardSong(spell_id)) { @@ -3789,6 +3781,12 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, bool reflect, bool use_r return false; } } + //Need this to account for special AOE cases. + if (IsClient() && IsHarmonySpell(spell_id) && !HarmonySpellLevelCheck(spell_id, spelltar)) { + MessageString(Chat::SpellFailure, SPELL_NO_EFFECT); + return false; + } + // Block next spell effect should be used up first(since its blocking the next spell) if(CanBlockSpell()) { int buff_count = GetMaxTotalSlots(); diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 9c493a915..7e195ab14 100755 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -4977,3 +4977,21 @@ void ZoneDatabase::SetEXPModifier(uint32 character_id, uint32 zone_id, double ex ); database.QueryDatabase(query); } + +void ZoneDatabase::UpdateGMStatus(uint32 accID, int newStatus) +{ + if (accID) { + std::string query = fmt::format( + SQL( + UPDATE + `account` + SET `status` = {} + WHERE + `id` = {} + ), + newStatus, + accID + ); + database.QueryDatabase(query); + } +} diff --git a/zone/zonedb.h b/zone/zonedb.h index 7f523e4e1..61eb97ae1 100644 --- a/zone/zonedb.h +++ b/zone/zonedb.h @@ -289,6 +289,9 @@ public: void DeleteBuyLines(uint32 CharID); void UpdateBuyLine(uint32 CharID, uint32 BuySlot, uint32 Quantity); + + void UpdateGMStatus(uint32 accID, int newStatus); + /** ************************************************ * Character