1. -- This file is part of Jiddo's advanced NpcSystem v3.0x. This npcsystem is free to use by anyone, for any purpuse.
  2. -- Initial release date: 2007-02-21
  3. -- Credits: Jiddo, honux(I'm using a modified version of his Find function).
  4. -- Please include full credits whereever you use this system, or parts of it.
  5. -- For support, questions and updates, please consult the following thread:
  6. -- http://otfans.net/showthread.php?t=67810
  7. if(NpcHandler == nil) then
  8. -- Constant talkdelay behaviors.
  9. TALKDELAY_NONE = 0 -- No talkdelay. Npc will reply immedeatly.
  10. TALKDELAY_ONTHINK = 1 -- Talkdelay handled through the onThink callback function. (Default)
  11. TALKDELAY_EVENT = 2 -- Not yet implemented
  12. -- Currently applied talkdelay behavior. TALKDELAY_ONTHINK is default.
  13. NPCHANDLER_TALKDELAY = TALKDELAY_ONTHINK
  14. -- Constant indexes for defining default messages.
  15. MESSAGE_GREET = 1 -- When the player greets the npc.
  16. MESSAGE_FAREWELL = 2 -- When the player unGreets the npc.
  17. MESSAGE_BUY = 3 -- When the npc asks the player if he wants to buy something.
  18. MESSAGE_SELL = 4 -- When the npc asks the player if he wants to sell something.
  19. MESSAGE_ONBUY = 5 -- When the player successfully buys something
  20. MESSAGE_ONSELL = 6 -- When the player successfully sells something
  21. MESSAGE_NEEDMOREMONEY = 7 -- When the player does not have enough money
  22. MESSAGE_NOTHAVEITEM = 8 -- When the player is trying to sell an item he does not have.
  23. MESSAGE_IDLETIMEOUT = 9 -- When the player has been idle for longer then idleTime allows.
  24. MESSAGE_WALKAWAY = 10 -- When the player walks out of the talkRadius of the npc.
  25. MESSAGE_ALREADYFOCUSED = 11 -- When the player already has the focus of this nopc.
  26. MESSAGE_PLACEDINQUEUE = 12 -- When the player has been placed in the costumer queue.
  27. MESSAGE_DECLINE = 13 -- When the player sais no to something.
  28. -- Constant indexes for callback functions. These are also used for module callback ids.
  29. CALLBACK_CREATURE_APPEAR = 1
  30. CALLBACK_CREATURE_DISAPPEAR = 2
  31. CALLBACK_CREATURE_SAY = 3
  32. CALLBACK_ONTHINK = 4
  33. CALLBACK_GREET = 5
  34. CALLBACK_FAREWELL = 6
  35. CALLBACK_MESSAGE_DEFAULT = 7
  36. -- Addidional module callback ids
  37. CALLBACK_MODULE_INIT = 10
  38. CALLBACK_MODULE_RESET = 11
  39. -- Constant strings defining the keywords to replace in the default messages.
  40. TAG_PLAYERNAME = '|PLAYERNAME|'
  41. TAG_ITEMCOUNT = '|ITEMCOUNT|'
  42. TAG_TOTALCOST = '|TOTALCOST|'
  43. TAG_ITEMNAME = '|ITEMNAME|'
  44. TAG_QUEUESIZE = '|QUEUESIZE|'
  45. NpcHandler = {
  46. keywordHandler = nil,
  47. queue = nil,
  48. focus = 0,
  49. talkStart = 0,
  50. idleTime = 30,
  51. talkRadius = 5,
  52. talkDelayTime = 1, -- Seconds to delay outgoing messages.
  53. talkDelay = nil,
  54. callbackFunctions = nil,
  55. modules = nil,
  56. messages = {
  57. -- These are the default replies of all npcs. They can/should be changed individually for each npc.
  58. [MESSAGE_GREET] = 'Welcome, |PLAYERNAME|! I have been expecting you.',
  59. [MESSAGE_FAREWELL] = 'Good bye, |PLAYERNAME|!',
  60. [MESSAGE_BUY] = 'Do you want to buy |ITEMCOUNT| |ITEMNAME| for |TOTALCOST| gold coins?',
  61. [MESSAGE_SELL] = 'Do you want to sell |ITEMCOUNT| |ITEMNAME| for |TOTALCOST| gold coins?',
  62. [MESSAGE_ONBUY] = 'It was a pleasure doing business with you.',
  63. [MESSAGE_ONSELL] = 'Thank you for this item, |PLAYERNAME|.',
  64. [MESSAGE_NEEDMOREMONEY] = 'You do not have enough money.',
  65. [MESSAGE_NOTHAVEITEM] = 'You don\'t even have that item!',
  66. [MESSAGE_IDLETIMEOUT] = 'Next please!',
  67. [MESSAGE_WALKAWAY] = 'How rude!',
  68. [MESSAGE_ALREADYFOCUSED]= '|PLAYERNAME|, I am already talking to you.',
  69. [MESSAGE_PLACEDINQUEUE] = '|PLAYERNAME|, please wait for your turn. There are |QUEUESIZE| customers before you.',
  70. [MESSAGE_DECLINE] = 'Not good enough, is it?'
  71. }
  72. }
  73. -- Creates a new NpcHandler with an empty callbackFunction stack.
  74. function NpcHandler:new(keywordHandler)
  75. local obj = {}
  76. obj.callbackFunctions = {}
  77. obj.modules = {}
  78. obj.talkDelay = {
  79. message = nil,
  80. time = nil
  81. }
  82. obj.queue = Queue:new(obj)
  83. obj.keywordHandler = keywordHandler
  84. obj.messages = {}
  85. setmetatable(obj.messages, self.messages)
  86. self.messages.__index = self.messages
  87. setmetatable(obj, self)
  88. self.__index = self
  89. return obj
  90. end
  91. -- Re-defines the maximum idle time allowed for a player when talking to this npc.
  92. function NpcHandler:setMaxIdleTime(newTime)
  93. self.idleTime = newTime
  94. end
  95. -- Attaches a new costumer queue to this npchandler.
  96. function NpcHandler:setQueue(newQueue)
  97. self.queue = newQueue
  98. self.queue:setHandler(self)
  99. end
  100. -- Attackes a new keyword handler to this npchandler
  101. function NpcHandler:setKeywordHandler(newHandler)
  102. self.keywordHandler = newHandler
  103. end
  104. -- Function used to change the focus of this npc.
  105. function NpcHandler:changeFocus(newFocus)
  106. self.focus = newFocus
  107. self:updateFocus()
  108. end
  109. -- This function should be called on each onThink and makes sure the npc faces the player it is talking to.
  110. -- Should also be called whenever a new player is focused.
  111. function NpcHandler:updateFocus()
  112. doNpcSetCreatureFocus(self.focus)
  113. end
  114. -- Used when the npc should un-focus the player.
  115. function NpcHandler:releaseFocus()
  116. self:changeFocus(0)
  117. end
  118. -- Returns the callback function with the specified id or nil if no such callback function exists.
  119. function NpcHandler:getCallback(id)
  120. local ret = nil
  121. if(self.callbackFunctions ~= nil) then
  122. ret = self.callbackFunctions[id]
  123. end
  124. return ret
  125. end
  126. -- Changes the callback function for the given id to callback.
  127. function NpcHandler:setCallback(id, callback)
  128. if(self.callbackFunctions ~= nil) then
  129. self.callbackFunctions[id] = callback
  130. end
  131. end
  132. -- Adds a module to this npchandler and inits it.
  133. function NpcHandler:addModule(module)
  134. if(self.modules ~= nil) then
  135. table.insert(self.modules, module)
  136. module:init(self)
  137. end
  138. end
  139. -- Calls the callback function represented by id for all modules added to this npchandler with the given arguments.
  140. function NpcHandler:processModuleCallback(id, ...)
  141. local ret = true
  142. for i, module in pairs(self.modules) do
  143. local tmpRet = true
  144. if(id == CALLBACK_CREATURE_APPEAR and module.callbackOnCreatureAppear ~= nil) then
  145. tmpRet = module:callbackCreatureAppear(unpack(arg))
  146. elseif(id == CALLBACK_CREATURE_DISAPPEAR and module.callbackOnCreatureDisappear ~= nil) then
  147. tmpRet = module:callbackCreatureDisappear(unpack(arg))
  148. elseif(id == CALLBACK_CREATURE_SAY and module.callbackOnCreatureSay ~= nil) then
  149. tmpRet = module:callbackCreatureSay(unpack(arg))
  150. elseif(id == CALLBACK_ONTHINK and module.callbackOnThink ~= nil) then
  151. tmpRet = module:callbackOnThink(unpack(arg))
  152. elseif(id == CALLBACK_GREET and module.callbackOnGreet ~= nil) then
  153. tmpRet = module:callbackOnGreet(unpack(arg))
  154. elseif(id == CALLBACK_FAREWELL and module.callbackOnFarewell ~= nil) then
  155. tmpRet = module:callbackOnFarewell(unpack(arg))
  156. elseif(id == CALLBACK_MESSAGE_DEFAULT and module.callbackOnMessageDefault ~= nil) then
  157. tmpRet = module:callbackOnMessageDefault(unpack(arg))
  158. elseif(id == CALLBACK_MODULE_RESET and module.callbackOnModuleReset ~= nil) then
  159. tmpRet = module:callbackOnModuleReset(unpack(arg))
  160. end
  161. if(not tmpRet) then
  162. ret = false
  163. break
  164. end
  165. end
  166. return ret
  167. end
  168. -- Returns the message represented by id.
  169. function NpcHandler:getMessage(id)
  170. local ret = nil
  171. if(self.messages ~= nil) then
  172. ret = self.messages[id]
  173. end
  174. return ret
  175. end
  176. -- Changes the default response message with the specified id to newMessage.
  177. function NpcHandler:setMessage(id, newMessage)
  178. if(self.messages ~= nil) then
  179. self.messages[id] = newMessage
  180. end
  181. end
  182. -- Translates all message tags found in msg using parseInfo
  183. function NpcHandler:parseMessage(msg, parseInfo)
  184. local ret = msg
  185. for search, replace in pairs(parseInfo) do
  186. ret = string.gsub(ret, search, replace)
  187. end
  188. return ret
  189. end
  190. -- Makes sure the npc un-focuses the furrently focused player, and greets the next player in the queue is it is not empty.
  191. function NpcHandler:unGreet()
  192. if(self.focus == 0) then
  193. return
  194. end
  195. local callback = self:getCallback(CALLBACK_FAREWELL)
  196. if(callback == nil or callback()) then
  197. if(self:processModuleCallback(CALLBACK_FAREWELL)) then
  198. if(self.queue == nil or not self.queue:greetNext()) then
  199. local msg = self:getMessage(MESSAGE_FAREWELL)
  200. local parseInfo = { [TAG_PLAYERNAME] = getPlayerName(self.focus) }
  201. msg = self:parseMessage(msg, parseInfo)
  202. self:say(msg)
  203. self:releaseFocus()
  204. end
  205. end
  206. end
  207. end
  208. -- Greets a new player.
  209. function NpcHandler:greet(cid)
  210. if(cid ~= 0) then
  211. local callback = self:getCallback(CALLBACK_GREET)
  212. if(callback == nil or callback(cid)) then
  213. if(self:processModuleCallback(CALLBACK_GREET, cid)) then
  214. local msg = self:getMessage(MESSAGE_GREET)
  215. local parseInfo = { [TAG_PLAYERNAME] = getPlayerName(cid) }
  216. msg = self:parseMessage(msg, parseInfo)
  217. self:say(msg)
  218. else
  219. return
  220. end
  221. else
  222. return
  223. end
  224. end
  225. self:changeFocus(cid)
  226. end
  227. -- Handles onCreatureAppear events. If you with to handle this yourself, please use the CALLBACK_CREATURE_APPEAR callback.
  228. function NpcHandler:onCreatureAppear(cid)
  229. local callback = self:getCallback(CALLBACK_CREATURE_APPEAR)
  230. if(callback == nil or callback(cid)) then
  231. if(self:processModuleCallback(CALLBACK_CREATURE_APPEAR, cid)) then
  232. end
  233. end
  234. end
  235. -- Handles onCreatureDisappear events. If you with to handle this yourself, please use the CALLBACK_CREATURE_DISAPPEAR callback.
  236. function NpcHandler:onCreatureDisappear(cid)
  237. local callback = self:getCallback(CALLBACK_CREATURE_DISAPPEAR)
  238. if(callback == nil or callback(cid)) then
  239. if(self:processModuleCallback(CALLBACK_CREATURE_DISAPPEAR, cid)) then
  240. if(self.focus == cid) then
  241. self:unGreet()
  242. end
  243. end
  244. end
  245. end
  246. -- Handles onCreatureSay events. If you with to handle this yourself, please use the CALLBACK_CREATURE_SAY callback.
  247. function NpcHandler:onCreatureSay(cid, msgtype, msg)
  248. local callback = self:getCallback(CALLBACK_CREATURE_SAY)
  249. if(callback == nil or callback(cid, msgtype, msg)) then
  250. if(self:processModuleCallback(CALLBACK_CREATURE_SAY, cid, msgtype, msg)) then
  251. if(not self:isInRange(cid)) then
  252. return
  253. end
  254. if(self.keywordHandler ~= nil) then
  255. local ret = self.keywordHandler:processMessage(cid, msg)
  256. if(not ret) then
  257. local callback = self:getCallback(CALLBACK_MESSAGE_DEFAULT)
  258. if(callback ~= nil and callback(cid, msgtype, msg)) then
  259. self.talkStart = os.time()
  260. end
  261. else
  262. self.talkStart = os.time()
  263. end
  264. end
  265. end
  266. end
  267. end
  268. -- Handles onThink events. If you with to handle this yourself, please use the CALLBACK_ONTHINK callback.
  269. function NpcHandler:onThink()
  270. local callback = self:getCallback(CALLBACK_ONTHINK)
  271. if(callback == nil or callback()) then
  272. if(NPCHANDLER_TALKDELAY == TALKDELAY_ONTHINK and self.talkDelay.time ~= nil and self.talkDelay.message ~= nil and os.time() >= self.talkDelay.time) then
  273. selfSay(self.talkDelay.message)
  274. self.talkDelay.time = nil
  275. self.talkDelay.message = nil
  276. end
  277. if(self:processModuleCallback(CALLBACK_ONTHINK)) then
  278. if(self.focus ~= 0) then
  279. if(not self:isInRange(self.focus)) then
  280. self:onWalkAway(self.focus)
  281. elseif(os.time()-self.talkStart > self.idleTime) then
  282. self:unGreet()
  283. else
  284. self:updateFocus()
  285. end
  286. end
  287. end
  288. end
  289. end
  290. -- Tries to greet the player iwth the given cid. This function does not override queue order, current focus etc.
  291. function NpcHandler:onGreet(cid)
  292. if(self:isInRange(cid)) then
  293. if(self.focus == 0) then
  294. self:greet(cid)
  295. elseif(cid == self.focus) then
  296. local msg = self:getMessage(MESSAGE_ALREADYFOCUSED)
  297. local parseInfo = { [TAG_PLAYERNAME] = getPlayerName(cid) }
  298. msg = self:parseMessage(msg, parseInfo)
  299. self:say(msg)
  300. else
  301. if(not self.queue:isInQueue(cid)) then
  302. self.queue:push(cid)
  303. end
  304. local msg = self:getMessage(MESSAGE_PLACEDINQUEUE)
  305. local parseInfo = { [TAG_PLAYERNAME] = getPlayerName(cid), [TAG_QUEUESIZE] = self.queue:getSize() }
  306. msg = self:parseMessage(msg, parseInfo)
  307. self:say(msg)
  308. end
  309. end
  310. end
  311. -- Simply calls the underlying unGreet function.
  312. function NpcHandler:onFarewell()
  313. self:unGreet()
  314. end
  315. -- Should be called on this npc's focus if the distance to focus is greater then talkRadius.
  316. function NpcHandler:onWalkAway(cid)
  317. if(cid == self.focus) then
  318. local callback = self:getCallback(CALLBACK_CREATURE_DISAPPEAR)
  319. if(callback == nil or callback()) then
  320. if(self:processModuleCallback(CALLBACK_CREATURE_DISAPPEAR, cid)) then
  321. if(self.queue == nil or not self.queue:greetNext()) then
  322. local msg = self:getMessage(MESSAGE_WALKAWAY)
  323. local parseInfo = { [TAG_PLAYERNAME] = getPlayerName(self.focus) }
  324. msg = self:parseMessage(msg, parseInfo)
  325. self:say(msg)
  326. self:releaseFocus()
  327. end
  328. end
  329. end
  330. end
  331. end
  332. -- Returns true if cid is within the talkRadius of this npc.
  333. function NpcHandler:isInRange(cid)
  334. local playerPos = getPlayerPosition(cid)
  335. if playerPos == LUA_ERROR or playerPos == LUA_NO_ERROR then
  336. return false
  337. end
  338. local sx, sy, sz = selfGetPosition()
  339. local dx = math.abs(sx-playerPos.x)
  340. local dy = math.abs(sy-playerPos.y)
  341. local dz = math.abs(sz-playerPos.z)
  342. local dist = (dx^2 + dy^2)^0.5
  343. return (dist <= self.talkRadius and dz == 0)
  344. end
  345. -- Resets the npc into it's initial state (in regard of the keyrodhandler).
  346. -- All modules are also receiving a reset call through their callbackOnModuleReset function.
  347. function NpcHandler:resetNpc()
  348. if(self:processModuleCallback(CALLBACK_MODULE_RESET)) then
  349. self.keywordHandler:reset()
  350. end
  351. end
  352. -- Makes the npc represented by this instance of NpcHandler say something.
  353. -- This implements the currently set type of talkdelay.
  354. -- shallDelay is a boolean value. If it is false, the message is not delayed. Default value is true.
  355. function NpcHandler:say(message, shallDelay)
  356. if(shallDelay == nil) then
  357. shallDelay = true
  358. end
  359. if(NPCHANDLER_TALKDELAY == TALKDELAY_NONE or shallDelay == false) then
  360. selfSay(message)
  361. return
  362. end
  363. self.talkDelay.message = message
  364. self.talkDelay.time = os.time()+self.talkDelayTime
  365. end
  366. end

npchandler.lua:455