You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ItemManager.ts 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import { allItems, _Tiers, Tiers } from "../../Types/Items";
  2. import { Inject, Injectable } from "../../Injector/ServiceDecorator";
  3. import { ItemManagerFeatureIfc, ItemManagerIfc } from "./RPCInterface";
  4. import { FrontworkComponent } from "../../Types/FrontworkComponent";
  5. import { TableDefinitionExporter } from "../../Types/Interfaces";
  6. import { TableDefiniton, Item, User, Character, SRToken, SRPriority, Spec, Signup, Raid, Stats } from "../../Types/Types";
  7. import { IAdmin } from "../../Admin/Interface";
  8. import { IItemManager } from "./Interface";
  9. import { IUserManager } from "../User/Interface";
  10. import { ICharacterManager } from "../Character/Interface";
  11. import { IPubSub } from "../PubSub/Interface";
  12. import { IRaidManager } from "../Raid/Interface";
  13. import { getLogger } from "frontblock-generic/Types";
  14. const fetch = require('node-fetch')
  15. const xml2js = require('xml2js');
  16. const parser = new xml2js.Parser(/* options */);
  17. @Injectable(IItemManager)
  18. export class ItemManager
  19. implements FrontworkComponent<ItemManagerIfc, ItemManagerFeatureIfc>, TableDefinitionExporter, IItemManager {
  20. name = "ItemManager" as "ItemManager";
  21. @Inject(IAdmin)
  22. private admin: IAdmin
  23. @Inject(IUserManager)
  24. private userManager: IUserManager
  25. @Inject(ICharacterManager)
  26. private character: ICharacterManager
  27. @Inject(IPubSub)
  28. private pubsub: IPubSub<any>
  29. @Inject(IRaidManager)
  30. private raidManager: IRaidManager
  31. exportRPCs = () => [
  32. this.getItems,
  33. this.getItem,
  34. this.buyToken,
  35. this.calculatePriorities,
  36. this.getToken,
  37. this.getTokens,
  38. this.getAllPriorities
  39. ]
  40. exportRPCFeatures = () => [{
  41. name: 'managePriorities' as 'managePriorities',
  42. exportRPCs: () => [
  43. this.setPriority,
  44. this.deletePriority
  45. ],
  46. }, {
  47. name: 'reset' as 'reset',
  48. exportRPCs: () => [
  49. this.wipeCurrencyAndItems
  50. ]
  51. }]
  52. notifyRaid = async (raid: Raid | { id: number }) => {
  53. await this.notifyRaids()
  54. const data = await this.raidManager.getRaidData(<Raid>raid)
  55. await this.pubsub.publish("" + raid.id, data)
  56. }
  57. notifyRaids = async () => {
  58. await this.pubsub.publish('raids', undefined)
  59. }
  60. wipeCurrencyAndItems = async () => {
  61. await Promise.all([
  62. this.userManager.wipeCurrency(),
  63. Promise.all(_Tiers.map(tier => this.admin.knex(tier + 'tokens').where(true).del()))
  64. ])
  65. }
  66. getItems = async (): Promise<Item[]> => await this.admin.knex.select('*').from('items')
  67. getItem = async (name: string, tier?:Tiers): Promise<Item | undefined> => {
  68. let item = await this.admin.knex('items').select('*').where('itemname', '=', name).first()
  69. if(!item){
  70. item = await this.fetchItem(name, tier)
  71. if (!item)
  72. return
  73. await this.admin
  74. .knex('items')
  75. .insert({
  76. ...item,
  77. stats: JSON.stringify(item.stats)
  78. })
  79. return this.getItem(name, tier)
  80. }
  81. item.stats = JSON.parse(item.stats)
  82. return item
  83. }
  84. fetchItem = async (name: string, tier?:Tiers): Promise<Item | undefined> => {
  85. const res = await fetch('https://classic.wowhead.com/item=' + name + '&xml');
  86. const txt = await res.text();
  87. const r = await parser.parseStringPromise(txt);
  88. try{
  89. if(!r.wowhead.item || !r.wowhead.item[0])
  90. return
  91. const j = JSON.parse('{'+r.wowhead.item[0].json[0]+"}")
  92. if(!tier){
  93. if(j.sourcemore && Number.isInteger(j.sourcemore[0].t) && j.sourcemore[0].t > 0){
  94. tier = _Tiers[j.sourcemore[0].t-1]
  95. }else{
  96. tier = "MC"
  97. }
  98. }
  99. const stats = JSON.parse('{'+r.wowhead.item[0].jsonEquip[0]+"}")
  100. const item = <Item>{
  101. itemname: r.wowhead.item[0].name[0],
  102. iconname: r.wowhead.item[0].icon[0]._,
  103. url: r.wowhead.item[0].link[0],
  104. quality: r.wowhead.item[0].quality[0]._,
  105. tooltip: r.wowhead.item[0].htmlTooltip[0],
  106. stats: toStats(stats),
  107. tier: tier
  108. }
  109. return item
  110. }catch(e){
  111. getLogger('ItemManager', 'error').error(name, e);
  112. }
  113. }
  114. getTableDefinitions(): TableDefiniton[] {
  115. return [
  116. ...['null', ..._Tiers].map(tier => {
  117. return {
  118. name: tier + 'tokens',
  119. tableBuilder: (table) => {
  120. table.primary(['characterid', 'itemname'])
  121. table.integer("characterid")
  122. table.foreign("characterid").references("id").inTable('characters')
  123. table.string("itemname")
  124. table.foreign("itemname").references("itemname").inTable('items')
  125. table.string("signupid").nullable()
  126. table.foreign("signupid").references("id").inTable('signups').onDelete('SET NULL')
  127. table.integer("level").defaultTo(1)
  128. }
  129. }
  130. }), {
  131. name: 'priorities',
  132. tableBuilder: (table) => {
  133. table.increments('id').primary()
  134. table.string('itemname')
  135. table.foreign('itemname').references('itemname').inTable('items')
  136. table.string('race').nullable()
  137. table.integer('specid').nullable()
  138. table.foreign('specid').references('id').inTable('specs')
  139. table.integer('modifier')
  140. table.string('description').nullable()
  141. }
  142. }, {
  143. name: 'items',
  144. tableBuilder: (table) => {
  145. table.string('itemname').unique().notNullable().primary()
  146. table.string('iconname').notNullable()
  147. table.string('url').notNullable()
  148. table.string('quality').defaultTo('Epic').notNullable()
  149. table.boolean('hidden').defaultTo(false).notNullable()
  150. table.string('tooltip').notNullable()
  151. table.enu('tier', _Tiers).notNullable()
  152. table.json('stats').notNullable()
  153. }
  154. }]
  155. }
  156. buyToken = async (usertoken: string, charactername: string, itemname: string, signup: Signup): Promise<(SRToken & Character & Item) | undefined> => {
  157. const record = this.userManager.getUserRecordByToken(usertoken)
  158. const character = await this.character.getCharacterByName(charactername)
  159. if (!record || !character || record.user.username !== character.username) return
  160. const item = await this.getItem(itemname)
  161. if (!item) return
  162. const currency = await this.userManager.getCurrency(record.user, item.tier)
  163. if (currency < 1) return
  164. const streaks = await this.getTokens(character, [item.tier], false)
  165. const activeTokens = await this.getTokens(character, [item.tier], true)
  166. await this.userManager.decrementCurrency(record.user, item.tier, 1)
  167. const modifier = await this.calculatePriorities(itemname, character)
  168. const tx = await this.admin.knex.transaction()
  169. if (streaks.length > 0) {
  170. const myStreak = streaks.find(token => token.itemname === itemname)
  171. if (myStreak) {
  172. //getLogger('ItemManager').debug('update signupid and increment level')
  173. await this.admin
  174. .knex(item.tier + 'tokens')
  175. .transacting(tx)
  176. .where({
  177. characterid: character.id,
  178. itemname: item.itemname
  179. })
  180. .update({
  181. signupid: signup.id,
  182. level: myStreak.level + 1
  183. })
  184. }
  185. //getLogger('ItemManager').debug('delete streaks')
  186. await Promise.all(streaks.map(async s => await this.admin
  187. .knex(item.tier + 'tokens')
  188. .transacting(tx)
  189. .where({
  190. itemname: s.itemname,
  191. characterid: character.id,
  192. signupid: null
  193. })
  194. .del()
  195. ))
  196. if (myStreak) {
  197. await tx.commit()
  198. await this.notifyRaid({ id: signup.raidid })
  199. return await this.getToken(character, item)
  200. }
  201. }
  202. const matchingReserve = activeTokens.find(token => token.itemname === itemname)
  203. if (matchingReserve) {
  204. //getLogger('ItemManager').debug('upgrade reserve')
  205. await this.admin
  206. .knex(item.tier + 'tokens')
  207. .transacting(tx)
  208. .increment('level')
  209. .where({
  210. signupid: signup.id,
  211. characterid: character.id,
  212. itemname: item.itemname
  213. })
  214. } else {
  215. //getLogger('ItemManager').debug('new reserve')
  216. await this.admin
  217. .knex(item.tier + 'tokens')
  218. .transacting(tx)
  219. .insert({
  220. characterid: character.id,
  221. itemname: item.itemname,
  222. level: 1 + modifier,
  223. signupid: signup.id
  224. })
  225. }
  226. await tx.commit()
  227. await this.notifyRaid({ id: signup.raidid })
  228. return await this.getToken(character, item)
  229. }
  230. getAllPriorities = async (): Promise<(SRPriority & Spec & Item)[]> => this.admin
  231. .knex('priorities as p')
  232. .join('items as i', 'p.itemname', '=', 'i.itemname')
  233. .leftJoin('specs as s', 'p.specid', '=', 's.id')
  234. .select('*')
  235. deletePriority = async (priority: SRPriority): Promise<void> => {
  236. await this.admin.knex('priorities')
  237. .where(priority)
  238. .del()
  239. }
  240. setPriority = async (itemname: string, priority: SRPriority): Promise<void> => {
  241. const item = await this.getItem(itemname)
  242. await this.admin
  243. .knex('priorities')
  244. .insert(<SRPriority>{
  245. itemname: item!.itemname,
  246. ...priority
  247. })
  248. }
  249. getPriorities = async (itemname: string): Promise<SRPriority[]> => {
  250. return await this.admin
  251. .knex('priorities as p')
  252. .where('p.itemname', '=', itemname)
  253. .select('*')
  254. }
  255. calculatePriorities = async (itemname: string, character: Character): Promise<number> => {
  256. const rules: SRPriority[] = await this.admin
  257. .knex('priorities as p')
  258. .select('*')
  259. .join('items as i', 'i.itemname', '=', 'p.itemname')
  260. .where('p.itemname', '=', itemname)
  261. return rules.map(rule => {
  262. if (rule.specid && rule.race) {
  263. if (rule.specid === character.specid && rule.race === character.race)
  264. return rule.modifier
  265. else
  266. return 0
  267. }
  268. if (rule.specid) {
  269. if (rule.specid === character.specid)
  270. return rule.modifier
  271. else
  272. return 0
  273. }
  274. if (rule.race) {
  275. if (rule.race === character.race)
  276. return rule.modifier
  277. else
  278. return 0
  279. }
  280. return 0
  281. }).reduce((prev, curr) => prev + curr, 0)
  282. }
  283. getToken = async (character: Character, item: Item, valid = true): Promise<(SRToken & Character & Item) | undefined> => {
  284. return await this.admin
  285. .knex(item.tier + 'tokens as t')
  286. .select('*')
  287. .join('characters as c', 'c.id', '=', 't.characterid')
  288. .join('items as i', 'i.itemname', '=', 't.itemname')
  289. .where({
  290. characterid: character.id,
  291. "i.itemname": item.itemname
  292. })
  293. .andWhere(function () {
  294. if (valid) {
  295. this.whereNotNull('t.signupid')
  296. } else {
  297. this.whereNull('t.signupid')
  298. }
  299. })
  300. .first()
  301. }
  302. getTokens = async (character: Character, tiers: Tiers[], valid = true): Promise<(SRToken & Character & Item)[]> => {
  303. const ret = await Promise.all(tiers.map(async tier => {
  304. return await this.admin
  305. .knex(tier + 'tokens as t')
  306. .select('*')
  307. .join('characters as c', 'c.id', '=', 't.characterid')
  308. .join('items as i', 'i.itemname', '=', 't.itemname')
  309. .where({
  310. characterid: character.id,
  311. })
  312. .andWhere(function () {
  313. if (valid) {
  314. this.whereNotNull('t.signupid')
  315. } else {
  316. this.whereNull('t.signupid')
  317. }
  318. })
  319. }))
  320. return ret.flat()
  321. }
  322. countItems = async (): Promise<number> => {
  323. const count = await this.admin
  324. .knex('items')
  325. .count('*');
  326. return <number>count[0]['count(*)']
  327. }
  328. private initialized = false
  329. initialize = async () => {
  330. if (this.initialized) return
  331. this.initialized = true
  332. const itemTiers = allItems
  333. const countCache = await this.countItems()
  334. if (countCache != Object.values(itemTiers).flat().length) {
  335. await Promise.all(
  336. Object.entries(itemTiers)
  337. .map((kv: [string, string[]]) => Promise.all(
  338. kv[1].map(i => this.getItem(i, kv[0] as Tiers) )
  339. ))
  340. )
  341. }
  342. }
  343. }
  344. function toStats(obj:any){
  345. if(!obj) obj = {}
  346. return {
  347. stamina: obj.sta || 0,
  348. agility: obj.agi || 0,
  349. strength: obj.str || 0,
  350. intellect: obj.int || 0,
  351. spirit: obj.spi || 0,
  352. attackpower: obj.atkpwr || 0,
  353. meleehit: obj.mlehitpct || 0,
  354. meleecrit: obj.mlecritstrkpct || 0,
  355. rangeattackpower: obj.rgdatkpwr || 0,
  356. rangehit: obj.rgdhitpct || 0,
  357. rangecrit: obj.rgdcritstrkpct || 0,
  358. spellpower: obj.splpwr || 0,
  359. spellhit: obj.splhitpct || 0,
  360. spellcrit: obj.splcritstrkpct || 0,
  361. heal: obj.splheal || 0,
  362. frostspellpower: obj.frosplpwr || 0,
  363. firespellpower: obj.firsplpwr || 0,
  364. naturespellpower: obj.natsplpwr || 0,
  365. shadowspellpower: obj.shasplpwr || 0,
  366. arcanespellpower: obj.arcsplpwr || 0,
  367. fireresist: obj.firres || 0,
  368. arcaneresist: obj.arcres || 0,
  369. frostresist: obj.frores || 0,
  370. natureresist: obj.natres || 0,
  371. shadowresist: obj.shares || 0,
  372. armor: obj.armor || 0,
  373. defense: obj.def || 0,
  374. block: obj.blockpct || 0,
  375. blockvalue: obj.blockamount || 0,
  376. dodge: obj.dodgepct || 0,
  377. parry: obj.parrypct || 0,
  378. } as Stats
  379. }