Type::ITEM 'i' => [['is', 'src', 'ic'], 'o' => 'i.quality DESC, i.itemLevel DESC'], 'ic' => ['j' => ['?_icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.name AS iconString'], 'is' => ['j' => ['?_item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'], 's' => ['j' => ['?_spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.id'], 'e' => ['j' => ['?_events `e` ON `e`.`id` = `i`.`eventId`', true], 's' => ', e.holidayId'], 'src' => ['j' => ['?_source `src` ON `src`.`type` = 3 AND `src`.`typeId` = `i`.`id`', true], 's' => ', moreType, moreTypeId, moreZoneId, moreMask, src1, src2, src3, src4, src5, src6, src7, src8, src9, src10, src11, src12, src13, src14, src15, src16, src17, src18, src19, src20, src21, src22, src23, src24'] ); public function __construct(array $conditions = [], array $miscData = []) { parent::__construct($conditions, $miscData); foreach ($this->iterate() as &$_curTpl) { // item is scaling; overwrite other values if ($_curTpl['scalingStatDistribution'] > 0 && $_curTpl['scalingStatValue'] > 0) $this->initScalingStats(); // fix missing icons $_curTpl['iconString'] = $_curTpl['iconString'] ?: DEFAULT_ICON; // from json to json .. the gentle fuckups of legacy code integration $this->initJsonStats(); $this->jsonStats[$this->id] = (new StatsContainer())->fromJson($_curTpl, true)->toJson(Stat::FLAG_ITEM /* | Stat::FLAG_SERVERSIDE */); if ($miscData) { // readdress itemset .. is wrong for virtual sets if (isset($miscData['pcsToSet']) && isset($miscData['pcsToSet'][$this->id])) $this->json[$this->id]['itemset'] = $miscData['pcsToSet'][$this->id]; // additional rel attribute for listview rows if (isset($miscData['extraOpts']['relEnchant'])) $this->relEnchant = $miscData['extraOpts']['relEnchant']; } // unify those pesky masks $_ = &$_curTpl['requiredClass']; $_ &= ChrClass::MASK_ALL; if ($_ < 0 || $_ == ChrClass::MASK_ALL) $_ = 0; unset($_); $_ = &$_curTpl['requiredRace']; $_ &= ChrRace::MASK_ALL; if ($_ < 0 || $_ == ChrRace::MASK_ALL) $_ = 0; unset($_); // sources for ($i = 1; $i < 25; $i++) { if ($_ = $_curTpl['src'.$i]) $this->sources[$this->id][$i][] = $_; unset($_curTpl['src'.$i]); } } } // use if you JUST need the name public static function getName($id) { $n = DB::Aowow()->selectRow('SELECT name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 FROM ?_items WHERE id = ?d', $id); return Util::localizedString($n, 'name'); } // todo (med): information will get lost if one vendor sells one item multiple times with different costs (e.g. for item 54637) // wowhead seems to have had the same issues public function getExtendedCost($filter = [], &$reqRating = []) { if ($this->error) return []; $idx = $this->id; if (empty($this->vendors)) { $itemz = []; $xCostData = []; $rawEntries = DB::World()->select( 'SELECT nv.`item`, nv.`entry`, 0 AS "eventId", nv.`maxcount`, nv.`extendedCost`, nv.`incrtime` FROM npc_vendor nv WHERE { nv.`entry` IN (?a) AND } nv.`item` IN (?a) UNION SELECT nv2.`item`, nv1.`entry`, 0 AS "eventId", nv2.`maxcount`, nv2.`extendedCost`, nv2.`incrtime` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`item` = nv2.`entry` { AND nv1.`entry` IN (?a) } WHERE nv2.`item` IN (?a) UNION SELECT genv.`item`, c.`id` AS "entry", ge.`eventEntry` AS "eventId", genv.`maxcount`, genv.`extendedCost`, genv.`incrtime` FROM game_event_npc_vendor genv LEFT JOIN game_event ge ON genv.`eventEntry` = ge.`eventEntry` JOIN creature c ON c.`guid` = genv.`guid` WHERE { c.`id` IN (?a) AND } genv.`item` IN (?a)', empty($filter[Type::NPC]) || !is_array($filter[Type::NPC]) ? DBSIMPLE_SKIP : $filter[Type::NPC], array_keys($this->templates), empty($filter[Type::NPC]) || !is_array($filter[Type::NPC]) ? DBSIMPLE_SKIP : $filter[Type::NPC], array_keys($this->templates), empty($filter[Type::NPC]) || !is_array($filter[Type::NPC]) ? DBSIMPLE_SKIP : $filter[Type::NPC], array_keys($this->templates) ); foreach ($rawEntries as $costEntry) { if ($costEntry['extendedCost']) $xCostData[] = $costEntry['extendedCost']; if (!isset($itemz[$costEntry['item']][$costEntry['entry']])) $itemz[$costEntry['item']][$costEntry['entry']] = [$costEntry]; else $itemz[$costEntry['item']][$costEntry['entry']][] = $costEntry; } if ($xCostData) $xCostData = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemextendedcost WHERE id IN (?a)', $xCostData); $cItems = []; foreach ($itemz as $k => $vendors) { foreach ($vendors as $l => $vendor) { foreach ($vendor as $m => $vInfo) { $costs = []; if (!empty($xCostData[$vInfo['extendedCost']])) $costs = $xCostData[$vInfo['extendedCost']]; $data = array( 'stock' => $vInfo['maxcount'] ?: -1, 'event' => $vInfo['eventId'], 'restock' => $vInfo['incrtime'], 'reqRating' => $costs ? $costs['reqPersonalRating'] : 0, 'reqBracket' => $costs ? $costs['reqArenaSlot'] : 0 ); // hardcode arena(103) & honor(104) if (!empty($costs['reqArenaPoints'])) { $data[-103] = $costs['reqArenaPoints']; $this->jsGlobals[Type::CURRENCY][103] = 103; } if (!empty($costs['reqHonorPoints'])) { $data[-104] = $costs['reqHonorPoints']; $this->jsGlobals[Type::CURRENCY][104] = 104; } for ($i = 1; $i < 6; $i++) { if (!empty($costs['reqItemId'.$i]) && $costs['itemCount'.$i] > 0) { $data[$costs['reqItemId'.$i]] = $costs['itemCount'.$i]; $cItems[] = $costs['reqItemId'.$i]; } } // no extended cost or additional gold required if (!$costs || $this->getField('flagsExtra') & 0x04) { $this->getEntry($k); if ($_ = $this->getField('buyPrice')) $data[0] = $_; } $vendor[$m] = $data; } $vendors[$l] = $vendor; } $itemz[$k] = $vendors; } // convert items to currency if possible if ($cItems) { $moneyItems = new CurrencyList(array(['itemId', $cItems])); foreach ($moneyItems->getJSGlobals() as $type => $jsData) foreach ($jsData as $k => $v) $this->jsGlobals[$type][$k] = $v; foreach ($itemz as $itemId => $vendors) { foreach ($vendors as $npcId => $costData) { foreach ($costData as $itr => $cost) { foreach ($cost as $k => $v) { if (in_array($k, $cItems)) { $found = false; foreach ($moneyItems->iterate() as $__) { if ($moneyItems->getField('itemId') == $k) { unset($cost[$k]); $cost[-$moneyItems->id] = $v; $found = true; break; } } if (!$found) $this->jsGlobals[Type::ITEM][$k] = $k; } } $costData[$itr] = $cost; } $vendors[$npcId] = $costData; } $itemz[$itemId] = $vendors; } } $this->vendors = $itemz; } $result = $this->vendors; // apply filter if given $tok = !empty($filter[Type::ITEM]) ? $filter[Type::ITEM] : null; $cur = !empty($filter[Type::CURRENCY]) ? $filter[Type::CURRENCY] : null; foreach ($result as $itemId => &$data) { $reqRating = []; foreach ($data as $npcId => $entries) { foreach ($entries as $costs) { if ($tok || $cur) // bought with specific token or currency { $valid = false; foreach ($costs as $k => $qty) { if ((!$tok || $k == $tok) && (!$cur || $k == -$cur)) { $valid = true; break; } } if (!$valid) unset($data[$npcId]); } // reqRating ins't really a cost .. so pass it by ref instead of return // data was invalid and deleted or some source doesn't require arena rating if (!isset($data[$npcId]) || ($reqRating && !$reqRating[0])) continue; // use lowest total value if (!$costs['reqRating']) $reqRating = [0, 2]; else if ($costs['reqRating'] && (!$reqRating || $reqRating[0] > $costs['reqRating'])) $reqRating = [$costs['reqRating'], $costs['reqBracket']]; } } if (empty($data)) unset($result[$itemId]); } // restore internal index; $this->getEntry($idx); return $result; } public function getListviewData($addInfoMask = 0x0, $miscData = null) { /* * ITEMINFO_JSON (0x01): jsonStats (including spells) and subitems parsed * ITEMINFO_SUBITEMS (0x02): searched by comparison * ITEMINFO_VENDOR (0x04): costs-obj, when displayed as vendor * ITEMINFO_GEM (0x10): gem infos and score * ITEMINFO_MODEL (0x20): sameModelAs-Tab */ $data = []; // random item is random if ($addInfoMask & ITEMINFO_SUBITEMS) $this->initSubItems(); if ($addInfoMask & ITEMINFO_JSON) { $this->extendJsonStats(); Util::arraySumByKey($data, $this->jsonStats); } $extCosts = []; if ($addInfoMask & ITEMINFO_VENDOR) $extCosts = $this->getExtendedCost($miscData); $extCostOther = []; foreach ($this->iterate() as $__) { foreach ($this->json[$this->id] as $k => $v) $data[$this->id][$k] = $v; // json vs listview quirk $data[$this->id]['name'] = $data[$this->id]['quality'].Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW); unset($data[$this->id]['quality']); if (!empty($this->relEnchant) && $this->curTpl['randomEnchant']) { if (($x = array_search($this->curTpl['randomEnchant'], array_column($this->relEnchant, 'entry'))) !== false) { $data[$this->id]['rel'] = 'rand='.$this->relEnchant[$x]['ench']; $data[$this->id]['name'] .= ' '.$this->relEnchant[$x]['name']; } } if ($addInfoMask & ITEMINFO_JSON) { if ($_ = intVal(($this->curTpl['minMoneyLoot'] + $this->curTpl['maxMoneyLoot']) / 2)) $data[$this->id]['avgmoney'] = $_; if ($_ = $this->curTpl['repairPrice']) $data[$this->id]['repaircost'] = $_; } if ($addInfoMask & (ITEMINFO_JSON | ITEMINFO_GEM)) if (isset($this->curTpl['score'])) $data[$this->id]['score'] = $this->curTpl['score']; if ($addInfoMask & ITEMINFO_GEM) { $data[$this->id]['uniqEquip'] = ($this->curTpl['flags'] & ITEM_FLAG_UNIQUEEQUIPPED) ? 1 : 0; $data[$this->id]['socketLevel'] = 0; // not used with wotlk } if ($addInfoMask & ITEMINFO_VENDOR) { // just use the first results // todo (med): dont use first vendor; search for the right one if (!empty($extCosts[$this->id])) { $cost = reset($extCosts[$this->id]); foreach ($cost as $itr => $entries) { $currency = []; $tokens = []; $costArr = []; foreach ($entries as $k => $qty) { if (is_string($k)) continue; if ($k > 0) $tokens[] = [$k, $qty]; else if ($k < 0) $currency[] = [-$k, $qty]; } $costArr['stock'] = $entries['stock'];// display as column in lv $costArr['avail'] = $entries['stock'];// display as number on icon $costArr['cost'] = [empty($entries[0]) ? 0 : $entries[0]]; $costArr['restock'] = $entries['restock']; if ($entries['event']) if (Conditions::extendListviewRow($costArr, Conditions::SRC_NONE, $this->id, [Conditions::ACTIVE_EVENT, $entries['event']])) $this->jsGlobals[Type::WORLDEVENT][$entries['event']] = $entries['event']; if ($currency || $tokens) // fill idx:3 if required $costArr['cost'][] = $currency; if ($tokens) $costArr['cost'][] = $tokens; if (!empty($entries['reqRating'])) $costArr['reqarenartng'] = $entries['reqRating']; if ($itr > 0) $extCostOther[$this->id][] = $costArr; else $data[$this->id] = array_merge($data[$this->id], $costArr); } } if ($x = $this->curTpl['buyPrice']) $data[$this->id]['buyprice'] = $x; if ($x = $this->curTpl['sellPrice']) $data[$this->id]['sellprice'] = $x; if ($x = $this->curTpl['buyCount']) $data[$this->id]['stack'] = $x; } if ($this->curTpl['class'] == ITEM_CLASS_GLYPH) $data[$this->id]['glyph'] = $this->curTpl['subSubClass']; if ($x = $this->curTpl['requiredSkill']) $data[$this->id]['reqskill'] = $x; if ($x = $this->curTpl['requiredSkillRank']) $data[$this->id]['reqskillrank'] = $x; if ($x = $this->curTpl['requiredSpell']) $data[$this->id]['reqspell'] = $x; if ($x = $this->curTpl['requiredFaction']) $data[$this->id]['reqfaction'] = $x; if ($x = $this->curTpl['requiredFactionRank']) { $data[$this->id]['reqrep'] = $x; $data[$this->id]['standing'] = $x; // used in /faction item-listing } if ($x = $this->curTpl['slots']) $data[$this->id]['nslots'] = $x; $_ = $this->curTpl['requiredRace']; if ($_ && $_ & ChrRace::MASK_ALLIANCE != ChrRace::MASK_ALLIANCE && $_ & ChrRace::MASK_HORDE != ChrRace::MASK_HORDE) $data[$this->id]['reqrace'] = $_; if ($_ = $this->curTpl['requiredClass']) $data[$this->id]['reqclass'] = $_; // $data[$this->id]['classes'] ?? if ($this->curTpl['flags'] & ITEM_FLAG_HEROIC) $data[$this->id]['heroic'] = true; if ($addInfoMask & ITEMINFO_MODEL) if ($_ = $this->getField('displayId')) $data[$this->id]['displayid'] = $_; if ($this->getSources($s, $sm)) { $data[$this->id]['source'] = $s; if ($sm) $data[$this->id]['sourcemore'] = $sm; } if (!empty($this->curTpl['cooldown'])) $data[$this->id]['cooldown'] = $this->curTpl['cooldown'] / 1000; } foreach ($extCostOther as $itemId => $duplicates) foreach ($duplicates as $d) $data[] = array_merge($data[$itemId], $d); // we dont really use keys on data, but this may cause errors in future /* even more complicated crap modelviewer {type:X, displayid:Y, slot:z} .. not sure, when to set */ return $data; } public function getJSGlobals($addMask = GLOBALINFO_SELF, &$extra = []) { $data = $addMask & GLOBALINFO_RELATED ? $this->jsGlobals : []; foreach ($this->iterate() as $id => $__) { if ($addMask & GLOBALINFO_SELF) { $data[Type::ITEM][$id] = array( 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), 'quality' => $this->curTpl['quality'], 'icon' => $this->curTpl['iconString'] ); } if ($addMask & GLOBALINFO_EXTRA) { $extra[$id] = array( 'id' => $id, 'tooltip' => $this->renderTooltip(true), 'spells' => new \StdClass // placeholder for knownSpells ); } } return $data; } /* enhance (set by comparison tool or formated external links) ench: enchantmentId sock: bool (extraScoket (gloves, belt)) gems: array (:-separated itemIds) rand: >0: randomPropId; <0: randomSuffixId interactive (set to place javascript/anchors to manipulate level and ratings or link to filters (static tooltips vs popup tooltip)) subOf (tabled layout doesn't work if used as sub-tooltip in other item or spell tooltips; use line-break instead) */ public function getField($field, $localized = false, $silent = false, $enhance = []) { $res = parent::getField($field, $localized, $silent); if ($field == 'name' && !empty($enhance['r'])) if ($this->getRandEnchantForItem($enhance['r'])) $res .= ' '.Util::localizedString($this->enhanceR, 'name'); return $res; } public function renderTooltip($interactive = false, $subOf = 0, $enhance = []) { if ($this->error) return; $_name = Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_HTML); $_reqLvl = $this->curTpl['requiredLevel']; $_quality = $this->curTpl['quality']; $_flags = $this->curTpl['flags']; $_class = $this->curTpl['class']; $_subClass = $this->curTpl['subClass']; $_slot = $this->curTpl['slot']; $causesScaling = false; if (!empty($enhance['r'])) { if ($this->getRandEnchantForItem($enhance['r'])) { $_name .= ' '.Util::localizedString($this->enhanceR, 'name'); $randEnchant = ''; for ($i = 1; $i < 6; $i++) { if ($this->enhanceR['enchantId'.$i] <= 0) continue; $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?d', $this->enhanceR['enchantId'.$i]); if ($this->enhanceR['allocationPct'.$i] > 0) { $amount = intVal($this->enhanceR['allocationPct'.$i] * $this->generateEnchSuffixFactor()); $randEnchant .= ''.str_replace('$i', $amount, Util::localizedString($enchant, 'name')).'
'; } else $randEnchant .= ''.Util::localizedString($enchant, 'name').'
'; } } else unset($enhance['r']); } if (isset($enhance['s']) && !in_array($_slot, [INVTYPE_WRISTS, INVTYPE_WAIST, INVTYPE_HANDS])) unset($enhance['s']); // IMPORTAT: DO NOT REMOVE THE HTML-COMMENTS! THEY ARE REQUIRED TO UPDATE THE TOOLTIP CLIENTSIDE $x = ''; // upper table: stats if (!$subOf) $x .= '
'; // name; quality if ($subOf) $x .= ''.$_name.''; else $x .= ''.$_name.''; // heroic tag if (($_flags & ITEM_FLAG_HEROIC) && $_quality == ITEM_QUALITY_EPIC) $x .= '
'.Lang::item('heroic').''; // requires map (todo: reparse ?_zones for non-conflicting data; generate Link to zone) if ($_ = $this->curTpl['map']) { $map = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE mapId = ?d LIMIT 1', $_); $x .= '
'.Util::localizedString($map, 'name').''; } // requires area if ($this->curTpl['area']) { $area = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE Id=?d LIMIT 1', $this->curTpl['area']); $x .= '
'.Util::localizedString($area, 'name'); } // conjured if ($_flags & ITEM_FLAG_CONJURED) $x .= '
'.Lang::item('conjured'); // bonding if ($_flags & ITEM_FLAG_ACCOUNTBOUND) $x .= '
'.Lang::item('bonding', 0); else if ($this->curTpl['bonding']) $x .= '
'.Lang::item('bonding', $this->curTpl['bonding']); // unique || unique-equipped || unique-limited if ($this->curTpl['maxCount'] == 1) $x .= '
'.Lang::item('unique', 0); // not for currency tokens else if ($this->curTpl['maxCount'] && $this->curTpl['bagFamily'] != 8192) $x .= '
'.sprintf(Lang::item('unique', 1), $this->curTpl['maxCount']); else if ($_flags & ITEM_FLAG_UNIQUEEQUIPPED) $x .= '
'.Lang::item('uniqueEquipped', 0); else if ($this->curTpl['itemLimitCategory']) { $limit = DB::Aowow()->selectRow("SELECT * FROM ?_itemlimitcategory WHERE id = ?", $this->curTpl['itemLimitCategory']); $x .= '
'.sprintf(Lang::item($limit['isGem'] ? 'uniqueEquipped' : 'unique', 2), Util::localizedString($limit, 'name'), $limit['count']); } // required holiday if ($eId = $this->curTpl['eventId']) if ($hName = DB::Aowow()->selectRow('SELECT h.* FROM ?_holidays h JOIN ?_events e ON e.holidayId = h.id WHERE e.id = ?d', $eId)) $x .= '
'.sprintf(Lang::game('requires'), ''.Util::localizedString($hName, 'name').''); // item begins a quest if ($this->curTpl['startQuest']) $x .= '
'.Lang::item('startQuest').''; // containerType (slotCount) if ($this->curTpl['slots'] > 0) { $fam = $this->curTpl['bagFamily'] ? log($this->curTpl['bagFamily'], 2) + 1 : 0; $x .= '
'.Lang::item('bagSlotString', [$this->curTpl['slots'], Lang::item('bagFamily', $fam)]); } if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON, ITEM_CLASS_AMMUNITION])) { $x .= ''; // Class if ($_slot) $x .= ''; // Subclass if ($_class == ITEM_CLASS_ARMOR && $_subClass > 0) $x .= ''; else if ($_class == ITEM_CLASS_WEAPON) $x .= ''; else if ($_class == ITEM_CLASS_AMMUNITION) $x .= ''; $x .= '
'.Lang::item('inventoryType', $_slot).''.Lang::item('armorSubClass', $_subClass).''.Lang::item('weaponSubClass', $_subClass).''.Lang::item('projectileSubClass', $_subClass).'
'; } else if ($_slot && $_class != ITEM_CLASS_CONTAINER) // yes, slot can occur on random items and is then also displayed <_< .. excluding Bags >_> $x .= '
'.Lang::item('inventoryType', $_slot).'
'; else $x .= '
'; // Weapon/Ammunition Stats (not limited to weapons (see item:1700)) $speed = $this->curTpl['delay'] / 1000; $sc1 = $this->curTpl['dmgType1']; $sc2 = $this->curTpl['dmgType2']; $dmgmin = $this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2']; $dmgmax = $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']; $dps = $speed ? ($dmgmin + $dmgmax) / (2 * $speed) : 0; if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin && $dmgmax) { if ($sc1) $x .= sprintf(Lang::item('damage', 'ammo', 1), ($dmgmin + $dmgmax) / 2, Lang::game('sc', $sc1)).'
'; else $x .= sprintf(Lang::item('damage', 'ammo', 0), ($dmgmin + $dmgmax) / 2).'
'; } else if ($dps) { if ($this->curTpl['tplDmgMin1'] == $this->curTpl['tplDmgMax1']) $dmg = sprintf(Lang::item('damage', 'single', $sc1 ? 1 : 0), $this->curTpl['tplDmgMin1'], $sc1 ? Lang::game('sc', $sc1) : null); else $dmg = sprintf(Lang::item('damage', 'range', $sc1 ? 1 : 0), $this->curTpl['tplDmgMin1'], $this->curTpl['tplDmgMax1'], $sc1 ? Lang::game('sc', $sc1) : null); if ($_class == ITEM_CLASS_WEAPON) // do not use localized format here! $x .= '
'.$dmg.''.Lang::item('speed').' '.number_format($speed, 2).'
'; else $x .= ''.$dmg.'
'; // secondary damage is set if (($this->curTpl['dmgMin2'] || $this->curTpl['dmgMax2']) && $this->curTpl['dmgMin2'] != $this->curTpl['dmgMax2']) $x .= sprintf(Lang::item('damage', 'range', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $this->curTpl['dmgMax2'], $sc2 ? Lang::game('sc', $sc2) : null).'
'; else if ($this->curTpl['dmgMin2']) $x .= sprintf(Lang::item('damage', 'single', $sc2 ? 3 : 2), $this->curTpl['dmgMin2'], $sc2 ? Lang::game('sc', $sc2) : null).'
'; if ($_class == ITEM_CLASS_WEAPON) $x .= ''.Lang::item('dps', [$dps]).'
'; // display FeralAttackPower if set if ($fap = $this->getFeralAP()) $x .= '('.$fap.' '.Lang::item('fap').')
'; } // Armor if ($_class == ITEM_CLASS_ARMOR && $this->curTpl['armorDamageModifier'] > 0) { $spanI = 'class="q2"'; if ($interactive) $spanI = 'class="q2 tip" onmouseover="$WH.Tooltip.showAtCursor(event, $WH.sprintf(LANG.tooltip_armorbonus, '.$this->curTpl['armorDamageModifier'].'), 0, 0, \'q\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"'; $x .= ''.Lang::item('armor', [$this->curTpl['tplArmor']]).'
'; } else if ($this->curTpl['tplArmor']) $x .= ''.Lang::item('armor', [$this->curTpl['tplArmor']]).'
'; // Block (note: block value from field block and from field stats or parsed from itemSpells are displayed independently) if ($this->curTpl['tplBlock']) $x .= ''.sprintf(Lang::item('block'), $this->curTpl['tplBlock']).'
'; // Item is a gem (don't mix with sockets) if ($geId = $this->curTpl['gemEnchantmentId']) { $gemEnch = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?d', $geId); $x .= ''.Util::localizedString($gemEnch, 'name').'
'; // activation conditions for meta gems if (!empty($gemEnch['conditionId'])) { if ($gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE id = ?d', $gemEnch['conditionId'])) { for ($i = 1; $i < 6; $i++) { if (!$gemCnd['color'.$i]) continue; $vspfArgs = []; switch ($gemCnd['comparator'.$i]) { case 2: // requires less than ( || ) gems case 5: // requires at least than ( || ) gems $vspfArgs = [$gemCnd['value'.$i], Lang::item('gemColors', $gemCnd['color'.$i] - 1)]; break; case 3: // requires more than ( || ) gems $vspfArgs = [Lang::item('gemColors', $gemCnd['color'.$i] - 1), Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1)]; break; default: continue 2; } $x .= ''.Lang::achievement('reqNumCrt').' '.Lang::item('gemConditions', $gemCnd['comparator'.$i], $vspfArgs).'
'; } } } } // Random Enchantment - if random enchantment is set, prepend stats from it if ($this->curTpl['randomEnchant'] && empty($enhance['r'])) $x .= ''.Lang::item('randEnchant').'
'; else if (!empty($enhance['r'])) $x .= $randEnchant; // itemMods (display stats and save ratings for later use) for ($j = 1; $j <= 10; $j++) { $type = $this->curTpl['statType'.$j]; $qty = $this->curTpl['statValue'.$j]; if (!$qty || $type <= 0) continue; $statId = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $type); // base stat switch ($statId) { case Stat::MANA: case Stat::HEALTH: case Stat::AGILITY: case Stat::STRENGTH: case Stat::INTELLECT: case Stat::SPIRIT: case Stat::STAMINA: $x .= ''.Lang::item('statType', $type, [ord($qty > 0 ? '+' : '-'), abs($qty)]).'
'; break; default: // rating with % for reqLevel $green[] = $this->formatRating($statId, $type, $qty, $interactive, $causesScaling); } } // magic resistances foreach (Game::$resistanceFields as $j => $rowName) if ($rowName && $this->curTpl[$rowName] != 0) $x .= '+'.$this->curTpl[$rowName].' '.Lang::game('resistances', $j).'
'; // Enchantment if (isset($enhance['e'])) { if ($enchText = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?', $enhance['e'])) $x .= ''.Util::localizedString($enchText, 'name').'
'; else { unset($enhance['e']); $x .= ''; } } else // enchantment placeholder $x .= ''; // Sockets w/ Gems if (!empty($enhance['g'])) { $gems = DB::Aowow()->select(' SELECT it.id AS ARRAY_KEY, ic.name AS iconString, ae.*, it.gemColorMask AS colorMask FROM ?_items it JOIN ?_itemenchantment ae ON ae.id = it.gemEnchantmentId JOIN ?_icons ic ON ic.id = it.iconId WHERE it.id IN (?a)', $enhance['g']); foreach ($enhance['g'] as $k => $v) if ($v && !in_array($v, array_keys($gems))) // 0 is valid unset($enhance['g'][$k]); } else $enhance['g'] = []; // zero fill empty sockets $sockCount = isset($enhance['s']) ? 1 : 0; if (!empty($this->json[$this->id]['nsockets'])) $sockCount += $this->json[$this->id]['nsockets']; while ($sockCount > count($enhance['g'])) $enhance['g'][] = 0; $enhance['g'] = array_reverse($enhance['g']); $hasMatch = 1; // fill native sockets for ($j = 1; $j <= 3; $j++) { if (!$this->curTpl['socketColor'.$j]) continue; for ($i = 0; $i < 4; $i++) if (($this->curTpl['socketColor'.$j] & (1 << $i))) $colorId = $i; $pop = array_pop($enhance['g']); $col = $pop ? 1 : 0; $hasMatch &= $pop ? (($gems[$pop]['colorMask'] & (1 << $colorId)) ? 1 : 0) : 0; $icon = $pop ? sprintf('style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', Cfg::get('STATIC_URL'), strtolower($gems[$pop]['iconString'])) : null; $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', $colorId); if ($interactive) $x .= ''.$text.'
'; else $x .= ''.$text.'
'; } // fill extra socket if (isset($enhance['s'])) { $pop = array_pop($enhance['g']); $col = $pop ? 1 : 0; $icon = $pop ? sprintf('style="background-image: url(%s/images/wow/icons/tiny/%s.gif)"', Cfg::get('STATIC_URL'), strtolower($gems[$pop]['iconString'])) : null; $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', -1); if ($interactive) $x .= ''.$text.'
'; else $x .= ''.$text.'
'; } else // prismatic socket placeholder $x .= ''; if ($_ = $this->curTpl['socketBonus']) { $sbonus = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?d', $_); $x .= ''.Lang::item('socketBonus', [''.Util::localizedString($sbonus, 'name').'']).'
'; } // durability if ($dur = $this->curTpl['durability']) $x .= sprintf(Lang::item('durability'), $dur, $dur).'
'; // max duration if ($dur = $this->curTpl['duration']) { $rt = ''; if ($this->curTpl['flagsCustom'] & 0x1) $rt = $interactive ? ' ('.sprintf(Util::$dfnString, 'LANG.tooltip_realduration', Lang::item('realTime')).')' : ' ('.Lang::item('realTime').')'; $x .= Lang::formatTime(abs($dur) * 1000, 'item', 'duration').$rt."
"; } $jsg = []; // required classes if ($classes = Lang::getClassString($this->curTpl['requiredClass'], $jsg)) { foreach ($jsg as $js) if (empty($this->jsGlobals[Type::CHR_CLASS][$js])) $this->jsGlobals[Type::CHR_CLASS][$js] = $js; $x .= Lang::game('classes').Lang::main('colon').$classes.'
'; } // required races if ($races = Lang::getRaceString($this->curTpl['requiredRace'], $jsg)) { foreach ($jsg as $js) if (empty($this->jsGlobals[Type::CHR_RACE][$js])) $this->jsGlobals[Type::CHR_RACE][$js] = $js; $x .= Lang::game('races').Lang::main('colon').$races.'
'; } // required honorRank (not used anymore) if ($rhr = $this->curTpl['requiredHonorRank']) $x .= Lang::game('requires', [implode(' / ', Lang::game('pvpRank', $rhr))]).'
'; // required CityRank..? // what the f.. // required level if (($_flags & ITEM_FLAG_ACCOUNTBOUND) && $_quality == ITEM_QUALITY_HEIRLOOM) $x .= sprintf(Lang::item('reqLevelRange'), 1, MAX_LEVEL, ($interactive ? sprintf(Util::$changeLevelString, MAX_LEVEL) : ''.MAX_LEVEL)).'
'; else if ($_reqLvl > 1) $x .= sprintf(Lang::item('reqMinLevel'), $_reqLvl).'
'; // required arena team rating / personal rating / todo (low): sort out what kind of rating if (!empty($this->getExtendedCost([], $reqRating)[$this->id]) && $reqRating && $reqRating[0]) $x .= sprintf(Lang::item('reqRating', $reqRating[1]), $reqRating[0]).'
'; // item level if (in_array($_class, [ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON])) $x .= sprintf(Lang::item('itemLevel'), $this->curTpl['itemLevel']).'
'; // required skill if ($reqSkill = $this->curTpl['requiredSkill']) { $_ = ''.SkillList::getName($reqSkill).''; if ($this->curTpl['requiredSkillRank'] > 0) $_ .= ' ('.$this->curTpl['requiredSkillRank'].')'; $x .= sprintf(Lang::game('requires'), $_).'
'; } // required spell if ($reqSpell = $this->curTpl['requiredSpell']) $x .= Lang::game('requires2').' '.SpellList::getName($reqSpell).'
'; // required reputation w/ faction if ($reqFac = $this->curTpl['requiredFaction']) $x .= sprintf(Lang::game('requires'), ''.FactionList::getName($reqFac).' - '.Lang::game('rep', $this->curTpl['requiredFactionRank'])).'
'; // locked or openable if ($locks = Lang::getLocks($this->curTpl['lockId'], $arr, true)) $x .= ''.Lang::item('locked').'
'.implode('
', array_map(function($x) { return sprintf(Lang::game('requires'), $x); }, $locks)).'

'; else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE) $x .= ''.Lang::item('openClick').'
'; // upper table: done if (!$subOf) $x .= '
'; // spells on item if (!$this->canTeachSpell()) { $itemSpellsAndTrigger = []; for ($j = 1; $j <= 5; $j++) { if ($this->curTpl['spellId'.$j] > 0) { $cd = $this->curTpl['spellCooldown'.$j]; if ($cd < $this->curTpl['spellCategoryCooldown'.$j]) $cd = $this->curTpl['spellCategoryCooldown'.$j]; $extra = []; if ($cd >= 5000 && $this->curTpl['spellTrigger'.$j] != SPELL_TRIGGER_EQUIP) { $pt = Util::parseTime($cd); if (count(array_filter($pt)) == 1) // simple time: use simple method $extra[] = Lang::formatTime($cd, 'item', 'cooldown'); else // build block with generic time $extra[] = Lang::item('cooldown', 0, [Lang::formatTime($cd, 'game', 'timeAbbrev', true)]); } if ($this->curTpl['spellTrigger'.$j] == SPELL_TRIGGER_HIT) if ($ppm = $this->curTpl['spellppmRate'.$j]) $extra[] = Lang::spell('ppm', [$ppm]); $itemSpellsAndTrigger[$this->curTpl['spellId'.$j]] = [$this->curTpl['spellTrigger'.$j], $extra ? ' '.implode(', ', $extra) : '']; } } if ($itemSpellsAndTrigger) { $itemSpells = new SpellList(array(['s.id', array_keys($itemSpellsAndTrigger)])); foreach ($itemSpells->iterate() as $sId => $__) { [$parsed, $_, $scaling] = $itemSpells->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL); if (!$parsed && User::isInGroup(U_GROUP_EMPLOYEE)) $parsed = '<'.$itemSpells->getField('name', true, true).'>'; else if (!$parsed) continue; if ($scaling) $causesScaling = true; if ($interactive) { $link = '%s'; $parsed = preg_replace_callback('/([^;]*)( .*?<\/small>)([^&]*)/i', function($m) use($link) { $m[1] = $m[1] ? sprintf($link, $m[1]) : ''; $m[3] = $m[3] ? sprintf($link, $m[3]) : ''; return $m[1].$m[2].$m[3]; }, $parsed, -1, $nMatches ); if (!$nMatches) $parsed = sprintf($link, $parsed); } $green[] = Lang::item('trigger', $itemSpellsAndTrigger[$itemSpells->id][0]).$parsed.$itemSpellsAndTrigger[$itemSpells->id][1]; } } } // lower table (ratings, spells, ect) if (!$subOf) $x .= '
'; if (isset($green)) foreach ($green as $j => $bonus) if ($bonus) $x .= ''.$bonus.'
'; // Item Set $pieces = []; if ($setId = $this->getField('itemset')) { $condition = [ ['refSetId', $setId], // ['quality', $this->curTpl['quality']], ['minLevel', $this->curTpl['itemLevel'], '<='], ['maxLevel', $this->curTpl['itemLevel'], '>='] ]; $itemset = new ItemsetList($condition); if (!$itemset->error && $itemset->pieceToSet) { // handle special cases where: // > itemset has items of different qualities (handled by not limiting for this in the initial query) // > itemset is virtual and multiple instances have the same itemLevel but not quality (filter below) foreach ($itemset->iterate() as $id => $__) { if ($itemset->getField('quality') == $this->curTpl['quality']) { $itemset->pieceToSet = array_filter($itemset->pieceToSet, function($x) use ($id) { return $id == $x; }); break; } } $pieces = DB::Aowow()->select(' SELECT b.id AS ARRAY_KEY, b.name_loc0, b.name_loc2, b.name_loc3, b.name_loc4, b.name_loc6, b.name_loc8, GROUP_CONCAT(a.id SEPARATOR \':\') AS equiv FROM ?_items a, ?_items b WHERE a.slotBak = b.slotBak AND a.itemset = b.itemset AND b.id IN (?a) GROUP BY b.id;', array_keys($itemset->pieceToSet) ); foreach ($pieces as $k => &$p) $p = ''.Util::localizedString($p, 'name').''; $xSet = '
'.Lang::item('setName', [''.$itemset->getField('name', true).'', 0, count($pieces)]).''; if ($skId = $itemset->getField('skillId')) // bonus requires skill to activate { $xSet .= '
'.sprintf(Lang::game('requires'), ''.SkillList::getName($skId).''); if ($_ = $itemset->getField('skillLevel')) $xSet .= ' ('.$_.')'; $xSet .= '
'; } // list pieces $xSet .= '
'.implode('
', $pieces).'

'; // get bonuses $setSpellsAndIdx = []; for ($j = 1; $j <= 8; $j++) if ($_ = $itemset->getField('spell'.$j)) $setSpellsAndIdx[$_] = $j; $setSpells = []; if ($setSpellsAndIdx) { $boni = new SpellList(array(['s.id', array_keys($setSpellsAndIdx)])); foreach ($boni->iterate() as $__) { [$parsed, $_, $scaling] = $boni->parseText('description', $_reqLvl > 1 ? $_reqLvl : MAX_LEVEL); if ($scaling && $interactive) $causesScaling = true; $setSpells[] = array( 'tooltip' => $parsed, 'entry' => $itemset->getField('spell'.$setSpellsAndIdx[$boni->id]), 'bonus' => $itemset->getField('bonus'.$setSpellsAndIdx[$boni->id]) ); } } // sort and list bonuses $xSet .= ''; for ($i = 0; $i < count($setSpells); $i++) { for ($j = $i; $j < count($setSpells); $j++) { if ($setSpells[$j]['bonus'] >= $setSpells[$i]['bonus']) continue; $tmp = $setSpells[$i]; $setSpells[$i] = $setSpells[$j]; $setSpells[$j] = $tmp; } $xSet .= ''.Lang::item('setBonus', [$setSpells[$i]['bonus'], ''.$setSpells[$i]['tooltip'].'']).''; if ($i < count($setSpells) - 1) $xSet .= '
'; } $xSet .= '
'; } } // recipes, vanity pets, mounts if ($this->canTeachSpell()) { $craftSpell = new SpellList(array(['s.id', intVal($this->curTpl['spellId2'])])); if (!$craftSpell->error) { $xCraft = ''; if ($desc = $this->getField('description', true)) $x .= ''.Lang::item('trigger', SPELL_TRIGGER_USE).' '.$desc.'
'; // recipe handling (some stray Techniques have subclass == 0), place at bottom of tooltipp if ($_class == ITEM_CLASS_RECIPE || $this->curTpl['bagFamily'] == 16) { if ($craftSpell->canCreateItem()) { $craftItem = new ItemList(array(['i.id', (int)$craftSpell->curTpl['effect1CreateItemId']])); if (!$craftItem->error) if ($itemTT = $craftItem->renderTooltip($interactive, $this->id)) $xCraft .= '

'.$itemTT.'
'; } $reagentItems = []; for ($i = 1; $i <= 8; $i++) if ($rId = $craftSpell->getField('reagent'.$i)) $reagentItems[$rId] = $craftSpell->getField('reagentCount'.$i); if ($reagentItems) { $reagents = new ItemList(array(['i.id', array_keys($reagentItems)])); $reqReag = []; foreach ($reagents->iterate() as $__) $reqReag[] = ''.$reagents->getField('name', true).' ('.$reagentItems[$reagents->id].')'; $xCraft .= '

'.Lang::game('requires2').' '.implode(', ', $reqReag).'
'; } } } } // misc (no idea, how to organize the
better) $xMisc = []; // itemset: pieces and boni if (isset($xSet)) $xMisc[] = $xSet; // funny, yellow text at the bottom, omit if we have a recipe if ($this->curTpl['description_loc0'] && !$this->canTeachSpell()) $xMisc[] = '"'.Util::parseHtmlText($this->getField('description', true), false).'"'; // readable if ($this->curTpl['pageTextId']) $xMisc[] = ''.Lang::item('readClick').''; // charges for ($i = 1; $i < 6; $i++) { if (in_array($this->curTpl['spellTrigger'.$i], [SPELL_TRIGGER_USE, SPELL_TRIGGER_SOULSTONE, SPELL_TRIGGER_USE_NODELAY, SPELL_TRIGGER_LEARN]) && $this->curTpl['spellCharges'.$i]) { $xMisc[] = ''.Lang::item('charges', [abs($this->curTpl['spellCharges'.$i])]).''; break; } } // list required reagents if (isset($xCraft)) $xMisc[] = $xCraft; if ($xMisc) $x .= implode('
', $xMisc); if ($sp = $this->curTpl['sellPrice']) $x .= '
'.Lang::item('sellPrice').Lang::main('colon').Util::formatMoney($sp).'
'; if (!$subOf) $x .= '
'; // tooltip scaling if (!isset($xCraft)) { $itemId = $subOf ?: $this->id; $x .= ''; } return $x; } public function getRandEnchantForItem($randId) { // is it available for this item? .. does it even exist?! if (empty($this->enhanceR)) if (DB::World()->selectCell('SELECT 1 FROM item_enchantment_template WHERE entry = ?d AND ench = ?d', abs($this->getField('randomEnchant')), abs($randId))) if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomenchant WHERE id = ?d', $randId)) $this->enhanceR = $_; return !empty($this->enhanceR); } // from Trinity public function generateEnchSuffixFactor() { $rpp = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomproppoints WHERE id = ?', $this->curTpl['itemLevel']); if (!$rpp) return 0; switch ($this->curTpl['slot']) { // Items of that type don`t have points case INVTYPE_NON_EQUIP: case INVTYPE_BAG: case INVTYPE_TABARD: case INVTYPE_AMMO: case INVTYPE_QUIVER: case INVTYPE_RELIC: return 0; // Select point coefficient case INVTYPE_HEAD: case INVTYPE_BODY: case INVTYPE_CHEST: case INVTYPE_LEGS: case INVTYPE_2HWEAPON: case INVTYPE_ROBE: $suffixFactor = 1; break; case INVTYPE_SHOULDERS: case INVTYPE_WAIST: case INVTYPE_FEET: case INVTYPE_HANDS: case INVTYPE_TRINKET: $suffixFactor = 2; break; case INVTYPE_NECK: case INVTYPE_WRISTS: case INVTYPE_FINGER: case INVTYPE_SHIELD: case INVTYPE_CLOAK: case INVTYPE_HOLDABLE: $suffixFactor = 3; break; case INVTYPE_WEAPON: case INVTYPE_WEAPONMAINHAND: case INVTYPE_WEAPONOFFHAND: $suffixFactor = 4; break; case INVTYPE_RANGED: case INVTYPE_THROWN: case INVTYPE_RANGEDRIGHT: $suffixFactor = 5; break; default: return 0; } // Select rare/epic modifier switch ($this->curTpl['quality']) { case ITEM_QUALITY_UNCOMMON: return $rpp['uncommon'.$suffixFactor] / 10000; case ITEM_QUALITY_RARE: return $rpp['rare'.$suffixFactor] / 10000; case ITEM_QUALITY_EPIC: return $rpp['epic'.$suffixFactor] / 10000; case ITEM_QUALITY_LEGENDARY: case ITEM_QUALITY_ARTIFACT: return 0; // not have random properties default: break; } return 0; } public function extendJsonStats() { $enchantments = []; // buffer Ids for lookup id => src; src>0: socketBonus; src<0: gemEnchant foreach ($this->iterate() as $__) { // fetch and add socketbonusstats if (!empty($this->json[$this->id]['socketbonus'])) $enchantments[$this->json[$this->id]['socketbonus']][] = $this->id; // Item is a gem (don't mix with sockets) if ($geId = $this->curTpl['gemEnchantmentId']) $enchantments[$geId][] = -$this->id; } if ($enchantments) { $eStats = DB::Aowow()->select('SELECT *, typeId AS ARRAY_KEY FROM ?_item_stats WHERE `type` = ?d AND typeId IN (?a)', Type::ENCHANTMENT, array_keys($enchantments)); Util::checkNumeric($eStats); // and merge enchantments back foreach ($enchantments as $eId => $items) { if (empty($eStats[$eId])) continue; foreach ($items as $item) { if ($item > 0) // apply socketBonus $this->json[$item]['socketbonusstat'] = array_filter($eStats[$eId]); else /* if ($item < 0) */ // apply gemEnchantment Util::arraySumByKey($this->json[-$item], array_filter($eStats[$eId])); } } } foreach ($this->json as $item => $json) foreach ($json as $k => $v) if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore'])) unset($this->json[$item][$k]); } public function getOnUseStats() : ?StatsContainer { if ($this->curTpl['class'] != ITEM_CLASS_CONSUMABLE) return null; $onUseStats = new StatsContainer(); // convert Spells for ($h = 1; $h <= 5; $h++) { if ($this->curTpl['spellId'.$h] <= 0) continue; if ($this->curTpl['spellTrigger'.$h] != SPELL_TRIGGER_USE) continue; if ($spell = DB::Aowow()->selectRow( 'SELECT effect1AuraId, effect1MiscValue, effect1BasePoints, effect1DieSides, effect2AuraId, effect2MiscValue, effect2BasePoints, effect2DieSides, effect3AuraId, effect3MiscValue, effect3BasePoints, effect3DieSides FROM ?_spell WHERE id = ?d', $this->curTpl['spellId'.$h])) $onUseStats->fromSpell($spell); } return $onUseStats; } public function getSourceData(int $id = 0) : array { $data = []; foreach ($this->iterate() as $__) { if ($id && $id != $this->id) continue; $data[$this->id] = array( 'n' => $this->getField('name', true), 't' => Type::ITEM, 'ti' => $this->id, 'q' => $this->curTpl['quality'], // 'p' => PvP [NYI] 'icon' => $this->curTpl['iconString'] ); } return $data; } private function canTeachSpell() { if (!in_array($this->curTpl['spellId1'], LEARN_SPELLS)) return false; // needs learnable spell if (!$this->curTpl['spellId2']) return false; return true; } private function getFeralAP() : float { // must be weapon if ($this->curTpl['class'] != ITEM_CLASS_WEAPON) return 0.0; // thats fucked up.. if (!$this->curTpl['delay']) return 0.0; // must have enough damage $dps = ($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']) / (2 * $this->curTpl['delay'] / 1000); if ($dps <= 54.8) return 0.0; $subClasses = [ITEM_SUBCLASS_MISC_WEAPON]; $weaponTypeMask = DB::Aowow()->selectCell('SELECT `weaponTypeMask` FROM ?_classes WHERE `id` = ?d', ChrClass::DRUID->value); if ($weaponTypeMask) for ($i = 0; $i < 21; $i++) if ($weaponTypeMask & (1 << $i)) $subClasses[] = $i; // cannot be used by druids if (!in_array($this->curTpl['subClass'], $subClasses)) return 0.0; return round(($dps - 54.8) * 14); } public function isRangedWeapon() : bool { if ($this->curTpl['class'] != ITEM_CLASS_WEAPON) return false; return in_array($this->curTpl['subClassBak'], [ITEM_SUBCLASS_BOW, ITEM_SUBCLASS_GUN, ITEM_SUBCLASS_THROWN, ITEM_SUBCLASS_CROSSBOW, ITEM_SUBCLASS_WAND]); } private function formatRating(int $statId, int $itemMod, int $qty, bool $interactive = false, bool &$scaling = false) : string { // clamp level range $ssdLvl = isset($this->ssd[$this->id]) ? $this->ssd[$this->id]['maxLevel'] : 1; $reqLvl = $this->curTpl['requiredLevel'] > 1 ? $this->curTpl['requiredLevel'] : MAX_LEVEL; $level = min(max($reqLvl, $ssdLvl), MAX_LEVEL); // unknown rating if (!$statId) { if (User::isInGroup(U_GROUP_EMPLOYEE)) return Lang::item('statType', count(Lang::item('statType')) - 1, [$itemMod, $qty]); else return ''; } // level independent Bonus if (Stat::isLevelIndependent($statId)) return Lang::item('trigger', SPELL_TRIGGER_EQUIP).str_replace('%d', ''.$qty, Lang::item('statType', $itemMod)); // rating-Bonuses $scaling = true; if ($interactive) $js = ' ('.sprintf(Util::$changeLevelString, Util::setRatingLevel($level, $statId, $qty)).')'; else $js = ' ('.Util::setRatingLevel($level, $statId, $qty).')'; return Lang::item('trigger', SPELL_TRIGGER_EQUIP).str_replace('%d', ''.$qty.$js, Lang::item('statType', $itemMod)); } private function getSSDMod($type) { $mask = $this->curTpl['scalingStatValue']; $mask &= match ($type) { 'stats' => 0x04001F, 'armor' => 0xF001E0, 'dps' => 0x007E00, 'spell' => 0x008000, 'fap' => 0x010000, // unused default => 0x0 }; $field = null; for ($i = 0; $i < count(Util::$ssdMaskFields); $i++) if ($mask & (1 << $i)) $field = Util::$ssdMaskFields[$i]; return $field ? DB::Aowow()->selectCell('SELECT ?# FROM ?_scalingstatvalues WHERE id = ?d', $field, $this->ssd[$this->id]['maxLevel']) : 0; } private function initScalingStats() { $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ?_scalingstatdistribution WHERE id = ?d', $this->curTpl['scalingStatDistribution']); if (!$this->ssd[$this->id]) return; // stats and ratings for ($i = 1; $i <= 10; $i++) { if ($this->ssd[$this->id]['statMod'.$i] <= 0) { $this->templates[$this->id]['statType'.$i] = 0; $this->templates[$this->id]['statValue'.$i] = 0; } else { $this->templates[$this->id]['statType'.$i] = $this->ssd[$this->id]['statMod'.$i]; $this->templates[$this->id]['statValue'.$i] = intVal(($this->getSSDMod('stats') * $this->ssd[$this->id]['modifier'.$i]) / 10000); } } // armor: only replace if set if ($ssvArmor = $this->getSSDMod('armor')) $this->templates[$this->id]['armor'] = $ssvArmor; // if set dpsMod in ScalingStatValue use it for min/max damage // mle: 20% range / rgd: 30% range if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms { $range = isset($this->json[$this->id]['rgddps']) ? 0.3 : 0.2; $average = $extraDPS * $this->curTpl['delay'] / 1000; $this->templates[$this->id]['tplDmgMin1'] = floor((1 - $range) * $average); $this->templates[$this->id]['tplDmgMax1'] = floor((1 + $range) * $average); } // apply Spell Power from ScalingStatValue if set if ($spellBonus = $this->getSSDMod('spell')) { $this->templates[$this->id]['statType10'] = ITEM_MOD_SPELL_POWER; $this->templates[$this->id]['statValue10'] = $spellBonus; } } public function initSubItems() { if (!array_keys($this->templates)) return; $subItemIds = []; foreach ($this->iterate() as $__) if ($_ = $this->getField('randomEnchant')) $subItemIds[abs($_)] = $_; if (!$subItemIds) return; // remember: id < 0: randomSuffix; id > 0: randomProperty $subItemTpls = DB::World()->select( 'SELECT CAST( `entry` as SIGNED) AS ARRAY_KEY, CAST( `ench` as SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN (?a) UNION SELECT CAST(-`entry` as SIGNED) AS ARRAY_KEY, CAST(-`ench` as SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN (?a)', array_keys(array_filter($subItemIds, fn($v) => $v > 0)) ?: [0], array_keys(array_filter($subItemIds, fn($v) => $v < 0)) ?: [0] ); $randIds = []; foreach ($subItemTpls as $tpl) $randIds = array_merge($randIds, array_keys($tpl)); if (!$randIds) return; $randEnchants = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemrandomenchant WHERE id IN (?a)', $randIds); $enchIds = array_unique(array_merge( array_column($randEnchants, 'enchantId1'), array_column($randEnchants, 'enchantId2'), array_column($randEnchants, 'enchantId3'), array_column($randEnchants, 'enchantId4'), array_column($randEnchants, 'enchantId5') )); $enchants = new EnchantmentList(array(['id', $enchIds], Cfg::get('SQL_LIMIT_NONE'))); foreach ($enchants->iterate() as $eId => $_) { $this->rndEnchIds[$eId] = array( 'text' => $enchants->getField('name', true), 'stats' => $enchants->getStatGainForCurrent() ); } foreach ($this->iterate() as $mstItem => $__) { if (!$this->getField('randomEnchant')) continue; if (empty($subItemTpls[$this->getField('randomEnchant')])) continue; foreach ($subItemTpls[$this->getField('randomEnchant')] as $subId => $data) { if (empty($randEnchants[$subId])) continue; $data = array_merge($randEnchants[$subId], $data); $jsonEquip = []; $jsonText = []; for ($i = 1; $i < 6; $i++) { $enchId = $data['enchantId'.$i]; if ($enchId <= 0 || empty($this->rndEnchIds[$enchId])) continue; if ($data['allocationPct'.$i] > 0) // RandomSuffix: scaling Enchantment; enchId < 0 { $qty = intVal($data['allocationPct'.$i] * $this->generateEnchSuffixFactor()); $stats = array_fill_keys(array_keys($this->rndEnchIds[$enchId]['stats']), $qty); $jsonText[$enchId] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']); Util::arraySumByKey($jsonEquip, $stats); } else // RandomProperty: static Enchantment; enchId > 0 { $jsonText[$enchId] = $this->rndEnchIds[$enchId]['text']; Util::arraySumByKey($jsonEquip, $this->rndEnchIds[$enchId]['stats']); } } $this->subItems[$mstItem][$subId] = array( 'name' => Util::localizedString($data, 'name'), 'enchantment' => $jsonText, 'jsonequip' => $jsonEquip, 'chance' => $data['chance'] // hmm, only needed for item detail page... ); } if (!empty($this->subItems[$mstItem])) $this->json[$mstItem]['subitems'] = $this->subItems[$mstItem]; } } public function getScoreTotal($class = 0, $spec = [], $mhItem = 0, $ohItem = 0) { if (!$class || !$spec) return array_sum(array_column($this->json, 'gearscore')); $score = 0.0; $mh = $oh = []; foreach ($this->json as $j) { if ($j['id'] == $mhItem) $mh = $j; else if ($j['id'] == $ohItem) $oh = $j; else if ($j['gearscore']) { if ($j['slot'] == INVTYPE_RELIC) $score += 20; $score += round($j['gearscore']); } } $score += array_sum(Util::fixWeaponScores($class, $spec, $mh, $oh)); return $score; } private function initJsonStats() { $class = $this->curTpl['class']; $subclass = $this->curTpl['subClass']; $json = array( 'id' => $this->id, 'name' => $this->getField('name', true), 'quality' => ITEM_QUALITY_HEIRLOOM - $this->curTpl['quality'], 'icon' => $this->curTpl['iconString'], 'classs' => $class, 'subclass' => $subclass, 'subsubclass' => $this->curTpl['subSubClass'], 'heroic' => ($this->curTpl['flags'] & ITEM_FLAG_HEROIC) >> 3, 'side' => $this->curTpl['flagsExtra'] & 0x3 ? SIDE_BOTH - ($this->curTpl['flagsExtra'] & 0x3) : ChrRace::sideFromMask($this->curTpl['requiredRace']), 'slot' => $this->curTpl['slot'], 'slotbak' => $this->curTpl['slotBak'], 'level' => $this->curTpl['itemLevel'], 'reqlevel' => $this->curTpl['requiredLevel'], 'displayid' => $this->curTpl['displayId'], // 'commondrop' => 'true' / null // set if the item is a loot-filler-item .. check common ref-templates..? 'holres' => $this->curTpl['resHoly'], 'firres' => $this->curTpl['resFire'], 'natres' => $this->curTpl['resNature'], 'frores' => $this->curTpl['resFrost'], 'shares' => $this->curTpl['resShadow'], 'arcres' => $this->curTpl['resArcane'], 'armorbonus' => $class != ITEM_CLASS_ARMOR ? 0 : max(0, intVal($this->curTpl['armorDamageModifier'])), 'armor' => $this->curTpl['tplArmor'], 'dura' => $this->curTpl['durability'], 'itemset' => $this->curTpl['itemset'], 'socket1' => $this->curTpl['socketColor1'], 'socket2' => $this->curTpl['socketColor2'], 'socket3' => $this->curTpl['socketColor3'], 'nsockets' => ($this->curTpl['socketColor1'] > 0 ? 1 : 0) + ($this->curTpl['socketColor2'] > 0 ? 1 : 0) + ($this->curTpl['socketColor3'] > 0 ? 1 : 0), 'socketbonus' => $this->curTpl['socketBonus'], 'scadist' => $this->curTpl['scalingStatDistribution'], 'scaflags' => $this->curTpl['scalingStatValue'] ); if ($class == ITEM_CLASS_AMMUNITION) $json['dps'] = round(($this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2'] + $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']) / 2, 2); else if ($class == ITEM_CLASS_WEAPON) { $json['dmgtype1'] = $this->curTpl['dmgType1']; $json['dmgmin1'] = $this->curTpl['tplDmgMin1'] + $this->curTpl['dmgMin2']; $json['dmgmax1'] = $this->curTpl['tplDmgMax1'] + $this->curTpl['dmgMax2']; $json['speed'] = round($this->curTpl['delay'] / 1000, 2); $json['dps'] = $json['speed'] ? round(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1) : 0; if ($this->isRangedWeapon()) { $json['rgddmgmin'] = $json['dmgmin1']; $json['rgddmgmax'] = $json['dmgmax1']; $json['rgdspeed'] = $json['speed']; $json['rgddps'] = $json['dps']; } else { $json['mledmgmin'] = $json['dmgmin1']; $json['mledmgmax'] = $json['dmgmax1']; $json['mlespeed'] = $json['speed']; $json['mledps'] = $json['dps']; } if ($fap = $this->getFeralAP()) $json['feratkpwr'] = $fap; } if ($class == ITEM_CLASS_ARMOR || $class == ITEM_CLASS_WEAPON) $json['gearscore'] = Util::getEquipmentScore($json['level'], $this->getField('quality'), $json['slot'], $json['nsockets']); else if ($class == ITEM_CLASS_GEM) $json['gearscore'] = Util::getGemScore($json['level'], $this->getField('quality'), $this->getField('requiredSkill') == SKILL_JEWELCRAFTING, $this->id); // clear zero-values afterwards foreach ($json as $k => $v) if (!$v && !in_array($k, ['classs', 'subclass', 'quality', 'side', 'gearscore'])) unset($json[$k]); Util::checkNumeric($json); $this->json[$json['id']] = $json; } } class ItemListFilter extends Filter { private $ubFilter = []; // usable-by - limit weapon/armor selection per CharClass - itemClass => available itemsubclasses private $extCostQuery = 'SELECT `item` FROM npc_vendor WHERE `extendedCost` IN (?a) UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN (?a)'; public $extraOpts = []; // score for statWeights public $wtCnd = []; protected $enums = array( 16 => parent::ENUM_ZONE, // drops in zone 17 => parent::ENUM_FACTION, // requiresrepwith 99 => parent::ENUM_PROFESSION, // requiresprof 86 => parent::ENUM_PROFESSION, // craftedprof 87 => parent::ENUM_PROFESSION, // reagentforability 105 => parent::ENUM_HEROICDUNGEON, // drops in nh dungeon 106 => parent::ENUM_HEROICDUNGEON, // drops in hc dungeon 126 => parent::ENUM_ZONE, // rewardedbyquestin 147 => parent::ENUM_MULTIMODERAID, // drops in nh raid 10 148 => parent::ENUM_MULTIMODERAID, // drops in nh raid 25 149 => parent::ENUM_HEROICRAID, // drops in hc raid 10 150 => parent::ENUM_HEROICRAID, // drops in hc raid 25 152 => parent::ENUM_CLASSS, // class-specific 153 => parent::ENUM_RACE, // race-specific 160 => parent::ENUM_EVENT, // relatedevent 169 => parent::ENUM_EVENT, // requiresevent 158 => parent::ENUM_CURRENCY, // purchasablewithcurrency 118 => array( // itemcurrency 34853, 34854, 34855, 34856, 34857, 34858, 34848, 34851, 34852, 40625, 40626, 40627, 45632, 45633, 45634, 34169, 34186, 29754, 29753, 29755, 31089, 31091, 31090, 40610, 40611, 40612, 30236, 30237, 30238, 45635, 45636, 45637, 34245, 34332, 34339, 34345, 40631, 40632, 40633, 45638, 45639, 45640, 34244, 34208, 34180, 34229, 34350, 40628, 40629, 40630, 45641, 45642, 45643, 29757, 29758, 29756, 31092, 31094, 31093, 40613, 40614, 40615, 30239, 30240, 30241, 45644, 45645, 45646, 34342, 34211, 34243, 29760, 29761, 29759, 31097, 31095, 31096, 40616, 40617, 40618, 30242, 30243, 30244, 45647, 45648, 45649, 34216, 29766, 29767, 29765, 31098, 31100, 31099, 40619, 40620, 40621, 30245, 30246, 30247, 45650, 45651, 45652, 34167, 40634, 40635, 40636, 45653, 45654, 45655, 40637, 40638, 40639, 45656, 45657, 45658, 34170, 34192, 29763, 29764, 29762, 31101, 31103, 31102, 30248, 30249, 30250, 47557, 47558, 47559, 34233, 34234, 34202, 34195, 34209, 40622, 40623, 40624, 34193, 45659, 45660, 45661, 34212, 34351, 34215 ), 163 => array( // enchantment mats 34057, 22445, 11176, 34052, 11082, 34055, 16203, 10939, 11135, 11175, 22446, 16204, 34054, 14344, 11084, 11139, 22449, 11178, 10998, 34056, 16202, 10938, 11134, 11174, 22447, 20725, 14343, 34053, 10978, 11138, 22448, 11177, 11083, 10940, 11137, 22450 ), 91 => array( // tool 3, 14, 162, 168, 141, 2, 4, 169, 161, 15, 167, 81, 21, 165, 12, 62, 10, 101, 189, 6, 63, 41, 8, 7, 190, 9, 166, 121, 5 ), 66 => array( // profession specialization 1 => -1, 2 => [ 9788, 9787, 17041, 17040, 17039 ], 3 => -1, 4 => -1, 5 => [20219, 20222 ], 6 => -1, 7 => -1, 8 => [10656, 10658, 10660 ], 9 => -1, 10 => [26798, 26801, 26797 ], 11 => [ 9788, 9787, 17041, 17040, 17039, 20219, 20222, 10656, 10658, 10660, 26798, 26801, 26797], // i know, i know .. lazy as fuck 12 => false, 13 => -1, 14 => -1, 15 => -1 ), 128 => array( // source 1 => true, // Any 2 => false, // None 3 => 1, // Crafted 4 => 2, // Drop 5 => 3, // PvP 6 => 4, // Quest 7 => 5, // Vendor 9 => 10, // Starter 10 => 11, // Event 11 => 12, // Achievement 12 => 16 // Fished ) ); protected $genericFilter = array( 2 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 1 ], // bindonpickup [yn] 3 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 2 ], // bindonequip [yn] 4 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', 3 ], // bindonuse [yn] 5 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'bonding', [4, 5] ], // questitem [yn] 6 => [parent::CR_CALLBACK, 'cbQuestRelation', null, null ], // startsquest [side] 7 => [parent::CR_BOOLEAN, 'description_loc0', true ], // hasflavortext 8 => [parent::CR_BOOLEAN, 'requiredDisenchantSkill' ], // disenchantable 9 => [parent::CR_FLAG, 'flags', ITEM_FLAG_CONJURED ], // conjureditem 10 => [parent::CR_BOOLEAN, 'lockId' ], // locked 11 => [parent::CR_FLAG, 'flags', ITEM_FLAG_OPENABLE ], // openable 12 => [parent::CR_BOOLEAN, 'itemset' ], // partofset 13 => [parent::CR_BOOLEAN, 'randomEnchant' ], // randomlyenchanted 14 => [parent::CR_BOOLEAN, 'pageTextId' ], // readable 15 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'maxCount', 1 ], // unique [yn] 16 => [parent::CR_CALLBACK, 'cbDropsInZone', null, null ], // dropsin [zone] 17 => [parent::CR_ENUM, 'requiredFaction', true, true ], // requiresrepwith 18 => [parent::CR_CALLBACK, 'cbFactionQuestReward', null, null ], // rewardedbyfactionquest [side] 20 => [parent::CR_NUMERIC, 'is.str', NUM_CAST_INT, true ], // str 21 => [parent::CR_NUMERIC, 'is.agi', NUM_CAST_INT, true ], // agi 22 => [parent::CR_NUMERIC, 'is.sta', NUM_CAST_INT, true ], // sta 23 => [parent::CR_NUMERIC, 'is.int', NUM_CAST_INT, true ], // int 24 => [parent::CR_NUMERIC, 'is.spi', NUM_CAST_INT, true ], // spi 25 => [parent::CR_NUMERIC, 'is.arcres', NUM_CAST_INT, true ], // arcres 26 => [parent::CR_NUMERIC, 'is.firres', NUM_CAST_INT, true ], // firres 27 => [parent::CR_NUMERIC, 'is.natres', NUM_CAST_INT, true ], // natres 28 => [parent::CR_NUMERIC, 'is.frores', NUM_CAST_INT, true ], // frores 29 => [parent::CR_NUMERIC, 'is.shares', NUM_CAST_INT, true ], // shares 30 => [parent::CR_NUMERIC, 'is.holres', NUM_CAST_INT, true ], // holres 32 => [parent::CR_NUMERIC, 'is.dps', NUM_CAST_FLOAT, true ], // dps 33 => [parent::CR_NUMERIC, 'is.dmgmin1', NUM_CAST_INT, true ], // dmgmin1 34 => [parent::CR_NUMERIC, 'is.dmgmax1', NUM_CAST_INT, true ], // dmgmax1 35 => [parent::CR_CALLBACK, 'cbDamageType', null, null ], // damagetype [enum] 36 => [parent::CR_NUMERIC, 'is.speed', NUM_CAST_FLOAT, true ], // speed 37 => [parent::CR_NUMERIC, 'is.mleatkpwr', NUM_CAST_INT, true ], // mleatkpwr 38 => [parent::CR_NUMERIC, 'is.rgdatkpwr', NUM_CAST_INT, true ], // rgdatkpwr 39 => [parent::CR_NUMERIC, 'is.rgdhitrtng', NUM_CAST_INT, true ], // rgdhitrtng 40 => [parent::CR_NUMERIC, 'is.rgdcritstrkrtng', NUM_CAST_INT, true ], // rgdcritstrkrtng 41 => [parent::CR_NUMERIC, 'is.armor', NUM_CAST_INT, true ], // armor 42 => [parent::CR_NUMERIC, 'is.defrtng', NUM_CAST_INT, true ], // defrtng 43 => [parent::CR_NUMERIC, 'is.block', NUM_CAST_INT, true ], // block 44 => [parent::CR_NUMERIC, 'is.blockrtng', NUM_CAST_INT, true ], // blockrtng 45 => [parent::CR_NUMERIC, 'is.dodgertng', NUM_CAST_INT, true ], // dodgertng 46 => [parent::CR_NUMERIC, 'is.parryrtng', NUM_CAST_INT, true ], // parryrtng 48 => [parent::CR_NUMERIC, 'is.splhitrtng', NUM_CAST_INT, true ], // splhitrtng 49 => [parent::CR_NUMERIC, 'is.splcritstrkrtng', NUM_CAST_INT, true ], // splcritstrkrtng 50 => [parent::CR_NUMERIC, 'is.splheal', NUM_CAST_INT, true ], // splheal 51 => [parent::CR_NUMERIC, 'is.spldmg', NUM_CAST_INT, true ], // spldmg 52 => [parent::CR_NUMERIC, 'is.arcsplpwr', NUM_CAST_INT, true ], // arcsplpwr 53 => [parent::CR_NUMERIC, 'is.firsplpwr', NUM_CAST_INT, true ], // firsplpwr 54 => [parent::CR_NUMERIC, 'is.frosplpwr', NUM_CAST_INT, true ], // frosplpwr 55 => [parent::CR_NUMERIC, 'is.holsplpwr', NUM_CAST_INT, true ], // holsplpwr 56 => [parent::CR_NUMERIC, 'is.natsplpwr', NUM_CAST_INT, true ], // natsplpwr 57 => [parent::CR_NUMERIC, 'is.shasplpwr', NUM_CAST_INT, true ], // shasplpwr 59 => [parent::CR_NUMERIC, 'durability', NUM_CAST_INT, true ], // dura 60 => [parent::CR_NUMERIC, 'is.healthrgn', NUM_CAST_INT, true ], // healthrgn 61 => [parent::CR_NUMERIC, 'is.manargn', NUM_CAST_INT, true ], // manargn 62 => [parent::CR_CALLBACK, 'cbCooldown', null, null ], // cooldown [op] [int] 63 => [parent::CR_NUMERIC, 'buyPrice', NUM_CAST_INT, true ], // buyprice 64 => [parent::CR_NUMERIC, 'sellPrice', NUM_CAST_INT, true ], // sellprice 65 => [parent::CR_CALLBACK, 'cbAvgMoneyContent', null, null ], // avgmoney [op] [int] 66 => [parent::CR_ENUM, 'requiredSpell' ], // requiresprofspec 68 => [parent::CR_CALLBACK, 'cbObtainedBy', 15, null ], // otdisenchanting [yn] 69 => [parent::CR_CALLBACK, 'cbObtainedBy', 16, null ], // otfishing [yn] 70 => [parent::CR_CALLBACK, 'cbObtainedBy', 17, null ], // otherbgathering [yn] 71 => [parent::CR_FLAG, 'cuFlags', ITEM_CU_OT_ITEMLOOT ], // otitemopening [yn] 72 => [parent::CR_CALLBACK, 'cbObtainedBy', 2, null ], // otlooting [yn] 73 => [parent::CR_CALLBACK, 'cbObtainedBy', 19, null ], // otmining [yn] 74 => [parent::CR_FLAG, 'cuFlags', ITEM_CU_OT_OBJECTLOOT ], // otobjectopening [yn] 75 => [parent::CR_CALLBACK, 'cbObtainedBy', 21, null ], // otpickpocketing [yn] 76 => [parent::CR_CALLBACK, 'cbObtainedBy', 23, null ], // otskinning [yn] 77 => [parent::CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true ], // atkpwr 78 => [parent::CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true ], // mlehastertng 79 => [parent::CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true ], // resirtng 80 => [parent::CR_CALLBACK, 'cbHasSockets', null, null ], // has sockets [enum] 81 => [parent::CR_CALLBACK, 'cbFitsGemSlot', null, null ], // fits gem slot [enum] 83 => [parent::CR_FLAG, 'flags', ITEM_FLAG_UNIQUEEQUIPPED ], // uniqueequipped 84 => [parent::CR_NUMERIC, 'is.mlecritstrkrtng', NUM_CAST_INT, true ], // mlecritstrkrtng 85 => [parent::CR_CALLBACK, 'cbObjectiveOfQuest', null, null ], // objectivequest [side] 86 => [parent::CR_CALLBACK, 'cbCraftedByProf', null, null ], // craftedprof [enum] 87 => [parent::CR_CALLBACK, 'cbReagentForAbility', null, null ], // reagentforability [enum] 88 => [parent::CR_CALLBACK, 'cbObtainedBy', 20, null ], // otprospecting [yn] 89 => [parent::CR_FLAG, 'flags', ITEM_FLAG_PROSPECTABLE ], // prospectable 90 => [parent::CR_CALLBACK, 'cbAvgBuyout', null, null ], // avgbuyout [op] [int] 91 => [parent::CR_ENUM, 'totemCategory', false, true ], // tool 92 => [parent::CR_CALLBACK, 'cbObtainedBy', 5, null ], // soldbyvendor [yn] 93 => [parent::CR_CALLBACK, 'cbObtainedBy', 3, null ], // otpvp [pvp] 94 => [parent::CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true ], // splpen 95 => [parent::CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true ], // mlehitrtng 96 => [parent::CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true ], // critstrkrtng 97 => [parent::CR_NUMERIC, 'is.feratkpwr', NUM_CAST_INT, true ], // feratkpwr 98 => [parent::CR_FLAG, 'flags', ITEM_FLAG_PARTYLOOT ], // partyloot 99 => [parent::CR_ENUM, 'requiredSkill' ], // requiresprof 100 => [parent::CR_NUMERIC, 'is.nsockets', NUM_CAST_INT ], // nsockets 101 => [parent::CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng 102 => [parent::CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng 103 => [parent::CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng 104 => [parent::CR_STRING, 'description', STR_LOCALIZED ], // flavortext 105 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 1 ], // dropsinnormal [heroicdungeon-any] 106 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 2 ], // dropsinheroic [heroicdungeon-any] 107 => [parent::CR_NYI_PH, null, 1, ], // effecttext [str] not yet parsed ['effectsParsed_loc'.Lang::getLocale()->value, $crv] 109 => [parent::CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int] 111 => [parent::CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank 113 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots 114 => [parent::CR_NUMERIC, 'is.armorpenrtng', NUM_CAST_INT, true ], // armorpenrtng 115 => [parent::CR_NUMERIC, 'is.health', NUM_CAST_INT, true ], // health 116 => [parent::CR_NUMERIC, 'is.mana', NUM_CAST_INT, true ], // mana 117 => [parent::CR_NUMERIC, 'is.exprtng', NUM_CAST_INT, true ], // exprtng 118 => [parent::CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithitem [enum] 119 => [parent::CR_NUMERIC, 'is.hitrtng', NUM_CAST_INT, true ], // hitrtng 123 => [parent::CR_NUMERIC, 'is.splpwr', NUM_CAST_INT, true ], // splpwr 124 => [parent::CR_CALLBACK, 'cbHasRandEnchant', null, null ], // randomenchants [str] 125 => [parent::CR_CALLBACK, 'cbReqArenaRating', null, null ], // reqarenartng [op] [int] todo (low): 'find out, why "IN (W, X, Y) AND IN (X, Y, Z)" doesn't result in "(X, Y)" 126 => [parent::CR_CALLBACK, 'cbQuestRewardIn', null, null ], // rewardedbyquestin [zone-any] 128 => [parent::CR_CALLBACK, 'cbSource', null, null ], // source [enum] 129 => [parent::CR_CALLBACK, 'cbSoldByNPC', null, null ], // soldbynpc [str-small] 130 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments 132 => [parent::CR_CALLBACK, 'cbGlyphType', null, null ], // glyphtype [enum] 133 => [parent::CR_FLAG, 'flags', ITEM_FLAG_ACCOUNTBOUND ], // accountbound 134 => [parent::CR_NUMERIC, 'is.mledps', NUM_CAST_FLOAT, true ], // mledps 135 => [parent::CR_NUMERIC, 'is.mledmgmin', NUM_CAST_INT, true ], // mledmgmin 136 => [parent::CR_NUMERIC, 'is.mledmgmax', NUM_CAST_INT, true ], // mledmgmax 137 => [parent::CR_NUMERIC, 'is.mlespeed', NUM_CAST_FLOAT, true ], // mlespeed 138 => [parent::CR_NUMERIC, 'is.rgddps', NUM_CAST_FLOAT, true ], // rgddps 139 => [parent::CR_NUMERIC, 'is.rgddmgmin', NUM_CAST_INT, true ], // rgddmgmin 140 => [parent::CR_NUMERIC, 'is.rgddmgmax', NUM_CAST_INT, true ], // rgddmgmax 141 => [parent::CR_NUMERIC, 'is.rgdspeed', NUM_CAST_FLOAT, true ], // rgdspeed 142 => [parent::CR_STRING, 'ic.name' ], // icon 143 => [parent::CR_CALLBACK, 'cbObtainedBy', 18, null ], // otmilling [yn] 144 => [parent::CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewithhonor [yn] 145 => [parent::CR_CALLBACK, 'cbPvpPurchasable', 'reqArenaPoints', null ], // purchasablewitharena [yn] 146 => [parent::CR_FLAG, 'flags', ITEM_FLAG_HEROIC ], // heroic 147 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 1, ], // dropsinnormal10 [multimoderaid-any] 148 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 2, ], // dropsinnormal25 [multimoderaid-any] 149 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 4, ], // dropsinheroic10 [heroicraid-any] 150 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 8, ], // dropsinheroic25 [heroicraid-any] 151 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id 152 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredClass', ChrClass::MASK_ALL], // classspecific [enum] 153 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredRace', ChrRace::MASK_ALL ], // racespecific [enum] 154 => [parent::CR_FLAG, 'flags', ITEM_FLAG_REFUNDABLE ], // refundable 155 => [parent::CR_FLAG, 'flags', ITEM_FLAG_USABLE_ARENA ], // usableinarenas 156 => [parent::CR_FLAG, 'flags', ITEM_FLAG_USABLE_SHAPED ], // usablewhenshapeshifted 157 => [parent::CR_FLAG, 'flags', ITEM_FLAG_SMARTLOOT ], // smartloot 158 => [parent::CR_CALLBACK, 'cbPurchasableWith', null, null ], // purchasablewithcurrency [enum] 159 => [parent::CR_FLAG, 'flags', ITEM_FLAG_MILLABLE ], // millable 160 => [parent::CR_NYI_PH, null, 1, ], // relatedevent [enum] like 169 .. crawl though npc_vendor and loot_templates of event-related spawns 161 => [parent::CR_CALLBACK, 'cbAvailable', null, null ], // availabletoplayers [yn] 162 => [parent::CR_FLAG, 'flags', ITEM_FLAG_DEPRECATED ], // deprecated 163 => [parent::CR_CALLBACK, 'cbDisenchantsInto', null, null ], // disenchantsinto [disenchanting] 165 => [parent::CR_NUMERIC, 'repairPrice', NUM_CAST_INT, true ], // repaircost 167 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos 168 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'spellId1', LEARN_SPELLS ], // teachesspell [yn] 169 => [parent::CR_ENUM, 'e.holidayId', true, true ], // requiresevent 171 => [parent::CR_CALLBACK, 'cbObtainedBy', 8, null ], // otredemption [yn] 172 => [parent::CR_CALLBACK, 'cbObtainedBy', 12, null ], // rewardedbyachievement [yn] 176 => [parent::CR_STAFFFLAG, 'flags' ], // flags 177 => [parent::CR_STAFFFLAG, 'flagsExtra' ], // flags2 ); protected $inputFields = array( 'wt' => [parent::V_CALLBACK, 'cbWeightKeyCheck', true ], // weight keys 'wtv' => [parent::V_RANGE, [1, 999], true ], // weight values 'jc' => [parent::V_LIST, [1], false], // use jewelcrafter gems for weight calculation 'gm' => [parent::V_LIST, [2, 3, 4], false], // gem rarity for weight calculation 'cr' => [parent::V_RANGE, [1, 177], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters 'upg' => [parent::V_REGEX, '/[^\d:]/ui', false], // upgrade item ids 'gb' => [parent::V_LIST, [0, 1, 2, 3], false], // search result grouping 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'ub' => [parent::V_LIST, [[1, 9], 11], false], // usable by classId 'qu' => [parent::V_RANGE, [0, 7], true ], // quality ids 'ty' => [parent::V_CALLBACK, 'cbTypeCheck', true ], // item type - dynamic by current group 'sl' => [parent::V_CALLBACK, 'cbSlotCheck', true ], // item slot - dynamic by current group 'si' => [parent::V_LIST, [1, 2, 3, -1, -2], false], // side 'minle' => [parent::V_RANGE, [1, 999], false], // item level min 'maxle' => [parent::V_RANGE, [1, 999], false], // item level max 'minrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // required level min 'maxrl' => [parent::V_RANGE, [1, MAX_LEVEL], false] // required level max ); public function __construct($fromPOST = false, $opts = []) { $classes = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `weaponTypeMask` AS "0", `armorTypeMask` AS "1" FROM ?_classes'); foreach ($classes as $cId => [$weaponTypeMask, $armorTypeMask]) { // preselect misc subclasses $this->ubFilter[$cId] = [ITEM_CLASS_WEAPON => [ITEM_SUBCLASS_MISC_WEAPON], ITEM_CLASS_ARMOR => [ITEM_SUBCLASS_MISC_ARMOR]]; for ($i = 0; $i < 21; $i++) if ($weaponTypeMask & (1 << $i)) $this->ubFilter[$cId][ITEM_CLASS_WEAPON][] = $i; for ($i = 0; $i < 11; $i++) if ($armorTypeMask & (1 << $i)) $this->ubFilter[$cId][ITEM_CLASS_ARMOR][] = $i; } parent::__construct($fromPOST, $opts); } public function createConditionsForWeights() : array { if (empty($this->fiData['v']['wt'])) return []; $this->wtCnd = []; $select = []; $wtSum = 0; foreach ($this->fiData['v']['wt'] as $k => $v) { if ($idx = Stat::getIndexFrom(Stat::IDX_FILTER_CR_ID, $v)) { $str = Stat::getJsonString($idx); $qty = intVal($this->fiData['v']['wtv'][$k]); $select[] = '(IFNULL(`is`.`'.$str.'`, 0) * '.$qty.')'; $this->wtCnd[] = ['is.'.$str, 0, '>']; $wtSum += $qty; } } if (count($this->wtCnd) > 1) array_unshift($this->wtCnd, 'OR'); else if (count($this->wtCnd) == 1) $this->wtCnd = $this->wtCnd[0]; if ($select) { $this->extraOpts['is']['s'][] = ', IF(is.typeId IS NULL, 0, ('.implode(' + ', $select).') / '.$wtSum.') AS score'; $this->extraOpts['is']['o'][] = 'score DESC'; $this->extraOpts['i']['o'][] = null; // remove default ordering } else $this->extraOpts['is']['s'][] = ', 0 AS score'; // prevent errors return $this->wtCnd; } public function isCurrencyFor(int $itemId) : bool { return in_array($itemId, self::ENUM_CURRENCY); } protected function createSQLForValues() : array { $parts = []; $_v = $this->fiData['v']; // weights if (!empty($_v['wt']) && !empty($_v['wtv'])) { // gm - gem quality (qualityId) // jc - jc-gems included (bool) if ($_ = $this->createConditionsForWeights()) $parts[] = $_; foreach ($_v['wt'] as $_) $this->formData['extraCols'][] = $_; } // upgrade for [form only] if (isset($_v['upg'])) { $_ = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `slot` FROM ?_items WHERE `class` IN (?a) AND `id` IN (?a)', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR], explode(':', $_v['upg'])); if ($_ === null) { unset($_v['upg']); unset($this->formData['form']['upg']); } else { $this->formData['form']['upg'] = $_; if ($_) $parts[] = ['slot', $_]; } } // group by [form only] if (isset($_v['gb'])) $this->formData['form']['gb'] = $_v['gb']; // name if (isset($_v['na'])) if ($_ = $this->modularizeString(['name_loc'.Lang::getLocale()->value])) $parts[] = $_; // usable-by (not excluded by requiredClass && armor or weapons match mask from ?_classes) if (isset($_v['ub'])) { $parts[] = array( 'AND', ['OR', ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']], [ 'OR', ['class', [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR], '!'], ['AND', ['class', ITEM_CLASS_WEAPON], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_WEAPON]]], ['AND', ['class', ITEM_CLASS_ARMOR], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_ARMOR]]] ] ); } // quality [list] if (isset($_v['qu'])) $parts[] = ['quality', $_v['qu']]; // type if (isset($_v['ty'])) $parts[] = ['subclass', $_v['ty']]; // slot if (isset($_v['sl'])) $parts[] = ['slot', $_v['sl']]; // side if (isset($_v['si'])) { $ex = [['requiredRace', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!']; $notEx = ['OR', ['requiredRace', 0], [['requiredRace', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]]; switch ($_v['si']) { case SIDE_BOTH: $parts[] = ['OR', [['flagsExtra', 0x3, '&'], [0, 3]], ['requiredRace', ChrRace::MASK_ALL], ['requiredRace', 0]]; break; case SIDE_HORDE: $parts[] = ['AND', [['flagsExtra', 0x3, '&'], [0, 1]], ['OR', $notEx, ['requiredRace', ChrRace::MASK_HORDE, '&']]]; break; case -SIDE_HORDE: $parts[] = ['OR', [['flagsExtra', 0x3, '&'], 1], ['AND', $ex, ['requiredRace', ChrRace::MASK_HORDE, '&']]]; break; case SIDE_ALLIANCE: $parts[] = ['AND', [['flagsExtra', 0x3, '&'], [0, 2]], ['OR', $notEx, ['requiredRace', ChrRace::MASK_ALLIANCE, '&']]]; break; case -SIDE_ALLIANCE: $parts[] = ['OR', [['flagsExtra', 0x3, '&'], 2], ['AND', $ex, ['requiredRace', ChrRace::MASK_ALLIANCE, '&']]]; break; } } // itemLevel min if (isset($_v['minle'])) $parts[] = ['itemLevel', $_v['minle'], '>=']; // itemLevel max if (isset($_v['maxle'])) $parts[] = ['itemLevel', $_v['maxle'], '<=']; // reqLevel min if (isset($_v['minrl'])) $parts[] = ['requiredLevel', $_v['minrl'], '>=']; // reqLevel max if (isset($_v['maxrl'])) $parts[] = ['requiredLevel', $_v['maxrl'], '<=']; return $parts; } protected function cbFactionQuestReward(int $cr, int $crs, string $crv) : ?array { return match ($crs) { 1 => ['src.src4', null, '!'], // Yes 2 => ['src.src4', SIDE_ALLIANCE], // Alliance 3 => ['src.src4', SIDE_HORDE], // Horde 4 => ['src.src4', SIDE_BOTH], // Both 5 => ['src.src4', null], // No default => null }; } protected function cbAvailable(int $cr, int $crs, string $crv) : ?array { if ($this->int2Bool($crs)) return [['cuFlags', CUSTOM_UNAVAILABLE, '&'], 0, $crs ? null : '!']; return null; } protected function cbHasSockets(int $cr, int $crs, string $crv) : ?array { return match ($crs) { // Meta, Red, Yellow, Blue 1, 2, 3, 4 => ['OR', ['socketColor1', 1 << ($crs - 1)], ['socketColor2', 1 << ($crs - 1)], ['socketColor3', 1 << ($crs - 1)]], 5 => ['is.nsockets', 0, '!'], // Yes 6 => ['is.nsockets', 0], // No default => null }; } protected function cbFitsGemSlot(int $cr, int $crs, string $crv) : ?array { return match ($crs) { // Meta, Red, Yellow, Blue 1, 2, 3, 4 => ['AND', ['gemEnchantmentId', 0, '!'], ['gemColorMask', 1 << ($crs - 1), '&']], 5 => ['gemEnchantmentId', 0, '!'], // Yes 6 => ['gemEnchantmentId', 0], // No default => null }; } protected function cbGlyphType(int $cr, int $crs, string $crv) : ?array { return match ($crs) { // major, minor 1, 2 => ['AND', ['class', ITEM_CLASS_GLYPH], ['subSubClass', $crs]], default => null }; } protected function cbHasRandEnchant(int $cr, int $crs, string $crv) : ?array { $n = preg_replace(parent::PATTERN_NAME, '', $crv); $n = $this->transformString($n, false); $randIds = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, ABS(`id`) AS `id`, name_loc?d, `name_loc0` FROM ?_itemrandomenchant WHERE name_loc?d LIKE ?', Lang::getLocale()->value, Lang::getLocale()->value, $n); $tplIds = $randIds ? DB::World()->select('SELECT `entry`, `ench` FROM item_enchantment_template WHERE `ench` IN (?a)', array_column($randIds, 'id')) : []; foreach ($tplIds as &$set) { $z = array_column($randIds, 'id'); $x = array_search($set['ench'], $z); if (isset($randIds[-$z[$x]])) { $set['entry'] *= -1; $set['ench'] *= -1; } $set['name'] = Util::localizedString($randIds[$set['ench']], 'name', true); } // only enhance search results if enchantment by name is unique (implies only one enchantment per item is available) if (count(array_unique(array_column($randIds, 'name_loc0'))) == 1) $this->extraOpts['relEnchant'] = $tplIds; if ($tplIds) return ['randomEnchant', array_column($tplIds, 'entry')]; else return [0]; // no results aren't really input errors } protected function cbReqArenaRating(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; $this->formData['extraCols'][] = $cr; $items = [0]; if ($costs = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE `reqPersonalrating` '.$crs.' '.$crv)) $items = DB::World()->selectCol($this->extCostQuery, $costs, $costs); return ['id', $items]; } protected function cbClassRaceSpec(int $cr, int $crs, string $crv, string $field, int $mask) : ?array { if (!isset($this->enums[$cr][$crs])) return null; $_ = $this->enums[$cr][$crs]; if (is_bool($_)) return $_ ? ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 0, '>']] : ['OR', [[$field, $mask, '&'], $mask], [$field, 0]]; else if (is_int($_)) return ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 1 << ($_ - 1), '&']]; return null; } protected function cbDamageType(int $cr, int $crs, string $crv) : ?array { if (!$this->checkInput(parent::V_RANGE, [SPELL_SCHOOL_NORMAL, SPELL_SCHOOL_ARCANE], $crs)) return null; return ['OR', ['dmgType1', $crs], ['dmgType2', $crs]]; } protected function cbArmorBonus(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_FLOAT) || !$this->int2Op($crs)) return null; $this->formData['extraCols'][] = $cr; return ['AND', ['armordamagemodifier', $crv, $crs], ['class', ITEM_CLASS_ARMOR]]; } protected function cbCraftedByProf(int $cr, int $crs, string $crv) : ?array { if (!isset($this->enums[$cr][$crs])) return null; $_ = $this->enums[$cr][$crs]; if (is_bool($_)) return ['src.src1', null, $_ ? '!' : null]; else if (is_int($_)) return ['s.skillLine1', $_]; return null; } protected function cbQuestRewardIn(int $cr, int $crs, string $crv) : ?array { if (in_array($crs, $this->enums[$cr])) return ['AND', ['src.src4', null, '!'], ['src.moreZoneId', $crs]]; else if ($crs == parent::ENUM_ANY) return ['src.src4', null, '!']; // well, this seems a bit redundant.. return null; } protected function cbDropsInZone(int $cr, int $crs, string $crv) : ?array { if (in_array($crs, $this->enums[$cr])) return ['AND', ['src.src2', null, '!'], ['src.moreZoneId', $crs]]; else if ($crs == parent::ENUM_ANY) return ['src.src2', null, '!']; // well, this seems a bit redundant.. return null; } protected function cbDropsInInstance(int $cr, int $crs, string $crv, int $moreFlag, int $modeBit) : ?array { if (in_array($crs, $this->enums[$cr])) return ['AND', ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&'], ['src.moreZoneId', $crs]]; else if ($crs == parent::ENUM_ANY) return ['AND', ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&']]; return null; } protected function cbPurchasableWith(int $cr, int $crs, string $crv) : ?array { if (in_array($crs, $this->enums[$cr])) $_ = (array)$crs; else if ($crs == parent::ENUM_ANY) $_ = $this->enums[$cr]; else return null; $costs = DB::Aowow()->selectCol( 'SELECT `id` FROM ?_itemextendedcost WHERE `reqItemId1` IN (?a) OR `reqItemId2` IN (?a) OR `reqItemId3` IN (?a) OR `reqItemId4` IN (?a) OR `reqItemId5` IN (?a)', $_, $_, $_, $_, $_ ); if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) return ['id', $items]; return null; } protected function cbSoldByNPC(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_INT)) return null; if ($iIds = DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `entry` = ?d UNION SELECT `item` FROM game_event_npc_vendor v JOIN creature c ON c.`guid` = v.`guid` WHERE c.`id` = ?d', $crv, $crv)) return ['i.id', $iIds]; else return [0]; } protected function cbAvgBuyout(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; foreach (Profiler::getRealms() as $rId => $__) { // todo: do something sensible.. // // todo (med): get the avgbuyout into the listview // if ($_ = DB::Characters()->select('SELECT ii.itemEntry AS ARRAY_KEY, AVG(ah.buyoutprice / ii.count) AS buyout FROM auctionhouse ah JOIN item_instance ii ON ah.itemguid = ii.guid GROUP BY ii.itemEntry HAVING buyout '.$crs.' ?f', $c[1])) // return ['i.id', array_keys($_)]; // else // return [0]; return [1]; } return [0]; } protected function cbAvgMoneyContent(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; $this->formData['extraCols'][] = $cr; return ['AND', ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $crv, $crs]]; } protected function cbCooldown(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; $crv *= 1000; // field supplied in milliseconds $this->formData['extraCols'][] = $cr; $this->extraOpts['is']['s'][] = ', GREATEST(`spellCooldown1`, `spellCooldown2`, `spellCooldown3`, `spellCooldown4`, `spellCooldown5`) AS "cooldown"'; return [ 'OR', ['AND', ['spellTrigger1', SPELL_TRIGGER_USE], ['spellId1', 0, '!'], ['spellCooldown1', 0, '>'], ['spellCooldown1', $crv, $crs]], ['AND', ['spellTrigger2', SPELL_TRIGGER_USE], ['spellId2', 0, '!'], ['spellCooldown2', 0, '>'], ['spellCooldown2', $crv, $crs]], ['AND', ['spellTrigger3', SPELL_TRIGGER_USE], ['spellId3', 0, '!'], ['spellCooldown3', 0, '>'], ['spellCooldown3', $crv, $crs]], ['AND', ['spellTrigger4', SPELL_TRIGGER_USE], ['spellId4', 0, '!'], ['spellCooldown4', 0, '>'], ['spellCooldown4', $crv, $crs]], ['AND', ['spellTrigger5', SPELL_TRIGGER_USE], ['spellId5', 0, '!'], ['spellCooldown5', 0, '>'], ['spellCooldown5', $crv, $crs]], ]; } protected function cbQuestRelation(int $cr, int $crs, string $crv) : ?array { return match ($crs) { // any 1 => ['startQuest', 0, '>'], // exclude horde only 2 => ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_HORDE]], // exclude alliance only 3 => ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_ALLIANCE]], // both 4 => ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]], // none 5 => ['startQuest', 0], default => null }; } protected function cbFieldHasVal(int $cr, int $crs, string $crv, string $field, mixed $val) : ?array { if ($this->int2Bool($crs)) return [$field, $val, $crs ? null : '!']; return null; } protected function cbObtainedBy(int $cr, int $crs, string $crv, string $field) : ?array { if ($this->int2Bool($crs)) return ['src.src'.$field, null, $crs ? '!' : null]; return null; } protected function cbPvpPurchasable(int $cr, int $crs, string $crv, string $field) : ?array { if (!$this->int2Bool($crs)) return null; $costs = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE ?# > 0', $field); if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) return ['id', $items, $crs ? null : '!']; return null; } protected function cbDisenchantsInto(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crs, NUM_CAST_INT)) return null; if (!in_array($crs, $this->enums[$cr])) return null; $refResults = []; $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `item` = ?d AND `reference` = 0', LOOT_REFERENCE, $crs); while ($newRefs) { $refResults += $newRefs; $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `reference` IN (?a)', LOOT_REFERENCE, $newRefs); } $lootIds = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE {`reference` IN (?a) OR }(`reference` = 0 AND `item` = ?d)', LOOT_DISENCHANT, $refResults ?: DBSIMPLE_SKIP, $crs); return $lootIds ? ['disenchantId', $lootIds] : [0]; } protected function cbObjectiveOfQuest(int $cr, int $crs, string $crv) : ?array { $w = ''; switch ($crs) { case 1: // Yes case 5: // No $w = 1; break; case 2: // Alliance $w = '`reqRaceMask` & '.ChrRace::MASK_ALLIANCE.' AND (`reqRaceMask` & '.ChrRace::MASK_HORDE.') = 0'; break; case 3: // Horde $w = '`reqRaceMask` & '.ChrRace::MASK_HORDE.' AND (`reqRaceMask` & '.ChrRace::MASK_ALLIANCE.') = 0'; break; case 4: // Both $w = '(`reqRaceMask` & '.ChrRace::MASK_ALLIANCE.' AND `reqRaceMask` & '.ChrRace::MASK_HORDE.') OR `reqRaceMask` = 0'; break; default: return null; } $itemIds = DB::Aowow()->selectCol(sprintf( 'SELECT `reqItemId1` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId2` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId3` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId4` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId5` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId6` FROM ?_quests WHERE %1$s', $w )); if ($itemIds) return ['id', $itemIds, $crs == 5 ? '!' : null]; return [0]; } protected function cbReagentForAbility(int $cr, int $crs, string $crv) : ?array { if (!isset($this->enums[$cr][$crs])) return null; $_ = $this->enums[$cr][$crs]; if ($_ === null) return null; $ids = []; $spells = DB::Aowow()->select( // todo (med): hmm, selecting all using SpellList would exhaust 128MB of memory :x .. see, that we only select the fields that are really needed 'SELECT `reagent1`, `reagent2`, `reagent3`, `reagent4`, `reagent5`, `reagent6`, `reagent7`, `reagent8`, `reagentCount1`, `reagentCount2`, `reagentCount3`, `reagentCount4`, `reagentCount5`, `reagentCount6`, `reagentCount7`, `reagentCount8` FROM ?_spell WHERE `skillLine1` IN (?a)', is_bool($_) ? array_filter($this->enums[99], "is_numeric") : $_ ); foreach ($spells as $spell) for ($i = 1; $i < 9; $i++) if ($spell['reagent'.$i] > 0 && $spell['reagentCount'.$i] > 0) $ids[] = $spell['reagent'.$i]; if (empty($ids)) return [0]; else if ($_) return ['id', $ids]; else return ['id', $ids, '!']; } protected function cbSource(int $cr, int $crs, string $crv) : ?array { if (!isset($this->enums[$cr][$crs])) return null; $_ = $this->enums[$cr][$crs]; if (is_int($_)) // specific return ['src.src'.$_, null, '!']; else if ($_) // any { $foo = ['OR']; foreach ($this->enums[$cr] as $bar) if (is_int($bar)) $foo[] = ['src.src'.$bar, null, '!']; return $foo; } else // none return ['src.typeId', null]; } protected function cbTypeCheck(string &$v) : bool { if (!$this->parentCats) return false; if (!Util::checkNumeric($v, NUM_CAST_INT)) return false; $c = $this->parentCats; if (isset($c[2]) && is_array(Lang::item('cat', $c[0], 1, $c[1]))) $catList = Lang::item('cat', $c[0], 1, $c[1], 1, $c[2]); else if (isset($c[1]) && is_array(Lang::item('cat', $c[0]))) $catList = Lang::item('cat', $c[0], 1, $c[1]); else $catList = Lang::item('cat', $c[0]); // consumables - always if ($c[0] == ITEM_CLASS_CONSUMABLE) return in_array($v, array_keys(Lang::item('cat', 0, 1))); // weapons - only if parent else if ($c[0] == ITEM_CLASS_WEAPON && !isset($c[1])) return in_array($v, array_keys(Lang::spell('weaponSubClass'))); // armor - only if parent else if ($c[0] == ITEM_CLASS_ARMOR && !isset($c[1])) return in_array($v, array_keys(Lang::item('cat', ITEM_CLASS_ARMOR, 1))); // uh ... other stuff... else if (!isset($c[1]) && in_array($c[0], [ITEM_CLASS_CONTAINER, ITEM_CLASS_GEM, ITEM_CLASS_TRADEGOOD, ITEM_CLASS_RECIPE, ITEM_CLASS_MISC])) return in_array($v, array_keys($catList[1])); return false; } protected function cbSlotCheck(string &$v) : bool { if (!Util::checkNumeric($v, NUM_CAST_INT)) return false; // todo (low): limit to concrete slots $sl = array_keys(Lang::item('inventoryType')); $c = $this->parentCats; // no selection if (!isset($c[0])) return in_array($v, $sl); // consumables - any; perm / temp item enhancements else if ($c[0] == ITEM_CLASS_CONSUMABLE && (!isset($c[1]) || in_array($c[1], [-3, 6]))) return in_array($v, $sl); // weapons - always else if ($c[0] == ITEM_CLASS_WEAPON) return in_array($v, $sl); // armor - any; any armor else if ($c[0] == ITEM_CLASS_ARMOR && (!isset($c[1]) || in_array($c[1], [ITEM_SUBCLASS_CLOTH_ARMOR, ITEM_SUBCLASS_LEATHER_ARMOR, ITEM_SUBCLASS_MAIL_ARMOR, ITEM_SUBCLASS_PLATE_ARMOR]))) return in_array($v, $sl); return false; } protected function cbWeightKeyCheck(string &$v) : bool { if (preg_match('/\W/i', $v)) return false; return Stat::getIndexFrom(Stat::IDX_FILTER_CR_ID, $v) > 0; } } ?>