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

-- ----------------------------------------------------------------------------
-- 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