You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
529 lines
15 KiB
529 lines
15 KiB
-- ----------------------------------------------------------------------------
|
|
-- Localized Lua globals.
|
|
-- ----------------------------------------------------------------------------
|
|
-- Functions
|
|
local pairs = _G.pairs
|
|
local time = _G.time
|
|
|
|
-- Libraries
|
|
local table = _G.table
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- AddOn namespace.
|
|
-- ----------------------------------------------------------------------------
|
|
local AddOnFolderName, private = ...
|
|
local Data = private.Data
|
|
local Enum = private.Enum
|
|
local EventMessage = private.EventMessage
|
|
|
|
|
|
local LibStub = _G.LibStub
|
|
local HereBeDragons = LibStub("HereBeDragons-2.0")
|
|
local LibSharedMedia = LibStub("LibSharedMedia-3.0")
|
|
local NPCScan = LibStub("AceAddon-3.0"):GetAddon(AddOnFolderName)
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- Helpers.
|
|
-- ----------------------------------------------------------------------------
|
|
local ProcessDetection
|
|
do
|
|
local throttledNPCs = {}
|
|
|
|
function ProcessDetection(detectionData)
|
|
local npcID = detectionData.npcID
|
|
local profile = private.db.profile
|
|
local detection = profile.detection
|
|
local throttleTime = throttledNPCs[npcID]
|
|
local now = time()
|
|
|
|
if not Data.Scanner.NPCs[npcID] or (throttleTime and now < throttleTime + detection.intervalSeconds) or (not detection.whileOnTaxi and _G.UnitOnTaxi("player")) then
|
|
return
|
|
end
|
|
|
|
throttledNPCs[npcID] = now
|
|
|
|
detectionData.npcName = detectionData.npcName or NPCScan:GetNPCNameFromID(npcID)
|
|
detectionData.unitClassification = detectionData.unitClassification or "rare"
|
|
|
|
NPCScan:Pour(_G.ERR_ZONE_EXPLORED:format(("%s %s"):format(detectionData.npcName, _G.PARENS_TEMPLATE:format(detectionData.sourceText))), 0, 1, 0)
|
|
NPCScan:DispatchSensoryCues()
|
|
NPCScan:SendMessage(EventMessage.DetectedNPC, detectionData)
|
|
|
|
-- TODO: Make the Overlays object listen for the DetectedNPC message and run its own methods
|
|
private.Overlays.Found(npcID)
|
|
private.Overlays.Remove(npcID)
|
|
end
|
|
end
|
|
|
|
local function ProcessUnit(unitToken, sourceText)
|
|
if _G.UnitIsUnit("player", unitToken) then
|
|
return
|
|
end
|
|
|
|
local npcID = private.UnitTokenToCreatureID(unitToken)
|
|
|
|
if npcID then
|
|
local unitIsDead = _G.UnitIsDead(unitToken)
|
|
|
|
if private.db.profile.detection.ignoreDeadNPCs and unitIsDead then
|
|
return
|
|
end
|
|
|
|
local detectionData = {
|
|
isDead = unitIsDead,
|
|
npcID = npcID,
|
|
npcName = _G.UnitName(unitToken),
|
|
sourceText = sourceText,
|
|
unitClassification = _G.UnitClassification(unitToken),
|
|
unitCreatureType = _G.UnitCreatureType(unitToken),
|
|
unitLevel = _G.UnitLevel(unitToken),
|
|
unitToken = unitToken,
|
|
}
|
|
|
|
ProcessDetection(detectionData)
|
|
|
|
NPCScan:SendMessage(EventMessage.UnitInformationAvailable, detectionData)
|
|
end
|
|
end
|
|
|
|
local function CanAddToScanList(npcID)
|
|
local profile = private.db.profile
|
|
|
|
if profile.blacklist.npcIDs[npcID] then
|
|
private.Debug("Skipping %s (%d) - blacklisted.", NPCScan:GetNPCNameFromID(npcID), npcID)
|
|
return false
|
|
end
|
|
|
|
local npc = Data.NPCs[npcID]
|
|
|
|
-- This is a custom NPC addition; no further processing is possible.
|
|
if not npc then
|
|
return true
|
|
end
|
|
|
|
if npc.factionGroup == _G.UnitFactionGroup("player") then
|
|
return false
|
|
end
|
|
|
|
local isTameable = npc.isTameable
|
|
local detection = profile.detection
|
|
|
|
if isTameable and not detection.tameables then
|
|
return false
|
|
end
|
|
|
|
if not isTameable and not detection.rares then
|
|
return false
|
|
end
|
|
|
|
if npc:HasActiveWorldQuest() then
|
|
return true
|
|
end
|
|
|
|
local hasQuest = npc:HasQuest()
|
|
|
|
if hasQuest then
|
|
if not npc:IsQuestComplete() then
|
|
return true
|
|
elseif detection.ignoreCompletedQuestObjectives then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local achievementID = npc.achievementID
|
|
|
|
if achievementID then
|
|
if detection.achievementIDs[achievementID] == Enum.DetectionGroupStatus.Disabled then
|
|
return false
|
|
end
|
|
|
|
if detection.ignoreCompletedAchievementCriteria and npc:IsAchievementCriteriaComplete() then
|
|
return false
|
|
end
|
|
elseif npc.worldQuestID and not hasQuest then
|
|
-- Ignore NPCs with an inactive World Quest but no tracking quest and no achievement.
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function MergeUserDefinedWithScanList(npcList)
|
|
if npcList and private.db.profile.detection.userDefined then
|
|
for npcID in pairs(npcList) do
|
|
Data.Scanner.NPCs[npcID] = _G.setmetatable({}, private.NPCMetatable)
|
|
end
|
|
end
|
|
end
|
|
|
|
function NPCScan:UpdateScanList(_, mapID)
|
|
local scannerData = Data.Scanner
|
|
mapID = mapID or HereBeDragons:GetPlayerZone()
|
|
|
|
if not mapID or mapID < 0 then
|
|
return
|
|
end
|
|
|
|
scannerData.mapID = mapID
|
|
scannerData.continentID = Data.Maps[mapID].continentID
|
|
|
|
for npcID in pairs(scannerData.NPCs) do
|
|
private.Overlays.Remove(npcID)
|
|
end
|
|
|
|
table.wipe(scannerData.NPCs)
|
|
scannerData.NPCCount = 0
|
|
|
|
local profile = private.db.profile
|
|
local userDefined = profile.userDefined
|
|
|
|
-- No zone or continent specified, so always look for these.
|
|
MergeUserDefinedWithScanList(userDefined.npcIDs)
|
|
|
|
if profile.blacklist.mapIDs[scannerData.mapID] or profile.detection.continentIDs[scannerData.continentID] == Enum.DetectionGroupStatus.Disabled then
|
|
private.Debug("continentID or mapID is blacklisted; terminating update.")
|
|
self:SendMessage(EventMessage.ScannerDataUpdated, scannerData)
|
|
|
|
return
|
|
end
|
|
|
|
local zoneNPCCount = 0
|
|
local npcList = Data.Maps[scannerData.mapID].NPCs
|
|
|
|
if npcList then
|
|
for npcID in pairs(npcList) do
|
|
if CanAddToScanList(npcID) then
|
|
zoneNPCCount = zoneNPCCount + 1;
|
|
scannerData.NPCs[npcID] = Data.NPCs[npcID]
|
|
|
|
private.Overlays.Add(npcID)
|
|
end
|
|
end
|
|
|
|
scannerData.NPCCount = zoneNPCCount
|
|
end
|
|
|
|
MergeUserDefinedWithScanList(userDefined.continentNPCs[scannerData.continentID])
|
|
MergeUserDefinedWithScanList(userDefined.mapNPCs[scannerData.mapID])
|
|
|
|
self:SendMessage(EventMessage.ScannerDataUpdated, scannerData)
|
|
end
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- Events.
|
|
-- ----------------------------------------------------------------------------
|
|
local function UpdateScanListAchievementCriteria()
|
|
local needsUpdate = false
|
|
|
|
for _, npc in pairs(Data.Scanner.NPCs) do
|
|
if npc.achievementID and npc.achievementCriteriaID and not npc:IsAchievementCriteriaComplete() then
|
|
local _, _, isCompleted = _G.GetAchievementCriteriaInfoByID(npc.achievementID, npc.achievementCriteriaID)
|
|
|
|
if isCompleted then
|
|
npc.isCriteriaCompleted = isCompleted
|
|
|
|
private.GetOrUpdateNPCOptions()
|
|
|
|
if private.db.profile.detection.ignoreCompletedAchievementCriteria then
|
|
needsUpdate = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if needsUpdate then
|
|
NPCScan:UpdateScanList()
|
|
end
|
|
end
|
|
|
|
private.UpdateScanListAchievementCriteria = UpdateScanListAchievementCriteria
|
|
|
|
local function UpdateScanListQuestObjectives()
|
|
local needsUpdate = false
|
|
|
|
if private.db.profile.detection.ignoreCompletedQuestObjectives then
|
|
local NPCs = Data.Scanner.NPCs
|
|
|
|
for npcID in pairs(NPCs) do
|
|
if NPCs[npcID]:IsQuestComplete() then
|
|
needsUpdate = true
|
|
end
|
|
end
|
|
|
|
if needsUpdate then
|
|
NPCScan:UpdateScanList()
|
|
end
|
|
end
|
|
end
|
|
|
|
private.UpdateScanListQuestObjectives = UpdateScanListQuestObjectives
|
|
|
|
function NPCScan:ACHIEVEMENT_EARNED(_, achievementID)
|
|
if Data.Achievements[achievementID] then
|
|
Data.Achievements[achievementID].isCompleted = true
|
|
|
|
if private.db.profile.detection.ignoreCompletedAchievementCriteria then
|
|
-- Disable tracking for the achievement, since the above setting implies it.
|
|
private.db.profile.detection.achievementIDs[achievementID] = Enum.DetectionGroupStatus.Disabled
|
|
end
|
|
|
|
UpdateScanListAchievementCriteria()
|
|
end
|
|
end
|
|
|
|
function NPCScan:CRITERIA_UPDATE()
|
|
UpdateScanListAchievementCriteria()
|
|
end
|
|
|
|
-- Apparently some vignette NPC daily quests are only flagged as complete after looting...
|
|
function NPCScan:LOOT_CLOSED()
|
|
UpdateScanListQuestObjectives()
|
|
end
|
|
|
|
function NPCScan:NAME_PLATE_UNIT_ADDED(_, unitToken)
|
|
ProcessUnit(unitToken, _G.UNIT_NAMEPLATES)
|
|
end
|
|
|
|
function NPCScan:PLAYER_ENTERING_WORLD()
|
|
self:UpdateScanList("PLAYER_ENTERING_WORLD", _G.C_Map.GetBestMapForUnit("player"))
|
|
end
|
|
|
|
function NPCScan:PLAYER_TARGET_CHANGED()
|
|
ProcessUnit("target", _G.TARGET)
|
|
end
|
|
|
|
function NPCScan:UPDATE_MOUSEOVER_UNIT()
|
|
local mouseoverID = private.UnitTokenToCreatureID("mouseover")
|
|
|
|
if mouseoverID ~= private.UnitTokenToCreatureID("target") then
|
|
ProcessUnit("mouseover", _G.MOUSE_LABEL)
|
|
end
|
|
end
|
|
|
|
do
|
|
local VignetteSourceToPreference = {
|
|
[_G.MINIMAP_LABEL] = "ignoreMiniMap",
|
|
[_G.WORLD_MAP] = "ignoreWorldMap",
|
|
}
|
|
|
|
local IgnoredVignetteAtlasName = {
|
|
VignetteLoot = true,
|
|
VignetteLootElite = true,
|
|
}
|
|
|
|
local function IsIgnoringSource(sourceText)
|
|
return private.db.profile.detection[VignetteSourceToPreference[sourceText]]
|
|
end
|
|
|
|
local function ProcessVignetteGUID(vignetteGUID)
|
|
if not vignetteGUID then
|
|
return
|
|
end
|
|
|
|
local vignetteInfo = _G.C_VignetteInfo.GetVignetteInfo(vignetteGUID);
|
|
|
|
if not vignetteInfo or IgnoredVignetteAtlasName[vignetteInfo.atlasName] then
|
|
return
|
|
end
|
|
|
|
local sourceText = vignetteInfo.onWorldMap and _G.WORLD_MAP or _G.MINIMAP_LABEL
|
|
|
|
if IsIgnoringSource(sourceText) then
|
|
return
|
|
end
|
|
|
|
local vignetteName = vignetteInfo.name
|
|
local vignetteNPCs = private.VignetteIDToNPCMapping[vignetteInfo.vignetteID]
|
|
|
|
local npcID = private.GUIDToCreatureID(vignetteInfo.objectGUID)
|
|
|
|
if vignetteNPCs then
|
|
for index = 1, #vignetteNPCs do
|
|
local vignetteNPC = vignetteNPCs[index]
|
|
local npc = Data.Scanner.NPCs[vignetteNPC.npcID]
|
|
|
|
if npc then
|
|
ProcessDetection({
|
|
npcID = vignetteNPC.npcID,
|
|
sourceText = sourceText,
|
|
unitClassification = npc.classification,
|
|
vignetteName = vignetteName,
|
|
})
|
|
end
|
|
end
|
|
|
|
return
|
|
else
|
|
private.Debug("Unknown vignette: %s - vignetteID %d (NPC ID %d) in mapID %d", vignetteInfo.name, vignetteInfo.vignetteID, npcID or -1, _G.C_Map.GetBestMapForUnit("player"))
|
|
end
|
|
|
|
local npc = npcID and Data.Scanner.NPCs[npcID] or nil
|
|
|
|
-- The objectGUID can be but isn't always an NPC ID, since some NPCs must be summoned from the vignette object.
|
|
if npc then
|
|
ProcessDetection({
|
|
npcID = npcID,
|
|
sourceText = sourceText,
|
|
unitClassification = npc.classification,
|
|
vignetteName = vignetteName,
|
|
})
|
|
|
|
return
|
|
end
|
|
|
|
local questID = private.QuestIDFromName[vignetteName]
|
|
|
|
if questID then
|
|
for questNPCID, questNPC in pairs(private.QuestNPCs[questID]) do
|
|
ProcessDetection({
|
|
npcID = questNPCID,
|
|
sourceText = sourceText,
|
|
unitClassification = questNPC.classification,
|
|
vignetteName = vignetteName,
|
|
})
|
|
end
|
|
|
|
return
|
|
elseif sourceText == _G.WORLD_MAP then
|
|
return
|
|
end
|
|
|
|
npcID = private.NPCIDFromName[vignetteName]
|
|
npc = npcID and Data.Scanner.NPCs[npcID] or nil
|
|
|
|
if npc then
|
|
ProcessDetection({
|
|
npcID = npcID,
|
|
sourceText = sourceText,
|
|
unitClassification = npc.classification,
|
|
vignetteName = vignetteName,
|
|
})
|
|
|
|
return
|
|
end
|
|
end
|
|
|
|
function NPCScan:VIGNETTE_MINIMAP_UPDATED(_, vignetteGUID)
|
|
ProcessVignetteGUID(vignetteGUID)
|
|
end
|
|
|
|
function NPCScan:VIGNETTES_UPDATED()
|
|
local vignetteGUIDs = _G.C_VignetteInfo.GetVignettes()
|
|
|
|
for index = 1, #vignetteGUIDs do
|
|
ProcessVignetteGUID(vignetteGUIDs[index])
|
|
end
|
|
end
|
|
end -- do-block
|
|
|
|
-- ----------------------------------------------------------------------------
|
|
-- Sensory cues.
|
|
-- ----------------------------------------------------------------------------
|
|
do
|
|
local MAX_FLASH_LOOPS = 3
|
|
|
|
local flashFrame = _G.CreateFrame("Frame")
|
|
flashFrame:Hide()
|
|
flashFrame:SetAllPoints()
|
|
flashFrame:SetAlpha(0)
|
|
flashFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
|
|
local flashTexture = flashFrame:CreateTexture()
|
|
flashTexture:SetBlendMode("ADD")
|
|
flashTexture:SetAllPoints()
|
|
|
|
local fadeAnimationGroup = flashFrame:CreateAnimationGroup()
|
|
fadeAnimationGroup:SetLooping("BOUNCE")
|
|
|
|
fadeAnimationGroup:SetScript("OnLoop", function(self, loopState)
|
|
if loopState == "FORWARD" then
|
|
self.LoopCount = self.LoopCount + 1
|
|
|
|
if self.LoopCount >= MAX_FLASH_LOOPS then
|
|
self:Stop()
|
|
flashFrame:Hide()
|
|
end
|
|
end
|
|
end)
|
|
|
|
fadeAnimationGroup:SetScript("OnPlay", function(self)
|
|
self.LoopCount = 0
|
|
end)
|
|
|
|
local fadeAnimIn = private.CreateAlphaAnimation(fadeAnimationGroup, 0, 1, 0.5)
|
|
fadeAnimIn:SetEndDelay(0.25)
|
|
|
|
local ALERT_SOUND_THROTTLE_INTERVAL_SECONDS = 2
|
|
local lastSoundTime = time()
|
|
|
|
function NPCScan:PlayFlashAnimation(texturePath, color)
|
|
flashTexture:SetTexture(LibSharedMedia:Fetch("background", texturePath))
|
|
flashTexture:SetVertexColor(color.r, color.g, color.b, color.a)
|
|
flashFrame:Show()
|
|
|
|
fadeAnimationGroup:Pause() -- Forces OnPlay to fire again if it was already playing
|
|
fadeAnimationGroup:Play()
|
|
end
|
|
|
|
local PlayAlertSounds
|
|
do
|
|
local SOUND_RESTORE_INTERVAL_SECONDS = 5
|
|
local soundsAreOverridden
|
|
|
|
local StoredSoundCVars = {}
|
|
local SoundChannelCVars = {
|
|
Ambience = "Sound_EnableAmbience",
|
|
Master = "Sound_EnableAllSound",
|
|
Music = "Sound_EnableMusic",
|
|
SFX = "Sound_EnableSFX",
|
|
}
|
|
|
|
local function ResetStoredSoundCVars()
|
|
for cvar, value in pairs(StoredSoundCVars) do
|
|
_G.SetCVar(cvar, value)
|
|
end
|
|
|
|
soundsAreOverridden = nil
|
|
end
|
|
|
|
function PlayAlertSounds(overrideSoundCVars)
|
|
local soundPreferences = private.db.profile.alert.sound
|
|
|
|
if overrideSoundCVars and not soundsAreOverridden then
|
|
local channelCVar = SoundChannelCVars[soundPreferences.channel]
|
|
|
|
StoredSoundCVars[channelCVar] = _G.GetCVar(channelCVar)
|
|
_G.SetCVar(channelCVar, 1)
|
|
|
|
StoredSoundCVars.Sound_EnableSoundWhenGameIsInBG = _G.GetCVar("Sound_EnableSoundWhenGameIsInBG")
|
|
_G.SetCVar("Sound_EnableSoundWhenGameIsInBG", 1)
|
|
|
|
soundsAreOverridden = true
|
|
NPCScan:ScheduleTimer(ResetStoredSoundCVars, SOUND_RESTORE_INTERVAL_SECONDS)
|
|
end
|
|
|
|
for soundName in pairs(soundPreferences.sharedMediaNames) do
|
|
if soundPreferences.sharedMediaNames[soundName] ~= false then
|
|
_G.PlaySoundFile(LibSharedMedia:Fetch("sound", soundName), soundPreferences.channel)
|
|
end
|
|
end
|
|
end
|
|
|
|
private.PlayAlertSounds = PlayAlertSounds
|
|
end
|
|
|
|
function NPCScan:DispatchSensoryCues()
|
|
local alert = private.db.profile.alert
|
|
local now = time()
|
|
|
|
if alert.screenFlash.isEnabled then
|
|
self:PlayFlashAnimation(alert.screenFlash.texture, alert.screenFlash.color)
|
|
end
|
|
|
|
if alert.sound.isEnabled and now > lastSoundTime + ALERT_SOUND_THROTTLE_INTERVAL_SECONDS then
|
|
PlayAlertSounds(alert.sound.ignoreMute)
|
|
|
|
lastSoundTime = now
|
|
end
|
|
end
|
|
end
|
|
|