692 lines
18 KiB
Lua
692 lines
18 KiB
Lua
--[[
|
|
***************************************************************************
|
|
* Copyright © 2022-2023 - Stendhal *
|
|
***************************************************************************
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************
|
|
]]
|
|
|
|
|
|
--[[
|
|
Stendhal quest: Lost Engagement Ring
|
|
|
|
Steps:
|
|
|
|
Reward:
|
|
- karma
|
|
- extended keyring (12 slots instead of 8)
|
|
|
|
Notes:
|
|
- If player loses ring, talk to Ari & say "lost" to reset quest slot.
|
|
]]
|
|
|
|
local quest_slot = "lost_engagement_ring"
|
|
|
|
local ari
|
|
local bride_name = "Emma"
|
|
|
|
local minLevel = 50
|
|
local karmaCompleteReward = 50
|
|
|
|
local ring_itemdata = "Ari's ring"
|
|
|
|
-- location where ring may be on Athor island
|
|
local ring_locations = {
|
|
{49, 30},
|
|
{123, 3},
|
|
{115, 122},
|
|
{49, 106},
|
|
}
|
|
|
|
local questNotStartedCondition = conditions:create("QuestNotStartedCondition", {quest_slot})
|
|
local questCompletedCondition = conditions:create("QuestCompletedCondition", {quest_slot})
|
|
local questActiveCondition = conditions:create("QuestActiveCondition", {quest_slot})
|
|
local hasRingCondition = conditions:create("PlayerHasItemdataItemWithHimCondition", {
|
|
"engagement ring",
|
|
ring_itemdata,
|
|
})
|
|
local hasKeyringCondition = conditions:create(function(player, sentence, npc)
|
|
return player:hasFeature("keyring")
|
|
end)
|
|
local minLevelCondition = conditions:create("LevelGreaterThanCondition", {minLevel - 1})
|
|
|
|
--[[ FIXME: conditions:create is struggling with constructors that take varargs
|
|
local visitedAthorCondition = conditions:create("PlayerVisitedZonesCondition", {
|
|
"0_athor_island",
|
|
})
|
|
]]
|
|
local visitedAthorCondition = luajava.newInstance(
|
|
"games.stendhal.server.entity.npc.condition.PlayerVisitedZonesCondition",
|
|
{"0_athor_island"})
|
|
|
|
local canStartCondition = conditions:andC({
|
|
questNotStartedCondition,
|
|
conditions:notC(questCompletedCondition),
|
|
hasKeyringCondition,
|
|
visitedAthorCondition,
|
|
minLevelCondition,
|
|
})
|
|
|
|
local chooseRingLocation = function()
|
|
return ring_locations[random:randUniform(1, 4)]
|
|
end
|
|
|
|
local setQuestAction = function(player, sentence, npc)
|
|
-- choose random location
|
|
local selected = chooseRingLocation()
|
|
player:setQuest(quest_slot, 0, selected[1])
|
|
player:setQuest(quest_slot, 1, selected[2])
|
|
end
|
|
|
|
local resetQuestAction = function(player, sentence, npc)
|
|
if player:hasQuest(quest_slot) then
|
|
local slots = player:getQuest(quest_slot):split(";")
|
|
local x = slots[1]
|
|
local y = slots[2]
|
|
|
|
if not string.isNumber(x) then
|
|
x = slots[2]
|
|
y = slots[3]
|
|
end
|
|
|
|
if not string.isNumber(x) or not string.isNumber(y) then
|
|
logger:warn(quest_slot .. " quest: malformatted quest slot, setting"
|
|
.. " new ring coordinates")
|
|
|
|
local selected = chooseRingLocation()
|
|
x = selected[1]
|
|
y = selected[2]
|
|
end
|
|
|
|
player:setQuest(quest_slot, x .. ";" .. y)
|
|
end
|
|
end
|
|
|
|
local rewardAction = function(player, sentence, npc)
|
|
player:addKarma(karmaCompleteReward)
|
|
player:setFeature("keyring", "3 4")
|
|
|
|
local slots = player:getQuest(quest_slot):split(";")
|
|
if #slots > 2 then
|
|
player:setQuest(quest_slot, 0, "done")
|
|
elseif #slots > 1 then
|
|
-- quest slot might have been reset if player reported ring "lost"
|
|
player:setQuest(quest_slot, "done;" .. slots[1] .. ";" .. slots[2])
|
|
else
|
|
-- failsafe
|
|
player:setQuest(quest_slot, "done")
|
|
end
|
|
end
|
|
|
|
local prepareNPC = function()
|
|
ari = entities:getNPC("Ari")
|
|
ari:setIgnorePlayers(false)
|
|
ari:addGoodbye()
|
|
end
|
|
|
|
local prepareRequestStep = function()
|
|
|
|
ari:add(
|
|
ConversationStates.IDLE,
|
|
ConversationPhrases.GREETING_MESSAGES,
|
|
conditions:orC({
|
|
{
|
|
questNotStartedCondition,
|
|
conditions:orC({
|
|
conditions:notC(hasKeyringCondition),
|
|
conditions:notC(visitedAthorCondition),
|
|
conditions:notC(minLevelCondition),
|
|
}),
|
|
},
|
|
questCompletedCondition,
|
|
}),
|
|
ConversationStates.ATTENDING,
|
|
"Hi there!",
|
|
nil)
|
|
|
|
-- player doesn't meet minimum level requirement
|
|
ari:add(
|
|
ConversationStates.ATTENDING,
|
|
ConversationPhrases.QUEST_MESSAGES,
|
|
{
|
|
questNotStartedCondition,
|
|
conditions:notC(minLevelCondition),
|
|
},
|
|
ConversationStates.ATTENDING,
|
|
"I don't think you have the experience to help me. Come back when"
|
|
.. " you are stronger.",
|
|
nil)
|
|
|
|
-- player does not have keyring
|
|
ari:add(
|
|
ConversationStates.ATTENDING,
|
|
ConversationPhrases.QUEST_MESSAGES,
|
|
{
|
|
questNotStartedCondition,
|
|
minLevelCondition,
|
|
conditions:notC(hasKeyringCondition),
|
|
},
|
|
ConversationStates.ATTENDING,
|
|
"I don't think you have the experience to help me. Maybe if you knew"
|
|
.. " more about keyrings.",
|
|
nil)
|
|
|
|
-- player has not visited Athor
|
|
ari:add(
|
|
ConversationStates.ATTENDING,
|
|
ConversationPhrases.QUEST_MESSAGES,
|
|
{
|
|
questNotStartedCondition,
|
|
minLevelCondition,
|
|
hasKeyringCondition,
|
|
conditions:notC(visitedAthorCondition),
|
|
},
|
|
ConversationStates.ATTENDING,
|
|
"I don't think you have the experience to help me. Maybe if you were"
|
|
.. " more familiar with Athor island.",
|
|
nil)
|
|
|
|
-- player has keyring & can start quest
|
|
ari:add(
|
|
ConversationStates.IDLE,
|
|
ConversationPhrases.GREETING_MESSAGES,
|
|
canStartCondition,
|
|
ConversationStates.ATTENDING,
|
|
"Hey! You look like an experienced adventurer. Perhaps you could help me with a #task.",
|
|
nil)
|
|
|
|
ari:add(
|
|
ConversationStates.ATTENDING,
|
|
ConversationPhrases.QUEST_MESSAGES,
|
|
canStartCondition,
|
|
ConversationStates.QUEST_OFFERED,
|
|
"I have lost my engagement ring, and I am too embarrassed to tell " .. bride_name .. ". Would you help me?",
|
|
nil)
|
|
|
|
-- player has already completed quest
|
|
ari:add(
|
|
ConversationStates.ATTENDING,
|
|
ConversationPhrases.QUEST_MESSAGES,
|
|
questCompletedCondition,
|
|
ConversationStates.ATTENDING,
|
|
"Thank you, but I have everything I need now.",
|
|
nil)
|
|
|
|
ari:add(
|
|
ConversationStates.ATTENDING,
|
|
ConversationPhrases.QUEST_MESSAGES,
|
|
questActiveCondition,
|
|
ConversationStates.ATTENDING,
|
|
"You are already helping me find my engagement ring.",
|
|
nil)
|
|
|
|
ari:add(
|
|
ConversationStates.QUEST_OFFERED,
|
|
ConversationPhrases.NO_MESSAGES,
|
|
nil,
|
|
ConversationStates.IDLE,
|
|
"I don't want to leave " .. bride_name .. "'s side. I hope I can find someone to help me.",
|
|
actions:create("DecreaseKarmaAction", {15}))
|
|
|
|
-- player accepts quest
|
|
ari:add(
|
|
ConversationStates.QUEST_OFFERED,
|
|
ConversationPhrases.YES_MESSAGES,
|
|
nil,
|
|
ConversationStates.IDLE,
|
|
"Thank you so much! I lost my ring while visiting Athor Island."
|
|
.. " Please let me know when you find it. And don't say anything to "
|
|
.. bride_name .. ".",
|
|
setQuestAction)
|
|
end
|
|
|
|
local prepareBringStep = function()
|
|
ari:add(
|
|
ConversationStates.IDLE,
|
|
ConversationPhrases.GREETING_MESSAGES,
|
|
questActiveCondition,
|
|
ConversationStates.QUESTION_1,
|
|
"Did you find my ring? If there is something I can do to #help, please let me know.",
|
|
nil)
|
|
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.HELP_MESSAGES,
|
|
nil,
|
|
ConversationStates.IDLE,
|
|
"I may have dropped it while walking along the beach. If that is the case,"
|
|
.. " you may need something to dig in the sand or something that can"
|
|
.. " detect metal. Maybe a local pawn shop has a tool you could use.",
|
|
nil)
|
|
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.NO_MESSAGES,
|
|
nil,
|
|
ConversationStates.IDLE,
|
|
"Please keep looking.",
|
|
nil)
|
|
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.YES_MESSAGES,
|
|
conditions:notC(hasRingCondition),
|
|
ConversationStates.IDLE,
|
|
"You don't have my ring. Please, keep looking.",
|
|
nil)
|
|
|
|
local finishMessage = "Thank you so much! As a reward, I will give you this keyring."
|
|
.. " It is larger than the one you have."
|
|
local finishAction = {
|
|
actions:create("DropItemdataItemAction", {"engagement ring", ring_itemdata}),
|
|
actions:create(rewardAction),
|
|
}
|
|
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.YES_MESSAGES,
|
|
hasRingCondition,
|
|
ConversationStates.IDLE,
|
|
finishMessage,
|
|
finishAction)
|
|
|
|
-- compatibility so players can say "done"
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.FINISH_MESSAGES,
|
|
hasRingCondition,
|
|
ConversationStates.IDLE,
|
|
finishMessage,
|
|
finishAction)
|
|
|
|
-- player reports that ring was misplaced
|
|
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
"lost",
|
|
conditions:notC(hasRingCondition),
|
|
ConversationStates.IDLE,
|
|
"You lost my lost engagement ring? That's kind of ironic."
|
|
.. " I bet you dropped it near where you found it."
|
|
.. " Check there again.",
|
|
resetQuestAction)
|
|
|
|
ari:add(
|
|
ConversationStates.QUESTION_1,
|
|
"lost",
|
|
hasRingCondition,
|
|
ConversationStates.IDLE,
|
|
"You lost my lost engagement ring? That's kind of ironic."
|
|
.. " I bet you're teasing me.",
|
|
nil)
|
|
end
|
|
|
|
|
|
-- set up metal detector lender
|
|
|
|
-- we use a quest slot so player cannot borrow multiple metal detectors
|
|
local lender_slot = "sawyer_metal_detector"
|
|
local loanActive = conditions:create("QuestActiveCondition", {lender_slot})
|
|
local lend_phrases = {"loan", "lend", "metal detector", "borrow", "loaning", "lending", "borrowing"}
|
|
|
|
-- items that Sawyer will take in exchange for his metal detector
|
|
local collateral = {
|
|
-- sword
|
|
"black sword",
|
|
"chaos dagger",
|
|
"demon fire sword",
|
|
"golden blade",
|
|
"golden orc sword",
|
|
"hell dagger",
|
|
--"immortal sword",
|
|
"imperator sword",
|
|
"nihonto",
|
|
"orcish sword",
|
|
"royal dagger",
|
|
"soul dagger",
|
|
"xeno sword",
|
|
|
|
-- axe
|
|
"black halberd",
|
|
"black scythe",
|
|
"chaos axe",
|
|
"durin axe",
|
|
--"magic twoside axe",
|
|
|
|
-- club
|
|
"ice war hammer",
|
|
"vulcano hammer",
|
|
|
|
-- ranged
|
|
"mithril bow",
|
|
|
|
-- armor
|
|
"black armor",
|
|
"ice armor",
|
|
--"magic plate armor",
|
|
--"mainio armor",
|
|
"mithril armor",
|
|
"royal armor",
|
|
|
|
-- boots
|
|
--"black boots",
|
|
--"magic plate boots",
|
|
"mithril boots",
|
|
"royal boots",
|
|
|
|
-- cloak
|
|
--"black cloak",
|
|
"lich cloak",
|
|
--"magic cloak",
|
|
"mithril cloak",
|
|
"royal cloak",
|
|
"vampire cloak",
|
|
|
|
-- helmet
|
|
"black helmet",
|
|
"liberty helmet",
|
|
"magic chain helmet",
|
|
"mithril helmet",
|
|
"royal helmet",
|
|
|
|
-- legs
|
|
--"black legs",
|
|
--"jewelled legs",
|
|
--"magic plate legs",
|
|
"mithril legs",
|
|
"royal legs",
|
|
|
|
-- ring
|
|
--"emerald ring",
|
|
"enhanced imperial ring",
|
|
"imperial ring",
|
|
"turtle shell ring",
|
|
|
|
-- shield
|
|
"black shield",
|
|
"ice shield",
|
|
--"magic plate shield",
|
|
"mithril shield",
|
|
"royal shield",
|
|
--"xeno shield",
|
|
}
|
|
|
|
local loanMetalDetector = function(player, lender, detector, offer, bound_to, info_s)
|
|
-- player walked away before receiving metal detector
|
|
if lender:getAttending() == nil then
|
|
lender:say("Where did you go? Oh well, guess I'll just put this back on the"
|
|
.. " shelf.")
|
|
sawyersShelf:returnMetalDetector(detector)
|
|
lender:setCurrentState(ConversationStates.IDLE)
|
|
return
|
|
end
|
|
|
|
-- problem with metal detector item
|
|
if detector == nil then
|
|
lender:say("Uh oh! It seems my metal detector is broken. Sorry pal. Please open"
|
|
.. " a bug report https://stendhalgame.org/development/bug.html")
|
|
lender:setCurrentState(ConversationStates.ATTENDING)
|
|
return
|
|
end
|
|
|
|
local traded = player:getFirstEquipped(offer)
|
|
|
|
if traded == nil then
|
|
lender:say("Ha! Thought you could trick me?")
|
|
lender:setCurrentState(ConversationStates.ATTENDING)
|
|
return
|
|
end
|
|
|
|
-- handle items with itemdata & bound items
|
|
local bound_to = traded:getBoundTo() or ""
|
|
local info_s = traded:getItemData() or ""
|
|
|
|
local slot_state = offer .. ";" .. bound_to .. ";" .. info_s
|
|
|
|
detector:setItemData("Sawyer;" .. slot_state)
|
|
detector:setBoundTo(player:getName())
|
|
detector:setUndroppableOnDeath(true)
|
|
|
|
player:setQuest(lender_slot, slot_state)
|
|
player:drop(traded)
|
|
player:equipOrPutOnGround(detector)
|
|
|
|
lender:say("Okay, here you go. My metal detector for your " .. offer
|
|
.. ". Be careful with it. If it gets lost, you won't be able to"
|
|
.. " #return it and I will keep your " .. offer .. ". Anything"
|
|
.. " else I can help you with?")
|
|
lender:setCurrentState(ConversationStates.ATTENDING)
|
|
end
|
|
|
|
local handleLendRequest = function(player, sentence, lender)
|
|
local offer = sentence:getTrimmedText()
|
|
|
|
-- FIXME: using table.contains function breaks ChatAction.fire method created with actions:create
|
|
|
|
-- player ends conversation
|
|
--if not table.contains(arrays:toTable(ConversationPhrases.GOODBYE_MESSAGES), offer:lower()) then
|
|
for _, msg in ipairs(arrays:toTable(ConversationPhrases.GOODBYE_MESSAGES)) do
|
|
if offer:lower() == msg then
|
|
lender:getEntity():endConversation()
|
|
return
|
|
end
|
|
end
|
|
|
|
-- player decides not to trade
|
|
-- FIXME: arrays:toTable does not parse ConversationPhrases.NO_MESSAGES correctly
|
|
for _, msg in ipairs({"no", "nope", "nothing", "none"}) do
|
|
if offer:lower() == msg then
|
|
lender:say("Okay then. What else can I help you with?")
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Sawyer will not accept offered item
|
|
--if not table.contains(collateral, offer) then
|
|
local accepted = false
|
|
for _, acceptable in ipairs(collateral) do
|
|
if offer == acceptable then
|
|
accepted = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not accepted then
|
|
lender:say("Hmmm, I'm not interested in that. What else you got?")
|
|
lender:setCurrentState(ConversationStates.QUESTION_1)
|
|
return;
|
|
end
|
|
|
|
-- player isn't carrying the item
|
|
if not player:isEquipped(offer) then
|
|
lender:say("You're not even carrying one of those. Come on, what do you have?")
|
|
lender:setCurrentState(ConversationStates.QUESTION_1)
|
|
return
|
|
end
|
|
|
|
local detector = sawyersShelf:getMetalDetector()
|
|
|
|
-- metal detector from shelf was already loaned out
|
|
if detector == nil then
|
|
lender:say("Hmmmm. I guess I forgot that I already loaned it out...")
|
|
lender:say("!me is thinking.")
|
|
lender:setCurrentState(ConversationStates.BUSY)
|
|
|
|
game:runAfter(10, function()
|
|
lender:say("Oh! That's right, I keep a spare just in case.")
|
|
detector = sawyersShelf:getSpareMetalDetector()
|
|
|
|
game:runAfter(5, function()
|
|
loanMetalDetector(player, lender, detector, offer,
|
|
bound_to, info_s)
|
|
end)
|
|
end)
|
|
|
|
return
|
|
end
|
|
|
|
-- metal detector from shelf
|
|
loanMetalDetector(player, lender, detector, offer, bound_to, info_s)
|
|
end
|
|
|
|
local handleReturnRequest = function(player, sentence, lender)
|
|
if not player:isEquipped("metal detector") then
|
|
lender:say("You aren't even carryng a metal detector.")
|
|
return
|
|
end
|
|
|
|
local detector = player:getFirstEquipped("metal detector")
|
|
local detector_info = (detector:getItemData() or ""):split(";")
|
|
|
|
-- Sawyer doesn't recognize the metal detector
|
|
if #detector_info == 0 or detector_info[1] ~= "Sawyer" then
|
|
lender:say("This isn't mine. Take it back. I want MY metal detector.")
|
|
return
|
|
end
|
|
|
|
local item_name = detector_info[2]
|
|
local bound_to = detector_info[3]
|
|
local info_s = detector_info[4]
|
|
|
|
local item = entities:getItem(item_name)
|
|
|
|
-- problem with item
|
|
if item == nil then
|
|
lender:say("Uh oh! It seems I have broken your " .. item_name .. "."
|
|
.. " Sorry pal. Maybe you could contact #/support and get someone"
|
|
.. " to fix it for me.")
|
|
return
|
|
end
|
|
|
|
if bound_to ~= "" then
|
|
item:setBoundTo(bound_to)
|
|
end
|
|
|
|
if info_s ~= "" then
|
|
item:setItemData(info_s)
|
|
end
|
|
|
|
player:drop(detector)
|
|
player:equipOrPutOnGround(item)
|
|
player:setQuest(lender_slot, nil)
|
|
|
|
sawyersShelf:returnMetalDetector(detector)
|
|
lender:say("Okay. Here is your " .. item_name .. ". Good as new. Is there"
|
|
.. " anything else I can help you with?")
|
|
end
|
|
|
|
local prepareMetalDetectorLender = function()
|
|
local lender = entities:getNPC("Sawyer")
|
|
if lender == nil then
|
|
logger:error("Cannot set up metal detector lender Sawyer for Lost Engagement Ring quest")
|
|
return
|
|
end
|
|
|
|
-- hint to players that they can borrow metal detector
|
|
lender:addOffer("I also have some items that I don't mind #loaning out.")
|
|
|
|
lender:add(
|
|
ConversationStates.ATTENDING,
|
|
lend_phrases,
|
|
conditions:notC(loanActive),
|
|
ConversationStates.QUESTION_1,
|
|
"So, you want to borrow my metal detector? Well, I don't lend things out"
|
|
.. " without some form of collateral. What would you like to leave"
|
|
.. " behind?",
|
|
nil)
|
|
|
|
lender:add(
|
|
ConversationStates.ATTENDING,
|
|
lend_phrases,
|
|
loanActive,
|
|
ConversationStates.ATTENDING,
|
|
"I've already loaned you my metal detector. You can #return it whenever you like.",
|
|
nil)
|
|
|
|
lender:add(
|
|
ConversationStates.QUESTION_1,
|
|
"",
|
|
conditions:notC(loanActive),
|
|
ConversationStates.ATTENDING,
|
|
nil,
|
|
handleLendRequest)
|
|
|
|
lender:add(
|
|
ConversationStates.ATTENDING,
|
|
"return",
|
|
conditions:notC(loanActive),
|
|
ConversationStates.ATTENDING,
|
|
"Return what? I haven't loaned anything to you.",
|
|
nil)
|
|
|
|
lender:add(
|
|
ConversationStates.ATTENDING,
|
|
"return",
|
|
loanActive,
|
|
ConversationStates.QUESTION_1,
|
|
"You want to return my metal detector?",
|
|
nil)
|
|
|
|
lender:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.NO_MESSAGES,
|
|
loanActive,
|
|
ConversationStates.ATTENDING,
|
|
"Okay. What else can I help you with?",
|
|
nil)
|
|
|
|
lender:add(
|
|
ConversationStates.QUESTION_1,
|
|
ConversationPhrases.YES_MESSAGES,
|
|
loanActive,
|
|
ConversationStates.ATTENDING,
|
|
nil,
|
|
handleReturnRequest)
|
|
end
|
|
|
|
|
|
local quest = quests:create(quest_slot, "Lost Engagement Ring",
|
|
"A couple to be married in Fado City needs help.")
|
|
quest:setMinLevel(minLevel)
|
|
quest:setRegion(Region.FADO_CITY)
|
|
quest:setNPCName("Ari")
|
|
|
|
quest:setHistoryFunction(function(player)
|
|
local history = {}
|
|
|
|
local state = player:getQuest(quest_slot, 0)
|
|
if state == "done" then
|
|
table.insert(history, "I found Ari's ring. Now he is ready to wed with Emma.")
|
|
elseif state == "found_ring" then
|
|
if player:isEquippedWithItemdata("engagement ring", ring_itemdata) then
|
|
table.insert(history, "I have found Ari's ring and should bring it to him.")
|
|
else
|
|
table.insert(history, "I found Ari's ring, but I seem to have misplaced it."
|
|
.. " I should report to Ari that I <em>lost</em> it.")
|
|
end
|
|
else
|
|
table.insert(history, "Ari has lost his engagement ring while vacationing on Athor Island.")
|
|
if player:isEquipped("metal detector") then
|
|
table.insert(history, "I may be able to find it with this metal detector.")
|
|
elseif player:isEquipped("shovel") then
|
|
table.insert(history, "I may be able to find it with this shovel.")
|
|
else
|
|
table.insert(history, "I will need a tool to find it.")
|
|
end
|
|
end
|
|
|
|
return history
|
|
end)
|
|
|
|
quest:register(function()
|
|
prepareNPC()
|
|
prepareRequestStep()
|
|
prepareBringStep()
|
|
prepareMetalDetectorLender()
|
|
end)
|