diff --git a/changelog.txt b/changelog.txt index 0403a5c7f..dafc65268 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,62 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 04/06/2014 == +Uleat: Changed Mob::CanThisClassDualWield() behavior. This should let non-monk/beastlord dual-wielding classes attack with either fist as long as the other hand is occupied. +Notes: + See this thread for more information and to provide feedback: http://www.eqemulator.org/forums/showthread.php?p=229328#post229328 + + +== 04/05/2014 == +Akkadius: Fix for the Fix for the Fix: Rule Combat:OneProcPerWeapon was created so that you can revert to the original proc functionality + for custom servers that have balanced their content around having more than 1 aug proc on weapons. By having this rule set to 'false' you revert this functionality. + This rule is set to 'true' by default as the original functionality from Live was intended to be +Akkadius: (Performance Adjustment) Removed AsyncLoadVariables from InterserverTimer.Check() in both zone and world. By watching the MySQL general.log file on mass zone idle activity, you can + see that the query 'SELECT varname, value, unix_timestamp() FROM variables where unix_timestamp(ts) >= timestamp' is called every 10 seconds. This function is loading + variables that are initially loaded on World and Zone bootup. When running a large amount of zone servers, the amount of MySQL chatter that is produced is enormous and + unnecessary. For example, if I ran 400 zone servers, I would see 3,456,000 unnecessary queries from all idle or active zone processes in a 24 hour interval. +Secrets: Added a rule to enable multiple procs from the same weapon's other slots if a proc is deemed to trigger, Defaults to true. + If Combat:OneProcPerWeapon is not enabled, we reset the try for that weapon regardless of if we procced or not. + This is for some servers that may want to have as many procs triggering from weapons as possible in a single round. + +Optional SQL: utils/sql/git/optional/2014_04_05_ProcRules.sql + +== 04/04/2014 == +Kayen: Implemented 'Physical Resists' (Resist Type 9) to be consistent with live based on extensive parsing. + SQL will add new field to npc_types 'PhR' and fill in database with values consistent with observations. + +Required SQL: utils/sql/git/optional/2014_04_04_PhysicalResists.sql + +== 04/03/2014 == +Kayen: Implemented live like spell projectiles (ie. Mage Bolts). + +Optional SQL: utils/sql/git/optional/2014_04_03_SpellProjectileRules.sql +Note: The rules in this SQL are for setting the item id for the graphic used by the projectile on different clients. + +== 04/01/2014 == +demonstar55: Implemented ability for a merchant to open and close shop. + Lua quest functions: e.self:MerchantOpenShop() and e.self:MerchantCloseShop() + GM Commands: #merchant_open_shop (short: #open_shop) and #merchant_close_shop (short: #close_shop) + default to status 100, just in case you need to force the merchants status +Trevius: Fixed potential endless quest loop with EVENT_COMBAT and WipeHateList(). + +== 03/31/2014 == +Uleat: Fix for unconscious skillups. +Uleat: Fix for crash issue with nullptr reference in recent Client::SummonItem() work. +Uleat: Added rule for GM Status check code in Client::SummonItem(). +Note: Rule default is set to 250..but, implementation is on hold until load item code handles the database 'minstatus' field. +Uleat: Added RuleB(Bots, BotLevelsWithOwner). Bots will auto-update as their owner levels/de-levels. Appearance packets are sent to show the 'leveling effect' as well as updating client entities. +Trevius: Prevented an endless loop crash related to EVENT_TASK_STAGE_COMPLETE. + +Optional Bot SQL: utils/sql/git/bot/optional/2014_03_31_BotLevelsWithOwnerRule.sql +Note: This sql is required to activate the optional behavior. + +== 03/27/2014 == +Kayen: SE_Gate will now use have a fail chance as defined by its base value in the spell data. +Kayen: SE_Succor will now have a baseline fail chance of (2%). Rule added to adjust this as needed. +Kayen: SE_FeignDeath will now have a fail chance as defined by its base value in the spell data. + +Optional SQL: utils/sql/git/optional/2014_03_27_SuccorFailRule.sql + == 03/22/2014 == Uleat: Moved the existing 'load_bots' and 'drop_bots' sqls into the emu git repository for the time being. Look to the /utils/sql/git/bots/ folder to find them. The 'load_bots' sql has been updated to include the below fix, as well as diff --git a/common/ruletypes.h b/common/ruletypes.h index fd50d1514..dec5a6a8f 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -138,6 +138,7 @@ RULE_BOOL( Pets, UnTargetableSwarmPet, false ) RULE_CATEGORY_END() RULE_CATEGORY( GM ) +RULE_INT ( GM, MinStatusToSummonItem, 250) RULE_INT ( GM, MinStatusToZoneAnywhere, 250 ) RULE_CATEGORY_END() @@ -302,6 +303,10 @@ RULE_BOOL ( Spells, UseCHAScribeHack, false) //ScribeSpells and TrainDiscs quest RULE_BOOL ( Spells, BuffLevelRestrictions, true) //Buffs will not land on low level toons like live RULE_INT ( Spells, RootBreakCheckChance, 70) //Determines chance for a root break check to occur each buff tick. RULE_INT ( Spells, FearBreakCheckChance, 70) //Determines chance for a fear break check to occur each buff tick. +RULE_INT ( Spells, SuccorFailChance, 2) //Determines chance for a succor spell not to teleport an invidual player +RULE_INT ( Spells, FRProjectileItem_Titanium, 1113) // Item id for Titanium clients for Fire 'spell projectile'. +RULE_INT ( Spells, FRProjectileItem_SOF, 80684) // Item id for SOF clients for Fire 'spell projectile'. +RULE_INT ( Spells, FRProjectileItem_NPC, 80684) // Item id for NPC Fire 'spell projectile'. RULE_CATEGORY_END() RULE_CATEGORY( Combat ) @@ -391,6 +396,7 @@ RULE_BOOL ( Combat, UseArcheryBonusRoll, false) //Make the 51+ archery bonus req 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_CATEGORY_END() RULE_CATEGORY( NPC ) @@ -446,6 +452,7 @@ RULE_BOOL ( Bots, BotSpellQuest, false ) // Anita Thrall's (Anita_Thrall.pl) Bot RULE_INT ( Bots, BotAAExpansion, 8 ) // Bots get AAs through this expansion RULE_BOOL ( Bots, BotGroupXP, false ) // Determines whether client gets xp for bots outside their group. RULE_BOOL ( Bots, BotBardUseOutOfCombatSongs, true) // Determines whether bard bots use additional out of combat songs. +RULE_BOOL ( Bots, BotLevelsWithOwner, false) // Auto-updates spawned bots as owner levels/de-levels (false is original behavior) RULE_CATEGORY_END() #endif diff --git a/common/spdat.h b/common/spdat.h index 347da070b..c7452426d 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -159,8 +159,8 @@ typedef enum { #define SE_WIS 9 // implemented #define SE_CHA 10 // implemented - used as a spacer #define SE_AttackSpeed 11 // implemented -#define SE_Invisibility 12 // implemented -#define SE_SeeInvis 13 // implemented +#define SE_Invisibility 12 // implemented - TO DO: Implemented Invisiblity Levels +#define SE_SeeInvis 13 // implemented - TO DO: Implemented See Invisiblity Levels #define SE_WaterBreathing 14 // implemented #define SE_CurrentMana 15 // implemented //#define SE_NPCFrenzy 16 // not used @@ -172,7 +172,7 @@ typedef enum { #define SE_Charm 22 // implemented #define SE_Fear 23 // implemented #define SE_Stamina 24 // implemented - Invigor and such -#define SE_BindAffinity 25 // implemented +#define SE_BindAffinity 25 // implemented - TO DO: Implement 2nd and 3rd Recall (value 2,3 ect). Sets additional bind points. #define SE_Gate 26 // implemented - Gate to bind point #define SE_CancelMagic 27 // implemented #define SE_InvisVsUndead 28 // implemented @@ -211,7 +211,7 @@ typedef enum { #define SE_Identify 61 // implemented //#define SE_ItemID 62 // not used #define SE_WipeHateList 63 // implemented -#define SE_SpinTarget 64 // implemented +#define SE_SpinTarget 64 // implemented - TO DO: Not sure stun portion is working correctly #define SE_InfraVision 65 // implemented #define SE_UltraVision 66 // implemented #define SE_EyeOfZomm 67 // implemented @@ -241,7 +241,7 @@ typedef enum { #define SE_SummonCorpse 91 // implemented #define SE_InstantHate 92 // implemented - add hate #define SE_StopRain 93 // implemented - Wake of Karana -#define SE_NegateIfCombat 94 // *not implemented? - Works client side but there is comment todo in spell effects...Component of Spirit of Scale +#define SE_NegateIfCombat 94 // implemented #define SE_Sacrifice 95 // implemented #define SE_Silence 96 // implemented #define SE_ManaPool 97 // implemented @@ -258,7 +258,7 @@ typedef enum { #define SE_Familiar 108 // implemented #define SE_SummonItemIntoBag 109 // implemented - summons stuff into container //#define SE_IncreaseArchery 110 // not used -#define SE_ResistAll 111 // implemented +#define SE_ResistAll 111 // implemented - Note: Physical Resists are not modified by this effect. #define SE_CastingLevel 112 // implemented #define SE_SummonHorse 113 // implemented #define SE_ChangeAggro 114 // implemented - Hate modifing buffs(ie horrifying visage) @@ -270,7 +270,7 @@ typedef enum { #define SE_HealRate 120 // implemented - reduces healing by a % #define SE_ReverseDS 121 // implemented //#define SE_ReduceSkill 122 // not used -#define SE_Screech 123 // implemented? Spell Blocker(can only have one buff with this effect at one time) +#define SE_Screech 123 // implemented Spell Blocker(If have buff with value +1 will block any effect with -1) #define SE_ImprovedDamage 124 // implemented #define SE_ImprovedHeal 125 // implemented #define SE_SpellResistReduction 126 // implemented @@ -456,7 +456,7 @@ typedef enum { //#define SE_ArmyOfTheDead 306 // *not implemented NecroAA - This ability calls up to five shades of nearby corpses back to life to serve the necromancer. The soulless abominations will mindlessly fight the target until called back to the afterlife some time later. The first rank summons up to three shades that serve for 60 seconds, and each additional rank adds one more possible shade and increases their duration by 15 seconds //#define SE_Appraisal 307 // *not implemented Rogue AA - This ability allows you to estimate the selling price of an item you are holding on your cursor. #define SE_SuspendMinion 308 // not implemented as bonus -#define SE_YetAnotherGate 309 // implemented +#define SE_GateCastersBindpoint 309 // implemented - Gate to casters bind point #define SE_ReduceReuseTimer 310 // implemented #define SE_LimitCombatSkills 311 // implemented - Excludes focus from procs (except if proc is a memorizable spell) //#define SE_Sanctuary 312 // *not implemented diff --git a/utils/sql/git/bots/optional/2014_03_31_BotLevelsWithOwnerRule.sql b/utils/sql/git/bots/optional/2014_03_31_BotLevelsWithOwnerRule.sql new file mode 100644 index 000000000..4700d9abf --- /dev/null +++ b/utils/sql/git/bots/optional/2014_03_31_BotLevelsWithOwnerRule.sql @@ -0,0 +1,4 @@ +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Bots:BotLevelsWithOwner', 'true', 'Auto-updates bots with ding.'); +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (2, 'Bots:BotLevelsWithOwner', 'true', 'Auto-updates bots with ding.'); +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (4, 'Bots:BotLevelsWithOwner', 'true', 'Auto-updates bots with ding.'); +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (10, 'Bots:BotLevelsWithOwner', 'true', 'Auto-updates bots with ding.'); diff --git a/utils/sql/git/optional/2014_03_27_SuccorFailRule.sql b/utils/sql/git/optional/2014_03_27_SuccorFailRule.sql new file mode 100644 index 000000000..9d60f8bcb --- /dev/null +++ b/utils/sql/git/optional/2014_03_27_SuccorFailRule.sql @@ -0,0 +1 @@ +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Spells:SuccorFailChance', '2', 'Determines chance for a succor spell not to teleport an invidual player.'); diff --git a/utils/sql/git/optional/2014_04_03_SpellProjectileRules.sql b/utils/sql/git/optional/2014_04_03_SpellProjectileRules.sql new file mode 100644 index 000000000..3601a5fef --- /dev/null +++ b/utils/sql/git/optional/2014_04_03_SpellProjectileRules.sql @@ -0,0 +1,3 @@ +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Spells:FRProjectileItem_Titanium', '1113', 'Item id for Titanium clients for Fire spell projectile.'); +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Spells:FRProjectileItem_SOF', '80684', 'Item id for Titanium clients for Fire spell projectile.'); +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Spells:FRProjectileItem_NPC', '80684', 'Item id for Titanium clients for Fire spell projectile.'); diff --git a/utils/sql/git/optional/2014_04_05_ProcRules.sql b/utils/sql/git/optional/2014_04_05_ProcRules.sql new file mode 100644 index 000000000..4566f45fd --- /dev/null +++ b/utils/sql/git/optional/2014_04_05_ProcRules.sql @@ -0,0 +1 @@ +INSERT INTO `rule_values` (`ruleset_id`, `rule_name`, `rule_value`, `notes`) VALUES (1, 'Combat:OneProcPerWeapon', 'true', 'If OneProcPerWeapon is not enabled, we reset the proc try for that weapon regardless of if we procced or not.'); diff --git a/utils/sql/git/required/2014_04_04_PhysicalResist.sql b/utils/sql/git/required/2014_04_04_PhysicalResist.sql new file mode 100644 index 000000000..f748f0b9f --- /dev/null +++ b/utils/sql/git/required/2014_04_04_PhysicalResist.sql @@ -0,0 +1,7 @@ +ALTER TABLE `npc_types` ADD `PhR` smallint( 5 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `Corrup`; + +-- Approximate baseline live npc values based on extensive parsing. +UPDATE npc_types SET PhR = 10 WHERE PhR = 0 AND level <= 50; +UPDATE npc_types SET PhR = (10 + (level - 50)) WHERE PhR = 0 AND (level > 50 AND level <= 60); +UPDATE npc_types SET PhR = (20 + ((level - 60)*4)) WHERE PhR = 0 AND level > 60; + diff --git a/world/net.cpp b/world/net.cpp index 057367b00..3c5df74a2 100644 --- a/world/net.cpp +++ b/world/net.cpp @@ -461,7 +461,7 @@ int main(int argc, char** argv) { if (InterserverTimer.Check()) { InterserverTimer.Start(); database.ping(); - AsyncLoadVariables(dbasync, &database); + // AsyncLoadVariables(dbasync, &database); ReconnectCounter++; if (ReconnectCounter >= 12) { // only create thread to reconnect every 10 minutes. previously we were creating a new thread every 10 seconds ReconnectCounter = 0; diff --git a/zone/StringIDs.h b/zone/StringIDs.h index afa598e1c..05bbffdec 100644 --- a/zone/StringIDs.h +++ b/zone/StringIDs.h @@ -80,6 +80,7 @@ #define CANNOT_AFFECT_NPC 251 //That spell can not affect this target NPC. #define SUSPEND_MINION_HAS_AGGRO 256 //Your pet is the focus of something's attention. #define NO_PET 255 //You do not have a pet. +#define GATE_FAIL 260 //Your gate is too unstable, and collapses. #define CORPSE_CANT_SENSE 262 //You cannot sense any corpses for this PC in this zone. #define SPELL_NO_HOLD 263 //Your spell did not take hold. #define CANNOT_CHARM 267 //This NPC cannot be charmed. @@ -208,6 +209,7 @@ #define AA_POINTS 1215 //points #define SPELL_FIZZLE_OTHER 1218 //%1's spell fizzles! #define MISSED_NOTE_OTHER 1219 //A missed note brings %1's song to a close! +#define SPELL_LEVEL_REQ 1226 //This spell only works on people who are level %1 and under. #define CORPSE_DECAY_NOW 1227 //This corpse is waiting to expire. #define SURNAME_REJECTED 1374 //Your new surname was rejected. Please try a different name. #define DUEL_DECLINE 1383 //%1 has declined your challenge to duel to the death. @@ -253,12 +255,14 @@ #define GAIN_RAIDEXP 5085 //You gained raid experience! #define DUNGEON_SEALED 5141 //The gateway to the dungeon is sealed off to you. Perhaps you would be able to enter if you needed to adventure there. #define ADVENTURE_COMPLETE 5147 //You received %1 points for successfully completing the adventure. +#define SUCCOR_FAIL 5169 //The portal collapes before you can escape! #define PET_ATTACKING 5501 //%1 tells you, 'Attacking %2 Master.' #define FATAL_BOW_SHOT 5745 //%1 performs a FATAL BOW SHOT!! #define MELEE_SILENCE 5806 //You *CANNOT* use this melee ability, you are suffering from amnesia! #define DISCIPLINE_REUSE_MSG 5807 //You can use the ability %1 again in %2 hour(s) %3 minute(s) %4 seconds. #define DISCIPLINE_REUSE_MSG2 5808 //You can use the ability %1 again in %2 minute(s) %3 seconds. #define FAILED_TAUNT 5811 //You have failed to taunt your target. +#define PHYSICAL_RESIST_FAIL 5817 //Your target avoided your %1 ability. #define AA_NO_TARGET 5825 //You must first select a target for this ability! #define FORAGE_MASTERY 6012 //Your forage mastery has enabled you to find something else! #define GUILD_BANK_CANNOT_DEPOSIT 6097 // Cannot deposit this item. Containers must be empty, and only one of each LORE and no NO TRADE or TEMPORARY items may be deposited. @@ -330,6 +334,7 @@ #define ALREADY_CASTING 12442 //You are already casting a spell! #define SENSE_CORPSE_NOT_NAME 12446 //You don't sense any corpses of that name. #define SENSE_CORPSE_NONE 12447 //You don't sense any corpses. +#define SCREECH_BUFF_BLOCK 12448 //Your immunity buff protected you from the spell %1! #define NOT_HOLDING_ITEM 12452 //You are not holding an item! #define SENSE_UNDEAD 12471 //You sense undead in this direction. #define SENSE_ANIMAL 12472 //You sense an animal in this direction. diff --git a/zone/attack.cpp b/zone/attack.cpp index be099adc6..324f5942d 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -1371,6 +1371,9 @@ bool Client::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, b invisible_animals = false; } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(hidden || improved_hidden){ hidden = false; improved_hidden = false; @@ -1983,6 +1986,9 @@ bool NPC::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool invisible_animals = false; } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(hidden || improved_hidden) { EQApplicationPacket* outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); @@ -4079,6 +4085,10 @@ void Mob::TryWeaponProc(const ItemInst *inst, const Item_Struct *weapon, Mob *on } } } + //If OneProcPerWeapon is not enabled, we reset the try for that weapon regardless of if we procced or not. + //This is for some servers that may want to have as many procs triggering from weapons as possible in a single round. + if(!RuleB(Combat, OneProcPerWeapon)) + proced = false; if (!proced && inst) { for (int r = 0; r < MAX_AUGMENT_SLOTS; r++) { @@ -4103,7 +4113,8 @@ void Mob::TryWeaponProc(const ItemInst *inst, const Item_Struct *weapon, Mob *on } } else { ExecWeaponProc(aug_i, aug->Proc.Effect, on); - break; + if (RuleB(Combat, OneProcPerWeapon)) + break; } } } diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 99941b893..56f8b9f5f 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -1358,16 +1358,27 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses* ne case SE_AttackSpeed2: { if ((effect_value - 100) > 0) { // Haste V2 - Stacks with V1 but does not Overcap + if (newbon->hastetype2 < 0) break; //Slowed - Don't apply haste2 if ((effect_value - 100) > newbon->hastetype2) { newbon->hastetype2 = effect_value - 100; } } + else if ((effect_value - 100) < 0) { // Slow + int real_slow_value = (100 - effect_value) * -1; + if (real_slow_value < newbon->hastetype2) + newbon->hastetype2 = real_slow_value; + } break; } case SE_AttackSpeed3: { - if (effect_value > 0) { // Haste V3 - Stacks and Overcaps + if (effect_value < 0){ //Slow + if (effect_value < newbon->hastetype3) + newbon->hastetype3 = effect_value; + } + + else if (effect_value > 0) { // Haste V3 - Stacks and Overcaps if (effect_value > newbon->hastetype3) { newbon->hastetype3 = effect_value; } @@ -1377,18 +1388,24 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses* ne case SE_AttackSpeed4: { - if (effect_value > 0) { + if (effect_value < 0) //A few spells use negative values(Descriptions all indicate it should be a slow) + effect_value = effect_value * -1; + + if (effect_value > 0 && effect_value > newbon->inhibitmelee) { + if (slow_mitigation){ int new_effect_value = SlowMitigation(false,caster,effect_value); if (new_effect_value > newbon->inhibitmelee) { - newbon->inhibitmelee = new_effect_value; - SlowMitigation(true,caster); + newbon->inhibitmelee = new_effect_value; + SlowMitigation(true,caster); } } + else if (effect_value > newbon->inhibitmelee) { - newbon->inhibitmelee = effect_value; + newbon->inhibitmelee = effect_value; } } + break; } @@ -2568,6 +2585,15 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses* ne newbon->AbsorbMagicAtt[1] = buffslot; } break; + + case SE_NegateIfCombat: + newbon->NegateIfCombat = true; + break; + + case SE_Screech: + newbon->Screech = effect_value; + break; + } } } diff --git a/zone/bot.cpp b/zone/bot.cpp index 764352704..a59cbe98f 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -3237,6 +3237,9 @@ void Bot::BotRangedAttack(Mob* other) { invisible_animals = false; } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(hidden || improved_hidden){ hidden = false; improved_hidden = false; @@ -5281,6 +5284,28 @@ uint32 Bot::GetBotOwnerCharacterID(uint32 botID, std::string* errorMessage) { return Result; } +void Bot::LevelBotWithClient(Client* client, uint8 level, bool sendlvlapp) { + // This essentially performs a '#bot update,' with appearance packets, based on the current methods. + // This should not be called outside of Client::SetEXP() due to it's lack of rule checks. + if(client) { + std::list blist = entity_list.GetBotsByBotOwnerCharacterID(client->CharacterID()); + + for(std::list::iterator biter = blist.begin(); biter != blist.end(); ++biter) { + Bot* bot = *biter; + if(bot && (bot->GetLevel() != client->GetLevel())) { + bot->SetPetChooser(false); // not sure what this does, but was in bot 'update' code + bot->CalcBotStats(false); + if(sendlvlapp) + bot->SendLevelAppearance(); + // modified from Client::SetLevel() + bot->SendAppearancePacket(AT_WhoLevel, level, true, true); // who level change + } + } + + blist.clear(); + } +} + std::string Bot::ClassIdToString(uint16 classId) { std::string Result; @@ -6640,6 +6665,9 @@ bool Bot::Attack(Mob* other, int Hand, bool FromRiposte, bool IsStrikethrough, b safe_delete(outapp); } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(GetTarget()) TriggerDefensiveProcs(weapon, other, Hand, damage); diff --git a/zone/bot.h b/zone/bot.h index b0ff52845..485d37377 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -352,6 +352,7 @@ public: static uint32 CreatedBotCount(uint32 botOwnerCharacterID, std::string* errorMessage); static uint32 AllowedBotSpawns(uint32 botOwnerCharacterID, std::string* errorMessage); static uint32 GetBotOwnerCharacterID(uint32 botID, std::string* errorMessage); + static void LevelBotWithClient(Client* client, uint8 level, bool sendlvlapp); //static bool SetBotOwnerCharacterID(uint32 botID, uint32 botOwnerCharacterID, std::string* errorMessage); static std::string ClassIdToString(uint16 classId); static std::string RaceIdToString(uint16 raceId); diff --git a/zone/client.cpp b/zone/client.cpp index ebf5d806c..827a3f3f8 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -2306,6 +2306,8 @@ uint64 Client::GetAllMoney() { } bool Client::CheckIncreaseSkill(SkillUseTypes skillid, Mob *against_who, int chancemodi) { + if (IsDead() || IsUnconscious()) + return false; if (IsAIControlled()) // no skillups while chamred =p return false; if (skillid > HIGHEST_SKILL) @@ -2349,6 +2351,10 @@ bool Client::CheckIncreaseSkill(SkillUseTypes skillid, Mob *against_who, int cha } void Client::CheckLanguageSkillIncrease(uint8 langid, uint8 TeacherSkill) { + if (IsDead() || IsUnconscious()) + return; + if (IsAIControlled()) + return; if (langid >= MAX_PP_LANGUAGE) return; // do nothing if langid is an invalid language diff --git a/zone/client.h b/zone/client.h index 97ee1b32f..76b2c6774 100644 --- a/zone/client.h +++ b/zone/client.h @@ -305,6 +305,7 @@ public: void SetHideMe(bool hm); inline uint16 GetPort() const { return port; } bool IsDead() const { return(dead); } + bool IsUnconscious() const { return ((cur_hp <= 0) ? true : false); } inline bool IsLFP() { return LFP; } void UpdateLFP(); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 441681fd0..cde2b143f 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -5421,6 +5421,12 @@ void Client::Handle_OP_ShopRequest(const EQApplicationPacket *app) action = 0; } + // 1199 I don't have time for that now. etc + if (!tmp->CastToNPC()->IsMerchantOpen()) { + tmp->Say_StringID(MakeRandomInt(1199, 1202)); + action = 0; + } + EQApplicationPacket* outapp = new EQApplicationPacket(OP_ShopRequest, sizeof(Merchant_Click_Struct)); Merchant_Click_Struct* mco=(Merchant_Click_Struct*)outapp->pBuffer; diff --git a/zone/client_process.cpp b/zone/client_process.cpp index f1e25226a..c28affd28 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -570,6 +570,9 @@ bool Client::Process() { viral_timer_counter = 0; } + if(projectile_timer.Check()) + SpellProjectileEffect(); + if(spellbonuses.GravityEffect == 1) { if(gravity_timer.Check()) DoGravityEffect(); diff --git a/zone/command.cpp b/zone/command.cpp index 40e37045e..8c2192c87 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -449,7 +449,11 @@ int command_init(void) { command_add("questerrors", "Shows quest errors.", 100, command_questerrors) || command_add("enablerecipe", "[recipe_id] - Enables a recipe using the recipe id.", 80, command_enablerecipe) || command_add("disablerecipe", "[recipe_id] - Disables a recipe using the recipe id.", 80, command_disablerecipe) || - command_add("npctype_cache", "[id] or all - Clears the npc type cache for either the id or all npcs.", 250, command_npctype_cache) + command_add("npctype_cache", "[id] or all - Clears the npc type cache for either the id or all npcs.", 250, command_npctype_cache) || + command_add("merchant_open_shop", "Opens a merchants shop", 100, command_merchantopenshop) || + command_add("open_shop", nullptr, 100, command_merchantopenshop) || + command_add("merchant_close_shop", "Closes a merchant shop", 100, command_merchantcloseshop) || + command_add("close_shop", nullptr, 100, command_merchantcloseshop) ) { command_deinit(); @@ -2638,19 +2642,35 @@ void command_makepet(Client *c, const Seperator *sep) void command_level(Client *c, const Seperator *sep) { uint16 level = atoi(sep->arg[1]); - if ((level <= 0) || ((level > RuleI(Character, MaxLevel)) && (c->Admin() < commandLevelAboveCap)) ) + + if ((level <= 0) || ((level > RuleI(Character, MaxLevel)) && (c->Admin() < commandLevelAboveCap))) { c->Message(0, "Error: #Level: Invalid Level"); - else if (c->Admin() < 100) + } + else if (c->Admin() < 100) { c->SetLevel(level, true); - else if (!c->GetTarget()) +#ifdef BOTS + if(RuleB(Bots, BotLevelsWithOwner)) + Bot::LevelBotWithClient(c, level, true); +#endif + } + else if (!c->GetTarget()) { c->Message(0, "Error: #Level: No target"); - else - if (!c->GetTarget()->IsNPC() && ((c->Admin() < commandLevelNPCAboveCap) && (level > RuleI(Character, MaxLevel)))) + } + else { + if (!c->GetTarget()->IsNPC() && ((c->Admin() < commandLevelNPCAboveCap) && (level > RuleI(Character, MaxLevel)))) { c->Message(0, "Error: #Level: Invalid Level"); - else + } + else { c->GetTarget()->SetLevel(level, true); - if(c->GetTarget() && c->GetTarget()->IsClient()) - c->GetTarget()->CastToClient()->SendLevelAppearance(); + if(c->GetTarget()->IsClient()) { + c->GetTarget()->CastToClient()->SendLevelAppearance(); +#ifdef BOTS + if(RuleB(Bots, BotLevelsWithOwner)) + Bot::LevelBotWithClient(c->GetTarget()->CastToClient(), level, true); +#endif + } + } + } } void command_spawn(Client *c, const Seperator *sep) @@ -11481,3 +11501,26 @@ void command_npctype_cache(Client *c, const Seperator *sep) c->Message(0, "#npctype_cache all"); } } + +void command_merchantopenshop(Client *c, const Seperator *sep) +{ + Mob *merchant = c->GetTarget(); + if (!merchant || merchant->GetClass() != MERCHANT) { + c->Message(0, "You must target a merchant to open their shop."); + return; + } + + merchant->CastToNPC()->MerchantOpenShop(); +} + +void command_merchantcloseshop(Client *c, const Seperator *sep) +{ + Mob *merchant = c->GetTarget(); + if (!merchant || merchant->GetClass() != MERCHANT) { + c->Message(0, "You must target a merchant to close their shop."); + return; + } + + merchant->CastToNPC()->MerchantCloseShop(); +} + diff --git a/zone/command.h b/zone/command.h index 48ca51b58..a36009309 100644 --- a/zone/command.h +++ b/zone/command.h @@ -324,6 +324,8 @@ void command_enablerecipe(Client *c, const Seperator *sep); void command_disablerecipe(Client *c, const Seperator *sep); void command_showspellslist(Client *c, const Seperator *sep); void command_npctype_cache(Client *c, const Seperator *sep); +void command_merchantopenshop(Client *c, const Seperator *sep); +void command_merchantcloseshop(Client *c, const Seperator *sep); #ifdef EQPROFILE void command_profiledump(Client *c, const Seperator *sep); diff --git a/zone/common.h b/zone/common.h index 581d11430..d720f3d43 100644 --- a/zone/common.h +++ b/zone/common.h @@ -5,6 +5,7 @@ #include "../common/spdat.h" #define HIGHEST_RESIST 9 //Max resist type value +#define MAX_SPELL_PROJECTILE 10 //Max amount of spell projectiles that can be active by a single mob. /* solar: macros for IsAttackAllowed, IsBeneficialAllowed */ #define _CLIENT(x) (x && x->IsClient() && !x->CastToClient()->IsBecomeNPC()) @@ -344,6 +345,8 @@ struct StatBonuses { int16 FrenziedDevastation; // base1= AArank(used) base2= chance increase spell criticals + all DD spells 2x mana. uint16 AbsorbMagicAtt[2]; // 0 = magic rune value 1 = buff slot uint16 MeleeRune[2]; // 0 = rune value 1 = buff slot + bool NegateIfCombat; // Bool Drop buff if cast or melee + int8 Screech; // -1 = Will be blocked if another Screech is +(1) // AAs int8 Packrat; //weight reduction for items, 1 point = 10% diff --git a/zone/effects.cpp b/zone/effects.cpp index c5e9dc20a..48e30639c 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -84,7 +84,7 @@ int32 Client::GetActSpellDamage(uint16 spell_id, int32 value, Mob* target) { chance += itembonuses.FrenziedDevastation + spellbonuses.FrenziedDevastation + aabonuses.FrenziedDevastation; - if (chance > 0){ + if (chance > 0 || (GetClass() == WIZARD && GetLevel() >= RuleI(Spells, WizCritLevel))) { int32 ratio = RuleI(Spells, BaseCritRatio); //Critical modifier is applied from spell effects only. Keep at 100 for live like criticals. @@ -99,7 +99,7 @@ int32 Client::GetActSpellDamage(uint16 spell_id, int32 value, Mob* target) { } else if (GetClass() == WIZARD && (GetLevel() >= RuleI(Spells, WizCritLevel)) && (MakeRandomInt(1,100) <= RuleI(Spells, WizCritChance))) { - ratio = MakeRandomInt(1,100); //Wizard innate critical chance is calculated seperately from spell effect and is not a set ratio. + ratio += MakeRandomInt(1,100); //Wizard innate critical chance is calculated seperately from spell effect and is not a set ratio. Critical = true; } diff --git a/zone/entity.cpp b/zone/entity.cpp index f5533cb84..ed5ee33f0 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -3415,9 +3415,14 @@ void EntityList::ReloadAllClientsTaskState(int TaskID) bool EntityList::IsMobInZone(Mob *who) { - auto it = mob_list.find(who->GetID()); - if (it != mob_list.end()) - return who == it->second; + //We don't use mob_list.find(who) because this code needs to be able to handle dangling pointers for the quest code. + auto it = mob_list.begin(); + while(it != mob_list.end()) { + if(it->second == who) { + return true; + } + ++it; + } return false; } diff --git a/zone/exp.cpp b/zone/exp.cpp index cbf7f5f6d..5e8eb49f8 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -326,7 +326,18 @@ void Client::SetEXP(uint32 set_exp, uint32 set_aaxp, bool isrezzexp) { } else Message(15, "Welcome to level %i!", check_level); + +#ifdef BOTS + uint8 myoldlevel = GetLevel(); +#endif + SetLevel(check_level); + +#ifdef BOTS + if(RuleB(Bots, BotLevelsWithOwner)) + // hack way of doing this..but, least invasive... (same criteria as gain level for sendlvlapp) + Bot::LevelBotWithClient(this, GetLevel(), (myoldlevel==check_level-1)); +#endif } //If were at max level then stop gaining experience if we make it to the cap diff --git a/zone/inventory.cpp b/zone/inventory.cpp index b18514056..5292dd1c0 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -209,7 +209,7 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, if(item == nullptr) { Message(13, "Item %u does not exist.", item_id); mlog(INVENTORY__ERROR, "Player %s on account %s attempted to create an item with an invalid id.\n(Item: %u, Aug1: %u, Aug2: %u, Aug3: %u, Aug4: %u, Aug5: %u)\n", - GetName(), account_name, item->ID, aug1, aug2, aug3, aug4, aug5); + GetName(), account_name, item_id, aug1, aug2, aug3, aug4, aug5); return false; } @@ -228,14 +228,20 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, return false; } + + // This code is ready to implement once the item load code is changed to process the 'minstatus' field. + // Checking #iteminfo in-game verfies that item->MinStatus is set to '0' regardless of field value. + // An optional sql script will also need to be added, once this goes live, to allow changing of the min status. + // check to make sure we are a GM if the item is GM-only /* - else if(item->gm && (this->Admin() < 100)) - Message(13, "You are not a GM and can not summon this item."); - mlog(INVENTORY__ERROR, "Player %s on account %s attempted to create a GM-only item with a status of %i.\n(Item: %u, Aug1: %u, Aug2: %u, Aug3: %u, Aug4: %u, Aug5: %u)\n", - GetName(), account_name, this->Admin(), item->ID, aug1, aug2, aug3, aug4, aug5); + else if(item->MinStatus && ((this->Admin() < item->MinStatus) || (this->Admin() < RuleI(GM, MinStatusToSummonItem)))) { + Message(13, "You are not a GM or do not have the status to summon this item."); + mlog(INVENTORY__ERROR, "Player %s on account %s attempted to create a GM-only item with a status of %i.\n(Item: %u, Aug1: %u, Aug2: %u, Aug3: %u, Aug4: %u, Aug5: %u, MinStatus: %u)\n", + GetName(), account_name, this->Admin(), item->ID, aug1, aug2, aug3, aug4, aug5, item->MinStatus); return false; + } */ uint32 augments[MAX_AUGMENT_SLOTS] = { aug1, aug2, aug3, aug4, aug5 }; @@ -276,12 +282,15 @@ bool Client::SummonItem(uint32 item_id, int16 charges, uint32 aug1, uint32 aug2, return false; } + + // Same as GM check above + // check to make sure we are a GM if the augment is GM-only /* - else if(augtest->gm && (this->Admin() < 100)) { - Message(13, "You are not a GM and can not summon this augment."); - mlog(INVENTORY__ERROR, "Player %s on account %s attempted to create a GM-only augment (Aug%i) with a status of %i.\n(Item: %u, Aug1: %u, Aug2: %u, Aug3: %u, Aug4: %u, Aug5: %u)\n", - GetName(), account_name, (iter + 1), this->Admin(), item->ID, aug1, aug2, aug3, aug4, aug5); + else if(augtest->MinStatus && ((this->Admin() < augtest->MinStatus) || (this->Admin() < RuleI(GM, MinStatusToSummonItem)))) { + Message(13, "You are not a GM or do not have the status to summon this augment."); + mlog(INVENTORY__ERROR, "Player %s on account %s attempted to create a GM-only augment (Aug%i) with a status of %i.\n(Item: %u, Aug1: %u, Aug2: %u, Aug3: %u, Aug4: %u, Aug5: %u, MinStatus: %u)\n", + GetName(), account_name, (iter + 1), this->Admin(), item->ID, aug1, aug2, aug3, aug4, aug5, item->MinStatus); return false; } diff --git a/zone/lua_npc.cpp b/zone/lua_npc.cpp index e9d1ae0ec..1dd61cc91 100644 --- a/zone/lua_npc.cpp +++ b/zone/lua_npc.cpp @@ -432,6 +432,16 @@ int Lua_NPC::GetScore() { return self->GetScore(); } +void Lua_NPC::MerchantOpenShop() { + Lua_Safe_Call_Void(); + self->MerchantOpenShop(); +} + +void Lua_NPC::MerchantCloseShop() { + Lua_Safe_Call_Void(); + self->MerchantCloseShop(); +} + luabind::scope lua_register_npc() { return luabind::class_("NPC") @@ -520,7 +530,9 @@ luabind::scope lua_register_npc() { .def("GetAttackSpeed", (float(Lua_NPC::*)(void))&Lua_NPC::GetAttackSpeed) .def("GetAccuracyRating", (int(Lua_NPC::*)(void))&Lua_NPC::GetAccuracyRating) .def("GetSpawnKillCount", (int(Lua_NPC::*)(void))&Lua_NPC::GetSpawnKillCount) - .def("GetScore", (int(Lua_NPC::*)(void))&Lua_NPC::GetScore); + .def("GetScore", (int(Lua_NPC::*)(void))&Lua_NPC::GetScore) + .def("MerchantOpenShop", (void(Lua_NPC::*)(void))&Lua_NPC::MerchantOpenShop) + .def("MerchantCloseShop", (void(Lua_NPC::*)(void))&Lua_NPC::MerchantCloseShop); } #endif diff --git a/zone/lua_npc.h b/zone/lua_npc.h index 1dbd33253..8c34ed17d 100644 --- a/zone/lua_npc.h +++ b/zone/lua_npc.h @@ -112,7 +112,9 @@ public: int GetAccuracyRating(); int GetSpawnKillCount(); int GetScore(); + void MerchantOpenShop(); + void MerchantCloseShop(); }; #endif -#endif \ No newline at end of file +#endif diff --git a/zone/mob.cpp b/zone/mob.cpp index 3c1500d19..87486714c 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -276,6 +276,14 @@ 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(); + memset(&itembonuses, 0, sizeof(StatBonuses)); memset(&spellbonuses, 0, sizeof(StatBonuses)); memset(&aabonuses, 0, sizeof(StatBonuses)); @@ -2080,27 +2088,35 @@ void Mob::SetAttackTimer() { } -bool Mob::CanThisClassDualWield(void) const -{ - if (!IsClient()) { +bool Mob::CanThisClassDualWield(void) const { + if(!IsClient()) { return(GetSkill(SkillDualWield) > 0); - } else { - const ItemInst* inst = CastToClient()->GetInv().GetItem(SLOT_PRIMARY); + } + else if(CastToClient()->HasSkill(SkillDualWield)) { + const ItemInst* pinst = CastToClient()->GetInv().GetItem(SLOT_PRIMARY); + const ItemInst* sinst = CastToClient()->GetInv().GetItem(SLOT_SECONDARY); + // 2HS, 2HB, or 2HP - if (inst && inst->IsType(ItemClassCommon)) { - const Item_Struct* item = inst->GetItem(); - if ((item->ItemType == ItemType2HBlunt) || (item->ItemType == ItemType2HSlash) || (item->ItemType == ItemType2HPiercing)) + if(pinst && pinst->IsWeapon()) { + const Item_Struct* item = pinst->GetItem(); + + if((item->ItemType == ItemType2HBlunt) || (item->ItemType == ItemType2HSlash) || (item->ItemType == ItemType2HPiercing)) return false; - } else { - //No weapon in hand... using hand-to-hand... - //only monks and beastlords? can dual wield their fists. - if(class_ != MONK && class_ != MONKGM && class_ != BEASTLORD && class_ != BEASTLORDGM) { - return false; - } } - return (CastToClient()->HasSkill(SkillDualWield)); // No skill = no chance + // OffHand Weapon + if(sinst && !sinst->IsWeapon()) + return false; + + // Dual-Wielding Empty Fists + if(!pinst && !sinst) + if(class_ != MONK && class_ != MONKGM && class_ != BEASTLORD && class_ != BEASTLORDGM) + return false; + + return true; } + + return false; } bool Mob::CanThisClassDoubleAttack(void) const @@ -2450,13 +2466,18 @@ bool Mob::RemoveFromHateList(Mob* mob) return bFound; } + void Mob::WipeHateList() { if(IsEngaged()) { + hate_list.Wipe(); AI_Event_NoLongerEngaged(); } - hate_list.Wipe(); + else + { + hate_list.Wipe(); + } } uint32 Mob::RandomTimer(int min,int max) { @@ -4353,6 +4374,49 @@ 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 = (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; diff --git a/zone/mob.h b/zone/mob.h index c8453d301..4ee5da5a8 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -192,6 +192,7 @@ public: virtual int32 GetActSpellCasttime(uint16 spell_id, int32 casttime); float ResistSpell(uint8 resist_type, uint16 spell_id, Mob *caster, bool use_resist_override = false, int resist_override = 0, bool CharismaCheck = false, bool CharmTick = false, bool IsRoot = false); + int ResistPhysical(int level_diff, uint8 caster_level); uint16 GetSpecializeSkillValue(uint16 spell_id) const; void SendSpellBarDisable(); void SendSpellBarEnable(uint16 spellid); @@ -222,6 +223,8 @@ 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); //Buff void BuffProcess(); @@ -337,6 +340,7 @@ public: inline virtual int16 GetPR() const { return PR + itembonuses.PR + spellbonuses.PR; } inline virtual int16 GetCR() const { return CR + itembonuses.CR + spellbonuses.CR; } inline virtual int16 GetCorrup() const { return Corrup + itembonuses.Corrup + spellbonuses.Corrup; } + inline virtual int16 GetPhR() const { return PhR; } inline StatBonuses GetItemBonuses() const { return itembonuses; } inline StatBonuses GetSpellBonuses() const { return spellbonuses; } inline StatBonuses GetAABonuses() const { return aabonuses; } @@ -915,6 +919,7 @@ protected: int16 DR; int16 PR; int16 Corrup; + int16 PhR; bool moving; int targeted; bool findable; @@ -1040,6 +1045,12 @@ 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]; + float rewind_x; float rewind_y; float rewind_z; diff --git a/zone/net.cpp b/zone/net.cpp index d870ee550..15e108e42 100644 --- a/zone/net.cpp +++ b/zone/net.cpp @@ -447,7 +447,7 @@ int main(int argc, char** argv) { if (InterserverTimer.Check()) { InterserverTimer.Start(); database.ping(); - AsyncLoadVariables(dbasync, &database); + // AsyncLoadVariables(dbasync, &database); entity_list.UpdateWho(); if (worldserver.TryReconnect() && (!worldserver.Connected())) worldserver.AsyncConnect(); diff --git a/zone/npc.cpp b/zone/npc.cpp index 2c21c94a7..f83d1d31b 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -158,6 +158,7 @@ NPC::NPC(const NPCType* d, Spawn2* in_respawn, float x, float y, float z, float FR = d->FR; PR = d->PR; Corrup = d->Corrup; + PhR = d->PhR; STR = d->STR; STA = d->STA; @@ -199,6 +200,7 @@ NPC::NPC(const NPCType* d, Spawn2* in_respawn, float x, float y, float z, float SetMana(GetMaxMana()); MerchantType = d->merchanttype; + merchant_open = GetClass() == MERCHANT; adventure_template_id = d->adventure_template; org_x = x; org_y = y; @@ -658,6 +660,9 @@ bool NPC::Process() viral_timer_counter = 0; } + if(projectile_timer.Check()) + SpellProjectileEffect(); + if(spellbonuses.GravityEffect == 1) { if(gravity_timer.Check()) DoGravityEffect(); @@ -2058,6 +2063,8 @@ void NPC::CalcNPCResists() { PR = (GetLevel() * 11)/10; if (!Corrup) Corrup = 15; + if (!PhR) + PhR = 10; return; } diff --git a/zone/npc.h b/zone/npc.h index f374c8fe6..e8877172b 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -209,6 +209,10 @@ public: void SetSecSkill(uint8 skill_type) { sec_melee_type = skill_type; } uint32 MerchantType; + bool merchant_open; + inline void MerchantOpenShop() { merchant_open = true; } + inline void MerchantCloseShop() { merchant_open = false; } + inline bool IsMerchantOpen() { return merchant_open; } void Depop(bool StartSpawnTimer = false); void Stun(int duration); void UnStun(); diff --git a/zone/special_attacks.cpp b/zone/special_attacks.cpp index d551962ea..8d95524fe 100644 --- a/zone/special_attacks.cpp +++ b/zone/special_attacks.cpp @@ -839,6 +839,9 @@ void Client::RangedAttack(Mob* other, bool CanDoubleAttack) { invisible_animals = false; } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(hidden || improved_hidden){ hidden = false; improved_hidden = false; @@ -1085,6 +1088,9 @@ void NPC::RangedAttack(Mob* other) invisible_animals = false; } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(hidden || improved_hidden){ hidden = false; improved_hidden = false; @@ -1227,6 +1233,9 @@ void Client::ThrowingAttack(Mob* other, bool CanDoubleAttack) { //old was 51 invisible_animals = false; } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(hidden || improved_hidden){ hidden = false; improved_hidden = false; diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 7fd2f4332..ce299ed36 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -326,7 +326,13 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) if(inuse) break; - Heal(); + int32 val = 0; + val = 7500*effect_value; + val = caster->GetActSpellHealing(spell_id, val, this); + + if (val > 0) + HealDamage(val, caster); + break; } @@ -396,10 +402,11 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) } case SE_Succor: - { + { + float x, y, z, heading; const char *target_zone; - + x = spell.base[1]; y = spell.base[0]; z = spell.base[2]; @@ -426,6 +433,14 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) if(IsClient()) { + if(MakeRandomInt(0, 99) < RuleI(Spells, SuccorFailChance)) { //2% Fail chance by default + + if(IsClient()) { + CastToClient()->Message_StringID(MT_SpellFailure,SUCCOR_FAIL); + } + break; + } + // Below are the spellid's for known evac/succor spells that send player // to the current zone's safe points. @@ -441,10 +456,10 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) #ifdef SPELL_EFFECT_SPAM LogFile->write(EQEMuLog::Debug, "Succor/Evacuation Spell In Same Zone."); #endif - if(IsClient()) - CastToClient()->MovePC(zone->GetZoneID(), zone->GetInstanceID(), x, y, z, heading, 0, EvacToSafeCoords); - else - GMMove(x, y, z, heading); + if(IsClient()) + CastToClient()->MovePC(zone->GetZoneID(), zone->GetInstanceID(), x, y, z, heading, 0, EvacToSafeCoords); + else + GMMove(x, y, z, heading); } else { #ifdef SPELL_EFFECT_SPAM @@ -457,7 +472,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) break; } - case SE_YetAnotherGate: //Shin: Used on Teleport Bind. + case SE_GateCastersBindpoint: //Shin: Used on Teleport Bind. case SE_Teleport: // gates, rings, circles, etc case SE_Teleport2: { @@ -489,7 +504,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) } } - if (effect == SE_YetAnotherGate && caster->IsClient()) + if (effect == SE_GateCastersBindpoint && caster->IsClient()) { //Shin: Teleport Bind uses caster's bind point x = caster->CastToClient()->GetBindX(); y = caster->CastToClient()->GetBindY(); @@ -857,7 +872,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) break; } - case SE_BindAffinity: + case SE_BindAffinity: //TO DO: Add support for secondary and tertiary gate abilities { #ifdef SPELL_EFFECT_SPAM snprintf(effect_desc, _EDLEN, "Bind Affinity"); @@ -989,13 +1004,18 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) break; } - case SE_Gate: + case SE_Gate: //TO DO: Add support for secondary and tertiary gate abilities (base2) { #ifdef SPELL_EFFECT_SPAM snprintf(effect_desc, _EDLEN, "Gate"); #endif - if(!spellbonuses.AntiGate) - Gate(); + if(!spellbonuses.AntiGate){ + + if(MakeRandomInt(0, 99) < effect_value) + Gate(); + else + caster->Message_StringID(MT_SpellFailure,GATE_FAIL); + } break; } @@ -1378,7 +1398,8 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) ( spell.base[i], Mob::GetDefaultGender(spell.base[i], GetGender()), - spell.base2[i] + spell.base2[i], + spell.max[i] ); if(spell.base[i] == OGRE){ SendAppearancePacket(AT_Size, 9); @@ -1554,8 +1575,15 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) if(spell_id == 2488) //Dook- Lifeburn fix break; - if(IsClient()) - CastToClient()->SetFeigned(true); + if(IsClient()) { + + if (MakeRandomInt(0, 99) > spells[spell_id].base[i]) { + CastToClient()->SetFeigned(false); + entity_list.MessageClose_StringID(this, false, 200, 10, STRING_FEIGNFAILED, GetName()); + } + else + CastToClient()->SetFeigned(true); + } break; } @@ -1691,19 +1719,25 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) // Now we should either be casting this on self or its being cast on a valid group member if(TargetClient) { - Corpse *corpse = entity_list.GetCorpseByOwner(TargetClient); - if(corpse) { - if(TargetClient == this->CastToClient()) - Message_StringID(4, SUMMONING_CORPSE, TargetClient->CastToMob()->GetCleanName()); - else - Message_StringID(4, SUMMONING_CORPSE_OTHER, TargetClient->CastToMob()->GetCleanName()); - corpse->Summon(CastToClient(), true, true); - } - else { - // No corpse found in the zone - Message_StringID(4, CORPSE_CANT_SENSE); + if (TargetClient->GetLevel() <= effect_value){ + + Corpse *corpse = entity_list.GetCorpseByOwner(TargetClient); + if(corpse) { + if(TargetClient == this->CastToClient()) + Message_StringID(4, SUMMONING_CORPSE, TargetClient->CastToMob()->GetCleanName()); + else + Message_StringID(4, SUMMONING_CORPSE_OTHER, TargetClient->CastToMob()->GetCleanName()); + + corpse->Summon(CastToClient(), true, true); + } + else { + // No corpse found in the zone + Message_StringID(4, CORPSE_CANT_SENSE); + } } + else + caster->Message_StringID(MT_SpellFailure, SPELL_LEVEL_REQ); } else { Message_StringID(4, TARGET_NOT_FOUND); @@ -5986,3 +6020,73 @@ bool Mob::PassCastRestriction(bool UseCastRestriction, int16 value, bool IsDama return false; } +bool Mob::TrySpellProjectile(Mob* spell_target, uint16 spell_id){ + + /*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). + 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. + Pending Implementation: What this code can not do is prevent damage if the bolt hits a barrier after passing the initial LOS check + because the target has moved while the bolt is in motion. (it is rare to actual get this to occur on live in normal game play) + */ + + if (!spell_target) + return false; + + uint8 anim = spells[spell_id].CastingAnim; + int bolt_id = -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; + break; + } + } + + if (bolt_id < 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); + } + + //Only use fire graphic for fire spells. + if (spells[spell_id].resisttype == RESIST_FIRE) { + + if (IsClient()){ + if (CastToClient()->GetClientVersionBit() <= 4) //Titanium needs alternate graphic. + ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_Titanium)), false, 1.5); + else + ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_SOF)), false, 1.5); + } + + else + ProjectileAnimation(spell_target,(RuleI(Spells, FRProjectileItem_NPC)), false, 1.5); + + if (spells[spell_id].CastingAnim == 64) + anim = 44; //Corrects for animation error. + } + + //Pending other types of projectile graphics. (They will function but with a default arrow graphic for now) + else + ProjectileAnimation(spell_target,0, 1, 1.5); + + DoAnim(anim, 0, true, IsClient() ? FilterPCSpells : FilterNPCSpells); //Override the default projectile animation. + return true; +} + + diff --git a/zone/spells.cpp b/zone/spells.cpp index 86d21ea7a..43a0d2b19 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -211,6 +211,9 @@ bool Mob::CastSpell(uint16 spell_id, uint16 target_id, uint16 slot, return(false); } + if (spellbonuses.NegateIfCombat) + BuffFadeByEffect(SE_NegateIfCombat); + if(IsClient() && GetTarget() && IsHarmonySpell(spell_id)) { for(int i = 0; i < EFFECT_COUNT; i++) { @@ -554,6 +557,15 @@ uint16 Mob::GetSpecializeSkillValue(uint16 spell_id) const { } void Client::CheckSpecializeIncrease(uint16 spell_id) { + // These are not active because CheckIncreaseSkill() already does so. + // It's such a rare occurance that adding them here is wasted..(ref only) + /* + if (IsDead() || IsUnconscious()) + return; + if (IsAIControlled()) + return; + */ + switch(spells[spell_id].skill) { case SkillAbjuration: CheckIncreaseSkill(SkillSpecializeAbjure, nullptr); @@ -577,6 +589,15 @@ void Client::CheckSpecializeIncrease(uint16 spell_id) { } void Client::CheckSongSkillIncrease(uint16 spell_id){ + // These are not active because CheckIncreaseSkill() already does so. + // It's such a rare occurance that adding them here is wasted..(ref only) + /* + if (IsDead() || IsUnconscious()) + return; + if (IsAIControlled()) + return; + */ + switch(spells[spell_id].skill) { case SkillSinging: @@ -1809,7 +1830,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 } // check line of sight to target if it's a detrimental spell - if(spell_target && IsDetrimentalSpell(spell_id) && !CheckLosFN(spell_target) && !IsHarmonySpell(spell_id)) + if(spell_target && IsDetrimentalSpell(spell_id) && !CheckLosFN(spell_target) && !IsHarmonySpell(spell_id) && spells[spell_id].targettype != ST_TargetOptional) { mlog(SPELLS__CASTING, "Spell %d: cannot see target %s", spell_target->GetName()); Message_StringID(13,CANT_SEE_TARGET); @@ -1874,7 +1895,12 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 if (isproc) { SpellOnTarget(spell_id, spell_target, false, true, resist_adjust, true); } else { - if(!SpellOnTarget(spell_id, spell_target, false, true, resist_adjust, false)) { + if (spells[spell_id].targettype == ST_TargetOptional){ + if (!TrySpellProjectile(spell_target, spell_id)) + return false; + } + + else if(!SpellOnTarget(spell_id, spell_target, false, true, resist_adjust, false)) { if(IsBuffSpell(spell_id) && IsBeneficialSpell(spell_id)) { // Prevent mana usage/timers being set for beneficial buffs if(casting_spell_type == 1) @@ -1883,6 +1909,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 } } } + if(IsPlayerIllusionSpell(spell_id) && IsClient() && CastToClient()->CheckAAEffect(aaEffectProjectIllusion)){ @@ -2586,6 +2613,14 @@ int Mob::CheckStackConflict(uint16 spellid1, int caster_level1, uint16 spellid2, { effect1 = sp1.effectid[i]; effect2 = sp2.effectid[i]; + + if (spellbonuses.Screech == 1) { + if (effect2 == SE_Screech && sp2.base[i] == -1) { + Message_StringID(MT_SpellFailure, SCREECH_BUFF_BLOCK, sp2.name); + return -1; + } + } + if(effect2 == SE_StackingCommand_Overwrite) { overwrite_effect = sp2.base[i]; @@ -2630,7 +2665,7 @@ int Mob::CheckStackConflict(uint16 spellid1, int caster_level1, uint16 spellid2, mlog(SPELLS__STACKING, "%s (%d) blocks effect %d on slot %d below %d, but we do not have that effect on that slot. Ignored.", sp1.name, spellid1, blocked_effect, blocked_slot, blocked_below_value); } - } + } } } else { mlog(SPELLS__STACKING, "%s (%d) and %s (%d) appear to be in the same line, skipping Stacking Overwrite/Blocking checks", @@ -3419,8 +3454,15 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob* spelltar, bool reflect, bool use_r if(spell_effectiveness == 0 || !IsPartialCapableSpell(spell_id) ) { mlog(SPELLS__RESISTS, "Spell %d was completely resisted by %s", spell_id, spelltar->GetName()); - Message_StringID(MT_SpellFailure, TARGET_RESISTED, spells[spell_id].name); - spelltar->Message_StringID(MT_SpellFailure, YOU_RESIST, spells[spell_id].name); + + if (spells[spell_id].resisttype == RESIST_PHYSICAL){ + Message_StringID(MT_SpellFailure, PHYSICAL_RESIST_FAIL,spells[spell_id].name); + spelltar->Message_StringID(MT_SpellFailure, YOU_RESIST, spells[spell_id].name); + } + else { + Message_StringID(MT_SpellFailure, TARGET_RESISTED, spells[spell_id].name); + spelltar->Message_StringID(MT_SpellFailure, YOU_RESIST, spells[spell_id].name); + } if(spelltar->IsAIControlled()){ int32 aggro = CheckAggroAmount(spell_id); @@ -4169,67 +4211,83 @@ float Mob::ResistSpell(uint8 resist_type, uint16 spell_id, Mob *caster, bool use } break; case RESIST_PHYSICAL: + { + if (IsNPC()) + target_resist = GetPhR(); + else + target_resist = 0; + } default: - //This is guessed but the others are right - target_resist = (GetSTA() / 4); + + target_resist = 0; } //Setup our base resist chance. int resist_chance = 0; + int level_mod = 0; //Adjust our resist chance based on level modifiers int temp_level_diff = GetLevel() - caster->GetLevel(); - if(IsNPC() && GetLevel() >= RuleI(Casting,ResistFalloff)) - { - int a = (RuleI(Casting,ResistFalloff)-1) - caster->GetLevel(); - if(a > 0) + + //Physical Resists are calclated using their own formula derived from extensive parsing. + if (resist_type == RESIST_PHYSICAL) { + level_mod = ResistPhysical(temp_level_diff, caster->GetLevel()); + } + + else { + + if(IsNPC() && GetLevel() >= RuleI(Casting,ResistFalloff)) { - temp_level_diff = a; - } - else - { - temp_level_diff = 0; - } - } - - if(IsClient() && GetLevel() >= 21 && temp_level_diff > 15) - { - temp_level_diff = 15; - } - - if(IsNPC() && temp_level_diff < -9) - { - temp_level_diff = -9; - } - - int level_mod = temp_level_diff * temp_level_diff / 2; - if(temp_level_diff < 0) - { - level_mod = -level_mod; - } - - if(IsNPC() && (caster->GetLevel() - GetLevel()) < -20) - { - level_mod = 1000; - } - - //Even more level stuff this time dealing with damage spells - if(IsNPC() && IsDamageSpell(spell_id) && GetLevel() >= 17) - { - int level_diff; - if(GetLevel() >= RuleI(Casting,ResistFalloff)) - { - level_diff = (RuleI(Casting,ResistFalloff)-1) - caster->GetLevel(); - if(level_diff < 0) + int a = (RuleI(Casting,ResistFalloff)-1) - caster->GetLevel(); + if(a > 0) { - level_diff = 0; + temp_level_diff = a; + } + else + { + temp_level_diff = 0; } } - else + + if(IsClient() && GetLevel() >= 21 && temp_level_diff > 15) { - level_diff = GetLevel() - caster->GetLevel(); + temp_level_diff = 15; + } + + if(IsNPC() && temp_level_diff < -9) + { + temp_level_diff = -9; + } + + level_mod = temp_level_diff * temp_level_diff / 2; + if(temp_level_diff < 0) + { + level_mod = -level_mod; + } + + if(IsNPC() && (caster->GetLevel() - GetLevel()) < -20) + { + level_mod = 1000; + } + + //Even more level stuff this time dealing with damage spells + if(IsNPC() && IsDamageSpell(spell_id) && GetLevel() >= 17) + { + int level_diff; + if(GetLevel() >= RuleI(Casting,ResistFalloff)) + { + level_diff = (RuleI(Casting,ResistFalloff)-1) - caster->GetLevel(); + if(level_diff < 0) + { + level_diff = 0; + } + } + else + { + level_diff = GetLevel() - caster->GetLevel(); + } + level_mod += (2 * level_diff); } - level_mod += (2 * level_diff); } if (CharismaCheck) @@ -4376,6 +4434,43 @@ float Mob::ResistSpell(uint8 resist_type, uint16 spell_id, Mob *caster, bool use } } +int Mob::ResistPhysical(int level_diff, uint8 caster_level) +{ + /* Physical resists use the standard level mod calculation in + conjunction with a resist fall off formula that greatly prevents you + from landing abilities on mobs that are higher level than you. + After level 12, every 4 levels gained the max level you can hit + your target without a sharp resist penalty is raised by 1. + Extensive parsing confirms this, along with baseline phyiscal resist rates used. + */ + + + if (level_diff == 0) + return level_diff; + + int level_mod = 0; + + if (level_diff > 0) { + + int ResistFallOff = 0; + + if (caster_level <= 12) + ResistFallOff = 3; + else + ResistFallOff = caster_level/4; + + if (level_diff > ResistFallOff || level_diff >= 15) + level_mod = ((level_diff * 10) + level_diff)*2; + else + level_mod = level_diff * level_diff / 2; + } + + else + level_mod = -(level_diff * level_diff / 2); + + return level_mod; +} + int16 Mob::CalcResistChanceBonus() { int resistchance = spellbonuses.ResistSpellChance + itembonuses.ResistSpellChance; diff --git a/zone/tasks.cpp b/zone/tasks.cpp index 44d4e1e69..221085d4e 100644 --- a/zone/tasks.cpp +++ b/zone/tasks.cpp @@ -1972,14 +1972,6 @@ void ClientTaskState::IncrementDoneCount(Client *c, TaskInformation* Task, int T Task->Activity[ActivityID].GoalCount, ActivityID); - if(Task->Activity[ActivityID].GoalMethod != METHODQUEST) - { - char buf[24]; - snprintf(buf, 23, "%d %d", ActiveTasks[TaskIndex].TaskID, ActiveTasks[TaskIndex].Activity[ActivityID].ActivityID); - buf[23] = '\0'; - parse->EventPlayer(EVENT_TASK_STAGE_COMPLETE, c, buf, 0); - } - // Flag the activity as complete ActiveTasks[TaskIndex].Activity[ActivityID].State = ActivityCompleted; // Unlock subsequent activities for this task @@ -1991,6 +1983,15 @@ void ClientTaskState::IncrementDoneCount(Client *c, TaskInformation* Task, int T taskmanager->SendSingleActiveTaskToClient(c, TaskIndex, TaskComplete, false); // Inform the client the task has been updated, both by a chat message c->Message(0, "Your task '%s' has been updated.", Task->Title); + + if(Task->Activity[ActivityID].GoalMethod != METHODQUEST) + { + char buf[24]; + snprintf(buf, 23, "%d %d", ActiveTasks[TaskIndex].TaskID, ActiveTasks[TaskIndex].Activity[ActivityID].ActivityID); + buf[23] = '\0'; + parse->EventPlayer(EVENT_TASK_STAGE_COMPLETE, c, buf, 0); + } + // If this task is now complete, the Completed tasks will have been // updated in UnlockActivities. Send the completed task list to the // client. This is the same sequence the packets are sent on live. diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 6c3b8ce6e..568780e7d 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -1047,6 +1047,7 @@ const NPCType* ZoneDatabase::GetNPCType (uint32 id) { "npc_types.FR," "npc_types.PR," "npc_types.Corrup," + "npc_types.PhR," "npc_types.mindmg," "npc_types.maxdmg," "npc_types.attack_count," @@ -1143,6 +1144,7 @@ const NPCType* ZoneDatabase::GetNPCType (uint32 id) { tmpNPCType->FR = atoi(row[r++]); tmpNPCType->PR = atoi(row[r++]); tmpNPCType->Corrup = atoi(row[r++]); + tmpNPCType->PhR = atoi(row[r++]); tmpNPCType->min_dmg = atoi(row[r++]); tmpNPCType->max_dmg = atoi(row[r++]); tmpNPCType->attack_count = atoi(row[r++]); diff --git a/zone/zonedump.h b/zone/zonedump.h index 726041d5b..2c036be61 100644 --- a/zone/zonedump.h +++ b/zone/zonedump.h @@ -75,6 +75,7 @@ struct NPCType int16 PR; int16 DR; int16 Corrup; + int16 PhR; uint8 haircolor; uint8 beardcolor; uint8 eyecolor1; // the eyecolors always seem to be the same, maybe left and right eye?