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