peq-quests/lua_modules/dragons_of_norrath.lua

523 lines
23 KiB
Lua

-- module for Dragons of Norrath expansion
local don = {}
-- DoN character state object to track progression with a DoN faction
don.character_state = {}
don.character_state.__index = don.character_state
don.faction_id = {
evil = 1021, -- Dark Reign
good = 1023 -- Norrath's Keepers
}
don.faction_name = {
[don.faction_id.evil] = "Dark Reign",
[don.faction_id.good] = "Norrath's Keepers"
}
don.faction = {
Ally = 1100,
Warmly = 750,
Kindly = 500,
HighAmiable = 300, -- 300 reaction level in live faction window
Amiable = 100,
Indifferent = 0,
Apprehensive = -100,
Dubious = -500,
Threaten = -750
}
don.nest_unlocked_content_flag = "don_nest_unlocked"
don.flags = {
key = {
[don.faction_id.evil] = "don_evil",
[don.faction_id.good] = "don_good",
},
-- Norrath's Keepers: Gift of the Keepers -- Dark Reign: Gift of the Dark Reign
t1 = {
said_help = bit.lshift(1, 0), -- said "help" for access to 3 solo quests
solo_quests_hail = bit.lshift(1, 1), -- hailed after finishing 3 solo quests for access to request mission
group_mission_done = bit.lshift(1, 2), -- finished 'Children of Gimblax' (good) or 'Have Note Will Travel' (evil) (can be done out of order)
complete = bit.lshift(1, 3), -- hailed after finishing 3 solo quests and gimblax
},
-- Norrath's Keepers: Valor of the Keepers -- Dark Reign: Tenacity of the Dark Reign
t2 = {
said_work = bit.lshift(1, 4), -- said "work" for access to 3 solo quests and non-progression raid 'Fanning the Flames'
solo_quests_hail = bit.lshift(1, 5), -- hailed after finishing 3 solo quests for access to request mission and ''Calling Emoush' raid
group_mission_done = bit.lshift(1, 6), -- finished 'Sickness of the Spirit' (good) or 'Drake Eggs' (evil) (can be done out of order)
stone_fragment = bit.lshift(1, 7), -- turned in 'Glowing Stone Fragment' from 'Calling Emoush' raid (with required access)
complete = bit.lshift(1, 8), -- finished both group mission and raid turn in (raid turn in completed last or hailed after missio with raid item already turned in)
},
-- Norrath's Keepers: Embrace of the Keepers -- Dark Reign: Embrace of the Dark Reign
t3 = {
hail_start = bit.lshift(1, 9), -- hailed after completing t2 for access to 3 solo quests and non-progression raids 'Reflections of Silver' and 'Kessdona's Perch'
solo_quests_hail = bit.lshift(1, 10), -- hailed after finishing 3 solo tasks for access to group mission and 'Trial of Perseverance' raid
group_mission_done = bit.lshift(1, 11), -- finished 'History of the Isle' (good) or 'The Gilded Scroll' (evil) (can be done out of order)
goblin_beads = bit.lshift(1, 12), -- turned in 'Goblin Warlord's Beads' from 'Trial of Perseverance' raid' (with required access)
spider_spinners = bit.lshift(1, 13), -- turned in 'Lava Spider Spinners' from 'Volkara's Bite' raid (with required access)
complete = bit.lshift(1, 14), -- finished group mission and both raid turn ins (raid turn ins completed last or hailed after mission with raid items already turned in)
},
-- Norrath's Keepers: Power of the Keepers -- Dark Reign: Power of the Dark Reign
t4 = {
hail_start = bit.lshift(1, 15), -- hailed after completing t3 for access to 'Guardian of the Sands' and 'Goblin Dojo' raids
sand_turned_in = bit.lshift(1, 16), -- turned in 'Quintessence of Sand' from 'Guardian of the Sands' raid (with required access)
stone_turned_in = bit.lshift(1, 17), -- turned in 'Meditation Stone' from 'Goblin Dojo' raid (with required access)
yarlir_access = bit.lshift(1, 18), -- said "defend" (good) or "prove" (evil) after turning in raid items for access to 'An End to the Storms' raid
yarlir_fang = bit.lshift(1, 19), -- turned in 'Yar'lir's Fang' from 'An End to the Storms' raid
complete = bit.lshift(1, 20), -- complete
},
-- Norrath's Keepers: Sanctity of the Keepers -- Dark Reign: Fervor of the Dark Reign
t5 = {
hail_start = bit.lshift(1, 21), -- hailed after completing t4 for access to group mission and 'The Curse of Ju'rek' raid
group_mission_done = bit.lshift(1, 22), -- finished 'Origins of the Curse' (good) or 'Rival Party' (evil) (can be done out of order)
diseased_wing = bit.lshift(1, 23), -- turned in 'Diseased Wing Fragment' from 'The Curse of Ju'rek' raid (with required access)
said_dragon = bit.lshift(1, 24), -- said "dragon" for access to for access to 'In the Shadows' raid (evil side gives after finishing group mission and raid turn in)
vish_scale = bit.lshift(1, 25), -- turned in 'Shadowscale of Vishimtar' from 'In the Shadows' raid
complete = bit.lshift(1, 26), -- complete
},
complete_hail = bit.lshift(1, 27), -- hailed after completing t5, required to continue "darkness" lore conversation
}
don.spell_rewards = {
[Class.BARD] = 76024, -- Spell: Storm Blade
[Class.BEASTLORD] = 76030, -- Spell: Growl of the Panther
[Class.BERSERKER] = 76035, -- Tome of Destroyer's Volley
[Class.CLERIC] = 76020, -- Spell: Ward of Retribution
[Class.DRUID] = 76023, -- Spell: Serpent Vines
[Class.ENCHANTER] = 76029, -- Spell: Rune of Rikkukin
[Class.MAGICIAN] = 76028, -- Spell: Summon: Molten Orb
[Class.MONK] = 76034, -- Tome of Dragon Fang
[Class.NECROMANCER] = 76026, -- Spell: Shadow Orb
[Class.PALADIN] = 76021, -- Tome of Guard of Righteousness
[Class.RANGER] = 76022, -- Spell: Earthen Shackles
[Class.ROGUE] = 76033, -- Tome of Assassin's Feint
[Class.SHADOWKNIGHT] = 76031, -- Tome of Soul Shield
[Class.SHAMAN] = 76025, -- Spell: Spirit of the Panther
[Class.WARRIOR] = 76032, -- Tome of Cyclone Attack
[Class.WIZARD] = 76027, -- Spell: Claw of Vox
}
-- live combines these into a single aa with ranks
don.aa = {
[don.faction_id.evil] = {
{ aa_id = 471, rank_id = 1361, name = "Gift of the Dark Reign" },
{ aa_id = 472, rank_id = 1362, name = "Tenacity of the Dark Reign" },
{ aa_id = 473, rank_id = 1363, name = "Embrace of the Dark Reign" },
{ aa_id = 474, rank_id = 1364, name = "Power of the Dark Reign" },
{ aa_id = 475, rank_id = 1365, name = "Fervor of the Dark Reign" },
},
[don.faction_id.good] = {
{ aa_id = 476, rank_id = 1366, name = "Gift of the Keepers" },
{ aa_id = 477, rank_id = 1367, name = "Valor of the Keepers" },
{ aa_id = 478, rank_id = 1368, name = "Embrace of the Keepers" },
{ aa_id = 479, rank_id = 1369, name = "Power of the Keepers" },
{ aa_id = 480, rank_id = 1370, name = "Sanctity of the Keepers" },
}
}
-- used to grant AA rewards for flag completion and fix missing AA in hail scripts
don.aa_rewards = {
[don.flags.t1.complete] = { aa_index = 1, next_flag = don.flags.t2.complete },
[don.flags.t2.complete] = { aa_index = 2, next_flag = don.flags.t3.complete },
[don.flags.t3.complete] = { aa_index = 3, next_flag = don.flags.t4.complete },
[don.flags.t4.complete] = { aa_index = 4, next_flag = don.flags.t5.complete },
[don.flags.t5.complete] = { aa_index = 5, next_flag = nil },
}
-- Called by global event_connect to check if player has invalid DoN faction
-- (positive with both sides) and resets the side with lowest faction if so
function don.fix_invalid_faction_state(client)
if client:GetGM() or not eq.is_dragons_of_norrath_enabled() then
return
end
local good_faction = client:GetModCharacterFactionLevel(don.faction_id.good)
local evil_faction = client:GetModCharacterFactionLevel(don.faction_id.evil)
-- this shouldn't be possible unless through some bug
if good_faction >= don.faction.Indifferent and evil_faction >= don.faction.Indifferent then
local reset = { faction_id = don.faction_id.evil, faction_level = evil_faction }
if good_faction < evil_faction then
reset = { faction_id = don.faction_id.good, faction_level = good_faction }
end
-- live reset this to -200 reaction (-100 standing) on an agnostic character
-- not sure if reset depends on deity faction mod or all characters reset the same
local reset_amount = -(200 + reset.faction_level)
client:Faction(reset.faction_id, reset_amount)
client:Message(MT.Yellow, ("You appear to have earned renown with both 'Norrath's Keepers' and the 'Dark Reign'. While we congratulate you on your efforts the two warring camps have found out that you are a spy. You will be granted the title of 'the Devious' and your faction with the '%s' will be adjusted accordingly."):format(don.faction_name[reset.faction_id]))
-- todo: lua api for CreateNewPlayerSuffix
-- remove any AA from the reset faction
don.remove_invalid_aa(client, reset.faction_id)
end
end
-- Remove DoN AA from player if faction drops below minimum (indifferent)
function don.remove_invalid_aa(client, faction_id)
local state = don.character_state.new(client, faction_id)
state:remove_invalid_aa()
end
-- solo progression quests
don.solo_quests = {
[don.faction_id.evil] = {
t1 = { 4827, 4828, 4829 }, -- The Burning Poison, Blood of the Basilisk, Clearing the Path
t2 = { 4830, 4831, 4832 }, -- Blood from Sand, Army of Stone, Reap the Kirin Mind
t3 = { 4833, 4834, 4835 }, -- Muddy the Waters, Knowledge is Power, Snowfoot Attack
},
[don.faction_id.good] = {
t1 = { 5044, 5045, 5046 }, -- Creating an Antidote, Burning Arrows, Greed of the Goblins
t2 = { 5047, 5048, 5049 }, -- Turn the Tides of the Sand, Recover the Lost Map, Defend the Kirin Contact
t3 = { 5050, 5051, 5052 }, -- Protect the Waters, The Sacred Scrolls, Snowfoot Revenge
}
}
-- progression group missions and completion flags\
-- `finish_if_flag`: flag to check if mission finishes a progression step (if a parallel flag was completed before the mission)
don.mission_flags = {
t1 = { add_flag = don.flags.t1.group_mission_done, finish_if_flag = don.flags.t1.solo_quests_hail },
t2 = { add_flag = don.flags.t2.group_mission_done, finish_if_flag = don.flags.t2.stone_fragment },
t3 = { add_flag = don.flags.t3.group_mission_done, finish_if_flag = bit.bor(don.flags.t3.goblin_beads, don.flags.t3.spider_spinners) },
t5 = { add_flag = don.flags.t5.group_mission_done, finish_if_flag = don.flags.t5.diseased_wing },
}
don.progression_missions = {
[401] = { faction_id = don.faction_id.good, flags = don.mission_flags.t1 }, -- Children of Gimblax [68+]
[4785] = { faction_id = don.faction_id.good, flags = don.mission_flags.t2 }, -- Sickness of the Spirit [68+]
[4799] = { faction_id = don.faction_id.good, flags = don.mission_flags.t3 }, -- History of the Isle
[4991] = { faction_id = don.faction_id.good, flags = don.mission_flags.t5 }, -- Origins of the Curse
[5015] = { faction_id = don.faction_id.evil, flags = don.mission_flags.t1 }, -- Have Note Will Travel [65+]
[5555] = { faction_id = don.faction_id.evil, flags = don.mission_flags.t2 }, -- Drake Eggs [68+]
[4771] = { faction_id = don.faction_id.evil, flags = don.mission_flags.t3 }, -- The Gilded Scroll
[4986] = { faction_id = don.faction_id.evil, flags = don.mission_flags.t5 }, -- Rival Party [68+]
}
-- Checks if a DoN progression mission was completed to set flags (called from global player)
function don.on_task_complete(client, task_id)
if not eq.is_dragons_of_norrath_enabled() then
return
end
if don.progression_missions[task_id] then
local mission = don.progression_missions[task_id]
local player = don.character_state.new(client, mission.faction_id)
if not player:has_flag(mission.flags.add_flag) then
-- if character is in lavastorm when a progression mission is completed they
-- see flag and message to speak to the progression npc if it finished a step
local is_lavastorm = eq.get_zone_short_name() == "lavastorm"
player:set_flag(mission.flags.add_flag, not is_lavastorm)
if is_lavastorm and player:has_flag(mission.flags.finish_if_flag) then
if mission.faction_id == don.faction_id.good then
client:Message(MT.Yellow, "Congratulations! Chieftain Relae Aderie at the Norrath's Keepers Enclave wishes to speak with you now.")
else
client:Message(MT.Yellow, "Congratulations! Commander Zaerr Ty'Dar at the Dark Reign Outpost wants to speak with you now.")
end
end
end
end
end
-- progression raid item trade requirements and flags\
-- `req_flag`: required flag, otherwise item returned\
-- `faction`: minimum faction level, if not met invokes 'on_low_faction' and item returned\
-- `add_flag`: flag set on successful trade (if player already has the flag item is returned)\
-- `finish_if_flag`: if player has this flag, trades invoke 'on_finish' instead of 'on_accept'\
don.trade_requirements = {
-- t2
[57202] = { -- Glowing Stone Fragment
min_faction = don.faction.Amiable,
req_flag = don.flags.t2.solo_quests_hail,
add_flag = don.flags.t2.stone_fragment,
finish_if_flag = don.flags.t2.group_mission_done
},
-- t3
[57203] = { -- Goblin Warlord's Beads
min_faction = don.faction.HighAmiable,
req_flag = don.flags.t3.solo_quests_hail,
add_flag = don.flags.t3.goblin_beads,
finish_if_flag = bit.bor(don.flags.t3.group_mission_done, don.flags.t3.spider_spinners),
},
[57206] = { -- Lava Spider Spinners
min_faction = don.faction.HighAmiable,
req_flag = don.flags.t3.solo_quests_hail,
add_flag = don.flags.t3.spider_spinners,
finish_if_flag = bit.bor(don.flags.t3.group_mission_done, don.flags.t3.goblin_beads),
},
-- t4
[57204] = { -- Meditation Stone
min_faction = don.faction.Kindly,
req_flag = don.flags.t4.hail_start,
add_flag = don.flags.t4.stone_turned_in,
finish_if_flag = don.flags.t4.sand_turned_in,
},
[57207] = { -- Quintessence of Sand
min_faction = don.faction.Kindly,
req_flag = don.flags.t4.hail_start,
add_flag = don.flags.t4.sand_turned_in,
finish_if_flag = don.flags.t4.stone_turned_in,
},
[57200] = { -- Yar`Lir's Fang
min_faction = don.faction.Kindly,
req_flag = don.flags.t4.yarlir_access,
add_flag = don.flags.t4.yarlir_fang,
finish_if_flag = nil
},
-- t5
[57205] = { -- Diseased Wing Fragments
min_faction = don.faction.Warmly,
req_flag = don.flags.t5.hail_start,
add_flag = don.flags.t5.diseased_wing,
finish_if_flag = don.flags.t5.group_mission_done
},
[57201] = { -- Shadowscale of Vishimtar
min_faction = don.faction.Warmly,
req_flag = don.flags.t5.said_dragon,
add_flag = don.flags.t5.vish_scale,
finish_if_flag = nil
},
}
-- Handle DoN raid item turn-in faction and flag requirements\
-- returns true if item should be returned and invokes callbacks based on player state
-- todo: unknown if live also confirms character level on raid item turn ins
function don.handle_trade(player, item_id, on_low_faction, on_accept, on_finish)
if not eq.is_dragons_of_norrath_enabled() then
return true
end
local return_item = true
local reqs = don.trade_requirements[item_id]
if reqs and not player:has_flag(reqs.add_flag) and player:has_flag(reqs.req_flag) then
if reqs.min_faction and not player:has_min_faction(reqs.min_faction) then
if on_low_faction then on_low_faction(player) end
else
player:set_flag(reqs.add_flag)
-- this turn-in may finalize a step and trigger different dialogue/flags
if reqs.finish_if_flag and player:has_flag(reqs.finish_if_flag) then
if on_finish then on_finish(player) end
else
if on_accept then on_accept(player) end
end
return_item = false
end
end
return return_item
end
-- filter task offers by mininum faction and flags\
-- `player_state` should be a don.character_state object
function don.get_filtered_tasks(player_state, task_table)
local task_offers = {}
local is_gm = player_state.client:GetGM()
for _, task in pairs(task_table) do
if is_gm or not task.min_faction or player_state:has_min_faction(task.min_faction) then
-- note: live task system also enforces flag filters in source ("does not meet requirements")
if is_gm or not task.flag or player_state:has_flag(task.flag) then
task_offers[#task_offers+1] = task.task_id
end
end
end
return task_offers
end
-- item ids used for the "Clues" mission
don.clue_item_ids = {
scrap_of_dark_cloth = 81904, -- Scrap of Dark Cloth
deformed_dragon_embryo = 81905, -- Deformed Dragon Embryo
desiccated_drake_corpse = 81913, -- Desiccated Drake Corpse
shattered_draconic_symbol = 81914, -- Shattered Draconic Symbol
dark_dragon_scale = 81915, -- Dark Dragon Scale
}
-- common method used by #Tatsujiro_the_Serene and #Xeib_Darkskies to complete the "Clues" mission
function don.finish_clues_mission(client, task_ids)
local clues_speak_id = 2
for _, task_id in ipairs(task_ids) do
if client:IsTaskActivityActive(task_id, clues_speak_id) then
for _, item_id in pairs(don.clue_item_ids) do
local inv = client:GetInventory()
if inv:HasItem(item_id, 1, bit.bor(InventoryWhere.Personal, InventoryWhere.Cursor)) ~= -1 then
client:UpdateTaskActivity(task_id, clues_speak_id, 1)
client:RemoveItem(item_id)
return item_id
end
end
end
end
return 0
end
function don.is_nest_unlocked()
return eq.is_content_flag_enabled(don.nest_unlocked_content_flag)
end
function don.unlock_nest()
-- todo: trigger eqswitch reloads for players already inside broodlands, otherwise they will need to re-zone
eq.set_content_flag(don.nest_unlocked_content_flag, true)
end
function don.character_state.new(client, faction_id)
if faction_id ~= don.faction_id.good and faction_id ~= don.faction_id.evil then
error("Cannot load don.character_state, faction_id must be for Norrath's Keepers or Dark Reign")
end
local inst = {}
setmetatable(inst, don.character_state)
inst.client = client
inst.level = client:GetLevel()
inst.faction_id = faction_id
inst.faction_name = don.faction_name[faction_id]
inst.faction_level = client:GetModCharacterFactionLevel(faction_id) -- "reaction" in live's faction window
inst.faction_standing = client:GetCharacterFactionLevel(faction_id) -- unmodded
inst.flag_key = don.flags.key[faction_id]
inst.solo_quests = don.solo_quests[faction_id]
inst.aa = don.aa[faction_id]
inst:load_flags()
return inst
end
function don.character_state:has_min_faction(faction_level)
return self.faction_level >= faction_level
end
function don.character_state:load_flags()
self.flags = tonumber(self.client:GetBucket(self.flag_key)) or 0
end
function don.character_state:reset_flags()
self.flags = 0
self.client:DeleteBucket(self.flag_key)
end
function don.character_state:has_flag(flag)
return bit.band(flag, self.flags) == flag
end
function don.character_state:remove_flag(flag)
self.flags = bit.band(self.flags, bit.bnot(flag))
self.client:SetBucket(self.flag_key, tostring(self.flags))
end
function don.character_state:set_flag(flag, silent)
silent = silent or false
local has_flag = self:has_flag(flag)
if not has_flag then
self.flags = bit.bor(self.flags, flag)
self.client:SetBucket(self.flag_key, tostring(self.flags))
if not silent then
self.client:Message(MT.Yellow, "You have received a character flag!")
end
end
self:reward_flag(flag, has_flag)
end
-- Reward player for the specified tier completion flag
function don.character_state:reward_flag(flag, had_flag)
local aa_reward = don.aa_rewards[flag]
if not aa_reward then
return
end
-- AA points only rewarded once since this may be called to re-grant DoN AA for faction changes
if not had_flag then
self.client:AddAAPoints(2)
self.client:QuestReward(self.client) -- zeroed out packet on live for trumpet sound (not sure if part of AddAAPoints)
-- level 69 class spell reward (live does not reward again if deleted)
if flag == don.flags.t2.complete then
self.client:SummonItem(don.spell_rewards[self.client:GetClass()])
end
end
-- grant AA and any missing ranks below this tier
for i=1, aa_reward.aa_index do
if self.client:GetAA(self.aa[i].rank_id) == 0 then
self.client:GrantAlternateAdvancementAbility(self.aa[i].aa_id, 1)
end
end
end
-- Removes all DoN AA if player does not have minimum required faction (indifferent)
-- live removes DoN AA if below indifferent on opposing faction token turn ins and opposing faction NPC hail
-- killing guards, completing Wayfarer tasks, or token turn ins that increase faction do not trigger it
function don.character_state:remove_invalid_aa()
if self:has_min_faction(don.faction.Indifferent) then
return
end
local messaged = false
for _, aa in ipairs(self.aa) do
if self.client:GetAA(aa.rank_id) > 0 then
if not messaged then
self.client:Message(MT.Red, ("You have lost the blessings of %s!"):format(self.faction_name))
messaged = true
end
self.client:Message(MT.Red, ("You have lost the ability '%s.'"):format(aa.name))
self.client:ResetAlternateAdvancementRank(aa.aa_id)
end
end
end
-- Live requires hailing Chieftain or Commander to restore DoN AA if removed for low faction.
-- This results in dialogue for highest completed tier and re-grants AA with previous ranks.
-- Restoring AA requires player to have faction for their highest completed tier
-- (e.g. if player finished t2, restoring AA requires amiable; t1 AA is not restored at indifferent).
--
-- Returns true if player has `flag` but not its AA reward and has not completed the next tier
function don.character_state:needs_aa(flag)
local aa_reward = don.aa_rewards[flag]
if not aa_reward then
return false
end
local aa = self.aa[aa_reward.aa_index]
return self:has_flag(flag) and (not aa_reward.next_flag or not self:has_flag(aa_reward.next_flag)) and self.client:GetAA(aa.rank_id) == 0
end
function don.character_state:assign_quests(task_table)
for _, task_id in pairs(task_table) do
if not self.client:IsTaskActive(task_id) and not self.client:IsTaskCompleted(task_id) then
self.client:AssignTask(task_id)
end
end
end
function don.character_state:has_finished_quests(task_table)
for _, task_id in pairs(task_table) do
if not self.client:IsTaskCompleted(task_id) then
return false
end
end
return true
end
function don.character_state:has_finished_t1_quests()
return self:has_finished_quests(self.solo_quests.t1)
end
function don.character_state:has_finished_t2_quests()
return self:has_finished_quests(self.solo_quests.t2)
end
function don.character_state:has_finished_t3_quests()
return self:has_finished_quests(self.solo_quests.t3)
end
return don