mirror of
https://github.com/Blaststar/Questie-Octo-Vanilla.git
synced 2026-10-21 21:00:50 +00:00
282 lines
10 KiB
Lua
282 lines
10 KiB
Lua
-- Questie-Octo canonical quest model.
|
|
-- Questie-facing semantic model over pfQuest/Tortoise quest truth.
|
|
|
|
QuestieOcto.QuestModel = QuestieOcto.QuestModel or {}
|
|
local QM = QuestieOcto.QuestModel
|
|
|
|
QM.cache = {}
|
|
|
|
local function CopyArray(src)
|
|
if not src then return nil end
|
|
local out={}
|
|
local keys={}
|
|
|
|
-- These sources are arrays, but pairs() does not guarantee their numeric
|
|
-- order on Lua 5.0. Preserve the database/compiler order explicitly so
|
|
-- objective #1/#2 cannot be swapped before BuildObjectiveData consumes it.
|
|
for k in pairs(src) do
|
|
if type(k)=="number" then table.insert(keys,k) end
|
|
end
|
|
table.sort(keys)
|
|
|
|
for i=1,table.getn(keys) do
|
|
table.insert(out,src[keys[i]])
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function CopyGroups(src)
|
|
if not src then return nil end
|
|
local out={}
|
|
for _,group in pairs(src) do
|
|
table.insert(out,CopyArray(group) or {})
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Supplied Questie 3.3.5 classic corrections:
|
|
-- these quests have their item objective before the normal category order.
|
|
local ITEM_OBJECTIVE_FIRST={
|
|
[503]=true,
|
|
[5088]=true,
|
|
}
|
|
|
|
-- Quests that are not normal static NPC offers. Questie 6 blacklists these
|
|
-- because the NPC only exposes the quest after a scripted world interaction.
|
|
-- Keeping the real starter relation is useful for quest truth/tooltips, but it
|
|
-- must not become a permanent map pickup marker.
|
|
local CONDITIONAL_OFFERS={
|
|
[3861]="Use /chicken on a Chicken until it temporarily offers CLUCK!.",
|
|
[7946]="Requires Jubjub to be lured back with Dark Iron Ale.",
|
|
}
|
|
|
|
-- Some situational quests benefit from one deliberately chosen discovery marker
|
|
-- without pretending every scripted source is a permanent questgiver. CLUCK! can
|
|
-- be triggered from many Chickens, but the Westfall Chicken at 55.6,30.9 is used
|
|
-- as the single representative pickup/turn-in marker so new players can discover
|
|
-- that the quest exists without covering every Chicken spawn with quest icons.
|
|
-- The underlying starter/finisher relation remains NPC 620 and server truth is
|
|
-- unchanged; this table is presentation only.
|
|
local CONDITIONAL_MAP_MARKERS={
|
|
[3861]={
|
|
creatureID=620,
|
|
coords={{55.6,30.9,40,300}},
|
|
},
|
|
}
|
|
|
|
-- Server repeatability and map presentation are separate. CLUCK! is technically
|
|
-- repeatable, but Questie-Octo already treats it as one-and-done after the first
|
|
-- completion. Present any marker that is legitimately shown for it as an
|
|
-- ordinary yellow quest rather than a blue repeatable quest.
|
|
local NORMAL_REPEATABLE_PRESENTATION={
|
|
[3861]=true,
|
|
}
|
|
|
|
local function AddObjectiveData(list,kind,id)
|
|
if not id then return end
|
|
|
|
local typ=nil
|
|
if kind=="creature" then typ="monster"
|
|
elseif kind=="gameObject" then typ="object"
|
|
elseif kind=="item" then typ="item"
|
|
end
|
|
|
|
table.insert(list,{
|
|
kind=kind,
|
|
type=typ,
|
|
id=id
|
|
})
|
|
end
|
|
|
|
-- 1+4+8+64=77 Alliance races, 2+16+32+128=178 Horde races (pfQuest bitraces).
|
|
local function NoteFaction(fac,seen)
|
|
if fac=="A" then seen.a=true
|
|
elseif fac=="H" then seen.h=true
|
|
elseif fac=="AH" or fac=="HA" then seen.a=true; seen.h=true end
|
|
end
|
|
|
|
-- Mirror pfQuest's GetRaceMaskByID starter fallback so a faction-specific quest
|
|
-- with no explicit race field is derived from its quest giver's faction.
|
|
local function StarterRaceMask(starts)
|
|
if not starts then return 0 end
|
|
local seen={}
|
|
for _,id in pairs(starts.creature or {}) do
|
|
NoteFaction(QuestieOcto.DatabaseAPI:GetCreatureFaction(id),seen)
|
|
end
|
|
for _,id in pairs(starts.gameObject or {}) do
|
|
NoteFaction(QuestieOcto.DatabaseAPI:GetObjectFaction(id),seen)
|
|
end
|
|
if seen.a and seen.h then return 255 end
|
|
if seen.a then return 77 end
|
|
if seen.h then return 178 end
|
|
return 0
|
|
end
|
|
|
|
local function BuildObjectiveData(questID,objectives)
|
|
local result={}
|
|
local itemFirst=ITEM_OBJECTIVE_FIRST[questID] and true or false
|
|
|
|
if itemFirst then
|
|
for i=1,table.getn(objectives.item or {}) do
|
|
AddObjectiveData(result,"item",objectives.item[i])
|
|
end
|
|
end
|
|
|
|
-- Questie 3.3.5/7/8 DB compiler category order.
|
|
for i=1,table.getn(objectives.creature or {}) do
|
|
AddObjectiveData(result,"creature",objectives.creature[i])
|
|
end
|
|
|
|
for i=1,table.getn(objectives.gameObject or {}) do
|
|
AddObjectiveData(result,"gameObject",objectives.gameObject[i])
|
|
end
|
|
|
|
if not itemFirst then
|
|
for i=1,table.getn(objectives.item or {}) do
|
|
AddObjectiveData(result,"item",objectives.item[i])
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function QM:Clear()
|
|
self.cache={}
|
|
end
|
|
|
|
|
|
local function ObservedRepeatables()
|
|
QuestieOctoGlobalDB=QuestieOctoGlobalDB or {}
|
|
QuestieOctoGlobalDB.observedRepeatableQuests=QuestieOctoGlobalDB.observedRepeatableQuests or {}
|
|
return QuestieOctoGlobalDB.observedRepeatableQuests
|
|
end
|
|
|
|
function QM:MarkObservedRepeatable(questID)
|
|
questID=tonumber(questID)
|
|
if not questID then return false end
|
|
local db=ObservedRepeatables()
|
|
if db[questID] then return false end
|
|
db[questID]=true
|
|
if self.cache[questID] then
|
|
self.cache[questID].repeatable=true
|
|
self.cache[questID].presentationRepeatable=not NORMAL_REPEATABLE_PRESENTATION[questID]
|
|
end
|
|
if QuestieOcto.AvailableQuests and QuestieOcto.AvailableQuests.Schedule then
|
|
QuestieOcto.AvailableQuests:Schedule(true,0.02)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function QM:Get(questID)
|
|
if self.cache[questID] then return self.cache[questID] end
|
|
if not QuestieOcto.DatabaseAPI:IsReady() then return nil end
|
|
|
|
local raw=QuestieOcto.DatabaseAPI:GetQuestRaw(questID)
|
|
if not raw then return nil end
|
|
|
|
local q={
|
|
id=questID,
|
|
title=QuestieOcto.DatabaseAPI:GetQuestTitle(questID),
|
|
descriptionText=QuestieOcto.DatabaseAPI.GetQuestDescriptionText and QuestieOcto.DatabaseAPI:GetQuestDescriptionText(questID) or nil,
|
|
objectiveText=QuestieOcto.DatabaseAPI.GetQuestObjectiveText and QuestieOcto.DatabaseAPI:GetQuestObjectiveText(questID) or nil,
|
|
|
|
-- lvl is the displayed quest level. min/max are the actual server
|
|
-- acceptance bounds and are deliberately kept separate.
|
|
level=tonumber(raw["lvl"] or 0) or 0,
|
|
requiredLevel=tonumber(raw["min"] or 0) or 0,
|
|
maximumLevel=tonumber(raw["max"] or 0) or 0,
|
|
|
|
-- Questie 6 uses quest Type 41 as the canonical PvP classification.
|
|
-- This comes from the authoritative Turtle enrichment, including only
|
|
-- verified local corrections for misclassified custom PvP quests.
|
|
questType=tonumber(raw["type"] or 0) or 0,
|
|
pvp=(tonumber(raw["type"] or 0) or 0)==41,
|
|
|
|
raceMask=raw["race"],
|
|
classMask=raw["class"],
|
|
requiredSkill=raw["skill"],
|
|
requiredSkillValue=tonumber(raw["skillValue"] or 1) or 1,
|
|
repMinFaction=tonumber(raw["repMinFaction"]),
|
|
repMinValue=tonumber(raw["repMinValue"] or 0) or 0,
|
|
repMaxFaction=tonumber(raw["repMaxFaction"]),
|
|
repMaxValue=tonumber(raw["repMaxValue"] or 0) or 0,
|
|
|
|
-- Preserve the authoritative raw event association, but normalize the two
|
|
-- Darkmoon Faire location IDs into one logical event for every runtime
|
|
-- consumer. Turtle/pfQuest reuse the same Faire NPCs and quest offers at
|
|
-- both Elwynn and Mulgore while many shared quest rows are inconsistently
|
|
-- tagged as event 4 or event 5. Keeping rawEventID makes diagnostics lossless;
|
|
-- eventID is the logical availability/presentation identity.
|
|
rawEventID=tonumber(raw["event"]),
|
|
eventID=(tonumber(raw["event"])==5 and 4 or tonumber(raw["event"])),
|
|
event=raw["event"],
|
|
repeatable=(raw["repeatable"] or ObservedRepeatables()[tonumber(questID)]) and true or false,
|
|
hideAfterFirstCompletion=raw["hideAfterFirstCompletion"] and true or false,
|
|
daily=raw["daily"] and true or false,
|
|
yearly=raw["yearly"] and true or false,
|
|
hardcore=raw["hardcore"] and true or false,
|
|
timed=raw["timed"] and true or false,
|
|
disabled=raw["disabled"] and true or false,
|
|
conditionalOffer=CONDITIONAL_OFFERS[tonumber(questID)],
|
|
conditionalMapMarker=CONDITIONAL_MAP_MARKERS[tonumber(questID)],
|
|
exclusive=raw["exclusive"] and true or false,
|
|
nextChain=tonumber(raw["nextChain"]),
|
|
|
|
-- pfQuest's ordinary pre remains OR semantics. The enrichment restores
|
|
-- signed active predecessors and negative ExclusiveGroup all-of groups
|
|
-- separately so their server meaning is not flattened again.
|
|
preQuestSingle=CopyArray(raw["pre"]),
|
|
preQuestActive=CopyArray(raw["preActive"]),
|
|
preQuestAll=CopyGroups(raw["preAll"]),
|
|
|
|
-- close is only authoritative when the migrated server still marks the
|
|
-- quest as a positive ExclusiveGroup member (q.exclusive=true).
|
|
exclusiveTo=CopyArray(raw["close"]),
|
|
|
|
starts={
|
|
creature=raw["start"] and CopyArray(raw["start"]["U"]) or nil,
|
|
gameObject=raw["start"] and CopyArray(raw["start"]["O"]) or nil,
|
|
item=raw["start"] and CopyArray(raw["start"]["I"]) or nil,
|
|
},
|
|
|
|
finishes={
|
|
creature=raw["end"] and CopyArray(raw["end"]["U"]) or nil,
|
|
gameObject=raw["end"] and CopyArray(raw["end"]["O"]) or nil,
|
|
},
|
|
|
|
objectives={
|
|
creature=raw["obj"] and CopyArray(raw["obj"]["U"]) or nil,
|
|
gameObject=raw["obj"] and CopyArray(raw["obj"]["O"]) or nil,
|
|
item=raw["obj"] and CopyArray(raw["obj"]["I"]) or nil,
|
|
irItems=raw["obj"] and CopyArray(raw["obj"]["IR"]) or nil,
|
|
-- A = quest-bound AreaTrigger objective. This is deliberately distinct
|
|
-- from generic map exploration/fog data: only triggers referenced by an
|
|
-- actual quest are retained here.
|
|
areaTrigger=raw["obj"] and CopyArray(raw["obj"]["A"]) or nil,
|
|
},
|
|
}
|
|
|
|
-- Keep server repeatability authoritative for completion/filtering while
|
|
-- allowing a narrow presentation exception such as CLUCK!.
|
|
q.presentationRepeatable=q.repeatable and not NORMAL_REPEATABLE_PRESENTATION[tonumber(questID)] and true or false
|
|
-- CLUCK! was explicitly chosen to stay an ordinary yellow discovery marker.
|
|
-- Keep that presentation exception independent from the Turtle low-level gray
|
|
-- marker rule so a high-level player does not turn its representative marker gray.
|
|
q.presentationAlwaysNormal=NORMAL_REPEATABLE_PRESENTATION[tonumber(questID)] and true or false
|
|
|
|
-- IR items are intentionally NOT added to objectiveData. They are a
|
|
-- pfQuest-special interaction relationship that becomes target guidance
|
|
-- only while the player actually possesses the required item.
|
|
q.objectiveData=BuildObjectiveData(questID,q.objectives)
|
|
|
|
-- A neutral race field lets faction-only quests leak to the wrong faction;
|
|
-- fall back to the quest starter's faction just like pfQuest does.
|
|
if not q.raceMask or tonumber(q.raceMask)==0 then
|
|
local starterMask=StarterRaceMask(q.starts)
|
|
if starterMask>0 then q.raceMask=starterMask end
|
|
end
|
|
|
|
self.cache[questID]=q
|
|
return q
|
|
end
|