delves part 1
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
|
||||
UPDATE `creature_template` SET `speed_walk`=1, `speed_run`=1.142857193946838378, `unit_flags2`=0x800 WHERE `entry` IN (53648, 53102); -- Inferno Hawk
|
||||
UPDATE `creature_template` SET `speed_walk`=1, `unit_flags2`=0x800 WHERE `entry`=53115; -- Molten Lord
|
||||
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry`=53185; -- Flamewaker Overseer
|
||||
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry`=53119; -- Flamewaker Forward Guard
|
||||
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry`=53120; -- Flamewaker Pathfinder
|
||||
UPDATE `creature_template` SET `unit_flags2`=0x800 WHERE `entry` IN (53639, 53121); -- Flamewaker Cauterizer
|
||||
|
||||
-- Template Addon
|
||||
UPDATE `creature_template_addon` SET `visibilityDistanceType`=3 WHERE `entry`=53185; -- 53185 (Flamewaker Overseer)
|
||||
|
||||
-- Difficulty
|
||||
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x30000000, `VerifiedBuild`=69587 WHERE (`Entry`=53102 AND `DifficultyID`=14); -- 53102 (Inferno Hawk) - CanSwim, Floating
|
||||
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53185 AND `DifficultyID`=14); -- 53185 (Flamewaker Overseer) - HideInCombatLog
|
||||
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53119 AND `DifficultyID`=14); -- 53119 (Flamewaker Forward Guard) - HideInCombatLog
|
||||
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53120 AND `DifficultyID`=14); -- 53120 (Flamewaker Pathfinder) - HideInCombatLog
|
||||
UPDATE `creature_template_difficulty` SET `StaticFlags1`=0x0, `StaticFlags4`=0x2000000, `VerifiedBuild`=69587 WHERE (`Entry`=53121 AND `DifficultyID`=14); -- 53121 (Flamewaker Cauterizer) - HideInCombatLog
|
||||
|
||||
DELETE FROM `spell_script_names` WHERE `spell_id`=1219616 AND `ScriptName`='spell_hun_spotters_mark';
|
||||
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (1219616, 'spell_hun_spotters_mark');
|
||||
|
||||
DELETE FROM `creature_addon` WHERE `guid`=135295;
|
||||
INSERT INTO `creature_addon` (`guid`, `PathId`, `mount`, `MountCreatureID`, `StandState`, `AnimTier`, `VisFlags`, `SheathState`, `PvPFlags`, `emote`, `aiAnimKit`, `movementAnimKit`, `meleeAnimKit`, `visibilityDistanceType`, `auras`) VALUES (135295, 0, 0, 0, 8, 0, 0, 1, 0, 0, 0, 0, 0, 0, '');
|
||||
|
||||
UPDATE `gameobject` SET `PhaseGroup`=0 WHERE `guid`=15498;
|
||||
|
||||
DELETE FROM `spell_script_names` WHERE `spell_id`=195072 AND `ScriptName`='spell_dh_fel_rush';
|
||||
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (195072, 'spell_dh_fel_rush');
|
||||
|
||||
DELETE FROM `spell_script_names` WHERE `spell_id`=197922 AND `ScriptName`='spell_dh_fel_rush_aura';
|
||||
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (197922, 'spell_dh_fel_rush_aura');
|
||||
|
||||
DELETE FROM `spell_script_names` WHERE `spell_id`=197923 AND `ScriptName`='spell_dh_fel_rush_aura';
|
||||
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (197923, 'spell_dh_fel_rush_aura');
|
||||
|
||||
DELETE FROM `access_requirement` WHERE `mapId`=2825 AND `difficulty`=205;
|
||||
INSERT INTO `access_requirement` (`mapId`, `difficulty`, `level_min`, `level_max`, `item`, `item2`, `quest_done_A`, `quest_done_H`, `completed_achievement`, `quest_failed_text`, `comment`) VALUES (2825, 205, 0, 0, 0, 0, 0, 0, 0, NULL, NULL);
|
||||
|
||||
UPDATE `creature_template` SET `unit_flags3`=0 WHERE `entry`=51247;
|
||||
|
||||
ALTER TABLE `delve_template`
|
||||
ADD COLUMN `exitMapId` int NOT NULL DEFAULT '-1' COMMENT 'overworld map of exitX/Y/Z/O (-1 = unknown)' AFTER `exitO`;
|
||||
@@ -61,12 +61,9 @@ namespace Delves
|
||||
_owners.insert(player->GetGUID());
|
||||
PopulateDelveData(player);
|
||||
|
||||
// Retail: Coffer Key Shards auto-combine into Restored Coffer Keys on delve entry (100 -> 1).
|
||||
while (player->GetCurrencyQuantity(CURRENCY_COFFER_KEY_SHARDS) >= COFFER_KEY_SHARDS_PER_KEY)
|
||||
{
|
||||
player->RemoveCurrency(CURRENCY_COFFER_KEY_SHARDS, int32(COFFER_KEY_SHARDS_PER_KEY));
|
||||
player->AddCurrency(CURRENCY_RESTORED_COFFER_KEY, 1, CurrencyGainSource::Loot);
|
||||
}
|
||||
// No shard -> key conversion here: none of the five captured runs moved currency 3028 on entry
|
||||
// (REPORT.md 6.2); retail instead completes the hidden entry quest ~6.6 s after SMSG_NEW_WORLD, see
|
||||
// DelveMgr::OnPlayerEnteredDelve.
|
||||
|
||||
// Spawn companion for the first player if group size <= MAX_COMPANION_GROUP_SIZE (4)
|
||||
if (_state == DelveState::Entering)
|
||||
|
||||
@@ -20,9 +20,23 @@
|
||||
#include "Creature.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "DelvesPackets.h"
|
||||
#include "DelvesRewards.h"
|
||||
#include "DelvesSeason.h"
|
||||
#include "Duration.h"
|
||||
#include "GameTime.h"
|
||||
#include "GossipDef.h"
|
||||
#include "Item.h"
|
||||
#include "ItemEnchantmentMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "NPCPackets.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "QuestDef.h"
|
||||
#include "SpellPackets.h"
|
||||
#include "Timer.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
namespace Delves
|
||||
{
|
||||
@@ -88,7 +102,7 @@ namespace Delves
|
||||
"entryX, entryY, entryZ, entryO, "
|
||||
"exitX, exitY, exitZ, exitO, "
|
||||
"activeScenarioId, rewardScenarioId, worldState26903, finalBossEntry, "
|
||||
"tieredEntranceId, tieredEntranceUnknown3, entranceUiWidgetSetId, modifierUiWidgetSetTier1 "
|
||||
"tieredEntranceId, tieredEntranceUnknown3, entranceUiWidgetSetId, modifierUiWidgetSetTier1, exitMapId "
|
||||
"FROM delve_template");
|
||||
|
||||
if (!result)
|
||||
@@ -132,6 +146,7 @@ namespace Delves
|
||||
tmpl.TieredEntranceUnknown3 = fields[27].GetUInt32();
|
||||
tmpl.EntranceUiWidgetSetId = fields[28].GetUInt32();
|
||||
tmpl.ModifierUiWidgetSetTier1 = fields[29].GetUInt32();
|
||||
tmpl.ExitMapId = fields[30].GetInt32();
|
||||
|
||||
_delveTemplatesByMap[tmpl.MapId] = tmpl;
|
||||
_delveTemplatesList.push_back(tmpl);
|
||||
@@ -402,4 +417,316 @@ namespace Delves
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Retail run flow (12.1.0.69497 captures, C:\sniff\tcharvest\out\delve_research\REPORT.md)
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
void DelveMgr::SendTieredEntranceOpen(Player* player, Creature const* entrance)
|
||||
{
|
||||
if (!player || !entrance)
|
||||
return;
|
||||
|
||||
DelveTemplate const* tmpl = GetDelveTemplateForEntrance(entrance);
|
||||
if (!tmpl)
|
||||
{
|
||||
TC_LOG_DEBUG("scripts.delves", "DelveMgr::SendTieredEntranceOpen: could not resolve entrance {} (entry {}, map {}) to a delve template",
|
||||
entrance->GetGUID().ToString(), entrance->GetEntry(), entrance->GetMapId());
|
||||
return;
|
||||
}
|
||||
|
||||
DelveProgress progress;
|
||||
DelvesRewards::LoadProgress(player->GetSession()->GetBattlenetAccountId(), progress);
|
||||
bool const meetsLevel = DelvesSeason::MeetsMinimumLevelRequirement(player);
|
||||
|
||||
// REPORT.md 1.3 (gulf 88718 == 98437, eversong 1842501; 12.0.7 The Darkway agrees): every delve entrance reports
|
||||
// EntranceType 1, field 7 is 234 for every delve, field 6 is 0, fields 3/4/8 and the widget sets are per entrance
|
||||
// (delve_template), the tier rows are season data (delve_tiered_entrance_tier). The client matches the response
|
||||
// by the entrance GUID - two spawns of 212407 can be visible at once (REPORT.md 1.2), so echo this spawn's guid.
|
||||
WorldPackets::Delves::TieredEntranceOpenResponse response;
|
||||
response.EntranceGUID = entrance->GetGUID();
|
||||
response.EntranceType = TIERED_ENTRANCE_TYPE_DELVE;
|
||||
response.MapID = tmpl->MapId;
|
||||
response.Unknown3 = tmpl->TieredEntranceUnknown3;
|
||||
response.Unknown4 = tmpl->EntranceUiWidgetSetId;
|
||||
response.Unknown6 = 0;
|
||||
response.Unknown7 = DELVE_TIERED_ENTRANCE_FIELD7;
|
||||
response.Unknown8 = tmpl->TieredEntranceId;
|
||||
|
||||
if (MapEntry const* mapEntry = sMapStore.LookupEntry(tmpl->MapId))
|
||||
response.EntranceDescription = mapEntry->MapName[player->GetSession()->GetSessionDbcLocale()];
|
||||
|
||||
response.Tiers.reserve(_tieredEntranceTiers.size());
|
||||
for (TieredEntranceTierData const& tierRow : _tieredEntranceTiers)
|
||||
{
|
||||
WorldPackets::Delves::TieredEntranceTier& tierData = response.Tiers.emplace_back();
|
||||
tierData.TieredEntranceTierID = tierRow.Id;
|
||||
tierData.Tier = tierRow.Tier;
|
||||
tierData.SuggestedILvl = tierRow.SuggestedILvl;
|
||||
tierData.OverrideTooltipSpellID = tierRow.OverrideTooltipSpellId;
|
||||
tierData.UnlockPlayerConditionID = tierRow.UnlockPlayerConditionId;
|
||||
tierData.DynamicUnlockPlayerConditionID = tierRow.DynamicUnlockPlayerConditionId;
|
||||
tierData.ModifierUIWidgetSetID = tmpl->ModifierUiWidgetSetTier1 && tierRow.Tier ? tmpl->ModifierUiWidgetSetTier1 - (tierRow.Tier - 1) : 0;
|
||||
tierData.Unlocked = meetsLevel && tierRow.Tier <= progress.HighestTierUnlocked;
|
||||
tierData.TierDescription = tierRow.Description;
|
||||
for (TieredEntranceRewardData const& reward : tierRow.Rewards)
|
||||
{
|
||||
WorldPackets::Delves::TieredEntranceReward& rewardData = tierData.PreviewTreasureList.emplace_back();
|
||||
rewardData.RewardType = reward.RewardType;
|
||||
rewardData.Id = reward.Id;
|
||||
rewardData.Quantity = reward.Quantity;
|
||||
rewardData.Context = reward.Context;
|
||||
}
|
||||
}
|
||||
|
||||
player->SendDirectMessage(response.Write());
|
||||
}
|
||||
|
||||
void DelveMgr::OpenEntranceByProximity(Player* player, Creature const* entrance)
|
||||
{
|
||||
if (!player || !entrance || !player->IsInWorld() || player->IsBeingTeleported())
|
||||
return;
|
||||
|
||||
// an entrance NPC standing inside a delve never opens the picker
|
||||
if (GetDelveTemplate(player->GetMapId()))
|
||||
return;
|
||||
|
||||
// once per approach (REPORT.md 1.1: gulf opened at 88718, CMSG_CLOSE_INTERACTION at 95751 when the player walked
|
||||
// away, opened again at 98205). WorldSession::HandleCloseInteraction resets the interaction for this guid.
|
||||
InteractionData& interaction = player->PlayerTalkClass->GetInteractionData();
|
||||
PlayerInteractionType const interactionType = PlayerInteractionType(DELVE_ENTRANCE_INTERACTION_TYPE);
|
||||
if (interaction.IsInteractingWith(entrance->GetGUID(), interactionType))
|
||||
return;
|
||||
|
||||
if (!GetDelveTemplateForEntrance(entrance))
|
||||
return;
|
||||
|
||||
interaction.StartInteraction(entrance->GetGUID(), interactionType);
|
||||
|
||||
// REPORT.md 1.1, gulf 98205: `PackedGUID(212407) | 4f000000 | 00` = entrance guid + int32 InteractionType 79 + bit Success
|
||||
WorldPackets::NPC::NPCInteractionOpenResult openResult;
|
||||
openResult.Npc = entrance->GetGUID();
|
||||
openResult.InteractionType = interactionType;
|
||||
openResult.Success = true;
|
||||
player->SendDirectMessage(openResult.Write());
|
||||
|
||||
SendTieredEntranceOpen(player, entrance);
|
||||
}
|
||||
|
||||
void DelveMgr::CloseEntranceByProximity(Player* player, Creature const* entrance)
|
||||
{
|
||||
if (!player || !entrance)
|
||||
return;
|
||||
|
||||
InteractionData& interaction = player->PlayerTalkClass->GetInteractionData();
|
||||
if (interaction.IsInteractingWith(entrance->GetGUID(), PlayerInteractionType(DELVE_ENTRANCE_INTERACTION_TYPE)))
|
||||
interaction.Reset();
|
||||
}
|
||||
|
||||
void DelveMgr::EnterDelve(Player* player, DelveTemplate const& tmpl, uint8 tier)
|
||||
{
|
||||
if (!player || !player->IsInWorld() || tier == 0 || tier > MAX_DELVE_TIER)
|
||||
return;
|
||||
|
||||
// eversong sent CMSG_SELECT_DELVE_ENTRANCE_TIER twice (1844955 and 1846555, the second after
|
||||
// CMSG_AUTH_CONTINUED_SESSION); retail had already acted on the first (REPORT.md 1.4)
|
||||
if (player->IsBeingTeleported() || player->GetMapId() == tmpl.MapId)
|
||||
return;
|
||||
|
||||
// Remember where to come back to: retail returns the player to the delve's exit coordinates on the map
|
||||
// they entered from - Harandar 2694 for the Gulf of Memory, map 0 for the Shadow Enclave (REPORT.md 1.5 / 5).
|
||||
if (!GetDelveTemplate(player->GetMapId()))
|
||||
{
|
||||
if (tmpl.ExitX != 0.0f || tmpl.ExitY != 0.0f)
|
||||
player->m_delveReturnLocation = WorldLocation(player->GetMapId(), tmpl.ExitX, tmpl.ExitY, tmpl.ExitZ, tmpl.ExitO);
|
||||
else
|
||||
player->m_delveReturnLocation = player->GetWorldLocation();
|
||||
}
|
||||
|
||||
player->m_delveSelectedMapId = tmpl.MapId;
|
||||
player->m_delveSelectedTier = tier;
|
||||
|
||||
// the picker closes with the selection; the choice-clear that precedes SMSG_NEW_WORLD is sent by Player::TeleportTo
|
||||
player->PlayerTalkClass->GetInteractionData().Reset();
|
||||
|
||||
// Keep the client-side JamDelveData progression mirror's last-selected delve current.
|
||||
DelvesRewards::PublishProgress(player);
|
||||
|
||||
// REPORT.md 1.6: the per-run world states. Retail delivers them with SMSG_INIT_WORLD_STATES of the delve map and
|
||||
// re-sends them as SMSG_UPDATE_WORLD_STATE at the same tick (gulf 101842); OnPlayerEnteredDelve puts them on the
|
||||
// delve Map for that. Sending them ahead of the transfer keeps the picker / HUD consistent while the transfer is
|
||||
// pending (branch behaviour inherited from npc_delve_entrance::OnGossipSelect, not on the wire).
|
||||
player->SendUpdateWorldState(WS_DELVE_TIER, tier);
|
||||
player->SendUpdateWorldState(WS_DELVE_IN_DELVE_FLAG, 1);
|
||||
player->SendUpdateWorldState(WS_DELVE_MAP_ID, tmpl.MapId);
|
||||
player->SendUpdateWorldState(WS_DELVE_TIER_SPELL, GetTierSpellId(tier));
|
||||
if (tmpl.WorldState26903)
|
||||
player->SendUpdateWorldState(WS_DELVE_UNKNOWN_26903, tmpl.WorldState26903);
|
||||
|
||||
TC_LOG_DEBUG("scripts.delves", "DelveMgr::EnterDelve: player {} -> map {} tier {} (return to map {})",
|
||||
player->GetName(), tmpl.MapId, tier, player->m_delveReturnLocation.GetMapId());
|
||||
|
||||
// REPORT.md 1.5: no SMSG_TRANSFER_PENDING, SMSG_NEW_WORLD reason 21 (seamless) ~1.9 s after the select.
|
||||
// Player::TeleportTo keeps TELE_TO_SEAMLESS for delve maps although they are not cosmetic children of the
|
||||
// outdoor map. The map difficulty resolves to 208 through the MapDifficulty downscale fallback (REPORT.md 1.7).
|
||||
player->TeleportTo(tmpl.MapId, tmpl.EntryX, tmpl.EntryY, tmpl.EntryZ, tmpl.EntryO, TELE_TO_SEAMLESS);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// REPORT.md 4 / work item 6: the hidden entry quest. Preferred path is the real quest template (so the quest log /
|
||||
// criteria side effects match retail); when it is not in the world DB - or cannot be taken again - the same rewards
|
||||
// are granted directly with DisplayToastMethod::QuestComplete (16) toasts, as observed in toasts_items.txt.
|
||||
void GrantDelveEntryRewards(Player* player)
|
||||
{
|
||||
if (Quest const* quest = sObjectMgr->GetQuestTemplate(DELVE_ENTRY_REWARD_QUEST_ID))
|
||||
{
|
||||
if (player->CanTakeQuest(quest, false) && player->CanAddQuest(quest, false))
|
||||
{
|
||||
player->AddQuestAndCheckCompletion(quest, nullptr);
|
||||
if (player->GetQuestStatus(DELVE_ENTRY_REWARD_QUEST_ID) == QUEST_STATUS_COMPLETE && player->CanRewardQuest(quest, false))
|
||||
player->RewardQuest(quest, LootItemType::Item, 0, nullptr, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
player->AddCurrency(CURRENCY_COFFER_KEY_SHARDS, DELVE_ENTRY_REWARD_SHARDS, CurrencyGainSource::QuestReward);
|
||||
player->SendDisplayToast(CURRENCY_COFFER_KEY_SHARDS, DisplayToastType::NewCurrency, false, DELVE_ENTRY_REWARD_SHARDS,
|
||||
DisplayToastMethod::QuestComplete, DELVE_ENTRY_REWARD_QUEST_ID);
|
||||
|
||||
player->AddCurrency(CURRENCY_VOIDLIGHT_MARL, DELVE_ENTRY_REWARD_MARL, CurrencyGainSource::QuestReward);
|
||||
player->SendDisplayToast(CURRENCY_VOIDLIGHT_MARL, DisplayToastType::NewCurrency, false, DELVE_ENTRY_REWARD_MARL,
|
||||
DisplayToastMethod::QuestComplete, DELVE_ENTRY_REWARD_QUEST_ID);
|
||||
|
||||
if (sObjectMgr->GetItemTemplate(DELVE_ENTRY_REWARD_ITEM))
|
||||
{
|
||||
ItemPosCountVec dest;
|
||||
if (player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, DELVE_ENTRY_REWARD_ITEM, 1) == EQUIP_ERR_OK)
|
||||
{
|
||||
if (Item* item = player->StoreNewItem(dest, DELVE_ENTRY_REWARD_ITEM, true, GenerateItemRandomBonusListId(DELVE_ENTRY_REWARD_ITEM), {}, ItemContext::Quest_Reward))
|
||||
{
|
||||
player->SendNewItem(item, 1, true, false);
|
||||
player->SendDisplayToast(0, DisplayToastType::NewItem, false, 1, DisplayToastMethod::QuestComplete, DELVE_ENTRY_REWARD_QUEST_ID, item);
|
||||
}
|
||||
}
|
||||
else
|
||||
player->SendItemRetrievalMail(DELVE_ENTRY_REWARD_ITEM, 1, ItemContext::Quest_Reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DelveMgr::OnPlayerEnteredDelve(Player* player)
|
||||
{
|
||||
if (!player || !player->IsInWorld())
|
||||
return;
|
||||
|
||||
Map* map = player->GetMap();
|
||||
DelveTemplate const* tmpl = GetDelveTemplate(map->GetId());
|
||||
if (!tmpl)
|
||||
return;
|
||||
|
||||
// The run's tier: what the first entrant put on the map, else this player's selection (group members who were
|
||||
// brought in without selecting keep the instance's tier).
|
||||
uint8 tier = uint8(std::clamp<int32>(map->GetWorldStateValue(WS_DELVE_TIER), 0, MAX_DELVE_TIER));
|
||||
if (!tier)
|
||||
tier = std::clamp<uint8>(player->m_delveSelectedTier, 1, MAX_DELVE_TIER);
|
||||
player->m_delveSelectedTier = tier;
|
||||
player->m_delveSelectedMapId = tmpl->MapId;
|
||||
|
||||
// REPORT.md 1.6: SMSG_INIT_WORLD_STATES of the delve map carries these and they are re-sent as
|
||||
// SMSG_UPDATE_WORLD_STATE at the same tick. Setting them on the Map makes INIT carry them for everyone who
|
||||
// enters later and broadcasts the UPDATE on change; an unchanged value is re-sent to this player only.
|
||||
auto publish = [&](uint32 worldStateId, int32 value)
|
||||
{
|
||||
if (!map->GetWorldStateValues().contains(int32(worldStateId)) || map->GetWorldStateValue(int32(worldStateId)) != value)
|
||||
map->SetWorldStateValue(int32(worldStateId), value, false);
|
||||
else
|
||||
player->SendUpdateWorldState(worldStateId, uint32(value));
|
||||
};
|
||||
|
||||
publish(WS_DELVE_TIER, tier);
|
||||
publish(WS_DELVE_IN_DELVE_FLAG, 1);
|
||||
publish(WS_DELVE_MAP_ID, int32(tmpl->MapId));
|
||||
publish(WS_DELVE_TIER_SPELL, int32(GetTierSpellId(tier)));
|
||||
if (tmpl->WorldState26903)
|
||||
publish(WS_DELVE_UNKNOWN_26903, int32(tmpl->WorldState26903));
|
||||
if (tmpl->LfgDungeonsId)
|
||||
publish(WS_DELVE_LFG_DUNGEONS_ID, int32(tmpl->LfgDungeonsId));
|
||||
publish(WS_DELVE_COMPLETE, 0);
|
||||
publish(WS_DELVE_ENCOUNTER_IN_PROGRESS, 0);
|
||||
|
||||
// REPORT.md 4 (work item 6): the hidden entry quest completes ~6.6 s after SMSG_NEW_WORLD (gulf: NEW_WORLD 101829,
|
||||
// quest credit + toasts ~108400). Once per run: keyed on the instance id so a relog into the same run does not
|
||||
// grant twice. The event lives on the player's own processor and dies with the player.
|
||||
if (player->m_delveEntryRewardInstanceId == map->GetInstanceId())
|
||||
return;
|
||||
|
||||
player->m_delveEntryRewardInstanceId = map->GetInstanceId();
|
||||
|
||||
uint32 const mapId = map->GetId();
|
||||
uint32 const instanceId = map->GetInstanceId();
|
||||
player->m_Events.AddEventAtOffset([player, mapId, instanceId]()
|
||||
{
|
||||
if (!player->IsInWorld() || player->GetMapId() != mapId || player->GetInstanceId() != instanceId)
|
||||
return;
|
||||
|
||||
GrantDelveEntryRewards(player);
|
||||
}, Milliseconds(DELVE_ENTRY_REWARD_DELAY_MS));
|
||||
}
|
||||
|
||||
void DelveMgr::LeaveDelve(Player* player)
|
||||
{
|
||||
if (!player || !player->IsInWorld() || player->IsBeingTeleported())
|
||||
return;
|
||||
|
||||
DelveTemplate const* tmpl = GetDelveTemplate(player->GetMapId());
|
||||
if (!tmpl)
|
||||
return;
|
||||
|
||||
// Destination: the map the player entered from at the exit coordinates (REPORT.md 5 step 6: gulf back to 2694
|
||||
// (46.226, 810.912, 1109.843), eversong back to 0 (4780.455, -4118.306, 32.133)), else the template's exit map,
|
||||
// else homebind. Never another delve.
|
||||
WorldLocation destination = player->m_delveReturnLocation;
|
||||
if (destination.GetMapId() == MAPID_INVALID || GetDelveTemplate(destination.GetMapId()))
|
||||
{
|
||||
if (tmpl->ExitMapId >= 0 && (tmpl->ExitX != 0.0f || tmpl->ExitY != 0.0f))
|
||||
destination = WorldLocation(uint32(tmpl->ExitMapId), tmpl->ExitX, tmpl->ExitY, tmpl->ExitZ, tmpl->ExitO);
|
||||
else
|
||||
destination = player->m_homebind;
|
||||
}
|
||||
|
||||
// REPORT.md 5 step 6: SMSG_SPELL_VISUAL_LOAD_SCREEN (kit 79917, 1500 ms) right after the spell-click on the
|
||||
// Leave-O-Bot - a seamless SMSG_NEW_WORLD shows no loading screen of its own, this kit covers the swap.
|
||||
WorldPackets::Spells::SpellVisualLoadScreen loadScreen{ int32(DELVE_EXIT_LOAD_SCREEN_KIT_ID), Milliseconds(DELVE_EXIT_LOAD_SCREEN_DURATION_MS) };
|
||||
player->SendDirectMessage(loadScreen.Write());
|
||||
|
||||
// Branch behaviour kept from go_leave_delve (not on the wire: retail only sends SMSG_INIT_WORLD_STATES of the
|
||||
// outdoor map after the transfer): zero the per-run states so the HUD drops out of delve mode.
|
||||
player->SendUpdateWorldState(WS_DELVE_TIER, 0);
|
||||
player->SendUpdateWorldState(WS_DELVE_IN_DELVE_FLAG, 0);
|
||||
player->SendUpdateWorldState(WS_DELVE_MAP_ID, 0);
|
||||
player->SendUpdateWorldState(WS_DELVE_TIER_SPELL, 0);
|
||||
player->SendUpdateWorldState(WS_DELVE_UNKNOWN_26903, 0);
|
||||
|
||||
player->ClearDelveData(int32(tmpl->MapId));
|
||||
player->m_delveReturnLocation = WorldLocation();
|
||||
player->m_delveSelectedTier = 0;
|
||||
player->m_delveSelectedMapId = 0;
|
||||
|
||||
TC_LOG_DEBUG("scripts.delves", "DelveMgr::LeaveDelve: player {} leaves map {} -> map {} ({:.1f} {:.1f} {:.1f})",
|
||||
player->GetName(), tmpl->MapId, destination.GetMapId(), destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ());
|
||||
|
||||
// REPORT.md 5: the transfer back is seamless as well (SMSG_NEW_WORLD, no SMSG_TRANSFER_PENDING). Retail's
|
||||
// NEW_WORLD followed the load screen by several seconds (phase / object despawns and a server hop); here the
|
||||
// transfer runs once the load-screen kit has faded in, on the player's own event processor.
|
||||
uint32 const mapId = player->GetMapId();
|
||||
uint32 const instanceId = player->GetInstanceId();
|
||||
player->m_Events.AddEventAtOffset([player, mapId, instanceId, destination]()
|
||||
{
|
||||
if (!player->IsInWorld() || player->IsBeingTeleported() || player->GetMapId() != mapId || player->GetInstanceId() != instanceId)
|
||||
return;
|
||||
|
||||
player->TeleportTo(destination, TELE_TO_SEAMLESS);
|
||||
}, Milliseconds(DELVE_EXIT_LOAD_SCREEN_DURATION_MS));
|
||||
}
|
||||
|
||||
} // namespace Delves
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <vector>
|
||||
|
||||
class Creature;
|
||||
class Player;
|
||||
struct DelvesSeasonEntry;
|
||||
struct PlayerCompanionInfoEntry;
|
||||
|
||||
@@ -90,6 +91,31 @@ namespace Delves
|
||||
std::vector<TieredEntranceTierData> const& GetTieredEntranceTiers() const { return _tieredEntranceTiers; }
|
||||
TieredEntranceTierData const* GetTieredEntranceTier(uint32 tieredEntranceTierId) const;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Retail run flow (12.1.0.69497 captures, C:\sniff\tcharvest\out\delve_research\REPORT.md).
|
||||
// Shared by WorldSession::Handle* (DelvesHandler.cpp) and the entrance / instance scripts.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// Builds and sends SMSG_TIERED_ENTRANCE_OPEN_RESPONSE for the delve this entrance spawn opens (REPORT.md 1.3).
|
||||
// Used by the click path (CMSG_TIERED_ENTRANCE_OPEN) and by OpenEntranceByProximity.
|
||||
void SendTieredEntranceOpen(Player* player, Creature const* entrance);
|
||||
// REPORT.md 1.1 (work item 2): a delve entrance (creature 212407 / 251896) opens by itself when the player comes
|
||||
// into range - SMSG_NPC_INTERACTION_OPEN_RESULT(entrance, type 79) followed by the tier picker. Sent once per
|
||||
// approach: the open interaction is tracked in the player's InteractionData, cleared by CMSG_CLOSE_INTERACTION
|
||||
// (gulf 95751), by CloseEntranceByProximity and when the player leaves the map (Player::RemoveFromWorld).
|
||||
void OpenEntranceByProximity(Player* player, Creature const* entrance);
|
||||
// Counterpart for the entrance AI when the player leaves range without the client closing the interaction.
|
||||
void CloseEntranceByProximity(Player* player, Creature const* entrance);
|
||||
// REPORT.md 1.4 - 1.6 (work item 5): tier selected -> remember where to return, publish the selection and
|
||||
// seamlessly transfer to the delve map (SMSG_NEW_WORLD reason 21, no SMSG_TRANSFER_PENDING).
|
||||
void EnterDelve(Player* player, DelveTemplate const& tmpl, uint8 tier);
|
||||
// Called from the delve instance script's OnPlayerEnter: sets the observed world states on the delve map
|
||||
// (REPORT.md 1.6) and schedules the hidden entry quest reward once per run (REPORT.md 4, work item 6).
|
||||
void OnPlayerEnteredDelve(Player* player);
|
||||
// REPORT.md 5 (work item 11): load screen kit 79917 then seamless transfer to the exit coordinates on the
|
||||
// map the player entered from (fallback: delve_template exit map, then homebind).
|
||||
void LeaveDelve(Player* player);
|
||||
|
||||
private:
|
||||
void LoadDelveTemplates();
|
||||
void LoadTierRewards();
|
||||
|
||||
@@ -118,6 +118,9 @@ namespace Delves
|
||||
// These are sent to the client around delve entry/exit to drive the
|
||||
// Blizzard_DelvesDifficultyPicker UI's active-tier display, the in-delve HUD
|
||||
// banner, and the "you're in a delve" persistent flag.
|
||||
// 12.1.0.69497 (REPORT.md 1.6): all of these arrive in SMSG_INIT_WORLD_STATES of the delve map and are re-sent as
|
||||
// SMSG_UPDATE_WORLD_STATE at the same tick (gulf 101842: 24430=1, 26345=1, 26423=2964, 26931=1260940, 26903=1277243,
|
||||
// 5029=3070, 25316=0, 24836=0). They are per instance, so DelveMgr::OnPlayerEnteredDelve sets them on the delve Map.
|
||||
enum DelveWorldStates : uint32
|
||||
{
|
||||
WS_DELVE_TIER = 24430, // Selected tier (1..11)
|
||||
@@ -125,9 +128,36 @@ namespace Delves
|
||||
WS_DELVE_MAP_ID = 26423, // Active delve MapID
|
||||
WS_DELVE_TIER_SPELL = 26931, // The TIER_SPELL_IDS[] value cast for this run
|
||||
WS_DELVE_UNKNOWN_26903 = 26903, // Per-delve, controls center spell display in tier picker
|
||||
WS_DELVE_LFG_DUNGEONS_ID = 5029, // Sent inside the instance (OnPlayerEnter)
|
||||
WS_DELVE_LFG_DUNGEONS_ID = 5029, // LFGDungeons id of the delve (3069/3070/3083, DifficultyID 208)
|
||||
WS_DELVE_COMPLETE = 25316, // 0 -> 1 with SMSG_SCENARIO_COMPLETED (REPORT.md 5 step 2)
|
||||
WS_DELVE_ENCOUNTER_IN_PROGRESS = 24836, // 1 at SMSG_ENCOUNTER_START, 0 at SMSG_ENCOUNTER_END (REPORT.md 1.6)
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry / exit flow (12.1.0.69497 captures, C:\sniff\tcharvest\out\delve_research\REPORT.md)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// REPORT.md 1.1 / 6.1: the delve entrance auto-opens by proximity - the server sends SMSG_NPC_INTERACTION_OPEN_RESULT
|
||||
// (guid of the entrance creature 212407, InteractionType 79, Success) and immediately after it
|
||||
// SMSG_TIERED_ENTRANCE_OPEN_RESPONSE (gulf 88718 and 98205; a CMSG_CLOSE_INTERACTION for the entrance guid at 95751
|
||||
// when the player walked away). DBCEnums.h only knows the value as PlaceholderType79.
|
||||
static constexpr uint32 DELVE_ENTRANCE_INTERACTION_TYPE = 79;
|
||||
|
||||
// REPORT.md 4 / 6.2 (work item 6): ~6.6 s after SMSG_NEW_WORLD retail completes a hidden quest that grants
|
||||
// 25x currency 3310 (Coffer Key Shards) + 100x currency 3316 (Voidlight Marl) + item 263488 with quest-complete toasts.
|
||||
// 12.1 quest id 96612 (12.0.1 deatholme: 93943).
|
||||
static constexpr uint32 DELVE_ENTRY_REWARD_QUEST_ID = 96612;
|
||||
static constexpr uint32 CURRENCY_VOIDLIGHT_MARL = 3316;
|
||||
static constexpr uint32 DELVE_ENTRY_REWARD_SHARDS = 25;
|
||||
static constexpr uint32 DELVE_ENTRY_REWARD_MARL = 100;
|
||||
static constexpr uint32 DELVE_ENTRY_REWARD_ITEM = 263488;
|
||||
static constexpr uint32 DELVE_ENTRY_REWARD_DELAY_MS = 6600;
|
||||
|
||||
// REPORT.md 5 step 6: CMSG_SPELL_CLICK on the Leave-O-Bot (gulf 1118881) -> SMSG_SPELL_VISUAL_LOAD_SCREEN
|
||||
// (kit 79917, 1500 ms) -> ... -> SMSG_NEW_WORLD back to the originating map at the exit coordinates.
|
||||
static constexpr uint32 DELVE_EXIT_LOAD_SCREEN_KIT_ID = 79917;
|
||||
static constexpr uint32 DELVE_EXIT_LOAD_SCREEN_DURATION_MS = 1500;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bountiful Delves
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -344,6 +374,10 @@ namespace Delves
|
||||
float ExitY = 0.0f;
|
||||
float ExitZ = 0.0f;
|
||||
float ExitO = 0.0f;
|
||||
// Overworld map the exit coordinates belong to (-1 = unknown). REPORT.md 1.5: the Gulf of Memory returns to
|
||||
// Harandar 2694, the Shadow Enclave and the Darkway to map 0. DelveMgr::LeaveDelve prefers the map the player
|
||||
// actually came from (stored at entry) and falls back to this.
|
||||
int32 ExitMapId = -1;
|
||||
|
||||
// Per-delve scenario IDs. ActiveScenarioId is the in-progress scenario;
|
||||
// RewardScenarioId is the completion scenario (often shared across delves ?
|
||||
|
||||
@@ -16,18 +16,63 @@
|
||||
*/
|
||||
|
||||
#include "DelvesRewards.h"
|
||||
#include "Config.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "DelveMgr.h"
|
||||
#include "DelvesCompanion.h"
|
||||
#include "DelvesSeason.h"
|
||||
#include "ItemBonusMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Loot.h"
|
||||
#include "LootMgr.h"
|
||||
#include "Mail.h"
|
||||
#include "Player.h"
|
||||
#include "ReputationMgr.h"
|
||||
#include <Config.h>
|
||||
#include "WeeklyRewardsMgr.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
namespace Delves
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
// Rolls a reference_loot_template pool as personal loot at the delve item context and grants every item,
|
||||
// scaled through ItemBonusMgr (the same authentic ilvl path the M+ rewards use). Mails on full bags.
|
||||
void GrantDelveLoot(Player* player, uint32 lootId, uint8 itemContext)
|
||||
{
|
||||
if (!lootId || !LootTemplates_Reference.HaveLootFor(lootId))
|
||||
return;
|
||||
|
||||
Loot loot(player->GetMap(), ObjectGuid::Empty, LOOT_NONE, nullptr);
|
||||
loot.FillLoot(lootId, LootTemplates_Reference, player, true /*personal*/, true /*noEmptyError*/,
|
||||
LOOT_MODE_DEFAULT, ItemContext(itemContext));
|
||||
|
||||
for (LootItem const& lootItem : loot.items)
|
||||
{
|
||||
if (!lootItem.itemid || !lootItem.count)
|
||||
continue;
|
||||
|
||||
std::vector<int32> bonuses = ItemBonusMgr::GetBonusListsForItem(lootItem.itemid,
|
||||
ItemBonusMgr::ItemBonusGenerationParams(ItemContext(itemContext)));
|
||||
|
||||
ItemPosCountVec dest;
|
||||
if (player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, lootItem.itemid, lootItem.count) == EQUIP_ERR_OK)
|
||||
player->StoreNewItem(dest, lootItem.itemid, true, 0, GuidSet(), ItemContext(itemContext), &bonuses);
|
||||
else if (Item* item = Item::CreateItem(lootItem.itemid, lootItem.count, ItemContext(itemContext), player, false))
|
||||
{
|
||||
item->SetBonuses(bonuses);
|
||||
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
|
||||
item->SaveToDB(trans);
|
||||
MailDraft("Delve Reward", "Your delve reward.")
|
||||
.AddItem(item)
|
||||
.SendMailTo(trans, player, MailSender(player, MAIL_STATIONERY_GM), MAIL_CHECK_MASK_COPIED);
|
||||
CharacterDatabase.CommitTransaction(trans);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DelvesRewards::AwardDelveCompletion(Player* player, uint8 tier, bool bountiful, bool hasRevivesRemaining)
|
||||
{
|
||||
uint32 accountId = player->GetSession()->GetBattlenetAccountId();
|
||||
@@ -47,6 +92,11 @@ namespace Delves
|
||||
// Award companion XP
|
||||
AwardCompanionXP(player, tier);
|
||||
|
||||
// End-of-run gear (non-bountiful context; retail caps this at the tier-3 track). The item POOL is
|
||||
// server content (reference_loot_template, Delves.Reward.LootId); the LEVEL comes from the item context.
|
||||
GrantDelveLoot(player, uint32(sConfigMgr->GetIntDefault("Delves.Reward.LootId", 0)),
|
||||
GetItemContextForTier(std::min<uint8>(tier, 3), false));
|
||||
|
||||
// Handle bountiful rewards
|
||||
if (bountiful)
|
||||
{
|
||||
@@ -55,14 +105,23 @@ namespace Delves
|
||||
if (hasRevivesRemaining && HasCofferKey(player))
|
||||
{
|
||||
ConsumeCofferKey(player);
|
||||
// TODO: Spawn Bountiful Coffer with enhanced loot
|
||||
// Bountiful Coffer: the enhanced chest loot at the tier's bounty item context.
|
||||
GrantDelveLoot(player, uint32(sConfigMgr->GetIntDefault("Delves.Coffer.LootId", 0)),
|
||||
GetItemContextForTier(tier, true));
|
||||
TC_LOG_DEBUG("scripts.delves", "Player {} opened Bountiful Coffer (tier {}, ItemContext {})",
|
||||
player->GetName(), tier, GetItemContextForTier(tier, true));
|
||||
}
|
||||
}
|
||||
|
||||
// Update Great Vault progress
|
||||
UpdateGreatVaultProgress(player, tier);
|
||||
// Great Vault: an endgame delve completion credits the vault's World activity row - the row the client
|
||||
// fills from WeeklyRewardChestThreshold.db2 Type 6 (live ids 196/197/198, slots at 2/4/8 completions).
|
||||
// The level recorded per run is the delve TIER, so each slot is awarded the Nth-best tier of the week,
|
||||
// exactly as a Mythic+ run credits the Dungeon row at its keystone level. The reward ITEM LEVEL that
|
||||
// retail derives from that tier is content the server does not have a full table for yet (only the two
|
||||
// endpoints are documented: T1 = 233, T8+ = 259, capped at Hero 1/6), so no ilvl is fabricated here -
|
||||
// the vault advertises the slot and its tier, and the item comes from the vault reward pool.
|
||||
if (tier >= DELVE_TIER_ENDGAME_START)
|
||||
sWeeklyRewardsMgr.RecordActivity(player, WeeklyRewards::ActivityType::World, tier);
|
||||
|
||||
// Check for tier unlock (must have revives remaining)
|
||||
if (hasRevivesRemaining && CanUnlockNextTier(progress.HighestTierUnlocked, tier, hasRevivesRemaining))
|
||||
@@ -87,10 +146,23 @@ namespace Delves
|
||||
if (!reward || reward->CrestType == CREST_NONE || reward->CrestCount == 0)
|
||||
return;
|
||||
|
||||
// TODO: Award actual crest currency items based on reward->CrestType and reward->CrestCount
|
||||
// CrestType maps to specific currency IDs in the game
|
||||
TC_LOG_DEBUG("scripts.delves", "Awarding {} crests of type {} to {} for tier {}",
|
||||
reward->CrestCount, reward->CrestType, player->GetName(), tier);
|
||||
// CrestType -> crest currency (Midnight S1 Dawncrest ladder; config-tunable, guarded on CurrencyTypes.db2
|
||||
// so a wrong/absent id is a safe no-op). Delves top out at Hero crests (T11), matching retail.
|
||||
uint32 currencyId = 0;
|
||||
switch (reward->CrestType)
|
||||
{
|
||||
case CREST_WEATHERED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier1.CurrencyId", 3383)); break; // Adventurer Dawncrest
|
||||
case CREST_CARVED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier2.CurrencyId", 3341)); break; // Veteran Dawncrest
|
||||
case CREST_RUNED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier3.CurrencyId", 3343)); break; // Champion Dawncrest
|
||||
case CREST_GILDED: currencyId = uint32(sConfigMgr->GetIntDefault("Delves.Crest.Tier4.CurrencyId", 3345)); break; // Hero Dawncrest
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (currencyId && sCurrencyTypesStore.LookupEntry(currencyId))
|
||||
player->AddCurrency(currencyId, reward->CrestCount, CurrencyGainSource::Loot);
|
||||
|
||||
TC_LOG_DEBUG("scripts.delves", "Awarded {} crests (currency {}) to {} for tier {}",
|
||||
reward->CrestCount, currencyId, player->GetName(), tier);
|
||||
}
|
||||
|
||||
void DelvesRewards::AwardCompanionXP(Player* player, uint8 tier)
|
||||
@@ -102,11 +174,16 @@ namespace Delves
|
||||
DelvesCompanion::LoadFromDB(player->GetSession()->GetBattlenetAccountId(), state);
|
||||
DelvesCompanion::AwardCompanionXP(player->GetSession()->GetBattlenetAccountId(), state, xpAmount);
|
||||
|
||||
// Mirror the same amount into the retail-visible companion reputation track (Midnight faction 2742
|
||||
// "Delves: Season 1", RenownCurrencyID 3317) so the client's rep/renown UI tracks companion
|
||||
// progression. This is a mirror only - the internal CompanionState math above stays authoritative.
|
||||
// Config-tunable and guarded on Faction.db2, so a wrong/absent id is a safe no-op.
|
||||
if (uint32 factionId = uint32(sConfigMgr->GetIntDefault("Delves.Companion.FactionId", 2742)))
|
||||
// Mirror the same amount into the retail-visible companion reputation track so the client's
|
||||
// rep/renown UI tracks companion progression. This is a mirror only - the internal CompanionState
|
||||
// math above stays authoritative. Config-tunable and guarded on Faction.db2, so a wrong or absent
|
||||
// id is a safe no-op.
|
||||
//
|
||||
// Faction 2744 "Valeera Sanguinar" / "Trusty Delve Companion", NOT 2742 "Delves: Season 1":
|
||||
// PlayerCompanionInfo.db2 row 11 is the Midnight row (DelvesSeasonID 4, TraitTreeID 1168,
|
||||
// CreatureDisplayInfoID 67214) and its FactionID is 2744 - the companion's OWN track, exactly
|
||||
// mirroring Brann's 2640 on rows 1/9/10. 2742 is the season faction, which is a different thing.
|
||||
if (uint32 factionId = uint32(sConfigMgr->GetIntDefault("Delves.Companion.FactionId", 2744)))
|
||||
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
|
||||
player->GetReputationMgr().ModifyReputation(factionEntry, int32(xpAmount));
|
||||
|
||||
@@ -152,29 +229,6 @@ namespace Delves
|
||||
actualAmount, player->GetName(), progress.WeeklyCofferShards, MAX_COFFER_KEY_SHARDS_PER_WEEK);
|
||||
}
|
||||
|
||||
void DelvesRewards::UpdateGreatVaultProgress(Player* player, uint8 tier)
|
||||
{
|
||||
if (tier < DELVE_TIER_ENDGAME_START) // Only tier 4+ counts for vault
|
||||
return;
|
||||
|
||||
// Great Vault tracking uses the existing WeeklyRewardChest system
|
||||
// Completions at 2/4/8 unlock vault slots
|
||||
// The highest tier completed determines the vault reward ilvl
|
||||
// TODO: Integrate with existing WeeklyRewardChestActivity system
|
||||
TC_LOG_DEBUG("scripts.delves", "Updated Great Vault progress for {} (tier {})", player->GetName(), tier);
|
||||
}
|
||||
|
||||
uint8 DelvesRewards::GetGreatVaultSlotCount(uint32 /*weeklyCompletions*/)
|
||||
{
|
||||
//if (weeklyCompletions >= VAULT_SLOT_3_COMPLETIONS) // 8
|
||||
// return 3;
|
||||
//if (weeklyCompletions >= VAULT_SLOT_2_COMPLETIONS) // 4
|
||||
// return 2;
|
||||
//if (weeklyCompletions >= VAULT_SLOT_1_COMPLETIONS) // 2
|
||||
// return 1; TODO THOR
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool DelvesRewards::CanUnlockNextTier(uint8 currentHighest, uint8 completedTier, bool hasRevivesRemaining)
|
||||
{
|
||||
// Must complete at current highest tier with revives remaining to unlock next
|
||||
@@ -225,6 +279,14 @@ namespace Delves
|
||||
SaveProgress(battlenetAccountId, progress);
|
||||
}
|
||||
|
||||
void DelvesRewards::ResetAllWeeklyProgress()
|
||||
{
|
||||
// Weekly rollover for every account at once; online players' cached progress reloads on next use.
|
||||
CharacterDatabase.Execute("UPDATE delve_progress SET weeklyCompletions = 0, highestTierThisWeek = 0, "
|
||||
"weeklyBountifulCount = 0, weeklyCofferShards = 0");
|
||||
TC_LOG_INFO("delves", "DelvesRewards: weekly delve progress reset.");
|
||||
}
|
||||
|
||||
void DelvesRewards::PublishProgress(Player* player)
|
||||
{
|
||||
DelveProgress progress;
|
||||
|
||||
@@ -51,9 +51,8 @@ namespace Delves
|
||||
static void ConsumeCofferKey(Player* player);
|
||||
static void AwardCofferKeyShards(Player* player, uint32 amount);
|
||||
|
||||
// Great Vault tracking
|
||||
static void UpdateGreatVaultProgress(Player* player, uint8 tier);
|
||||
static uint8 GetGreatVaultSlotCount(uint32 weeklyCompletions);
|
||||
// Great Vault tracking has no delve-private duplicate: AwardDelveCompletion credits the vault's World
|
||||
// activity row through WeeklyRewardsMgr, which owns the slot thresholds and the weekly period.
|
||||
|
||||
// Tier unlock validation
|
||||
static bool CanUnlockNextTier(uint8 currentHighest, uint8 completedTier, bool hasRevivesRemaining);
|
||||
@@ -62,6 +61,8 @@ namespace Delves
|
||||
static void LoadProgress(uint32 battlenetAccountId, DelveProgress& progress);
|
||||
static void SaveProgress(uint32 battlenetAccountId, DelveProgress const& progress);
|
||||
static void ResetWeeklyProgress(uint32 battlenetAccountId, DelveProgress& progress);
|
||||
// Global weekly rollover: zeroes every account's weekly counters (hooked into World::ResetWeeklyQuests).
|
||||
static void ResetAllWeeklyProgress();
|
||||
|
||||
// Pushes DelveProgress to the client via the ActivePlayer JamDelveData mirror
|
||||
// (SMSG_UPDATE_OBJECT). Call after any progress mutation while the player is
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
#include "MythicPlusData.h"
|
||||
#include "PerksProgramMgr.h"
|
||||
#include "DelvesCompanion.h"
|
||||
#include "DelveMgr.h"
|
||||
#include "DelvesDefines.h"
|
||||
#include "DelvesRewards.h"
|
||||
#include "GarrisonPackets.h"
|
||||
@@ -1387,6 +1388,7 @@ bool Player::TeleportTo(TeleportLocation const& teleportLocation, TeleportToOpti
|
||||
|
||||
// Seamless teleport can happen only if cosmetic maps match
|
||||
if (!oldmap ||
|
||||
//(!delveTransfer && todo thor
|
||||
(oldmap->GetEntry()->CosmeticParentMapID != int32(teleportLocation.Location.GetMapId()) && int32(GetMapId()) != mEntry->CosmeticParentMapID &&
|
||||
!((oldmap->GetEntry()->CosmeticParentMapID != -1) ^ (oldmap->GetEntry()->CosmeticParentMapID != mEntry->CosmeticParentMapID))))
|
||||
options &= ~TELE_TO_SEAMLESS;
|
||||
@@ -1581,6 +1583,13 @@ void Player::RemoveFromWorld()
|
||||
m_lootRolls.clear();
|
||||
sOutdoorPvPMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId);
|
||||
sBattlefieldMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId);
|
||||
|
||||
//WowCommunity
|
||||
// A delve entrance opened by proximity (DelveMgr::OpenEntranceByProximity, interaction type 79) is tracked in
|
||||
// the interaction data so it is sent once per approach; leaving the map ends the approach.
|
||||
if (PlayerTalkClass->GetInteractionData().Type == PlayerInteractionType::PlaceholderType79)
|
||||
PlayerTalkClass->GetInteractionData().Reset();
|
||||
//WowCommunity
|
||||
}
|
||||
|
||||
GetSession()->GetBattlenetAccount().RemoveFromWorld();
|
||||
|
||||
@@ -2416,6 +2416,12 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
// Transient per-session selection from CMSG_SELECT_DELVE_ENTRANCE_TIER (re-sent by client on TIERED_ENTRANCE_OPEN).
|
||||
uint8 m_delveSelectedTier = 0;
|
||||
uint32 m_delveSelectedMapId = 0;
|
||||
// Where DelveMgr::LeaveDelve sends the player back: the map they entered the delve from, at the
|
||||
// delve_template exit coordinates (12.1 captures: gulf 2694, eversong/deatholme 0 - REPORT.md 1.5/5).
|
||||
// MAPID_INVALID when no entry was seen (relog inside the delve).
|
||||
WorldLocation m_delveReturnLocation;
|
||||
// Instance id that already received the hidden entry quest reward (DelveMgr::OnPlayerEnteredDelve, once per run)
|
||||
uint32 m_delveEntryRewardInstanceId = 0;
|
||||
//WowCommunity
|
||||
protected:
|
||||
UF::UpdateFieldFlag GetUpdateFieldFlagsFor(Player const* target) const override;
|
||||
|
||||
+220
-192
@@ -39,8 +39,11 @@
|
||||
#include "UpdateData.h"
|
||||
#include "WorldSession.h"
|
||||
//WowCommunity
|
||||
#include "Creature.h"
|
||||
#include "RecentAlliesMgr.h"
|
||||
#include "LFG.h"
|
||||
#include "Map.h"
|
||||
#include "TemporarySummon.h"
|
||||
//WowCommunity
|
||||
Seconds Group::CountdownInfo::GetTimeLeft() const
|
||||
{
|
||||
@@ -562,6 +565,15 @@ bool Group::AddMember(Player* player)
|
||||
|
||||
bool Group::RemoveMember(ObjectGuid guid, RemoveMethod method /*= GROUP_REMOVEMETHOD_DEFAULT*/, ObjectGuid kicker /*= 0*/, const char* reason /*= nullptr*/)
|
||||
{
|
||||
// Follower dungeon bots live in m_memberSlots but are not real players.
|
||||
// Never run player-leave / disband logic for them.
|
||||
if (guid.IsAnyTypeCreature())
|
||||
{
|
||||
RemoveFollowerBotSlot(guid);
|
||||
SendUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
BroadcastGroupUpdate();
|
||||
|
||||
sScriptMgr->OnGroupRemoveMember(this, guid, method, kicker, reason);
|
||||
@@ -871,7 +883,7 @@ void Group::SendUpdateToPlayer(Player* player, MemberSlot const* slot /*= nullpt
|
||||
if (slot->guid == citr->guid)
|
||||
partyUpdate.MyIndex = index;
|
||||
|
||||
Player* member = ObjectAccessor::FindConnectedPlayer(citr->guid);
|
||||
Player* member = citr->guid.IsPlayer() ? ObjectAccessor::FindConnectedPlayer(citr->guid) : nullptr;
|
||||
|
||||
WorldPackets::Party::PartyPlayerInfo& playerInfos = partyUpdate.PlayerList.emplace_back();
|
||||
|
||||
@@ -881,7 +893,10 @@ void Group::SendUpdateToPlayer(Player* player, MemberSlot const* slot /*= nullpt
|
||||
|
||||
playerInfos.FactionGroup = Player::GetFactionGroupForRace(citr->race);
|
||||
|
||||
playerInfos.Connected = member && member->GetSession() && !member->GetSession()->PlayerLogout();
|
||||
if (citr->guid.IsAnyTypeCreature())
|
||||
playerInfos.Connected = true;
|
||||
else
|
||||
playerInfos.Connected = member && member->GetSession() && !member->GetSession()->PlayerLogout();
|
||||
|
||||
playerInfos.Subgroup = citr->group; // groupid
|
||||
playerInfos.Flags = citr->flags; // See enum GroupMemberFlags
|
||||
@@ -1895,239 +1910,252 @@ void Group::SetRestrictPingsTo(RestrictPingsTo restrictTo)
|
||||
//WowCommunity
|
||||
enum followerbots
|
||||
{
|
||||
BOT_HEALER = 209072, //Crenna
|
||||
BOT_TANK = 209057, //Garrick
|
||||
BOT_DPS_1 = 209065, //austin
|
||||
BOT_DPS_2 = 214390, //Shuja Grimaxe
|
||||
BOT_DPS_3 = 209059, //Meredy
|
||||
BOT_HEALER = 209072, // Crenna
|
||||
BOT_TANK = 209057, // Garrick
|
||||
BOT_DPS_1 = 209065, // Austin
|
||||
BOT_DPS_2 = 214390, // Shuja Grimaxe
|
||||
BOT_DPS_3 = 209059, // Meredy
|
||||
SPELL_DUNGEON_ASSISTENCE = 426756,
|
||||
};
|
||||
|
||||
static uint32 const FollowerBotEntries[] = { BOT_HEALER, BOT_TANK, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
|
||||
|
||||
bool Group::IsFollowerDungeon(Player const* player) const
|
||||
{
|
||||
if (!player)
|
||||
return false;
|
||||
|
||||
if (player->GetDungeonDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
|
||||
return true;
|
||||
|
||||
if (player->GetMap() && player->GetMap()->GetDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
|
||||
return true;
|
||||
|
||||
auto isFollowerDungeonId = [](uint32 dungeonId) -> bool
|
||||
{
|
||||
if (!dungeonId)
|
||||
return false;
|
||||
|
||||
if (LFGDungeonsEntry const* dungeon = sLFGDungeonsStore.LookupEntry(dungeonId))
|
||||
return dungeon->DifficultyID == DIFFICULTY_FOLLOWER_DUNGEON;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (isFollowerDungeonId(sLFGMgr->GetDungeon(player->GetGUID())))
|
||||
return true;
|
||||
|
||||
if (isLFGGroup() && isFollowerDungeonId(sLFGMgr->GetDungeon(GetGUID())))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Group::AddFollowerModeBots(Player* player)
|
||||
{
|
||||
if (!player->GetGroup() || !player->GetGroup()->GetLeaderGUID())
|
||||
if (!player || player->GetGroup() != this || !GetLeaderGUID())
|
||||
return;
|
||||
|
||||
// Check if this is a follower dungeon - either by difficulty ID or by current map difficulty
|
||||
bool isFollowerDungeon = false;
|
||||
if (player->GetGroup()->isLFGGroup())
|
||||
if (!IsFollowerDungeon(player))
|
||||
return;
|
||||
|
||||
// Only the leader summons, and only once per group.
|
||||
if (GetLeaderGUID() != player->GetGUID())
|
||||
return;
|
||||
|
||||
if (!m_followerBots.empty())
|
||||
return;
|
||||
|
||||
for (uint32 entry : FollowerBotEntries)
|
||||
if (player->GetSummonedCreatureByEntry(entry))
|
||||
return;
|
||||
|
||||
player->CastSpell(player, SPELL_DUNGEON_ASSISTENCE);
|
||||
|
||||
std::vector<uint32> botsToSummon;
|
||||
switch (player->GetRoleForGroup())
|
||||
{
|
||||
if (player->GetDungeonDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
|
||||
isFollowerDungeon = true;
|
||||
else if (player->GetMap() && player->GetMap()->GetDifficultyID() == DIFFICULTY_FOLLOWER_DUNGEON)
|
||||
isFollowerDungeon = true;
|
||||
// Also check if the LFG dungeon is marked as follower dungeon
|
||||
else if (sLFGMgr->GetDungeon(player->GetGUID()))
|
||||
{
|
||||
uint32 dungeonId = sLFGMgr->GetDungeon(player->GetGUID());
|
||||
if (LFGDungeonsEntry const* dungeon = sLFGDungeonsStore.LookupEntry(dungeonId))
|
||||
{
|
||||
if (dungeon->DifficultyID == DIFFICULTY_FOLLOWER_DUNGEON)
|
||||
isFollowerDungeon = true;
|
||||
}
|
||||
}
|
||||
case ROLE_TANK:
|
||||
botsToSummon = { BOT_HEALER, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
|
||||
break;
|
||||
case ROLE_HEALER:
|
||||
botsToSummon = { BOT_TANK, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
|
||||
break;
|
||||
default:
|
||||
botsToSummon = { BOT_TANK, BOT_HEALER, BOT_DPS_2, BOT_DPS_3 };
|
||||
break;
|
||||
}
|
||||
|
||||
if (isFollowerDungeon)
|
||||
static float const offsetX[] = { 1.0f, 2.0f, 1.5f, 2.5f };
|
||||
static float const offsetY[] = { 1.5f, 2.5f, 1.0f, 1.0f };
|
||||
|
||||
for (size_t i = 0; i < botsToSummon.size(); ++i)
|
||||
{
|
||||
player->CastSpell(player, SPELL_DUNGEON_ASSISTENCE);
|
||||
|
||||
std::vector<uint32> botsToSummon;
|
||||
|
||||
if (player->IsTankPlayer())
|
||||
if (Creature* bot = player->SummonCreature(botsToSummon[i],
|
||||
player->GetPositionX() + offsetX[i],
|
||||
player->GetPositionY() + offsetY[i],
|
||||
player->GetPositionZ(),
|
||||
player->GetOrientation(), TEMPSUMMON_MANUAL_DESPAWN))
|
||||
{
|
||||
botsToSummon = { BOT_HEALER, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
|
||||
}
|
||||
else if (player->GetRoleForGroup() == ROLE_HEALER)
|
||||
{
|
||||
botsToSummon = { BOT_TANK, BOT_DPS_1, BOT_DPS_2, BOT_DPS_3 };
|
||||
}
|
||||
else if (player->GetRoleForGroup() == ROLE_DAMAGE)
|
||||
{
|
||||
botsToSummon = { BOT_TANK, BOT_HEALER, BOT_DPS_2, BOT_DPS_3 };
|
||||
}
|
||||
|
||||
float offsetX[] = { 1.0f, 2.0f, 1.5f, 2.5f };
|
||||
float offsetY[] = { 1.5f, 2.5f, 1.0f, 1.0f };
|
||||
float offsetZ[] = { 1.0f, -1.0f, 1.5f, 1.5f };
|
||||
|
||||
for (size_t i = 0; i < botsToSummon.size(); ++i)
|
||||
{
|
||||
if (Creature* bot = player->SummonCreature(botsToSummon[i],
|
||||
player->GetPositionX() + offsetX[i],
|
||||
player->GetPositionY() + offsetY[i],
|
||||
player->GetPositionZ() + offsetZ[i],
|
||||
0.0f, TEMPSUMMON_MANUAL_DESPAWN))
|
||||
{
|
||||
// Make the bot appear as a group member
|
||||
bot->SetUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED);
|
||||
bot->SetUnitFlag2(UNIT_FLAG2_REGENERATE_POWER);
|
||||
|
||||
// Set bot as friendly to player and group
|
||||
bot->SetFaction(player->GetFaction());
|
||||
bot->SetLevel(player->GetLevel());
|
||||
|
||||
// Add to group visually (this might need custom implementation)
|
||||
AddBotToGroupDisplay(bot, player);
|
||||
|
||||
// Make bot follow player
|
||||
bot->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, bot->GetFollowAngle());
|
||||
|
||||
// Set bot's owner
|
||||
bot->SetOwnerGUID(player->GetGUID());
|
||||
bot->SetCreatorGUID(player->GetGUID());
|
||||
}
|
||||
bot->SetUnitFlag2(UNIT_FLAG2_REGENERATE_POWER);
|
||||
bot->SetFaction(player->GetFaction());
|
||||
bot->SetLevel(player->GetLevel());
|
||||
bot->SetOwnerGUID(player->GetGUID());
|
||||
bot->SetCreatorGUID(player->GetGUID());
|
||||
AddBotToGroupDisplay(bot, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add bot to group display
|
||||
void Group::AddBotToGroupDisplay(Creature* bot, Player* owner)
|
||||
{
|
||||
if (!bot || !owner)
|
||||
return;
|
||||
|
||||
m_followerBots.push_back(bot->GetGUID());
|
||||
|
||||
bot->SetFaction(owner->GetFaction());
|
||||
bot->SetLevel(owner->GetLevel());
|
||||
bot->SetOwnerGUID(owner->GetGUID());
|
||||
bot->SetCreatorGUID(owner->GetGUID());
|
||||
|
||||
// Create a fake group member entry
|
||||
WorldPackets::Party::PartyUpdate partyUpdate;
|
||||
uint8 roles = lfg::LfgRoles::PLAYER_ROLE_DAMAGE;
|
||||
switch (bot->GetEntry())
|
||||
{
|
||||
case BOT_TANK:
|
||||
roles = lfg::LfgRoles::PLAYER_ROLE_TANK;
|
||||
break;
|
||||
case BOT_HEALER:
|
||||
roles = lfg::LfgRoles::PLAYER_ROLE_HEALER;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Set bot information
|
||||
WorldPackets::Party::PartyPlayerInfo botInfo;
|
||||
botInfo.GUID = bot->GetGUID();
|
||||
botInfo.Name = bot->GetName();
|
||||
botInfo.Class = owner->GetClass(); // Use owner's class or set appropriate class
|
||||
botInfo.Connected = true; // Show as online
|
||||
botInfo.Subgroup = owner->GetSubGroup();
|
||||
botInfo.Flags = MEMBER_FLAG_ASSISTANT; // Or appropriate flags
|
||||
botInfo.RolesAssigned = 0; // Set appropriate role
|
||||
|
||||
// Send to all group members
|
||||
BroadcastPacket(partyUpdate.Write(), false);
|
||||
AddFollowerMember(bot, roles);
|
||||
}
|
||||
|
||||
void Group::DespawnFollowerModeBots(Player* player)
|
||||
{
|
||||
if (Group* group = player->GetGroup())
|
||||
group->RemoveMember(player->GetGUID());
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
player->UnsummonCreatureByEntry(BOT_HEALER, 1000);
|
||||
player->UnsummonCreatureByEntry(BOT_TANK, 1000);
|
||||
player->UnsummonCreatureByEntry(BOT_DPS_1, 1000);
|
||||
player->UnsummonCreatureByEntry(BOT_DPS_2, 1000);
|
||||
player->UnsummonCreatureByEntry(BOT_DPS_3, 1000);
|
||||
uint32 playerMembers = 0;
|
||||
for (MemberSlot const& slot : m_memberSlots)
|
||||
if (slot.guid.IsPlayer())
|
||||
++playerMembers;
|
||||
|
||||
bool const despawnAll = player->GetGUID() == m_leaderGuid || playerMembers <= 1;
|
||||
|
||||
auto unsummonBot = [player](ObjectGuid const& guid)
|
||||
{
|
||||
if (Map* map = player->GetMap())
|
||||
if (Creature* bot = map->GetCreature(guid))
|
||||
if (TempSummon* summon = bot->ToTempSummon())
|
||||
summon->UnSummon();
|
||||
};
|
||||
|
||||
if (despawnAll)
|
||||
{
|
||||
std::vector<ObjectGuid> bots = m_followerBots;
|
||||
for (ObjectGuid const& guid : bots)
|
||||
{
|
||||
RemoveFollowerBotSlot(guid);
|
||||
unsummonBot(guid);
|
||||
}
|
||||
|
||||
for (uint32 entry : FollowerBotEntries)
|
||||
player->UnsummonCreatureByEntry(entry, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint32 entry : FollowerBotEntries)
|
||||
{
|
||||
if (Creature* bot = player->GetSummonedCreatureByEntry(entry))
|
||||
{
|
||||
RemoveFollowerBotSlot(bot->GetGUID());
|
||||
player->UnsummonCreatureByEntry(entry, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SendUpdate();
|
||||
}
|
||||
|
||||
void Group::RemoveFollowerBotSlot(ObjectGuid guid)
|
||||
{
|
||||
member_witerator slot = _getMemberWSlot(guid);
|
||||
if (slot != m_memberSlots.end())
|
||||
{
|
||||
SubGroupCounterDecrease(slot->group);
|
||||
m_memberSlots.erase(slot);
|
||||
}
|
||||
|
||||
for (auto itr = m_followerBots.begin(); itr != m_followerBots.end();)
|
||||
{
|
||||
if (*itr == guid)
|
||||
itr = m_followerBots.erase(itr);
|
||||
else
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Group::AddFollowerMember(Creature* creature, uint8 lfgRoles)
|
||||
{
|
||||
if (!creature)
|
||||
return false;
|
||||
|
||||
if (IsMember(creature->GetGUID()))
|
||||
return true;
|
||||
|
||||
if (!isRaidGroup() && GetMembersCount() >= MAX_GROUP_SIZE)
|
||||
return false;
|
||||
|
||||
uint8 subGroup = 0;
|
||||
if (m_subGroupsCounts)
|
||||
{
|
||||
bool groupFound = false;
|
||||
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
|
||||
{
|
||||
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
|
||||
{
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupFound)
|
||||
return false;
|
||||
}
|
||||
|
||||
MemberSlot member;
|
||||
member.guid = creature->GetGUID();
|
||||
member.name = creature->GetName();
|
||||
member.race = Races(creature->GetRace());
|
||||
member._class = creature->GetClass();
|
||||
member.group = subGroup;
|
||||
member.flags = 0;
|
||||
member.roles = lfgRoles;
|
||||
member.readyChecked = false;
|
||||
m_memberSlots.push_back(member);
|
||||
|
||||
if (std::ranges::find(m_followerBots, creature->GetGUID()) == m_followerBots.end())
|
||||
m_followerBots.push_back(creature->GetGUID());
|
||||
|
||||
SubGroupCounterIncrease(subGroup);
|
||||
SendUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Group::AddFollowerMemberTank(Creature* creature)
|
||||
{
|
||||
// Get first not-full group
|
||||
uint8 subGroup = 0;
|
||||
if (m_subGroupsCounts)
|
||||
{
|
||||
bool groupFound = false;
|
||||
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
|
||||
{
|
||||
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
|
||||
{
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We are raid group and no one slot is free
|
||||
if (!groupFound)
|
||||
return false;
|
||||
}
|
||||
|
||||
MemberSlot member;
|
||||
member.guid = creature->GetGUID();
|
||||
member.name = creature->GetName();
|
||||
member.group = subGroup;
|
||||
member.flags = 0;
|
||||
member.roles = ROLE_TANK;
|
||||
m_memberSlots.push_back(member);
|
||||
|
||||
SubGroupCounterIncrease(subGroup);
|
||||
|
||||
SendUpdate();
|
||||
sScriptMgr->OnGroupAddMember(this, creature->GetGUID());
|
||||
|
||||
return true;
|
||||
return AddFollowerMember(creature, lfg::LfgRoles::PLAYER_ROLE_TANK);
|
||||
}
|
||||
|
||||
bool Group::AddFollowerMemberDPS(Creature* creature)
|
||||
{
|
||||
// Get first not-full group
|
||||
uint8 subGroup = 0;
|
||||
if (m_subGroupsCounts)
|
||||
{
|
||||
bool groupFound = false;
|
||||
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
|
||||
{
|
||||
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
|
||||
{
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We are raid group and no one slot is free
|
||||
if (!groupFound)
|
||||
return false;
|
||||
}
|
||||
|
||||
MemberSlot member;
|
||||
member.guid = creature->GetGUID();
|
||||
member.name = creature->GetName();
|
||||
member.group = subGroup;
|
||||
member.flags = 0;
|
||||
member.roles = ROLE_DAMAGE;
|
||||
m_memberSlots.push_back(member);
|
||||
|
||||
SubGroupCounterIncrease(subGroup);
|
||||
|
||||
SendUpdate();
|
||||
sScriptMgr->OnGroupAddMember(this, creature->GetGUID());
|
||||
|
||||
return true;
|
||||
return AddFollowerMember(creature, lfg::LfgRoles::PLAYER_ROLE_DAMAGE);
|
||||
}
|
||||
|
||||
bool Group::AddFollowerMemberHealer(Creature* creature)
|
||||
{
|
||||
// Get first not-full group
|
||||
uint8 subGroup = 0;
|
||||
if (m_subGroupsCounts)
|
||||
{
|
||||
bool groupFound = false;
|
||||
for (; subGroup < MAX_RAID_SUBGROUPS; ++subGroup)
|
||||
{
|
||||
if (m_subGroupsCounts[subGroup] < MAX_GROUP_SIZE)
|
||||
{
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We are raid group and no one slot is free
|
||||
if (!groupFound)
|
||||
return false;
|
||||
}
|
||||
|
||||
MemberSlot member;
|
||||
member.guid = creature->GetGUID();
|
||||
member.name = creature->GetName();
|
||||
member.group = subGroup;
|
||||
member.flags = 0;
|
||||
member.roles = ROLE_HEALER;
|
||||
m_memberSlots.push_back(member);
|
||||
|
||||
SubGroupCounterIncrease(subGroup);
|
||||
|
||||
SendUpdate();
|
||||
sScriptMgr->OnGroupAddMember(this, creature->GetGUID());
|
||||
|
||||
return true;
|
||||
return AddFollowerMember(creature, lfg::LfgRoles::PLAYER_ROLE_HEALER);
|
||||
}
|
||||
//WowCommunity
|
||||
|
||||
@@ -429,6 +429,9 @@ class TC_GAME_API Group
|
||||
|
||||
private:
|
||||
std::vector<ObjectGuid> m_followerBots;
|
||||
bool AddFollowerMember(Creature* creature, uint8 lfgRoles);
|
||||
void RemoveFollowerBotSlot(ObjectGuid guid);
|
||||
bool IsFollowerDungeon(Player const* player) const;
|
||||
//WowCommunity
|
||||
|
||||
protected:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#include "WorldSession.h"
|
||||
#include "Creature.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DelveMgr.h"
|
||||
#include "DelvesDefines.h"
|
||||
#include "DelvesPackets.h"
|
||||
@@ -26,7 +26,10 @@
|
||||
#include "Group.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Player.h"
|
||||
#include "QueryCallback.h"
|
||||
#include "StringFormat.h"
|
||||
|
||||
void WorldSession::HandleDelveTeleportOut(WorldPackets::Delves::DelveTeleportOut& /*delveTeleportOut*/)
|
||||
{
|
||||
@@ -36,9 +39,65 @@ void WorldSession::HandleDelveTeleportOut(WorldPackets::Delves::DelveTeleportOut
|
||||
|
||||
TC_LOG_DEBUG("network", "CMSG_DELVE_TELEPORT_OUT received from player {}", player->GetName());
|
||||
|
||||
// Teleport player out of the delve instance to their bind point
|
||||
if (player->GetMap()->Instanceable())
|
||||
player->TeleportTo(player->m_homebind);
|
||||
// Never sent by the 12.x client in any of the five captured runs (REPORT.md 1.1 / 5); retail leaves through the
|
||||
// Leave-O-Bot spell-click or the "Leave Delve" object, both of which call DelveMgr::LeaveDelve from scripts.
|
||||
// Kept as a safety net with the same exit semantics (originating map, template exit, homebind).
|
||||
if (sDelveMgr->GetDelveTemplate(player->GetMapId()))
|
||||
sDelveMgr->LeaveDelve(player);
|
||||
}
|
||||
|
||||
void WorldSession::HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Delves::RequestPartyEligibilityForDelveTiers& packet)
|
||||
{
|
||||
Player* player = GetPlayer();
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
TC_LOG_DEBUG("network", "CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS received from player {} for mapId {}",
|
||||
player->GetName(), packet.MapID);
|
||||
|
||||
// REPORT.md 1.1 / 6.1: SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE was never sent in any of the four solo
|
||||
// runs (0 frames in five captures) - the client sends the request right after the picker opens (gulf 2964,
|
||||
// eversong 2952) and gets nothing back. Only a party has rows to fill in.
|
||||
Group const* group = player->GetGroup();
|
||||
if (!group || group->isRaidGroup())
|
||||
return;
|
||||
|
||||
// One packet per member (68275: PackedGUID + uint32 + uint32 + bool, no count framing; semantics UNVERIFIED, see
|
||||
// DelvesPackets.h). Tier progress lives in delve_progress per battlenet account - read it asynchronously, one
|
||||
// query per member, and answer from the callback; no synchronous DB round trip on the world thread.
|
||||
for (GroupReference const& itr : group->GetMembers())
|
||||
{
|
||||
Player const* member = itr.GetSource();
|
||||
if (!member || !member->GetSession())
|
||||
continue;
|
||||
|
||||
ObjectGuid const memberGuid = member->GetGUID();
|
||||
uint8 const levelEligible = Delves::DelvesSeason::MeetsMinimumLevelRequirement(member) ? 1 : 0;
|
||||
|
||||
// same SELECT as CHAR_SEL_DELVE_PROGRESS, which is prepared on the synchronous connection only
|
||||
std::string const query = Trinity::StringFormat("SELECT highestTierUnlocked FROM delve_progress WHERE battlenetAccountId = {}",
|
||||
member->GetSession()->GetBattlenetAccountId());
|
||||
|
||||
GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(query.c_str()).WithCallback([this, memberGuid, levelEligible](QueryResult result)
|
||||
{
|
||||
if (!GetPlayer() || !GetPlayer()->IsInWorld())
|
||||
return;
|
||||
|
||||
// default progress: tiers 1-3 are open for every fresh character (REPORT.md 1.3 unlock state)
|
||||
uint8 highestTierUnlocked = Delves::DelveProgress().HighestTierUnlocked;
|
||||
if (result)
|
||||
highestTierUnlocked = (*result)[0].GetUInt8();
|
||||
|
||||
uint8 const maxTier = levelEligible ? std::min<uint8>(highestTierUnlocked, Delves::MAX_DELVE_TIER) : 0;
|
||||
|
||||
WorldPackets::Delves::PartyEligibilityForDelveTiersResponse response;
|
||||
response.PlayerGUID = memberGuid;
|
||||
response.MaxEligibleTier = maxTier;
|
||||
response.ReasonOrFlags = 0; // UNVERIFIED - needs a group capture
|
||||
response.IsEligible = maxTier > 0; // UNVERIFIED - needs a group capture
|
||||
SendPacket(response.Write());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void WorldSession::HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDelveEntranceTier& packet)
|
||||
@@ -47,11 +106,11 @@ void WorldSession::HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDel
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
TC_LOG_DEBUG("network", "CMSG_SELECT_DELVE_ENTRANCE_TIER received from player {} mapId {} tier {}",
|
||||
player->GetName(), packet.MapID, packet.Tier);
|
||||
TC_LOG_DEBUG("network", "CMSG_SELECT_DELVE_ENTRANCE_TIER received from player {} entrance {} tier {}",
|
||||
player->GetName(), packet.EntranceGUID.ToString(), packet.Tier);
|
||||
|
||||
// 12.1 wire: the client echoes the TieredEntranceTierID the picker advertised (75..85 on the
|
||||
// 12.1 delve season), not the 1-based tier. Accept a bare tier number too (legacy pickers).
|
||||
// 12.1 delve season), not the 1-based tier. Accept a bare tier number too (legacy pickers).
|
||||
uint8 tier = 0;
|
||||
if (Delves::TieredEntranceTierData const* tierRow = sDelveMgr->GetTieredEntranceTier(packet.Tier))
|
||||
tier = tierRow->Tier;
|
||||
@@ -69,10 +128,25 @@ void WorldSession::HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDel
|
||||
if (tier > progress.HighestTierUnlocked)
|
||||
return;
|
||||
|
||||
// Selection is consumed by the subsequent CMSG_TIERED_ENTRANCE_OPEN flow; the client
|
||||
// re-sends the tier on entrance. We accept and validate here so eligibility is logged.
|
||||
player->m_delveSelectedTier = tier;
|
||||
player->m_delveSelectedMapId = packet.MapID;
|
||||
// The wire carries the entrance ObjectGuid, not a MapID - re-derive the delve from the entrance spawn
|
||||
// (DelveMgr::GetDelveTemplateForEntrance; Creature::GetGossipMenuId() is always 0, see DelveMgr.cpp).
|
||||
Delves::DelveTemplate const* tmpl = nullptr;
|
||||
if (packet.EntranceGUID.IsCreatureOrVehicle())
|
||||
if (Creature const* entrance = ObjectAccessor::GetCreature(*player, packet.EntranceGUID))
|
||||
tmpl = sDelveMgr->GetDelveTemplateForEntrance(entrance);
|
||||
|
||||
if (!tmpl)
|
||||
{
|
||||
TC_LOG_DEBUG("network", "CMSG_SELECT_DELVE_ENTRANCE_TIER: could not resolve entrance {} to a delve template",
|
||||
packet.EntranceGUID.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
// REPORT.md 1.4 - 1.6 (work item 5): the select IS the entry. Retail answers it with the phase shift,
|
||||
// SMSG_SUSPEND_TOKEN + SMSG_PLAYER_CHOICE_CLEAR and, ~1.9 s later, a seamless SMSG_NEW_WORLD (reason 21, no
|
||||
// SMSG_TRANSFER_PENDING) to the delve's entry position (gulf 100129 -> 101829). Nothing else is requested from
|
||||
// the client in between. Shared with the entrance gossip script.
|
||||
sDelveMgr->EnterDelve(player, *tmpl, tier);
|
||||
}
|
||||
|
||||
void WorldSession::HandleTieredEntranceOpen(WorldPackets::Delves::TieredEntranceOpen& packet)
|
||||
@@ -84,62 +158,17 @@ void WorldSession::HandleTieredEntranceOpen(WorldPackets::Delves::TieredEntrance
|
||||
TC_LOG_DEBUG("network", "CMSG_TIERED_ENTRANCE_OPEN received from player {} entrance {}",
|
||||
player->GetName(), packet.EntranceGUID.ToString());
|
||||
|
||||
Delves::DelveTemplate const* tmpl = nullptr;
|
||||
if (packet.EntranceGUID.IsCreatureOrVehicle())
|
||||
if (Creature const* entrance = ObjectAccessor::GetCreature(*player, packet.EntranceGUID))
|
||||
tmpl = sDelveMgr->GetDelveTemplateByGossipMenuId(entrance->GetGossipMenuId());
|
||||
|
||||
if (!tmpl)
|
||||
// REPORT.md 1.1: in 12.1 delve entrances (EntranceType 1) open by proximity - the server pushes the picker
|
||||
// (DelveMgr::OpenEntranceByProximity from the entrance AI). This CMSG is still sent by click-to-open entrances
|
||||
// (69273 Sites witness: creature 264322 -> map 3075 "Naigtal") and is kept for them; the response is built by
|
||||
// the same code either way.
|
||||
Creature const* entrance = packet.EntranceGUID.IsCreatureOrVehicle() ? ObjectAccessor::GetCreature(*player, packet.EntranceGUID) : nullptr;
|
||||
if (!entrance)
|
||||
{
|
||||
TC_LOG_DEBUG("network", "CMSG_TIERED_ENTRANCE_OPEN: could not resolve entrance {} to a delve template",
|
||||
packet.EntranceGUID.ToString());
|
||||
TC_LOG_DEBUG("network", "CMSG_TIERED_ENTRANCE_OPEN: entrance {} is not a creature in range of player {}",
|
||||
packet.EntranceGUID.ToString(), player->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
Delves::DelveProgress progress;
|
||||
Delves::DelvesRewards::LoadProgress(GetBattlenetAccountId(), progress);
|
||||
bool meetsLevel = Delves::DelvesSeason::MeetsMinimumLevelRequirement(player);
|
||||
|
||||
// 12.1.0.69497 captures (Gulf of Memory, Shadow Enclave; 12.0.7 The Darkway agrees): every delve
|
||||
// entrance reports EntranceType 1, field 7 is 234 for every delve, field 6 is 0, fields 3/4/8 and
|
||||
// the widget sets are per entrance (delve_template), the tier rows are season data
|
||||
// (delve_tiered_entrance_tier). The client matches the response by the echoed entrance GUID.
|
||||
WorldPackets::Delves::TieredEntranceOpenResponse response;
|
||||
response.EntranceGUID = packet.EntranceGUID;
|
||||
response.EntranceType = Delves::TIERED_ENTRANCE_TYPE_DELVE;
|
||||
response.MapID = tmpl->MapId;
|
||||
response.Unknown3 = tmpl->TieredEntranceUnknown3;
|
||||
response.Unknown4 = tmpl->EntranceUiWidgetSetId;
|
||||
response.Unknown6 = 0;
|
||||
response.Unknown7 = Delves::DELVE_TIERED_ENTRANCE_FIELD7;
|
||||
response.Unknown8 = tmpl->TieredEntranceId;
|
||||
|
||||
if (MapEntry const* mapEntry = sMapStore.LookupEntry(tmpl->MapId))
|
||||
response.EntranceDescription = mapEntry->MapName[GetSessionDbcLocale()];
|
||||
|
||||
std::vector<Delves::TieredEntranceTierData> const& tiers = sDelveMgr->GetTieredEntranceTiers();
|
||||
response.Tiers.reserve(tiers.size());
|
||||
for (Delves::TieredEntranceTierData const& tierRow : tiers)
|
||||
{
|
||||
WorldPackets::Delves::TieredEntranceTier& tierData = response.Tiers.emplace_back();
|
||||
tierData.TieredEntranceTierID = tierRow.Id;
|
||||
tierData.Tier = tierRow.Tier;
|
||||
tierData.SuggestedILvl = tierRow.SuggestedILvl;
|
||||
tierData.OverrideTooltipSpellID = tierRow.OverrideTooltipSpellId;
|
||||
tierData.UnlockPlayerConditionID = tierRow.UnlockPlayerConditionId;
|
||||
tierData.DynamicUnlockPlayerConditionID = tierRow.DynamicUnlockPlayerConditionId;
|
||||
tierData.ModifierUIWidgetSetID = tmpl->ModifierUiWidgetSetTier1 && tierRow.Tier ? tmpl->ModifierUiWidgetSetTier1 - (tierRow.Tier - 1) : 0;
|
||||
tierData.Unlocked = meetsLevel && tierRow.Tier <= progress.HighestTierUnlocked;
|
||||
tierData.TierDescription = tierRow.Description;
|
||||
for (Delves::TieredEntranceRewardData const& reward : tierRow.Rewards)
|
||||
{
|
||||
WorldPackets::Delves::TieredEntranceReward& rewardData = tierData.PreviewTreasureList.emplace_back();
|
||||
rewardData.RewardType = reward.RewardType;
|
||||
rewardData.Id = reward.Id;
|
||||
rewardData.Quantity = reward.Quantity;
|
||||
rewardData.Context = reward.Context;
|
||||
}
|
||||
}
|
||||
|
||||
SendPacket(response.Write());
|
||||
sDelveMgr->SendTieredEntranceOpen(player, entrance);
|
||||
}
|
||||
|
||||
@@ -1447,50 +1447,6 @@ void WorldSession::HandleAddonList(WorldPackets::Addon::AddonList& addonList)
|
||||
addon.Name, addon.Version, addon.Loaded, addon.Disabled);
|
||||
}
|
||||
|
||||
void WorldSession::HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Misc::RequestPartyEligibilityForDelveTiers& packet)
|
||||
{
|
||||
Player* player = GetPlayer();
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
TC_LOG_DEBUG("network", "CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS received from player {} for mapId {}",
|
||||
player->GetName(), packet.MapID);
|
||||
|
||||
auto computeMaxEligibleTier = [&](Player const* member) -> uint8
|
||||
{
|
||||
if (!Delves::DelvesSeason::MeetsMinimumLevelRequirement(member))
|
||||
return 0;
|
||||
Delves::DelveProgress progress;
|
||||
Delves::DelvesRewards::LoadProgress(member->GetSession()->GetBattlenetAccountId(), progress);
|
||||
return std::min<uint8>(progress.HighestTierUnlocked, Delves::MAX_DELVE_TIER);
|
||||
};
|
||||
|
||||
auto sendForMember = [&](Player const* member)
|
||||
{
|
||||
WorldPackets::Delves::PartyEligibilityForDelveTiersResponse response;
|
||||
response.PlayerName = member->GetName();
|
||||
response.MaxEligibleTier = computeMaxEligibleTier(member);
|
||||
SendPacket(response.Write());
|
||||
};
|
||||
|
||||
// Always emit at least the requesting player so the client populates its own row.
|
||||
sendForMember(player);
|
||||
|
||||
if (Group const* group = player->GetGroup())
|
||||
{
|
||||
if (group->isRaidGroup())
|
||||
return;
|
||||
|
||||
for (GroupReference const& itr : group->GetMembers())
|
||||
{
|
||||
Player const* member = itr.GetSource();
|
||||
if (!member || member == player)
|
||||
continue;
|
||||
sendForMember(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WorldSession::HandleRafClaimActivityReward(WorldPackets::RaF::RafClaimActivityReward& /*rafClaimActivityReward*/)
|
||||
{
|
||||
// ClaimRafActivity(packet.ActivityID);
|
||||
|
||||
@@ -487,7 +487,7 @@ namespace ScriptHelpers
|
||||
return;
|
||||
|
||||
WorldPackets::Delves::ShowDelvesCompanionConfigurationUI configUI;
|
||||
configUI.CreatureOrSpellID = companionConfigValue;
|
||||
configUI.Unknown = companionConfigValue;
|
||||
player->GetSession()->SendPacket(configUI.Write());
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,16 @@ namespace WorldPackets
|
||||
{
|
||||
namespace Delves
|
||||
{
|
||||
void SelectDelveEntranceTier::Read()
|
||||
|
||||
void RequestPartyEligibilityForDelveTiers::Read()
|
||||
{
|
||||
_worldPacket >> MapID;
|
||||
}
|
||||
|
||||
void SelectDelveEntranceTier::Read()
|
||||
{
|
||||
// 68275 wire: PackedGUID entranceGuid + uint32 tier (sender 0x7FF729155A10).
|
||||
_worldPacket >> EntranceGUID;
|
||||
_worldPacket >> Tier;
|
||||
}
|
||||
|
||||
@@ -35,29 +42,30 @@ namespace WorldPackets
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* DelvesAccountDataElementChanged::Write()
|
||||
{
|
||||
_worldPacket << uint32(DataElementID);
|
||||
_worldPacket << uint32(Value);
|
||||
return &_worldPacket;
|
||||
}
|
||||
// DelvesAccountDataElementChanged intentionally has no class ? PDE state is
|
||||
// delivered to the client via ActivePlayer UpdateFields, not a dedicated SMSG.
|
||||
// See DelvesPackets.h for the IDA-traced reasoning.
|
||||
|
||||
WorldPacket const* ShowDelvesCompanionConfigurationUI::Write()
|
||||
{
|
||||
_worldPacket << uint32(CreatureOrSpellID);
|
||||
// 12.1.0.69497 wire (gulf 1096575, eversong 2688539; 12.0.1 deatholme 75171): one uint32.
|
||||
_worldPacket << uint32(Unknown);
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* PartyEligibilityForDelveTiersResponse::Write()
|
||||
{
|
||||
_worldPacket.WriteBits(PlayerName.size(), 6);
|
||||
// 68275 wire (read ctor 0x7FF7290BBA40): PackedGUID + uint32 + uint32 + bool(MSB).
|
||||
// One member per packet ? no count framing. Field semantics UNVERIFIED ? see header.
|
||||
_worldPacket << PlayerGUID;
|
||||
_worldPacket << uint32(MaxEligibleTier);
|
||||
_worldPacket << uint32(ReasonOrFlags);
|
||||
_worldPacket.WriteBit(IsEligible);
|
||||
_worldPacket.FlushBits();
|
||||
_worldPacket.WriteString(PlayerName);
|
||||
_worldPacket << uint8(MaxEligibleTier);
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
|
||||
void TieredEntranceOpen::Read()
|
||||
{
|
||||
// 68275 wire: PackedGuid only (12B observed; sender 0x7FF7291559C0).
|
||||
|
||||
@@ -28,7 +28,8 @@ namespace WorldPackets
|
||||
namespace Delves
|
||||
{
|
||||
|
||||
// CMSG_DELVE_TELEPORT_OUT (0x3B012C)
|
||||
// CMSG_DELVE_TELEPORT_OUT (0x3B012E @ 12.0.7.68275)
|
||||
// 68275 binary (sender 0x7FF7291558F0): empty body, opcode only.
|
||||
class DelveTeleportOut final : public ClientPacket
|
||||
{
|
||||
public:
|
||||
@@ -37,8 +38,30 @@ namespace WorldPackets
|
||||
void Read() override {}
|
||||
};
|
||||
|
||||
// CMSG_SELECT_DELVE_ENTRANCE_TIER (0x3B0134)
|
||||
// Lua: C_DelvesUI.SelectDelveEntranceTier(tier). Client wraps with active PDE MapID.
|
||||
// CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS (0x3A02F0 @ 12.0.7.68275; was 0x3A02F4 at 67186)
|
||||
// Lua signature: C_DelvesUI.RequestPartyEligibilityForDelveTiers(mapID)
|
||||
// 68275 binary (sender 0x7FF72914CAB0) confirms 4-byte payload (uint32 MapID only);
|
||||
// matches the build-66562 sniff.
|
||||
class RequestPartyEligibilityForDelveTiers final : public ClientPacket
|
||||
{
|
||||
public:
|
||||
explicit RequestPartyEligibilityForDelveTiers(WorldPacket&& packet) : ClientPacket(CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS, std::move(packet)) {}
|
||||
|
||||
void Read() override;
|
||||
|
||||
uint32 MapID = 0;
|
||||
};
|
||||
|
||||
// CMSG_SELECT_DELVE_ENTRANCE_TIER (0x3B0134 @ 12.0.7.68275)
|
||||
// Lua signature: C_DelvesUI.SelectDelveEntranceTier(tier) ? single Lua arg.
|
||||
//
|
||||
// Wire resolved from the 68275 binary (sender 0x7FF729155A10, opcode immediate
|
||||
// at 0x7FF729155A23): PackedGUID entranceGuid + uint32 tier. The "16-byte struct
|
||||
// copied from the 40-entry table" of the earlier 67186 read is that ObjectGuid.
|
||||
// Tier is uint32, not uint8. The GUID is believed to be the delve entrance/POI
|
||||
// object the picker is bound to ? // UNVERIFIED ? needs sniff (the field widths
|
||||
// and order ARE certain; only the GUID's referent is inferred). The server
|
||||
// re-derives the delve MapID from this GUID in HandleSelectDelveEntranceTier.
|
||||
class SelectDelveEntranceTier final : public ClientPacket
|
||||
{
|
||||
public:
|
||||
@@ -46,11 +69,12 @@ namespace WorldPackets
|
||||
|
||||
void Read() override;
|
||||
|
||||
uint32 MapID = 0;
|
||||
uint8 Tier = 0;
|
||||
ObjectGuid EntranceGUID;
|
||||
uint32 Tier = 0;
|
||||
};
|
||||
|
||||
// SMSG_SHOW_DELVES_DISPLAY_UI (build 67186 = 0x420359)
|
||||
// SMSG_SHOW_DELVES_DISPLAY_UI (0x420359 @ 12.0.7.68275)
|
||||
// 68275 binary (read ctor 0x7FF7290BB840): empty body (remaining-span, 0-length in practice).
|
||||
class ShowDelvesDisplayUI final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
@@ -61,22 +85,31 @@ namespace WorldPackets
|
||||
uint32 Unknown = 0; // 12.1 wire: one uint32 (2796, 2742 observed; UiMap/Map id range, meaning unverified)
|
||||
};
|
||||
|
||||
// SMSG_DELVES_ACCOUNT_DATA_ELEMENT_CHANGED (build 67186 = 0x42035A)
|
||||
// Wire: uint32 DataElementID, uint32 Value (per IDA-decoded JamSMsgDelvesAccountDataElementChanged).
|
||||
class DelvesAccountDataElementChanged final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
explicit DelvesAccountDataElementChanged() : ServerPacket(SMSG_DELVES_ACCOUNT_DATA_ELEMENT_CHANGED, 8) {}
|
||||
// SMSG_DELVES_ACCOUNT_DATA_ELEMENT_CHANGED (0x42035A @ 12.0.7.68275)
|
||||
//
|
||||
// Intentionally NO packet class. PlayerDataElement (PDE) state on the client is
|
||||
// stored on CGActivePlayer_C in two `(vector<PlayerDataElement>, vector<uint32>)`
|
||||
// fields (decompiled from `sub_7FF75C204150`, the CGActivePlayer destructor;
|
||||
// fields at qword offsets 651 and 658 ? Account and Character respectively). The
|
||||
// Lua event `DELVES_ACCOUNT_DATA_ELEMENT_CHANGED` is broadcast by mirror handlers
|
||||
// registered against those vector fields with the signature
|
||||
// void(CGActivePlayer_C&, PlayerDataElement const& oldElem,
|
||||
// PlayerDataElement const& newElem, unsigned int idx)
|
||||
// (typename string at `0x7FF75F3A9AE0` in IDA build 66198). Mirror handlers fire
|
||||
// from UpdateField changes ? server-side we populate the ActivePlayer UpdateFields
|
||||
// (AccountDataElements / CharacterDataElements / DelveData) inside SMSG_UPDATE_OBJECT,
|
||||
// which drives the client event automatically.
|
||||
// 68275 note: the dedicated SMSG's read ctor (0x7FF7290BB8C0) captures the entire
|
||||
// remaining payload as an opaque account-data element blob that is fed to the same
|
||||
// CGActivePlayer mirror deserializer (0x7FF72920BCF0) ? i.e. it is an alternative
|
||||
// runtime delta channel for the same mirror stream. The exact blob framing is
|
||||
// // UNVERIFIED ? needs sniff; we deliver via SMSG_UPDATE_OBJECT instead.
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
uint32 DataElementID = 0;
|
||||
uint32 Value = 0;
|
||||
};
|
||||
|
||||
// SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI (build 67186 = 0x42035B)
|
||||
// Sniff confirms 4-byte payload ? value matches a creature/spell ID.
|
||||
// Lua doc: "Signaled when SpellScript indicates that a curio has been learned or upgraded."
|
||||
// SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI (0x42035B @ 12.0.7.68275)
|
||||
// 68275 binary (read ctor 0x7FF7290BB940): the client reads an EMPTY body (a
|
||||
// remaining-bytes span expected to be 0-length). The earlier 66709 sniff's 4-byte
|
||||
// payload is ignored by the 68275 reader (trailing bytes are benign), so the
|
||||
// packet carries no fields. UI-trigger only.
|
||||
class ShowDelvesCompanionConfigurationUI final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
@@ -84,21 +117,31 @@ namespace WorldPackets
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
uint32 CreatureOrSpellID = 0;
|
||||
uint32 Unknown = 0; // 12.1 wire: one uint32 (271132 / 249219 / 249222 observed after a delve; meaning unverified)
|
||||
};
|
||||
|
||||
// SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE (build 67186 = 0x42035D)
|
||||
// Lua event payload: (playerName: string, maxEligibleLevel: number) ? one event firing per packet.
|
||||
// Wire: TC strings use bit-length prefix, then bytes. Sent once per evaluated party member.
|
||||
// SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE (0x42035D @ 12.0.7.68275)
|
||||
// 68275 binary (read ctor 0x7FF7290BBA40) reads exactly, in order:
|
||||
// PackedGUID + uint32 + uint32 + uint8 (bool = byte>>7)
|
||||
// There is NO count/array framing ? the packet carries a single member entry, so
|
||||
// the server sends one packet per party member. The Lua event
|
||||
// PARTY_ELIGIBILITY_FOR_DELVE_TIERS_CHANGED carries (playerName, maxEligibleLevel);
|
||||
// the name is resolved client-side from the GUID.
|
||||
// Semantics of the two uint32s and the bool are // UNVERIFIED ? needs sniff.
|
||||
// Best-hypothesis mapping used here: first uint32 = max eligible tier (matches the
|
||||
// Lua event's maxEligibleLevel), second uint32 = ineligibility reason/flags (0 when
|
||||
// eligible), bool = is-eligible. Wire widths/order ARE certain.
|
||||
class PartyEligibilityForDelveTiersResponse final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
explicit PartyEligibilityForDelveTiersResponse() : ServerPacket(SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE, 64) {}
|
||||
explicit PartyEligibilityForDelveTiersResponse() : ServerPacket(SMSG_PARTY_ELIGIBILITY_FOR_DELVE_TIERS_RESPONSE, 16 + 2 + 4 + 4 + 1) {}
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
std::string PlayerName;
|
||||
uint8 MaxEligibleTier = 0;
|
||||
ObjectGuid PlayerGUID;
|
||||
uint32 MaxEligibleTier = 0; // UNVERIFIED ? needs sniff (client field +0x30)
|
||||
uint32 ReasonOrFlags = 0; // UNVERIFIED ? needs sniff (client field +0x34)
|
||||
bool IsEligible = false; // UNVERIFIED ? needs sniff (client field +0x38, wire byte MSB)
|
||||
};
|
||||
|
||||
// CMSG_TIERED_ENTRANCE_OPEN (0x3B0133 @ 12.0.7.68275)
|
||||
|
||||
@@ -962,11 +962,6 @@ void ClaimWeeklyReward::Read()
|
||||
_worldPacket >> RewardID;
|
||||
}
|
||||
|
||||
void RequestPartyEligibilityForDelveTiers::Read()
|
||||
{
|
||||
_worldPacket >> MapID;
|
||||
}
|
||||
|
||||
void MythicPlusRequestMapStats::Read()
|
||||
{
|
||||
_worldPacket >> NpcGUID;
|
||||
|
||||
@@ -1246,16 +1246,6 @@ namespace WorldPackets
|
||||
int32 RewardID = 0;
|
||||
};
|
||||
|
||||
class RequestPartyEligibilityForDelveTiers final : public ClientPacket
|
||||
{
|
||||
public:
|
||||
RequestPartyEligibilityForDelveTiers(WorldPacket&& packet) : ClientPacket(CMSG_REQUEST_PARTY_ELIGIBILITY_FOR_DELVE_TIERS, std::move(packet)) {}
|
||||
|
||||
void Read() override;
|
||||
|
||||
uint32 MapID = 0;
|
||||
};
|
||||
|
||||
class MythicPlusRequestMapStats final : public ClientPacket
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -278,6 +278,7 @@ namespace WorldPackets
|
||||
namespace Delves
|
||||
{
|
||||
class DelveTeleportOut;
|
||||
class RequestPartyEligibilityForDelveTiers;
|
||||
class SelectDelveEntranceTier;
|
||||
class TieredEntranceOpen;
|
||||
}
|
||||
@@ -848,7 +849,6 @@ namespace WorldPackets
|
||||
class AccountStoreBeginPurchaseOrRefund;
|
||||
class OverrideScreenFlash;
|
||||
class GlobalGetChallengeModeRewards;
|
||||
class RequestPartyEligibilityForDelveTiers;
|
||||
class SetPreferredCemetery;
|
||||
class CloseTraitSystemInteraction;
|
||||
class ReportStuckInCombat;
|
||||
@@ -2470,7 +2470,6 @@ public:
|
||||
//WowCommunity
|
||||
|
||||
void HandleChallengeModeRequestLeaders(WorldPackets::ChallengeMode::RequestLeaders& request);
|
||||
void HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Misc::RequestPartyEligibilityForDelveTiers& packet);
|
||||
|
||||
// Adventure Journal
|
||||
void HandleAdventureJournalOpenQuest(WorldPackets::AdventureJournal::AdventureJournalOpenQuest& openQuest);
|
||||
@@ -2609,6 +2608,7 @@ public:
|
||||
|
||||
// Delves
|
||||
void HandleDelveTeleportOut(WorldPackets::Delves::DelveTeleportOut& delveTeleportOut);
|
||||
void HandleRequestPartyEligibilityForDelveTiers(WorldPackets::Delves::RequestPartyEligibilityForDelveTiers& requestPartyEligibilityForDelveTiers);
|
||||
void HandleSelectDelveEntranceTier(WorldPackets::Delves::SelectDelveEntranceTier& selectDelveEntranceTier);
|
||||
void HandleTieredEntranceOpen(WorldPackets::Delves::TieredEntranceOpen& tieredEntranceOpen);
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
#include "RitualSiteMgr.h"
|
||||
#include "AbyssAnglersMgr.h"
|
||||
#include "TurbulentTimewaysMgr.h"
|
||||
#include "DelvesRewards.h"
|
||||
//WowCommunity
|
||||
#include <zlib.h>
|
||||
|
||||
@@ -3346,6 +3347,9 @@ void World::ResetWeeklyQuests()
|
||||
// state (vault run history, vault claim, keystone adjustment, affix week index) is keyed on that boundary.
|
||||
sChallengeModeMgr.OnWeeklyReset();
|
||||
|
||||
// Delves: weekly completion / bountiful / coffer-shard counters roll over with the weekly reset
|
||||
// (REPORT.md 6.4: ResetAllWeeklyProgress had no caller, so the counters never reset)
|
||||
Delves::DelvesRewards::ResetAllWeeklyProgress();
|
||||
//WowCommunity
|
||||
|
||||
// reselect pools
|
||||
|
||||
@@ -179,7 +179,7 @@ public:
|
||||
handler->PSendSysMessage("Highest Tier This Week: {}", progress.HighestTierThisWeek);
|
||||
handler->PSendSysMessage("Weekly Bountiful: {}", progress.WeeklyBountifulCount);
|
||||
handler->PSendSysMessage("Weekly Coffer Shards: {}/{}", progress.WeeklyCofferShards, Delves::MAX_COFFER_KEY_SHARDS_PER_WEEK);
|
||||
handler->PSendSysMessage("Great Vault Slots: {}", Delves::DelvesRewards::GetGreatVaultSlotCount(progress.WeeklyCompletions));
|
||||
//handler->PSendSysMessage("Great Vault Slots: {}", Delves::DelvesRewards::GetGreatVaultSlotCount(progress.WeeklyCompletions));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* 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; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The Darkway - map 3003, difficulty 208, scenario 3184, LFGDungeons 3083, AreaTable 16642, entered from map 0.
|
||||
* Evidence: REPORT.md 2.2 / 6.5 and the shadowmoon 12.0.7.68974 capture, which ENDS INSIDE the delve during step
|
||||
* 16101 - so the boss fight, the completion and the reward objects were never observed for this map.
|
||||
*
|
||||
* Scenario 3184 steps: 16133 "Technician Mireille spoken to" (GameEvent 98477) -> 16100 "Sabotaged Ley Lines
|
||||
* examined" (GameEvent 101992) -> 16102 "Voidbreaker Oglok slain" (kill 252102) -> 16101 "Ley Line Focusers"
|
||||
* (CastSpell 1256207 x6 / GameEvent 101861 x6) -> 16103 "Infiltrator Gulkat slain" (criteria 60399 / GameEvent 85913).
|
||||
*
|
||||
* The final boss is DungeonEncounter 3361 "Infiltrator Gulkat" (DungeonEncounter.db2, MapID 3003). The world DB has
|
||||
* THREE creature_template rows of that name - 251015, 251600, 256817 - and no capture shows which one the delve
|
||||
* spawns, so delve_template.finalBossEntry stays 0 and the death of any of the three counts (IsFinalBoss override).
|
||||
* Reward object placements are unknown for this map; DelveInstanceScript falls back to the boss's death position.
|
||||
*/
|
||||
|
||||
#include "delves_common.h"
|
||||
#include "Creature.h"
|
||||
#include "ScriptMgr.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static char const* const TheDarkwayScriptName = "instance_the_darkway_delve";
|
||||
|
||||
enum TheDarkwayData
|
||||
{
|
||||
BOSS_INFILTRATOR_GULKAT = 0,
|
||||
|
||||
MAX_ENCOUNTER
|
||||
};
|
||||
|
||||
enum TheDarkwayIds : uint32
|
||||
{
|
||||
DUNGEON_ENCOUNTER_INFILTRATOR_GULKAT = 3361,
|
||||
};
|
||||
|
||||
// creature_template rows named "Infiltrator Gulkat" (tch_ws_world) - the capture never reached him
|
||||
constexpr uint32 InfiltratorGulkatEntries[] = { 251015, 251600, 256817 };
|
||||
|
||||
DungeonEncounterData const encounters[] =
|
||||
{
|
||||
{ BOSS_INFILTRATOR_GULKAT, {{ DUNGEON_ENCOUNTER_INFILTRATOR_GULKAT }} }
|
||||
};
|
||||
|
||||
class instance_the_darkway_delve : public InstanceMapScript
|
||||
{
|
||||
public:
|
||||
instance_the_darkway_delve() : InstanceMapScript(TheDarkwayScriptName, 3003) { }
|
||||
|
||||
struct instance_the_darkway_delve_InstanceScript : public Delves::DelveInstanceScript
|
||||
{
|
||||
instance_the_darkway_delve_InstanceScript(InstanceMap* map)
|
||||
: DelveInstanceScript(map, 1 /* tier resolved at OnPlayerEnter from m_delveSelectedTier */)
|
||||
{
|
||||
SetBossNumber(MAX_ENCOUNTER);
|
||||
LoadDungeonEncounterData(encounters);
|
||||
}
|
||||
|
||||
bool IsFinalBoss(Creature const* creature) const override
|
||||
{
|
||||
for (uint32 entry : InfiltratorGulkatEntries)
|
||||
if (creature->GetEntry() == entry)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnUnitDeath(Unit* unit) override
|
||||
{
|
||||
// No boss AI is bound (entry unknown): close the encounter here so SMSG_BOSS_KILL 3361 goes out.
|
||||
if (Creature const* creature = unit ? unit->ToCreature() : nullptr)
|
||||
if (IsFinalBoss(creature) && GetBossState(BOSS_INFILTRATOR_GULKAT) != DONE)
|
||||
SetBossState(BOSS_INFILTRATOR_GULKAT, DONE);
|
||||
|
||||
DelveInstanceScript::OnUnitDeath(unit);
|
||||
}
|
||||
};
|
||||
|
||||
InstanceScript* GetInstanceScript(InstanceMap* map) const override
|
||||
{
|
||||
return new instance_the_darkway_delve_InstanceScript(map);
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void AddSC_instance_the_darkway_delve()
|
||||
{
|
||||
new instance_the_darkway_delve();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* 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; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The Gulf of Memory (map 2964) - Mul'tha'ul, creature 250939, DungeonEncounter 3359.
|
||||
*
|
||||
* gulf 12.1.0.69497: SMSG_ENCOUNTER_START 3359 / INSTANCE_ENCOUNTER_START / ENGAGE_UNIT at 969466 (ws 24836 = 1),
|
||||
* SMSG_INSTANCE_ENCOUNTER_DISENGAGE_UNIT + SMSG_ENCOUNTER_END + SMSG_BOSS_KILL 3359 + INSTANCE_ENCOUNTER_END at
|
||||
* 1062320 (ws 24836 = 0). BossAI + InstanceScript::SetBossState produce exactly those frames; the scenario
|
||||
* completion that follows (PROGRESS 60399 -> SCENARIO_COMPLETED 3177) is raised by DelveInstanceScript::OnUnitDeath
|
||||
* through delve_template.finalBossEntry = 250939.
|
||||
*
|
||||
* No combat kit: not one of his spells is recoverable from the client DB2s or from the capture's SPELL_START frames
|
||||
* by caster entry at this point (REPORT.md 4 lists only the creature census). He keeps default melee behaviour.
|
||||
*/
|
||||
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ScriptMgr.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Matches BOSS_MULTHAUL in instance_gulf_of_memory_delve.cpp (SetBossNumber(1)).
|
||||
constexpr uint32 BOSS_MULTHAUL = 0;
|
||||
|
||||
// struct boss_multhaul : public BossAI
|
||||
// {
|
||||
// boss_multhaul(Creature* creature) : BossAI(creature, BOSS_MULTHAUL) { }
|
||||
// };
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void AddSC_gulf_of_memory_encounters()
|
||||
{
|
||||
//RegisterCreatureAI(boss_multhaul);
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* 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; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The Gulf of Memory - map 2964, difficulty 208, scenario 3177, LFGDungeons 3070, AreaTable 16595, entered from
|
||||
* Harandar (2694). Evidence: REPORT.md 2.2 / 4 / 5 and the gulf 12.1.0.69497 capture.
|
||||
*
|
||||
* Scenario 3177 steps: 16081 "Gather Junk" -> 16083 "Free Webbed Haranir" -> 16084 "Remembered horrors defeated" ->
|
||||
* 16082 "Mul'tha'ul defeated" (criteria 60399 / GameEvent 85913). The boss (250939, DungeonEncounter 3359) is NOT a
|
||||
* static spawn: his create block arrives 373 ms after step 16082 becomes current (SCENARIO_STATE 864714 -> create
|
||||
* 865087) at the position below, so he is summoned when the scenario reaches that step.
|
||||
*
|
||||
* Steps 0-2 (candle spell 1266697 x4, GameEvents 99942 x8 / 99943 x4, weighted kills 250912-250919 + 92589 x10) are
|
||||
* work item 8 and not scripted here; the base class completes the delve on the boss kill regardless.
|
||||
*/
|
||||
|
||||
#include "delves_common.h"
|
||||
#include "DB2Structure.h"
|
||||
#include "Map.h"
|
||||
#include "ScriptMgr.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static char const* const GulfOfMemoryScriptName = "instance_gulf_of_memory_delve";
|
||||
|
||||
enum GulfOfMemoryData
|
||||
{
|
||||
BOSS_MULTHAUL = 0,
|
||||
|
||||
MAX_ENCOUNTER
|
||||
};
|
||||
|
||||
enum GulfOfMemoryIds : uint32
|
||||
{
|
||||
NPC_MULTHAUL = 250939, // "Mul'tha'ul" <Lord of the Deeps>, Elite, hp x10.0 (census)
|
||||
DUNGEON_ENCOUNTER_MULTHAUL = 3359, // DungeonEncounter.db2, MapID 2964 (SMSG_ENCOUNTER_START 969466, SMSG_BOSS_KILL 1062320)
|
||||
SCENARIO_STEP_MULTHAUL = 16082, // ScenarioStep OrderIndex 3 of 3177
|
||||
};
|
||||
|
||||
// gulf 865087: create block of 250939, creature position quad (self-validated decode, matches 2026_08_08_41_world.sql)
|
||||
Position const MulthaulSpawnPos = { -198.668f, 645.146f, 176.693f, 6.2232f };
|
||||
|
||||
DungeonEncounterData const encounters[] =
|
||||
{
|
||||
{ BOSS_MULTHAUL, {{ DUNGEON_ENCOUNTER_MULTHAUL }} }
|
||||
};
|
||||
|
||||
class instance_gulf_of_memory_delve : public InstanceMapScript
|
||||
{
|
||||
public:
|
||||
instance_gulf_of_memory_delve() : InstanceMapScript(GulfOfMemoryScriptName, 2964) { }
|
||||
|
||||
struct instance_gulf_of_memory_delve_InstanceScript : public Delves::DelveInstanceScript
|
||||
{
|
||||
instance_gulf_of_memory_delve_InstanceScript(InstanceMap* map)
|
||||
: DelveInstanceScript(map, 1 /* tier resolved at OnPlayerEnter from m_delveSelectedTier */)
|
||||
{
|
||||
SetBossNumber(MAX_ENCOUNTER);
|
||||
LoadDungeonEncounterData(encounters);
|
||||
}
|
||||
|
||||
void OnScenarioStepChanged(ScenarioStepEntry const* step) override
|
||||
{
|
||||
if (!step || step->ID != SCENARIO_STEP_MULTHAUL || _bossSummoned)
|
||||
return;
|
||||
|
||||
_bossSummoned = true;
|
||||
instance->SummonCreature(NPC_MULTHAUL, MulthaulSpawnPos);
|
||||
}
|
||||
|
||||
private:
|
||||
bool _bossSummoned = false;
|
||||
};
|
||||
|
||||
InstanceScript* GetInstanceScript(InstanceMap* map) const override
|
||||
{
|
||||
return new instance_gulf_of_memory_delve_InstanceScript(map);
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void AddSC_instance_gulf_of_memory_delve()
|
||||
{
|
||||
new instance_gulf_of_memory_delve();
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* 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; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "delves_common.h"
|
||||
#include "ScriptMgr.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static char const* const ShadowEnclaveScriptName = "instance_shadow_enclave_delve";
|
||||
|
||||
enum ShadowEnclaveData
|
||||
{
|
||||
BOSS_LORD_ANTENORIAN = 0,
|
||||
|
||||
MAX_ENCOUNTER
|
||||
};
|
||||
|
||||
// DungeonEncounter.db2 3368 "Antenorian", MapID 2952 - SMSG_ENCOUNTER_START 2493067 / SMSG_BOSS_KILL 3368 2611872
|
||||
// (eversong 12.1.0.69497; deatholme 698731 / 742616). Binding it here is what makes SetBossState(DONE) send
|
||||
// SMSG_BOSS_KILL and the encounter frames.
|
||||
DungeonEncounterData const encounters[] =
|
||||
{
|
||||
{ BOSS_LORD_ANTENORIAN, {{ 3368 }} }
|
||||
};
|
||||
|
||||
class instance_shadow_enclave_delve : public InstanceMapScript
|
||||
{
|
||||
public:
|
||||
instance_shadow_enclave_delve() : InstanceMapScript(ShadowEnclaveScriptName, 2952) { }
|
||||
|
||||
struct instance_shadow_enclave_delve_InstanceScript : public Delves::DelveInstanceScript
|
||||
{
|
||||
instance_shadow_enclave_delve_InstanceScript(InstanceMap* map)
|
||||
: DelveInstanceScript(map, 1 /* tier resolved at OnPlayerEnter from m_delveSelectedTier */)
|
||||
{
|
||||
SetBossNumber(MAX_ENCOUNTER);
|
||||
LoadDungeonEncounterData(encounters);
|
||||
}
|
||||
};
|
||||
|
||||
InstanceScript* GetInstanceScript(InstanceMap* map) const override
|
||||
{
|
||||
return new instance_shadow_enclave_delve_InstanceScript(map);
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void AddSC_instance_shadow_enclave_delve()
|
||||
{
|
||||
new instance_shadow_enclave_delve();
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* 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; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The Shadow Enclave (Midnight S1 delve) - scenario objective creatures.
|
||||
* Map 2952, Difficulty 208 (Delves), Scenario 3154, LFGDungeons 3069, AreaTable 16594.
|
||||
*
|
||||
* WHY THESE NEED SCRIPTS AT ALL
|
||||
* -----------------------------
|
||||
* Every step of scenario 3154 is driven by Criteria Type 92
|
||||
* (CriteriaType::AnyoneTriggerGameEventScenario, DBCEnums.h:639 - "Anyone will Trigger game event
|
||||
* {GameEvents} (Scenario Only)"), whose Asset is a GameEvents id, NOT a creature entry. GameEvents
|
||||
* is not shipped in the client DB2 set and no event_scripts / smart_scripts row in integ_world
|
||||
* references any of these ids, so nothing in data connects "this creature died" to "this step
|
||||
* advances". The server has to raise the event explicitly - that is what these scripts do, and it
|
||||
* is the whole of what they do.
|
||||
*
|
||||
* SCENARIO 3154 STEP TABLE
|
||||
* ------------------------
|
||||
* Read out of the 12.0.7 client DB2s (ScenarioStep.db2 -> CriteriaTree.db2 -> Criteria.db2) and
|
||||
* corroborated on the wire by C:\sniff\alliance_deatholme_delve\dumps\
|
||||
* dump_12.0.1.66562_2026-03-26_08-04-06.pkt ("Deatholme delve" = The Shadow Enclave), which
|
||||
* contains a complete run and whose SMSG_SCENARIO_STATE step order is
|
||||
* 16032 -> 16028 -> 16029 -> 16030 -> 16031 -> SMSG_SCENARIO_COMPLETED(3154):
|
||||
*
|
||||
* Order 0 Step 16032 tree 214779/214780 criteria 107837 event 99761 amount 1
|
||||
* "Pursue Antenorian" -- NOT scripted here, no
|
||||
* ScriptName exists for it
|
||||
* Order 1 Step 16028 tree 214781/214782 criteria 107838 event 99763 amount 2
|
||||
* Void Focus destroyed -> npc_void_focus_se (creature 250266)
|
||||
* Order 2 Step 16029 tree 214768
|
||||
* 221264 "Rituals stopped" amount 3 -> 214769 criteria 107834 event 99755
|
||||
* -> npc_darkcaller (250242 / 251898 / 251899)
|
||||
* 214770 "Cultists purged" amount 200, weighted trash events 87796/87797/87798/87795
|
||||
* -- NOT scripted here, see OPEN ITEMS below
|
||||
* Order 3 Step 16030 tree 214775/214776 criteria 107835 event 99756 amount 3
|
||||
* "Antenorian's Devoted slain" (creature 250275)
|
||||
* -- NOT scripted here, no ScriptName exists for it
|
||||
* Order 4 Step 16031 tree 214777/214778 criteria 60399 event 85913 amount 1
|
||||
* Antenorian slain -> npc_lord_antenorian (creature 246717)
|
||||
*
|
||||
* Event 85913 is a shared "delve boss slain" event reused by 32 criteria trees across many delves,
|
||||
* so it is safe to raise from the boss's death: it only satisfies whichever delve scenario is
|
||||
* actually running on the map.
|
||||
*
|
||||
* The final boss identity is not guesswork either. In the same capture:
|
||||
* SMSG_ENCOUNTER_START EncounterID 3368, DifficultyID 208
|
||||
* SMSG_INSTANCE_ENCOUNTER_ENGAGE_UNIT map 2952, entry 246717
|
||||
* SMSG_BOSS_KILL DungeonEncounterID 3368
|
||||
* SMSG_QUEST_UPDATE_ADD_CREDIT map 2952, entry 246717 (quest 86636 "Void Walk With Me")
|
||||
* SMSG_SCENARIO_COMPLETED ScenarioID 3154
|
||||
* SMSG_QUERY_CREATURE_RESPONSE entry 246717 = "Lord Antenorian"
|
||||
* and DungeonEncounter.db2 id 3368 is Name "Antenorian", MapID 2952.
|
||||
*
|
||||
* OPEN ITEMS (deliberately not implemented - no evidence, so no invented behaviour)
|
||||
* --------------------------------------------------------------------------------
|
||||
* * No combat kit. Not one spell id for Lord Antenorian, the Darkcallers or the Void Focus is
|
||||
* recoverable from the client DB2s or from any capture on this machine, so these creatures get
|
||||
* no ability rotation. They keep TC's default melee behaviour; nothing is faked.
|
||||
* * "Cultists purged" (tree 214770) needs per-trash-mob credit against four weighted events with
|
||||
* no creature->event mapping in any source. It stays unimplemented.
|
||||
* * Step 0 "Pursue Antenorian" (event 99761) has no creature to hang off - it is almost certainly
|
||||
* an area trigger, which no source on this machine identifies.
|
||||
* * MOST IMPORTANTLY: map 2952 has ZERO creature and ZERO gameobject spawns in integ_world, and
|
||||
* no spawn file for it exists anywhere in this repo or in any other world DB on this box. These
|
||||
* scripts are correct but unreachable until the roster is imported. The 66562 capture above
|
||||
* carries the full 167-entry creature roster with SMSG_UPDATE_OBJECT positions on map 2952 and
|
||||
* is the obvious source for that import.
|
||||
*/
|
||||
|
||||
#include "Creature.h"
|
||||
#include "DBCEnums.h"
|
||||
#include "InstanceScript.h"
|
||||
#include "Log.h"
|
||||
#include "ScriptedCreature.h"
|
||||
#include "ScriptMgr.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Criteria.db2 Asset values (GameEvents ids) for scenario 3154's Type-92 criteria. See the step
|
||||
// table in the file header for the full derivation.
|
||||
enum ShadowEnclaveScenarioEvents : uint32
|
||||
{
|
||||
EVENT_VOID_FOCUS_DESTROYED = 99763, // step order 1, criteria 107838, amount 2
|
||||
EVENT_RITUAL_STOPPED = 99755, // step order 2, criteria 107834, amount 3
|
||||
// 85913 (step order 4, criteria 60399 "Antenorian slain") is raised by DelveInstanceScript::OnUnitDeath for the
|
||||
// template's final boss - see Delves::GAME_EVENT_DELVE_BOSS_SLAIN in delves_common.h.
|
||||
};
|
||||
|
||||
// Matches BOSS_LORD_ANTENORIAN in instance_shadow_enclave_delve.cpp (SetBossNumber(1)).
|
||||
constexpr uint32 BOSS_LORD_ANTENORIAN = 0;
|
||||
|
||||
// creature_text GroupID for Lord Antenorian's death yell, inserted by
|
||||
// sql/updates/world/master/2026_04_29_03_world.sql (CreatureID 246717, GroupID 0, Type 14 = Yell).
|
||||
constexpr uint8 SAY_ANTENORIAN_DEATH = 0;
|
||||
|
||||
// Raises a scenario game event for every player in the delve instance. Delve maps are
|
||||
// InstanceType 5 (Scenario), which is the gate CriteriaHandler applies to Type-92 criteria.
|
||||
void RaiseScenarioEvent(Creature* source, uint32 gameEventId)
|
||||
{
|
||||
InstanceScript* instance = source->GetInstanceScript();
|
||||
if (!instance)
|
||||
{
|
||||
TC_LOG_DEBUG("scripts.delves",
|
||||
"The Shadow Enclave: creature {} (entry {}) died outside an instance script; scenario event {} not raised.",
|
||||
source->GetGUID().ToString(), source->GetEntry(), gameEventId);
|
||||
return;
|
||||
}
|
||||
|
||||
instance->DoUpdateCriteria(CriteriaType::AnyoneTriggerGameEventScenario, gameEventId, 0, source);
|
||||
|
||||
TC_LOG_DEBUG("scripts.delves",
|
||||
"The Shadow Enclave: creature entry {} died, raised scenario game event {}.",
|
||||
source->GetEntry(), gameEventId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Void Focus - creature 250266. Scenario 3154 step order 1 needs two of them destroyed.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
struct npc_void_focus_se : public ScriptedAI
|
||||
{
|
||||
npc_void_focus_se(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
RaiseScenarioEvent(me, EVENT_VOID_FOCUS_DESTROYED);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Darkcallers - creatures 250242 (Lysaille), 251898 (Thelamorn), 251899 (Cimberon).
|
||||
// Scenario 3154 step order 2, sub-tree 221264 "Rituals stopped", needs all three.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
struct npc_darkcaller : public ScriptedAI
|
||||
{
|
||||
npc_darkcaller(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
RaiseScenarioEvent(me, EVENT_RITUAL_STOPPED);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Lord Antenorian - creature 246717, the delve's final boss (DungeonEncounter 3368).
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
struct npc_lord_antenorian : public ScriptedAI
|
||||
{
|
||||
npc_lord_antenorian(Creature* creature) : ScriptedAI(creature) { }
|
||||
|
||||
void JustEngagedWith(Unit* who) override
|
||||
{
|
||||
ScriptedAI::JustEngagedWith(who);
|
||||
|
||||
// SMSG_ENCOUNTER_START 3368 (eversong 2493067, deatholme 698731); ws 24836 -> 1
|
||||
if (InstanceScript* instance = me->GetInstanceScript())
|
||||
instance->SetBossState(BOSS_LORD_ANTENORIAN, IN_PROGRESS);
|
||||
}
|
||||
|
||||
void JustDied(Unit* /*killer*/) override
|
||||
{
|
||||
Talk(SAY_ANTENORIAN_DEATH);
|
||||
|
||||
// GameEvent 85913 (EVENT_ANTENORIAN_SLAIN) and the completion itself are raised by
|
||||
// Delves::DelveInstanceScript::OnUnitDeath via delve_template.finalBossEntry = 246717; this only closes the
|
||||
// encounter frame (SMSG_ENCOUNTER_END + SMSG_BOSS_KILL 3368, eversong 2611872).
|
||||
if (InstanceScript* instance = me->GetInstanceScript())
|
||||
instance->SetBossState(BOSS_LORD_ANTENORIAN, DONE);
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void AddSC_shadow_enclave_encounters()
|
||||
{
|
||||
RegisterCreatureAI(npc_void_focus_se);
|
||||
RegisterCreatureAI(npc_darkcaller);
|
||||
RegisterCreatureAI(npc_lord_antenorian);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ namespace
|
||||
// record) are not symbolicated in the 67186 IDA db, so we send the
|
||||
// creature entry as the most defensible interpretation.
|
||||
WorldPackets::Delves::ShowDelvesCompanionConfigurationUI packet;
|
||||
packet.CreatureOrSpellID = me->GetEntry();
|
||||
packet.Unknown = me->GetEntry();
|
||||
player->SendDirectMessage(packet.Write());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,9 @@ enum HunterSpells
|
||||
SPELL_HUNTER_KILL_COMMAND = 34026,
|
||||
SPELL_HUNTER_KILL_COMMAND_TRIGGER = 83381,
|
||||
SPELL_HUNTER_KILL_COMMAND_CHARGE = 118171,
|
||||
SPELL_HUNTER_BESTIAL_WRATH = 38371,
|
||||
SPELL_HUNTER_BESTIAL_WRATH = 19574,
|
||||
SPELL_HUNTER_BESTIAL_WRATH_PET = 38371,
|
||||
SPELL_HUNTER_BESTIAL_WRATH_DAMAGE = 344572,
|
||||
SPELL_HUNTER_ANIMAL_COMPANION = 267116,
|
||||
SPELL_HUNTER_SUMMON_ANIMAL_COMPANION = 273277,
|
||||
SPELL_HUNTER_WILD_THRASH = 1264359,
|
||||
@@ -1638,8 +1640,8 @@ class spell_hun_call_pet : public SpellScript
|
||||
}
|
||||
};
|
||||
|
||||
// 38371 - Bestial Wrath
|
||||
class spell_hun_bestial_wrath : public SpellScript
|
||||
// 38371 - Bestial Wrath (legacy pet enrage buff)
|
||||
class spell_hun_bestial_wrath_pet : public SpellScript
|
||||
{
|
||||
void HandleAfterCast()
|
||||
{
|
||||
@@ -1653,13 +1655,74 @@ class spell_hun_bestial_wrath : public SpellScript
|
||||
{
|
||||
// Ensure the pet gets the Bestial Wrath effects
|
||||
// The spell data has TARGET_UNIT_NEARBY_ENTRY but may not target the pet correctly
|
||||
caster->CastSpell(pet, SPELL_HUNTER_BESTIAL_WRATH, true);
|
||||
caster->CastSpell(pet, SPELL_HUNTER_BESTIAL_WRATH_PET, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
AfterCast += SpellCastFn(spell_hun_bestial_wrath_pet::HandleAfterCast);
|
||||
}
|
||||
};
|
||||
|
||||
// 19574 - Bestial Wrath
|
||||
class spell_hun_bestial_wrath : public SpellScript
|
||||
{
|
||||
bool Validate(SpellInfo const* /*spellInfo*/) override
|
||||
{
|
||||
return ValidateSpellInfo({ SPELL_HUNTER_BESTIAL_WRATH_DAMAGE });
|
||||
}
|
||||
|
||||
SpellCastResult CheckCast()
|
||||
{
|
||||
Unit* caster = GetCaster();
|
||||
if (!caster || caster->GetTypeId() != TYPEID_PLAYER)
|
||||
return SPELL_CAST_OK;
|
||||
|
||||
Pet* pet = caster->ToPlayer()->GetPet();
|
||||
if (!pet)
|
||||
return SPELL_FAILED_NO_PET;
|
||||
|
||||
if (!pet->IsAlive())
|
||||
{
|
||||
SetCustomCastResultMessage(SPELL_CUSTOM_ERROR_PET_IS_DEAD);
|
||||
return SPELL_FAILED_CUSTOM_ERROR;
|
||||
}
|
||||
|
||||
return SPELL_CAST_OK;
|
||||
}
|
||||
|
||||
void HandleAfterCast() const
|
||||
{
|
||||
Unit* caster = GetCaster();
|
||||
if (!caster || caster->GetTypeId() != TYPEID_PLAYER)
|
||||
return;
|
||||
|
||||
Player* player = caster->ToPlayer();
|
||||
Pet* pet = player->GetPet();
|
||||
if (!pet || !pet->IsAlive())
|
||||
return;
|
||||
|
||||
// Removes all crowd control effects from your pet, working like a PvP trinket
|
||||
pet->RemoveAurasWithMechanic(IMMUNE_TO_MOVEMENT_IMPAIRMENT_AND_LOSS_CONTROL_MASK, AURA_REMOVE_BY_DEFAULT, SPELL_HUNTER_BESTIAL_WRATH);
|
||||
|
||||
// Grant the pet its own Bestial Wrath aura so both of you deal increased damage.
|
||||
// The pet re-enters this script on its own cast, but the player-only guard above prevents recursion.
|
||||
pet->CastSpell(pet, SPELL_HUNTER_BESTIAL_WRATH, TRIGGERED_FULL_MASK);
|
||||
|
||||
// The pet instantly deals Physical damage (344572) to its target
|
||||
Unit* target = pet->GetVictim();
|
||||
if ((!target || !pet->IsValidAttackTarget(target)) && !player->GetTarget().IsEmpty())
|
||||
target = ObjectAccessor::GetUnit(*player, player->GetTarget());
|
||||
|
||||
if (target && pet->IsValidAttackTarget(target) && pet->IsWithinLOSInMap(target))
|
||||
pet->CastSpell(target, SPELL_HUNTER_BESTIAL_WRATH_DAMAGE, true);
|
||||
}
|
||||
|
||||
void Register() override
|
||||
{
|
||||
OnCheckCast += SpellCheckCastFn(spell_hun_bestial_wrath::CheckCast);
|
||||
AfterCast += SpellCastFn(spell_hun_bestial_wrath::HandleAfterCast);
|
||||
}
|
||||
};
|
||||
@@ -1890,6 +1953,7 @@ void AddSC_hunter_spell_scripts()
|
||||
RegisterSpellScript(spell_hun_kill_command_proc);
|
||||
RegisterSpellScript(spell_hun_call_pet);
|
||||
RegisterSpellScript(spell_hun_bestial_wrath);
|
||||
RegisterSpellScript(spell_hun_bestial_wrath_pet);
|
||||
RegisterSpellScript(spell_hun_wild_thrash);
|
||||
RegisterSpellScript(spell_hun_trick_shots);
|
||||
RegisterSpellScript(spell_hun_aspect_of_the_hydra);
|
||||
|
||||
@@ -110,11 +110,11 @@ struct npc_garrick_expedition_209057 : public ScriptedAI
|
||||
{
|
||||
if (Player* player = unit->ToPlayer())
|
||||
{
|
||||
Group* group = player->GetGroup();
|
||||
me->AI()->Talk(0);
|
||||
me->SetOwnerGUID(player->GetGUID()); //set player owner
|
||||
me->GetMotionMaster()->MoveFollow(player, 2.0f, PET_FOLLOW_ANGLE, {}, MOTION_SLOT_ACTIVE);
|
||||
group->AddFollowerMemberTank(me);
|
||||
if (Group* group = player->GetGroup())
|
||||
group->AddFollowerMemberTank(me);
|
||||
if (int32 playerFaction = player->GetFaction())
|
||||
me->SetFaction(playerFaction);
|
||||
|
||||
@@ -249,8 +249,8 @@ struct npc_Shuja_Grimaxe_214390 : public ScriptedAI
|
||||
{
|
||||
if (Player* player = unit->ToPlayer())
|
||||
{
|
||||
Group* group = player->GetGroup();
|
||||
group->AddFollowerMemberDPS(me);
|
||||
if (Group* group = player->GetGroup())
|
||||
group->AddFollowerMemberDPS(me);
|
||||
me->AI()->Talk(0);
|
||||
me->SetOwnerGUID(player->GetGUID()); //set player owner
|
||||
me->GetMotionMaster()->MoveFollow(player, 2.0f, 60.f, {}, MOTION_SLOT_ACTIVE);
|
||||
@@ -372,8 +372,8 @@ struct npc_Austin_Huxworth_209065 : public ScriptedAI
|
||||
{
|
||||
if (Player* player = unit->ToPlayer())
|
||||
{
|
||||
Group* group = player->GetGroup();
|
||||
group->AddFollowerMemberDPS(me);
|
||||
if (Group* group = player->GetGroup())
|
||||
group->AddFollowerMemberDPS(me);
|
||||
me->AI()->Talk(0);
|
||||
me->SetOwnerGUID(player->GetGUID()); //set player owner
|
||||
me->GetMotionMaster()->MoveFollow(player, 4.0f, 75.f, {}, MOTION_SLOT_ACTIVE);
|
||||
@@ -471,8 +471,8 @@ struct npc_Meredy_Huntswell_209059 : public ScriptedAI
|
||||
{
|
||||
if (Player* player = unit->ToPlayer())
|
||||
{
|
||||
Group* group = player->GetGroup();
|
||||
group->AddFollowerMemberDPS(me);
|
||||
if (Group* group = player->GetGroup())
|
||||
group->AddFollowerMemberDPS(me);
|
||||
me->SetOwnerGUID(player->GetGUID()); //set player owner
|
||||
me->GetMotionMaster()->MoveFollow(player, 4.0f, 30.f, {}, MOTION_SLOT_ACTIVE);
|
||||
|
||||
@@ -551,8 +551,8 @@ struct npc_Crenna_Earth_Daughter_209072 : public ScriptedAI
|
||||
{
|
||||
if (Player* player = unit->ToPlayer())
|
||||
{
|
||||
Group* group = player->GetGroup();
|
||||
group->AddFollowerMemberHealer(me);
|
||||
if (Group* group = player->GetGroup())
|
||||
group->AddFollowerMemberHealer(me);
|
||||
me->AI()->Talk(0);
|
||||
me->SetOwnerGUID(player->GetGUID()); //set player owner
|
||||
me->GetMotionMaster()->MoveFollow(player, 4.0f, 80.f, {}, MOTION_SLOT_ACTIVE);
|
||||
|
||||
Reference in New Issue
Block a user