1. --[[
  2. 4.0
  3. Transmogrification TBC - Gossip Menu
  4. By Rochet2
  5. Eluna version
  6. TODO:
  7. Make DB saving even better (Deleting)? What about coding?
  8. Fix the cost formula
  9. TODO in the distant future:
  10. Are the qualities right? Blizzard might have changed the quality requirements.
  11. What can and cant be used as source or target..?
  12. Cant transmogrify:
  13. rediculus items -- Foereaper: would be fun to stab people with a fish
  14. -- Cant think of any good way to handle this easily
  15. Cataclysm:
  16. Test on cata : implement UI xD?
  17. Item link icon to Are You sure text
  18. ]]
  19. local NPC_Entry = 60000
  20. local RequireGold = 1
  21. local GoldModifier = 1.0
  22. local GoldCost = 100000
  23. local RequireToken = false
  24. local TokenEntry = 49426
  25. local TokenAmount = 1
  26. local Qualities =
  27. {
  28. [0] = false, -- AllowPoor
  29. [1] = false, -- AllowCommon
  30. [2] = true , -- AllowUncommon
  31. [3] = true , -- AllowRare
  32. [4] = true , -- AllowEpic
  33. [5] = false, -- AllowLegendary
  34. [6] = false, -- AllowArtifact
  35. [7] = true , -- AllowHeirloom
  36. }
  37. local EQUIPMENT_SLOT_START = 0
  38. local EQUIPMENT_SLOT_HEAD = 0
  39. local EQUIPMENT_SLOT_NECK = 1
  40. local EQUIPMENT_SLOT_SHOULDERS = 2
  41. local EQUIPMENT_SLOT_BODY = 3
  42. local EQUIPMENT_SLOT_CHEST = 4
  43. local EQUIPMENT_SLOT_WAIST = 5
  44. local EQUIPMENT_SLOT_LEGS = 6
  45. local EQUIPMENT_SLOT_FEET = 7
  46. local EQUIPMENT_SLOT_WRISTS = 8
  47. local EQUIPMENT_SLOT_HANDS = 9
  48. local EQUIPMENT_SLOT_FINGER1 = 10
  49. local EQUIPMENT_SLOT_FINGER2 = 11
  50. local EQUIPMENT_SLOT_TRINKET1 = 12
  51. local EQUIPMENT_SLOT_TRINKET2 = 13
  52. local EQUIPMENT_SLOT_BACK = 14
  53. local EQUIPMENT_SLOT_MAINHAND = 15
  54. local EQUIPMENT_SLOT_OFFHAND = 16
  55. local EQUIPMENT_SLOT_RANGED = 17
  56. local EQUIPMENT_SLOT_TABARD = 18
  57. local EQUIPMENT_SLOT_END = 19
  58. local INVENTORY_SLOT_BAG_START = 19
  59. local INVENTORY_SLOT_BAG_END = 23
  60. local INVENTORY_SLOT_ITEM_START = 23
  61. local INVENTORY_SLOT_ITEM_END = 39
  62. local INVTYPE_CHEST = 5
  63. local INVTYPE_WEAPON = 13
  64. local INVTYPE_ROBE = 20
  65. local INVTYPE_WEAPONMAINHAND = 21
  66. local INVTYPE_WEAPONOFFHAND = 22
  67. local ITEM_CLASS_WEAPON = 2
  68. local ITEM_CLASS_ARMOR = 4
  69. local ITEM_SUBCLASS_WEAPON_BOW = 2
  70. local ITEM_SUBCLASS_WEAPON_GUN = 3
  71. local ITEM_SUBCLASS_WEAPON_CROSSBOW = 18
  72. local ITEM_SUBCLASS_WEAPON_FISHING_POLE = 20
  73. local PLAYER_VISIBLE_ITEM_1_ENTRYID = 260
  74. local INVENTORY_SLOT_BAG_0 = 255
  75. local SlotNames = {
  76. [EQUIPMENT_SLOT_HEAD ] = {"Head", nil, nil, nil, nil, nil, nil, nil, nil},
  77. [EQUIPMENT_SLOT_SHOULDERS ] = {"Shoulders", nil, nil, nil, nil, nil, nil, nil, nil},
  78. [EQUIPMENT_SLOT_BODY ] = {"Shirt", nil, nil, nil, nil, nil, nil, nil, nil},
  79. [EQUIPMENT_SLOT_CHEST ] = {"Chest", nil, nil, nil, nil, nil, nil, nil, nil},
  80. [EQUIPMENT_SLOT_WAIST ] = {"Waist", nil, nil, nil, nil, nil, nil, nil, nil},
  81. [EQUIPMENT_SLOT_LEGS ] = {"Legs", nil, nil, nil, nil, nil, nil, nil, nil},
  82. [EQUIPMENT_SLOT_FEET ] = {"Feet", nil, nil, nil, nil, nil, nil, nil, nil},
  83. [EQUIPMENT_SLOT_WRISTS ] = {"Wrists", nil, nil, nil, nil, nil, nil, nil, nil},
  84. [EQUIPMENT_SLOT_HANDS ] = {"Hands", nil, nil, nil, nil, nil, nil, nil, nil},
  85. [EQUIPMENT_SLOT_BACK ] = {"Back", nil, nil, nil, nil, nil, nil, nil, nil},
  86. [EQUIPMENT_SLOT_MAINHAND ] = {"Main hand", nil, nil, nil, nil, nil, nil, nil, nil},
  87. [EQUIPMENT_SLOT_OFFHAND ] = {"Off hand", nil, nil, nil, nil, nil, nil, nil, nil},
  88. [EQUIPMENT_SLOT_RANGED ] = {"Ranged", nil, nil, nil, nil, nil, nil, nil, nil},
  89. [EQUIPMENT_SLOT_TABARD ] = {"Tabard", nil, nil, nil, nil, nil, nil, nil, nil},
  90. }
  91. local Locales = {
  92. {"Update menu", nil, nil, nil, nil, nil, nil, nil, nil},
  93. {"Remove all transmogrifications", nil, nil, nil, nil, nil, nil, nil, nil},
  94. {"Remove transmogrifications from all equipped items?", nil, nil, nil, nil, nil, nil, nil, nil},
  95. {"Using this item for transmogrify will bind it to you and make it non-refundable and non-tradeable.\nDo you wish to continue?", nil, nil, nil, nil, nil, nil, nil, nil},
  96. {"Remove transmogrification from %s?", nil, nil, nil, nil, nil, nil, nil, nil},
  97. {"Back..", nil, nil, nil, nil, nil, nil, nil, nil},
  98. {"Remove transmogrification", nil, nil, nil, nil, nil, nil, nil, nil},
  99. {"Transmogrifications removed from equipped items", nil, nil, nil, nil, nil, nil, nil, nil},
  100. {"You have no transmogrified items equipped", nil, nil, nil, nil, nil, nil, nil, nil},
  101. {"%s transmogrification removed", nil, nil, nil, nil, nil, nil, nil, nil},
  102. {"No transmogrification on %s slot", nil, nil, nil, nil, nil, nil, nil, nil},
  103. {"%s transmogrified", nil, nil, nil, nil, nil, nil, nil, nil},
  104. {"Selected items are not suitable", nil, nil, nil, nil, nil, nil, nil, nil},
  105. {"Selected item does not exist", nil, nil, nil, nil, nil, nil, nil, nil},
  106. {"Equipment slot is empty", nil, nil, nil, nil, nil, nil, nil, nil},
  107. {"You don't have enough %ss", nil, nil, nil, nil, nil, nil, nil, nil},
  108. }
  109. local function LocText(id, p) -- "%s":format("test")
  110. if(Locales[id]) then
  111. local s = Locales[id][p:GetDbcLocale()+1] or Locales[id][1]
  112. if(s) then
  113. return s
  114. end
  115. end
  116. return "Text not found: "..(id or 0)
  117. end
  118. --[[
  119. typedef UNORDERED_MAP<uint32, uint32> transmogData
  120. typedef UNORDERED_MAP<uint32, transmogData> transmogMap
  121. static transmogMap entryMap -- entryMap[pGUID][iGUID] = entry
  122. static transmogData dataMap -- dataMap[iGUID] = pGUID
  123. ]]
  124. local entryMap = {}
  125. local dataMap = {}
  126. local function GetSlotName(slot, locale)
  127. if(not SlotNames[slot]) then return end
  128. return SlotNames[slot][locale and locale+1 or 1]
  129. end
  130. local function GetFakePrice(item)
  131. local sellPrice = item:GetSellPrice()
  132. local minPrice = item:GetRequiredLevel() * 1176
  133. if (sellPrice < minPrice) then
  134. sellPrice = minPrice
  135. end
  136. return sellPrice
  137. end
  138. local function GetFakeEntry(item)
  139. local guid = item and item:GetGUIDLow()
  140. if(guid and dataMap[guid]) then
  141. if(entryMap[dataMap[guid]]) then
  142. return entryMap[dataMap[guid]][guid]
  143. end
  144. end
  145. end
  146. local function DeleteFakeFromDB(itemGUID)
  147. if (dataMap[itemGUID]) then
  148. if(entryMap[dataMap[itemGUID]]) then
  149. entryMap[dataMap[itemGUID]][itemGUID] = nil
  150. end
  151. dataMap[itemGUID] = nil
  152. end
  153. CharDBExecute("DELETE FROM custom_transmogrification WHERE GUID = "..itemGUID)
  154. end
  155. local function DeleteFakeEntry(item)
  156. if (not GetFakeEntry(item)) then
  157. return false
  158. end
  159. item:GetOwner():UpdateUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (item:GetSlot() * 12), item:GetEntry())
  160. DeleteFakeFromDB(item:GetGUIDLow())
  161. return true
  162. end
  163. local function SetFakeEntry(item, entry)
  164. local player = item:GetOwner()
  165. if(player) then
  166. local pGUID = player:GetGUIDLow()
  167. local iGUID = item:GetGUIDLow()
  168. player:UpdateUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (item:GetSlot() * 12), entry)
  169. if(not entryMap[pGUID]) then
  170. entryMap[pGUID] = {}
  171. end
  172. entryMap[pGUID][iGUID] = entry
  173. dataMap[iGUID] = pGUID
  174. CharDBExecute("REPLACE INTO custom_transmogrification (GUID, FakeEntry, Owner) VALUES ("..iGUID..", "..entry..", "..pGUID..")")
  175. end
  176. end
  177. local function SuitableForTransmogrification(player, oldItem, newItem)
  178. -- not possibly the best structure here, but atleast I got my head around this
  179. if (not Qualities[newItem:GetQuality()]) then
  180. return false
  181. end
  182. if (not Qualities[oldItem:GetQuality()]) then
  183. return false
  184. end
  185. if(oldItem:GetDisplayId() == newItem:GetDisplayId()) then
  186. return false
  187. end
  188. --if (GetFakeEntry(oldItem))
  189. -- if (const ItemTemplate* fakeItemTemplate = sObjectMgr:GetItemTemplate(GetFakeEntry(oldItem)))
  190. -- if (fakeItemTemplate:DisplayInfoID == newItem:GetTemplate():DisplayInfoID)
  191. -- return false
  192. local fentry = GetFakeEntry(oldItem)
  193. if(fentry and fentry == newItem:GetEntry()) then
  194. return false
  195. end
  196. if (player:CanUseItem(newItem) ~= 0) then
  197. return false
  198. end
  199. local newClass = newItem:GetClass()
  200. local oldClass = oldItem:GetClass()
  201. local newSubClass = newItem:GetSubClass()
  202. local oldSubClass = oldItem:GetSubClass()
  203. local newInventorytype = newItem:GetInventoryType()
  204. local oldInventorytype = oldItem:GetInventoryType()
  205. if (newClass ~= oldClass) then
  206. return false
  207. end
  208. if (newClass == ITEM_CLASS_WEAPON and newSubClass ~= ITEM_SUBCLASS_WEAPON_FISHING_POLE and oldSubClass ~= ITEM_SUBCLASS_WEAPON_FISHING_POLE) then
  209. if (newSubClass == oldSubClass or ((newSubClass == ITEM_SUBCLASS_WEAPON_BOW or newSubClass == ITEM_SUBCLASS_WEAPON_GUN or newSubClass == ITEM_SUBCLASS_WEAPON_CROSSBOW) and (oldSubClass == ITEM_SUBCLASS_WEAPON_BOW or oldSubClass == ITEM_SUBCLASS_WEAPON_GUN or oldSubClass == ITEM_SUBCLASS_WEAPON_CROSSBOW))) then
  210. if (newInventorytype == oldInventorytype or (newInventorytype == INVTYPE_WEAPON and (oldInventorytype == INVTYPE_WEAPONMAINHAND or oldInventorytype == INVTYPE_WEAPONOFFHAND))) then
  211. return true
  212. else
  213. return false
  214. end
  215. else
  216. return false
  217. end
  218. elseif (newClass == ITEM_CLASS_ARMOR) then
  219. if (newSubClass == oldSubClass) then
  220. if (newInventorytype == oldInventorytype or (newInventorytype == INVTYPE_CHEST and oldInventorytype == INVTYPE_ROBE) or (newInventorytype == INVTYPE_ROBE and oldInventorytype == INVTYPE_CHEST)) then
  221. return true
  222. else
  223. return false
  224. end
  225. else
  226. return false
  227. end
  228. end
  229. return false
  230. end
  231. local menu_id = math.random(1000)
  232. local function OnGossipHello(event, player, creature)
  233. player:GossipClearMenu()
  234. for slot = EQUIPMENT_SLOT_START, EQUIPMENT_SLOT_END-1 do
  235. local newItem = player:GetItemByPos(-1, slot)
  236. if (newItem) then
  237. if (Qualities[newItem:GetQuality()]) then
  238. local slotName = GetSlotName(slot, player:GetDbcLocale())
  239. if (slotName) then
  240. player:GossipMenuAddItem(3, slotName, EQUIPMENT_SLOT_END, slot)
  241. end
  242. end
  243. end
  244. end
  245. player:GossipMenuAddItem(4, LocText(2, player), EQUIPMENT_SLOT_END+2, 0, false, LocText(3, player), 0)
  246. player:GossipMenuAddItem(7, LocText(1, player), EQUIPMENT_SLOT_END+1, 0)
  247. player:GossipSendMenu(100, creature, menu_id)
  248. end
  249. local _items = {}
  250. local function OnGossipSelect(event, player, creature, sender, uiAction)
  251. if sender == EQUIPMENT_SLOT_END then -- Show items you can use
  252. local oldItem = player:GetItemByPos(-1, uiAction)
  253. if (oldItem) then
  254. local lowGUID = player:GetGUIDLow()
  255. _items[lowGUID] = {} -- Remove this with logix
  256. local limit = 0
  257. local price = 0
  258. if(RequireGold == 1) then
  259. price = GetFakePrice(oldItem)*GoldModifier
  260. elseif(RequireGold == 2) then
  261. price = GoldCost
  262. end
  263. for i = INVENTORY_SLOT_ITEM_START, INVENTORY_SLOT_ITEM_END-1 do
  264. if (limit > 30) then
  265. break
  266. end
  267. local newItem = player:GetItemByPos(-1, i)
  268. if (newItem) then
  269. local display = newItem:GetDisplayId()
  270. if (SuitableForTransmogrification(player, oldItem, newItem)) then
  271. if (not _items[lowGUID][display]) then
  272. limit = limit + 1
  273. _items[lowGUID][display] = newItem
  274. local popup = LocText(4, player).."\n\n"..newItem:GetItemLink(player:GetDbcLocale()).."\n"
  275. if(RequireToken) then
  276. popup = popup.."\n"..TokenAmount.." x "..GetItemLink(TokenEntry, player:GetDbcLocale())
  277. end
  278. player:GossipMenuAddItem(4, newItem:GetItemLink(player:GetDbcLocale()), uiAction, display, false, popup, price)
  279. end
  280. end
  281. end
  282. end
  283. for i = INVENTORY_SLOT_BAG_START, INVENTORY_SLOT_BAG_END-1 do
  284. local bag = player:GetItemByPos(-1, i)
  285. if (bag) then
  286. for j = 0, bag:GetBagSize()-1 do
  287. if (limit > 30) then
  288. break
  289. end
  290. local newItem = player:GetItemByPos(i, j)
  291. if (newItem) then
  292. local display = newItem:GetDisplayId()
  293. if (SuitableForTransmogrification(player, oldItem, newItem)) then
  294. if (not _items[lowGUID][display]) then
  295. limit = limit + 1
  296. _items[lowGUID][display] = newItem
  297. player:GossipMenuAddItem(4, newItem:GetItemLink(player:GetDbcLocale()), uiAction, display, false, popup, price)
  298. end
  299. end
  300. end
  301. end
  302. end
  303. end
  304. player:GossipMenuAddItem(4, LocText(7, player), EQUIPMENT_SLOT_END+3, uiAction, false, LocText(5, player):format(GetSlotName(uiAction, player:GetDbcLocale())))
  305. player:GossipMenuAddItem(7, LocText(6, player), EQUIPMENT_SLOT_END+1, 0)
  306. player:GossipSendMenu(100, creature, menu_id)
  307. else
  308. OnGossipHello(event, player, creature)
  309. end
  310. elseif sender == EQUIPMENT_SLOT_END+1 then -- Back
  311. OnGossipHello(event, player, creature)
  312. elseif sender == EQUIPMENT_SLOT_END+2 then -- Remove Transmogrifications
  313. local removed = false
  314. for slot = EQUIPMENT_SLOT_START, EQUIPMENT_SLOT_END-1 do
  315. local newItem = player:GetItemByPos(-1, slot)
  316. if (newItem) then
  317. if (DeleteFakeEntry(newItem) and not removed) then
  318. removed = true
  319. end
  320. end
  321. end
  322. if (removed) then
  323. player:SendAreaTriggerMessage(LocText(8, player))
  324. -- player:PlayDirectSound(3337)
  325. else
  326. player:SendNotification(LocText(9, player))
  327. end
  328. OnGossipHello(event, player, creature)
  329. elseif sender == EQUIPMENT_SLOT_END+3 then -- Remove Transmogrification from single item
  330. local newItem = player:GetItemByPos(-1, uiAction)
  331. if (newItem) then
  332. if (DeleteFakeEntry(newItem)) then
  333. player:SendAreaTriggerMessage(LocText(10, player):format(GetSlotName(uiAction, player:GetDbcLocale())))
  334. -- player:PlayDirectSound(3337)
  335. else
  336. player:SendNotification(LocText(11, player):format(GetSlotName(uiAction, player:GetDbcLocale())))
  337. end
  338. end
  339. OnGossipSelect(event, player, creature, EQUIPMENT_SLOT_END, uiAction)
  340. else -- Transmogrify
  341. local lowGUID = player:GetGUIDLow()
  342. if(not RequireToken or player:GetItemCount(TokenEntry) >= TokenAmount) then
  343. local oldItem = player:GetItemByPos(-1, sender)
  344. if (oldItem) then
  345. if (_items[lowGUID] and _items[lowGUID][uiAction] and _items[lowGUID][uiAction]) then
  346. local newItem = _items[lowGUID][uiAction]
  347. if (newItem:GetOwnerGUID() == player:GetGUID() and (newItem:IsInBag() or newItem:GetBagSlot() == INVENTORY_SLOT_BAG_0) and SuitableForTransmogrification(player, oldItem, newItem)) then
  348. local price
  349. if(RequireGold == 1) then
  350. price = GetFakePrice(oldItem)*GoldModifier
  351. elseif(RequireGold == 2) then
  352. price = GoldCost
  353. end
  354. if(price) then player:ModifyMoney(-1*price) end
  355. if(RequireToken) then
  356. player:RemoveItem(TokenEntry, TokenAmount)
  357. end
  358. SetFakeEntry(oldItem, newItem:GetEntry())
  359. -- newItem:SetNotRefundable(player)
  360. newItem:SetBinding(1)
  361. -- player:PlayDirectSound(3337)
  362. player:SendAreaTriggerMessage(LocText(12, player):format(GetSlotName(sender, player:GetDbcLocale())))
  363. else
  364. player:SendNotification(LocText(13, player))
  365. end
  366. else
  367. player:SendNotification(LocText(14, player))
  368. end
  369. else
  370. player:SendNotification(LocText(15, player))
  371. end
  372. else
  373. player:SendNotification(LocText(16, player):format(GetItemLink(TokenEntry, player:GetDbcLocale())))
  374. end
  375. _items[lowGUID] = {}
  376. OnGossipSelect(event, player, creature, EQUIPMENT_SLOT_END, sender)
  377. end
  378. end
  379. local function OnLogin(event, player)
  380. local playerGUID = player:GetGUIDLow()
  381. entryMap[playerGUID] = {}
  382. local result = CharDBQuery("SELECT GUID, FakeEntry FROM custom_transmogrification WHERE Owner = "..playerGUID)
  383. if (result) then
  384. repeat
  385. local itemGUID = result:GetUInt32(0)
  386. local fakeEntry = result:GetUInt32(1)
  387. -- if (sObjectMgr:GetItemTemplate(fakeEntry)) then
  388. -- {
  389. dataMap[itemGUID] = playerGUID
  390. entryMap[playerGUID][itemGUID] = fakeEntry
  391. -- }
  392. -- else
  393. -- sLog:outError(LOG_FILTER_SQL, "Item entry (Entry: %u, itemGUID: %u, playerGUID: %u) does not exist, deleting.", fakeEntry, itemGUID, playerGUID)
  394. -- Transmogrification::DeleteFakeFromDB(itemGUID)
  395. -- end
  396. until not result:NextRow()
  397. for slot = EQUIPMENT_SLOT_START, EQUIPMENT_SLOT_END-1 do
  398. local item = player:GetItemByPos(-1, slot)
  399. if (item) then
  400. if(entryMap[playerGUID]) then
  401. if(entryMap[playerGUID][item:GetGUIDLow()]) then
  402. player:UpdateUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (item:GetSlot() * 12), entryMap[playerGUID][item:GetGUIDLow()])
  403. end
  404. end
  405. end
  406. end
  407. end
  408. end
  409. local function OnLogout(event, player)
  410. local pGUID = player:GetGUIDLow()
  411. entryMap[pGUID] = nil
  412. end
  413. local function OnEquip(event, player, item, bag, slot)
  414. local fentry = GetFakeEntry(item)
  415. if (fentry) then
  416. if(item:GetOwnerGUID() ~= player:GetGUID()) then
  417. DeleteFakeFromDB(item:GetGUIDLow())
  418. return
  419. end
  420. player:SetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (slot * 12), fentry)
  421. else
  422. -- player:SetUInt32Value(PLAYER_VISIBLE_ITEM_1_ENTRYID + (slot * 12), pItem:GetEntry())
  423. end
  424. end
  425. -- Note, Query is instant when Execute is delayed
  426. CharDBQuery([[
  427. CREATE TABLE IF NOT EXISTS `custom_transmogrification` (
  428. `GUID` INT(10) UNSIGNED NOT NULL COMMENT 'Item guidLow',
  429. `FakeEntry` INT(10) UNSIGNED NOT NULL COMMENT 'Item entry',
  430. `Owner` INT(10) UNSIGNED NOT NULL COMMENT 'Player guidLow',
  431. PRIMARY KEY (`GUID`)
  432. )
  433. COMMENT='version 4.0'
  434. COLLATE='latin1_swedish_ci'
  435. ENGINE=InnoDB;
  436. ]])
  437. print("Deleting non-existing transmogrification entries...")
  438. CharDBQuery("DELETE FROM custom_transmogrification WHERE NOT EXISTS (SELECT 1 FROM item_instance WHERE item_instance.guid = custom_transmogrification.GUID)")
  439. RegisterPlayerEvent(3, OnLogin)
  440. RegisterPlayerEvent(4, OnLogout)
  441. RegisterPlayerEvent(29, OnEquip)
  442. -- Test code
  443. --RegisterPlayerEvent(18, function(e,p,m,t,l) if(m == "test") then OnGossipHello(e,p,p) end end)
  444. --RegisterPlayerGossipEvent(menu_id, 2, OnGossipSelect)
  445. RegisterCreatureGossipEvent(NPC_Entry, 1, OnGossipHello)
  446. RegisterCreatureGossipEvent(NPC_Entry, 2, OnGossipSelect)
  447. local plrs = GetPlayersInWorld()
  448. if(plrs) then
  449. for k, player in ipairs(plrs) do
  450. OnLogin(k, player)
  451. end
  452. end