Loot/Modes

* work against more correctly assigning instance mode to entities and loot
    - added manually collected data for difficulty versions of gameobjects, just boss chests for now.
      update setup/source to default object source to base difficulty version if able
    - update spelldifficulty table to contain the (likely) mapmode it will be used in
  * refactored class loot
    - implement loot mode indicators on listview for creature and gameobject loot
    - show 'drops' listview tab on instance zone page
    - fixes against tribute chest systems (toc / ulduar)
    - fix icc gunship battle chest ownership
This commit is contained in:
Sarjuuk
2025-11-19 17:01:16 +01:00
parent be3701df91
commit a5051c9bf5
33 changed files with 1550 additions and 1038 deletions

View File

@@ -117,9 +117,9 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache
if ($this->typeId != CURRENCY_HONOR_POINTS && $this->typeId != CURRENCY_ARENA_POINTS)
{
// tabs: this currency is contained in..
$lootTabs = new Loot();
$lootTabs = new LootByItem($_relItemId);
if ($lootTabs->getByItem($_relItemId))
if ($lootTabs->getByItem())
{
$this->extendGlobalData($lootTabs->jsGlobals);
@@ -198,7 +198,7 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache
}
}
// tab: created by (spell) [for items its handled in Loot::getByContainer()]
// tab: created by (spell) [for items its handled in LootByItem]
if ($this->typeId == CURRENCY_HONOR_POINTS)
{
$createdBy = new SpellList(array(['effect1Id', SPELL_EFFECT_ADD_HONOR], ['effect2Id', SPELL_EFFECT_ADD_HONOR], ['effect3Id', SPELL_EFFECT_ADD_HONOR], 'OR'));

View File

@@ -443,9 +443,9 @@ class ItemBaseResponse extends TemplateResponse implements ICache
}
// tabs: this item is contained in..
$lootTabs = new Loot();
$lootTabs = new LootByItem($this->typeId);
$createdBy = [];
if ($lootTabs->getByItem($this->typeId))
if ($lootTabs->getByItem())
{
$this->extendGlobalData($lootTabs->jsGlobals);
@@ -454,27 +454,26 @@ class ItemBaseResponse extends TemplateResponse implements ICache
if (!$tabData['data'])
continue;
if ($idx == 16)
if ($idx == LootByItem::SPELL_CREATED)
$createdBy = array_column($tabData['data'], 'id');
if ($idx == 1)
if ($idx == LootByItem::ITEM_DISENCHANTED)
$tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=163;crs='.$this->typeId.';crv=0');
if ($idx == 4 && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd']))
if ($idx == LootByItem::NPC_DROPPED && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd']))
$tabData['note'] = match($sm[0]['dd'])
{
switch ($sm[0]['dd'])
{
case -1: $tabData['note'] = '$LANG.lvnote_itemdropsinnormalonly'; break;
case -2: $tabData['note'] = '$LANG.lvnote_itemdropsinheroiconly'; break;
case -3: $tabData['note'] = '$LANG.lvnote_itemdropsinnormalheroic'; break;
case 1: $tabData['note'] = '$LANG.lvnote_itemdropsinnormal10only'; break;
case 2: $tabData['note'] = '$LANG.lvnote_itemdropsinnormal25only'; break;
case 3: $tabData['note'] = '$LANG.lvnote_itemdropsinheroic10only'; break;
case 4: $tabData['note'] = '$LANG.lvnote_itemdropsinheroic25only'; break;
}
}
-1 => '$LANG.lvnote_itemdropsinnormalonly',
-2 => '$LANG.lvnote_itemdropsinheroiconly',
-3 => '$LANG.lvnote_itemdropsinnormalheroic',
1 => '$LANG.lvnote_itemdropsinnormal10only',
2 => '$LANG.lvnote_itemdropsinnormal25only',
3 => '$LANG.lvnote_itemdropsinheroic10only',
4 => '$LANG.lvnote_itemdropsinheroic25only',
default => null
};
if ($idx == 15 && !$this->map)
if ($idx == LootByItem::OBJECT_FISHED && !$this->map)
{
$nodeIds = array_map(fn($x) => $x['id'], $tabData['data']);
$fishedIn = new GameObjectList(array(['id', $nodeIds]));
@@ -505,16 +504,16 @@ class ItemBaseResponse extends TemplateResponse implements ICache
// tabs: this item contains..
$sourceFor = array(
[LOOT_ITEM, $this->typeId, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ],
[LOOT_PROSPECTING, $this->typeId, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']],
[LOOT_MILLING, $this->typeId, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']],
[LOOT_DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']]
[Loot::ITEM, $this->typeId, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ],
[Loot::PROSPECTING, $this->typeId, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']],
[Loot::MILLING, $this->typeId, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']],
[Loot::DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']]
);
foreach ($sourceFor as [$lootTemplate, $lootId, $tabName, $tabId, $extraCols, $hiddenCols])
{
$lootTab = new Loot();
if ($lootTab->getByContainer($lootTemplate, $lootId))
$lootTab = new LootByContainer();
if ($lootTab->getByContainer($lootTemplate, [$lootId]))
{
$this->extendGlobalData($lootTab->jsGlobals);
$extraCols = array_merge($extraCols, $lootTab->extraCols);
@@ -523,6 +522,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache
'data' => $lootTab->getResult(),
'name' => $tabName,
'id' => $tabId,
'computeDataFunc' => '$Listview.funcBox.initLootTable'
);
if ($extraCols)

View File

@@ -103,10 +103,9 @@ class NpcBaseResponse extends TemplateResponse implements ICache
/**********************/
$mapType = 0;
if ($maps = DB::Aowow()->selectCol('SELECT DISTINCT `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::NPC, $this->typeId))
if ($maps = DB::Aowow()->selectCell('SELECT IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::NPC, $this->typeId))
{
if (count($maps) == 1) // should only exist in one instance
$mapType = match (DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps[0]))
$mapType = match ((int)DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps[0]))
{
// MAP_TYPE_DUNGEON,
MAP_TYPE_DUNGEON_HC => 1,
@@ -244,7 +243,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache
}
if ($stats = $this->getCreatureStats($mapType, $_altIds))
$infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::npc('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]';
$infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::game('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]';
if ($infobox)
{
@@ -566,79 +565,108 @@ class NpcBaseResponse extends TemplateResponse implements ICache
}
// tabs: this creature contains..
$skinTab = ['tab_skinning', 'skinning', SKILL_SKINNING];
if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_HERBALISM)
$skinTab = ['tab_herbalism', 'herbalism', SKILL_HERBALISM];
else if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_MINING)
$skinTab = ['tab_mining', 'mining', SKILL_MINING];
else if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING)
$skinTab = ['tab_engineering', 'engineering', SKILL_ENGINEERING];
/*
extraCols: [Listview.extraCols.count, Listview.extraCols.percent, Listview.extraCols.mode],
_totalCount: 22531,
computeDataFunc: Listview.funcBox.initLootTable,
onAfterCreate: Listview.funcBox.addModeIndicator,
modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
*/
if ($this->subject->isGatherable())
$skinTab = ['$LANG.tab_herbalism', 'herbalism', SKILL_HERBALISM];
else if ($this->subject->isMineable())
$skinTab = ['$LANG.tab_mining', 'mining', SKILL_MINING];
else if ($this->subject->isSalvageable())
$skinTab = ['$LANG.tab_engineering', 'engineering', SKILL_ENGINEERING];
else
$skinTab = ['$LANG.tab_skinning', 'skinning', SKILL_SKINNING];
$sourceFor = array(
0 => [LOOT_CREATURE, $this->subject->getField('lootId'), '$LANG.tab_drops', 'drops', [ ], ''],
8 => [LOOT_PICKPOCKET, $this->subject->getField('pickpocketLootId'), '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel'], ''],
9 => [LOOT_SKINNING, $this->subject->getField('skinLootId'), '$LANG.'.$skinTab[0], $skinTab[1], ['side', 'slot', 'reqlevel'], '']
0 => [Loot::CREATURE, [4 => $this->subject->getField('lootId')], '$LANG.tab_drops', 'drops', [ ], ''],
1 => [Loot::GAMEOBJECT, [], '$LANG.tab_drops', 'drops-object', [ ], ''],
2 => [Loot::PICKPOCKET, [4 => $this->subject->getField('pickpocketLootId')], '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel'], ''],
3 => [Loot::SKINNING, [4 => $this->subject->getField('skinLootId')], $skinTab[0], $skinTab[1], ['side', 'slot', 'reqlevel'], '']
);
// temp: manually add loot for difficulty-versions
$langref = array(
"-2" => '$LANG.tab_heroic',
"-1" => '$LANG.tab_normal',
1 => '$$WH.sprintf(LANG.tab_normalX, 10)',
2 => '$$WH.sprintf(LANG.tab_normalX, 25)',
3 => '$$WH.sprintf(LANG.tab_heroicX, 10)',
4 => '$$WH.sprintf(LANG.tab_heroicX, 25)'
);
/* loot tabs to sub tabs
* (1 << 0) => '$LANG.tab_heroic',
* (1 << 1) => '$LANG.tab_normal',
* (1 << 2) => '$LANG.tab_drops',
* (1 << 3) => '$$WH.sprintf(LANG.tab_normalX, 10)',
* (1 << 4) => '$$WH.sprintf(LANG.tab_normalX, 25)',
* (1 << 5) => '$$WH.sprintf(LANG.tab_heroicX, 10)',
* (1 << 6) => '$$WH.sprintf(LANG.tab_heroicX, 25)'
*/
$getBit = function(int $type, int $difficulty) : int
{
if ($type == 1) // dungeon
return 1 << (2 - $difficulty);
if ($type == 2) // raid
return 1 << (2 + $difficulty);
return 4; // generic case
};
foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d ORDER BY `difficulty` ASC', $this->typeId) as $difficulty => $lgo)
{
$sourceFor[1][1][$getBit($mapType, $difficulty)] = $lgo['lootId'];
$sourceFor[1][5] = $sourceFor[1][5] ?: '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")';
}
if ($_altIds)
{
$sourceFor[0][2] = $mapType == 1 ? $langref[-1] : $langref[1];
if ($mapType == 1) // map generic loot to dungeon NH
{
$sourceFor[0][1] = [2 => $sourceFor[0][1][4]];
$sourceFor[2][1] = [2 => $sourceFor[2][1][4]];
$sourceFor[3][1] = [2 => $sourceFor[3][1][4]];
}
if ($mapType == 2) // map generic loot to raid 10NH
{
$sourceFor[0][1] = [8 => $sourceFor[0][1][4]];
$sourceFor[2][1] = [8 => $sourceFor[2][1][4]];
$sourceFor[3][1] = [8 => $sourceFor[3][1][4]];
}
foreach ($this->altNPCs->iterate() as $id => $__)
{
$mode = ($_altIds[$id] + 1) * ($mapType == 1 ? -1 : 1);
foreach (DB::Aowow()->select('SELECT o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8`, l.`difficulty` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d', $id) as $l)
$sourceFor[(($l['difficulty'] - 1) * 2) + 1] = [LOOT_GAMEOBJECT, $l['lootId'], $langref[$l['difficulty'] * ($mapType == 1 ? -1 : 1)], 'drops-object-'.$l['difficulty'], [], '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$l['id'].', "'.Util::localizedString($l, 'name').'")'];
if ($lootId = $this->altNPCs->getField('lootId'))
$sourceFor[($mode - 1) * 2] = [LOOT_CREATURE, $lootId, $langref[$mode], 'drops-'.abs($mode), [], ''];
}
}
foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d', $this->typeId) as $difficulty => $lgo)
$sourceFor[(($difficulty - 1) * 2) + 1] = [LOOT_GAMEOBJECT, $lgo['lootId'], $mapType ? $langref[$difficulty * ($mapType == 1 ? -1 : 1)] : '$LANG.tab_drops', 'drops-object-'.$difficulty, [], '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")'];
ksort($sourceFor);
foreach ($sourceFor as [$lootTpl, $lootId, $tabName, $tabId, $hiddenCols, $note])
foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d ORDER BY `difficulty` ASC', $id) as $difficulty => $lgo)
{
$creatureLoot = new Loot();
if ($creatureLoot->getByContainer($lootTpl, $lootId))
$sourceFor[1][1][$getBit($mapType, $difficulty)] = $lgo['lootId'];
$sourceFor[1][5] = $sourceFor[1][5] ?: '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")';
}
if ($lootId = $this->altNPCs->getField('lootId'))
$sourceFor[0][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId;
if ($lootId = $this->altNPCs->getField('pickpocketLootId'))
$sourceFor[2][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId;
if ($lootId = $this->altNPCs->getField('skinLootId'))
$sourceFor[3][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId;
}
}
foreach ($sourceFor as [$lootTpl, $lootEntries, $tabName, $tabId, $hiddenCols, $note])
{
$creatureLoot = new LootByContainer();
if ($creatureLoot->getByContainer($lootTpl, $lootEntries))
{
$extraCols = $creatureLoot->extraCols;
$extraCols[] = '$Listview.extraCols.percent';
array_push($extraCols, '$Listview.extraCols.count', '$Listview.extraCols.percent');
if (count($lootEntries) > 1)
$extraCols[] = '$Listview.extraCols.mode';
$hiddenCols[] = 'count';
$this->extendGlobalData($creatureLoot->jsGlobals);
$tabData = array(
'data' => $creatureLoot->getResult(),
'name' => $tabName,
'id' => $tabId,
'name' => $tabName,
'extraCols' => array_unique($extraCols),
'hiddenCols' => $hiddenCols ?: null,
'sort' => ['-percent', 'name']
'sort' => ['-percent', 'name'],
'_totalCount' => 10000,
'computeDataFunc' => '$Listview.funcBox.initLootTable',
'onAfterCreate' => '$Listview.funcBox.addModeIndicator',
);
if ($note)
$tabData['note'] = $note;
else if ($lootTpl == LOOT_SKINNING)
else if ($lootTpl == Loot::SKINNING)
$tabData['note'] = '<b>'.Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill($skinTab[2], $this->subject->getField('maxLevel') * 5), Lang::FMT_HTML).'</b>';
$this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile));
@@ -848,7 +876,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache
// base NPC
if ($base = $this->getRepForId([$this->typeId], $spilledParents))
$reputation[] = [Lang::npc('modes', 1, 0), $base];
$reputation[] = [Lang::game('modes', 1, 0), $base];
// difficulty dummys
if ($dummyIds && ($mapType == 1 || $mapType == 2))
@@ -862,7 +890,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache
// apply by difficulty
foreach ($alt as $mode => $dat)
$reputation[] = [Lang::npc('modes', $mapType, $mode), $dat];
$reputation[] = [Lang::game('modes', $mapType, $mode), $dat];
}
// get spillover factions and apply
@@ -964,7 +992,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache
if (!$this->altNPCs->getEntry($id))
continue;
$m = Lang::npc('modes', $mapType, $mode);
$m = Lang::game('modes', $mapType, $mode);
// Health
$health = $this->altNPCs->getBaseStats('health');

View File

@@ -22,6 +22,9 @@ class ObjectBaseResponse extends TemplateResponse implements ICache
public ?Book $book = null;
public ?array $relBoss = null;
private array $difficulties = [];
private int $mapType = 0;
private GameObjectList $subject;
public function __construct(string $id)
@@ -61,6 +64,37 @@ class ObjectBaseResponse extends TemplateResponse implements ICache
array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('object')));
/**********************/
/* Determine Map Type */
/**********************/
if ($objectdifficulty = DB::Aowow()->select( // has difficulty versions of itself
'SELECT `normal10` AS "0", `normal25` AS "1",
`heroic10` AS "2", `heroic25` AS "3",
`mapType` AS ARRAY_KEY
FROM ?_objectdifficulty
WHERE `normal10` = ?d OR `normal25` = ?d OR
`heroic10` = ?d OR `heroic25` = ?d',
$this->typeId, $this->typeId, $this->typeId, $this->typeId
))
{
$this->mapType = key($objectdifficulty);
$this->difficulties = array_pop($objectdifficulty);
}
else if ($maps = DB::Aowow()->selectCell('SELECT IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::OBJECT, $this->typeId))
{
$this->mapType = match ((int)DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps))
{
// MAP_TYPE_DUNGEON,
MAP_TYPE_DUNGEON_HC => 1,
// MAP_TYPE_RAID,
MAP_TYPE_MMODE_RAID,
MAP_TYPE_MMODE_RAID_HC => 2,
default => 0
};
}
/***********/
/* Infobox */
/***********/
@@ -198,6 +232,11 @@ class ObjectBaseResponse extends TemplateResponse implements ICache
if (Lang::getLocale() != Locale::EN)
$infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]';
// used in mode
foreach ($this->difficulties as $n => $id)
if ($id == $this->typeId)
$infobox[] = Lang::game('mode').Lang::game('modes', $this->mapType, $n);
// AI
if (User::isInGroup(U_GROUP_EMPLOYEE))
if ($_ = $this->subject->getField('ScriptOrAI'))
@@ -402,12 +441,37 @@ class ObjectBaseResponse extends TemplateResponse implements ICache
// tab: contains
if ($_ = $this->subject->getField('lootId'))
{
$goLoot = new Loot();
if ($goLoot->getByContainer(LOOT_GAMEOBJECT, $_))
// check if loot_link entry exists (only difficulty: 1)
if ($npcId = DB::Aowow()->selectCell('SELECT `npcId` FROM ?_loot_link WHERE `objectId` = ?d AND `difficulty` = 1', $this->typeId))
{
// get id set of npc
$lootEntries = DB::Aowow()->selectCol(
'SELECT ll.`difficulty` AS ARRAY_KEY, o.`lootId`
FROM ?_creature c
LEFT JOIN ?_loot_link ll ON ll.`npcId` IN (c.`id`, c.`difficultyEntry1`, c.`difficultyEntry2`, c.`difficultyEntry3`)
LEFT JOIN ?_objects o ON o.`id` = ll.`objectId`
WHERE c.`id` = ?d
ORDER BY ll.`difficulty` ASC',
$npcId
);
if ($this->mapType == 2 || count($lootEntries) > 2) // always raid
$lootEntries = array_combine(array_map(fn($x) => 1 << (2 + $x), array_keys($lootEntries)), array_values($lootEntries));
else if ($this->mapType == 1 || count($lootEntries) == 2) // dungeon or raid, assume dungeon
$lootEntries = array_combine(array_map(fn($x) => 1 << (2 - $x), array_keys($lootEntries)), array_values($lootEntries));
}
else
$lootEntries = [4 => $_];
$goLoot = new LootByContainer();
if ($goLoot->getByContainer(Loot::GAMEOBJECT, $lootEntries))
{
$extraCols = $goLoot->extraCols;
$extraCols[] = '$Listview.extraCols.percent';
$hiddenCols = ['source', 'side', 'slot', 'reqlevel'];
array_push($extraCols, '$Listview.extraCols.count', '$Listview.extraCols.percent');
if (count($lootEntries) > 1)
$extraCols[] = '$Listview.extraCols.mode';
$hiddenCols = ['source', 'side', 'slot', 'reqlevel', 'count'];
$this->extendGlobalData($goLoot->jsGlobals);
$lootResult = $goLoot->getResult();
@@ -426,7 +490,11 @@ class ObjectBaseResponse extends TemplateResponse implements ICache
'name' => '$LANG.tab_contains',
'sort' => ['-percent', 'name'],
'extraCols' => array_unique($extraCols),
'hiddenCols' => $hiddenCols ?: null
'hiddenCols' => $hiddenCols ?: null,
'sort' => ['-percent', 'name'],
'_totalCount' => 10000,
'computeDataFunc' => '$Listview.funcBox.initLootTable',
'onAfterCreate' => '$Listview.funcBox.addModeIndicator',
), ItemList::$brickFile));
}
}
@@ -471,6 +539,51 @@ class ObjectBaseResponse extends TemplateResponse implements ICache
), GameObjectList::$brickFile));
}
// tab: see also
if ($this->difficulties)
{
$conditions = array(
'AND',
['id', $this->difficulties],
['id', $this->typeId, '!']
);
$saObjects = new GameObjectList($conditions);
if (!$saObjects->error)
{
$data = $saObjects->getListviewData();
if ($this->difficulties)
{
$saE = ['$Listview.extraCols.mode'];
foreach ($data as $id => &$d)
{
if (($modeBit = array_search($id, $this->difficulties)) !== false)
{
if ($this->mapType)
$d['modes'] = ['mode' => 1 << ($modeBit + 3)];
else
$d['modes'] = ['mode' => 2 - $modeBit];
}
else
$d['modes'] = ['mode' => 0];
}
}
$tabData = array(
'data' => $data,
'id' => 'see-also',
'name' => '$LANG.tab_seealso',
'visibleCols' => ['level'],
);
if (isset($saE))
$tabData['extraCols'] = $saE;
$this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile));
}
}
// tab: Same model as
$sameModel = new GameObjectList(array(['displayId', $this->subject->getField('displayId')], ['id', $this->typeId, '!']));
if (!$sameModel->error)

View File

@@ -518,8 +518,8 @@ class QuestBaseResponse extends TemplateResponse implements ICache
// todo (med): this double list creation very much sucks ...
$getItemSource = function ($itemId, $method = 0) use (&$mapNPCs, &$mapGOs)
{
$lootTabs = new Loot();
if ($lootTabs->getByItem($itemId))
$lootTabs = new LootByItem($itemId);
if ($lootTabs->getByItem())
{
/*
todo (med): sanity check:
@@ -1175,8 +1175,8 @@ class QuestBaseResponse extends TemplateResponse implements ICache
$this->mail['header'][1] = Lang::mail('mailBy', [$senderTypeId, $ti]);
// while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item.
$mailLoot = new Loot();
if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId))
$mailLoot = new LootByContainer();
if ($mailLoot->getByContainer(Loot::MAIL, [$rmtId]))
{
$this->extendGlobalData($mailLoot->jsGlobals);
foreach ($mailLoot->getResult() as $loot)

View File

@@ -49,6 +49,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache
private int $firstRank = 0;
private array $modelInfo = [];
private array $difficulties = [];
private int $mapType = 0;
public function __construct(string $id)
{
@@ -68,14 +69,19 @@ class SpellBaseResponse extends TemplateResponse implements ICache
$this->extendGlobalData($jsg, $extra);
$this->modelInfo = $this->subject->getModelInfo($this->typeId);
$this->difficulties = DB::Aowow()->selectRow( // has difficulty versions of itself
if ($spelldifficulty = DB::Aowow()->select( // has difficulty versions of itself
'SELECT `normal10` AS "0", `normal25` AS "1",
`heroic10` AS "2", `heroic25` AS "3"
`heroic10` AS "2", `heroic25` AS "3",
`mapType` AS ARRAY_KEY
FROM ?_spelldifficulty
WHERE `normal10` = ?d OR `normal25` = ?d OR
`heroic10` = ?d OR `heroic25` = ?d',
$this->typeId, $this->typeId, $this->typeId, $this->typeId
);
))
{
$this->mapType = key($spelldifficulty);
$this->difficulties = array_pop($spelldifficulty);
}
// returns self or firstRank
if ($fr = DB::World()->selectCell('SELECT `first_spell_id` FROM spell_ranks WHERE `spell_id` = ?d', $this->typeId))
@@ -445,39 +451,28 @@ class SpellBaseResponse extends TemplateResponse implements ICache
['s.name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)]
);
if ($this->difficulties)
$conditions = ['OR', ['AND', ...$conditions], ['AND', ['s.id', $this->difficulties], ['s.id', $this->typeId, '!']]];
$saSpells = new SpellList($conditions);
if (!$saSpells->error)
{
$data = $saSpells->getListviewData();
if ($this->difficulties) // needs a way to distinguish between dungeon and raid :x; creature using this -> map -> areaType?
if ($this->difficulties)
{
$saE = ['$Listview.extraCols.mode'];
foreach ($data as $id => &$d)
{
if (($modeBit = array_search($id, $this->difficulties)) !== false)
{
if ($this->mapType)
$d['modes'] = ['mode' => 1 << ($modeBit + 3)];
else
$d['modes'] = ['mode' => 2 - $modeBit];
}
else
$d['modes'] = ['mode' => 0];
if ($this->difficulties[0] == $id) // b0001000
{
if (!$this->difficulties[2] && !$this->difficulties[3])
$d['modes']['mode'] |= 0x2;
else
$d['modes']['mode'] |= 0x8;
}
if ($this->difficulties[1] == $id) // b0010000
{
if (!$this->difficulties[2] && !$this->difficulties[3])
$d['modes']['mode'] |= 0x1;
else
$d['modes']['mode'] |= 0x10;
}
if ($this->difficulties[2] == $id) // b0100000
$d['modes']['mode'] |= 0x20;
if ($this->difficulties[3] == $id) // b1000000
$d['modes']['mode'] |= 0x40;
}
}
@@ -649,8 +644,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache
// tab: contains
// spell_loot_template
$spellLoot = new Loot();
if ($spellLoot->getByContainer(LOOT_SPELL, $this->typeId))
$spellLoot = new LootByContainer();
if ($spellLoot->getByContainer(Loot::SPELL, [$this->typeId]))
{
$this->extendGlobalData($spellLoot->jsGlobals);
@@ -662,7 +657,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache
'name' => '$LANG.tab_contains',
'id' => 'contains',
'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'],
'extraCols' => array_unique($extraCols)
'extraCols' => array_unique($extraCols),
'computeDataFunc' => '$Listview.funcBox.initLootTable'
), ItemList::$brickFile));
}
@@ -991,7 +987,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache
}
// tab: taught by npc
if ($this->subject->getSources($s) && in_array(SRC_TRAINER, $s))
if ($this->subject->getRawSource(SRC_TRAINER))
{
$trainers = DB::World()->select(
'SELECT cdt.`CreatureId` AS ARRAY_KEY, ts.`ReqSkillLine` AS "reqSkillId", ts.`ReqSkillRank` AS "reqSkillValue", ts.`ReqLevel` AS "reqLevel", ts.`ReqAbility1` AS "reqSpellId1", ts.`reqAbility2` AS "reqSpellId2"
@@ -2423,13 +2419,10 @@ class SpellBaseResponse extends TemplateResponse implements ICache
}
// accquisition.. 10: starter spell; 7: discovery
if ($this->subject->getSources($s))
{
if (in_array(SRC_STARTER, $s))
if ($this->subject->getRawSource(SRC_STARTER))
$infobox[] = Lang::spell('starter');
else if (in_array(SRC_DISCOVERY, $s))
else if ($this->subject->getRawSource(SRC_DISCOVERY))
$infobox[] = Lang::spell('discovered');
}
// training cost
if ($cost = $this->subject->getField('trainingCost'))
@@ -2462,7 +2455,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache
// used in mode
foreach ($this->difficulties as $n => $id)
if ($id == $this->typeId)
$infobox[] = Lang::game('mode').Lang::game('modes', $n);
$infobox[] = Lang::game('mode').Lang::game('modes', $this->mapType, $n);
// Creature Type from Aura: Shapeshift
foreach ($this->modelInfo as $mI)

View File

@@ -565,6 +565,48 @@ class ZoneBaseResponse extends TemplateResponse implements ICache
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
// tab: Drops
if (in_array($this->subject->getField('category'), [MAP_TYPE_DUNGEON, MAP_TYPE_RAID]))
{
// Issue 1 - if the bosses drop items that are also sold by vendors moreZoneId will be 0 as vendor location and boss location are likely in conflict with each other
// Issue 2 - if the boss/chest isn't spawned the loot will not show up
$items = new ItemList(array(Cfg::get('SQL_LIMIT_NONE'), ['src.moreZoneId', $this->typeId], ['src.src2', 0, '>'], ['quality', ITEM_QUALITY_UNCOMMON, '>=']), ['calcTotal' => true]);
$data = $items->getListviewData();
$subTabs = false;
foreach ($items->iterate() as $id => $__)
{
$src = $items->getRawSource(SRC_DROP);
$map = ($items->getField('moreMask') ?: 0) & (SRC_FLAG_DUNGEON_DROP | SRC_FLAG_RAID_DROP);
if (!$src || !$map)
continue;
$subTabs = true;
if ($map & SRC_FLAG_RAID_DROP)
$mode = ($src[0] << 3);
else
$mode = ($src[0] & 0x1 ? 0x2 : 0) | ($src[0] & 0x2 ? 0x1 : 0);
$data[$id] += ['modes' => ['mode' => $mode]];
}
$tabData = array(
'data' => $data,
'id' => 'drops',
'name' => '$LANG.tab_drops',
'extraCols' => $subTabs ? ['$Listview.extraCols.mode'] : null,
'computeDataFunc' => '$Listview.funcBox.initLootTable',
'onAfterCreate' => $subTabs ? '$Listview.funcBox.addModeIndicator' : null
);
if (!is_null(ItemListFilter::getCriteriaIndex(16, $this->typeId)))
$tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=16;crs='.$this->typeId.';crv=0');
$this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF));
$this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile));
}
// tab: NPCs
if ($cSpawns && !$creatureSpawns->error)
{
@@ -683,8 +725,8 @@ class ZoneBaseResponse extends TemplateResponse implements ICache
// tab: achievements
// tab: fished in zone
$fish = new Loot();
if ($fish->getByContainer(LOOT_FISHING, $this->typeId))
$fish = new LootByContainer();
if ($fish->getByContainer(Loot::FISHING, [$this->typeId]))
{
$this->extendGlobalData($fish->jsGlobals);
$xCols = array_merge(['$Listview.extraCols.percent'], $fish->extraCols);
@@ -701,7 +743,8 @@ class ZoneBaseResponse extends TemplateResponse implements ICache
'id' => 'fishing',
'extraCols' => array_unique($xCols),
'hiddenCols' => ['side'],
'note' => $note
'note' => $note,
'computeDataFunc' => '$Listview.funcBox.initLootTable'
), ItemList::$brickFile));
}

View File

@@ -398,22 +398,22 @@ class Conditions
public static function lootTableToConditionSource(string $lootTable) : int
{
switch ($lootTable)
return match ($lootTable)
{
case LOOT_FISHING: return self::SRC_FISHING_LOOT_TEMPLATE;
case LOOT_CREATURE: return self::SRC_CREATURE_LOOT_TEMPLATE;
case LOOT_GAMEOBJECT: return self::SRC_GAMEOBJECT_LOOT_TEMPLATE;
case LOOT_ITEM: return self::SRC_ITEM_LOOT_TEMPLATE;
case LOOT_DISENCHANT: return self::SRC_DISENCHANT_LOOT_TEMPLATE;
case LOOT_PROSPECTING: return self::SRC_PROSPECTING_LOOT_TEMPLATE;
case LOOT_MILLING: return self::SRC_MILLING_LOOT_TEMPLATE;
case LOOT_PICKPOCKET: return self::SRC_PICKPOCKETING_LOOT_TEMPLATE;
case LOOT_SKINNING: return self::SRC_SKINNING_LOOT_TEMPLATE;
case LOOT_MAIL: return self::SRC_MAIL_LOOT_TEMPLATE;
case LOOT_SPELL: return self::SRC_SPELL_LOOT_TEMPLATE;
case LOOT_REFERENCE: return self::SRC_REFERENCE_LOOT_TEMPLATE;
default: return self::SRC_NONE;
}
Loot::FISHING => self::SRC_FISHING_LOOT_TEMPLATE,
Loot::CREATURE => self::SRC_CREATURE_LOOT_TEMPLATE,
Loot::GAMEOBJECT => self::SRC_GAMEOBJECT_LOOT_TEMPLATE,
Loot::ITEM => self::SRC_ITEM_LOOT_TEMPLATE,
Loot::DISENCHANT => self::SRC_DISENCHANT_LOOT_TEMPLATE,
Loot::PROSPECTING => self::SRC_PROSPECTING_LOOT_TEMPLATE,
Loot::MILLING => self::SRC_MILLING_LOOT_TEMPLATE,
Loot::PICKPOCKET => self::SRC_PICKPOCKETING_LOOT_TEMPLATE,
Loot::SKINNING => self::SRC_SKINNING_LOOT_TEMPLATE,
Loot::MAIL => self::SRC_MAIL_LOOT_TEMPLATE,
Loot::SPELL => self::SRC_SPELL_LOOT_TEMPLATE,
Loot::REFERENCE => self::SRC_REFERENCE_LOOT_TEMPLATE,
default => self::SRC_NONE
};
}
public static function extendListviewRow(array &$lvRow, int $srcType, int $groupKey, array $condition) : bool

View File

@@ -306,6 +306,11 @@ abstract class DBTypeList
$this->error = false;
}
/**
* iterate over fetched templates
*
* @return array the current template
*/
public function &iterate() : \Generator
{
if (!$this->templates)
@@ -483,7 +488,7 @@ abstract class DBTypeList
't': type [always set]
'ti': typeId [always set]
'bd': BossDrop [0; 1] [Creature / GO]
'dd': DungeonDifficulty [-2: DungeonHC; -1: DungeonNM; 1: Raid10NM; 2:Raid25NM; 3:Raid10HM; 4: Raid25HM] [Creature / GO]
'dd': DungeonDifficulty [-2: DungeonHC; -1: DungeonNM; 1: Raid10NM; 2:Raid25NM; 3:Raid10HM; 4: Raid25HM; 99: filler trash] [Creature / GO]
'q': cssQuality [Items]
'z': zone [set when all happens in here]
'p': PvP [pvpSourceId]
@@ -632,7 +637,18 @@ trait spawnHelper
$wpSum = [];
$wpIdx = 0;
$worldPos = [];
$spawns = DB::Aowow()->select("SELECT * FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0", self::$type, $this->getFoundIDs()) ?: [];
$spawns = DB::Aowow()->select(
'SELECT CASE WHEN z.`type` = ?d THEN 1
WHEN z.`type` = ?d THEN 2
WHEN z.`type` = ?d THEN 2
ELSE 0
END AS "mapType", s.*
FROM ?_spawns s
JOIN ?_zones z ON s.areaId = z.id
WHERE s.`type` = ?d AND s.`typeId` IN (?a) AND s.`posX` > 0 AND s.`posY` > 0',
MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC,
self::$type, $this->getFoundIDs()
) ?: [];
if (!$skipAdmin && User::isInGroup(U_GROUP_MODERATOR))
if ($guids = array_column(array_filter($spawns, fn($x) => $x['guid'] > 0 || $x['type'] != Type::NPC), 'guid'))
@@ -696,13 +712,13 @@ trait spawnHelper
$info[2] = Lang::game('phases').Lang::main('colon').Util::asHex($s['phaseMask']);
if ($s['spawnMask'] == 15)
$info[3] = Lang::game('mode').Lang::game('modes', -1);
$info[3] = Lang::game('mode').Lang::game('modes', 0, -1);
else if ($s['spawnMask'])
{
$_ = [];
for ($i = 0; $i < 4; $i++)
if ($s['spawnMask'] & 1 << $i)
$_[] = Lang::game('modes', $i);
$_[] = Lang::game('modes', $s['mapType'], $i);
$info[4] = Lang::game('mode').implode(', ', $_);
}
@@ -880,8 +896,13 @@ trait profilerHelper
trait sourceHelper
{
protected $sources = [];
protected $sourceMore = null;
protected array $sources = [];
protected ?array $sourceMore = null;
public function getRawSource(int $src) : array
{
return $this->sources[$this->id][$src] ?? [];
}
public function getSources(?array &$s = [], ?array &$sm = []) : bool
{
@@ -923,7 +944,9 @@ trait sourceHelper
10H 0b0100 2 0b011
25H 0b1000 3 0b100
*/
if ($this->curTpl['moreMask'] & SRC_FLAG_DUNGEON_DROP)
if ($this->curTpl['moreMask'] & SRC_FLAG_COMMON)
$sm['dd'] = 99;
else if ($this->curTpl['moreMask'] & SRC_FLAG_DUNGEON_DROP)
$sm['dd'] = $this->sources[$this->id][SRC_DROP][0] * -1;
else if ($this->curTpl['moreMask'] & SRC_FLAG_RAID_DROP)
{

View File

@@ -57,8 +57,8 @@ class AchievementList extends DBTypeList
if ($rewards[$_id]['MailTemplateID'])
{
// using class Loot creates an inifinite loop cirling between Loot, ItemList and SpellList or something
// $mailSrc = new Loot();
// $mailSrc->getByContainer(LOOT_MAIL, $rewards[$_id]['MailTemplateID']);
// $mailSrc = new LootByContainer();
// $mailSrc->getByContainer(Loot::MAIL, $rewards[$_id]['MailTemplateID']);
// foreach ($mailSrc->iterate() as $loot)
// $_curTpl['rewards'][] = [Type::ITEM, $loot['id']];

View File

@@ -59,7 +59,7 @@ class CreatureList extends DBTypeList
$row3 = [Lang::game('level')];
$fam = $this->curTpl['family'];
if (!($this->curTpl['typeFlags'] & 0x4))
if (!($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB))
{
$level = $this->curTpl['minLevel'];
if ($level != $this->curTpl['maxLevel'])
@@ -154,6 +154,21 @@ class CreatureList extends DBTypeList
return ($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS) || ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB && $this->curTpl['rank']);
}
public function isMineable() : bool
{
return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING);
}
public function isGatherable() : bool
{
return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM);
}
public function isSalvageable() : bool
{
return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING);
}
public function getListviewData(int $addInfoMask = 0x0) : array
{
/* looks like this data differs per occasion

View File

@@ -70,7 +70,7 @@ class GameObjectList extends DBTypeList
$data[$this->id] = array(
'id' => $this->id,
'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW),
'type' => $this->curTpl['typeCat'],
'type' => $this->getField('typeCat'),
'location' => $this->getSpawns(SPAWNINFO_ZONES)
);

View File

@@ -1688,7 +1688,6 @@ class ItemList extends DBTypeList
'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'],
@@ -2486,14 +2485,14 @@ class ItemListFilter extends Filter
return null;
$refResults = [];
$newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `item` = ?d AND `reference` = 0', LOOT_REFERENCE, $crs);
$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);
$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);
$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];
}

View File

@@ -385,20 +385,6 @@ define('MAX_LEVEL', 80);
define('MAX_SKILL', 450);
define('WOW_BUILD', 12340);
// Loot handles
define('LOOT_FISHING', 'fishing_loot_template');
define('LOOT_CREATURE', 'creature_loot_template');
define('LOOT_GAMEOBJECT', 'gameobject_loot_template');
define('LOOT_ITEM', 'item_loot_template');
define('LOOT_DISENCHANT', 'disenchant_loot_template');
define('LOOT_PROSPECTING', 'prospecting_loot_template');
define('LOOT_MILLING', 'milling_loot_template');
define('LOOT_PICKPOCKET', 'pickpocketing_loot_template');
define('LOOT_SKINNING', 'skinning_loot_template');
define('LOOT_MAIL', 'mail_loot_template'); // used by achievements and quests
define('LOOT_SPELL', 'spell_loot_template');
define('LOOT_REFERENCE', 'reference_loot_template');
// Sides
define('SIDE_NONE', 0);
define('SIDE_ALLIANCE', 1);

View File

@@ -1,728 +0,0 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/* from TC wiki
fishing_loot_template no relation entry is linked with ID of the fishing zone or area
creature_loot_template entry many <- many creature_template lootid
gameobject_loot_template entry many <- many gameobject_template data1 Only GO type 3 (CHEST) or 25 (FISHINGHOLE)
item_loot_template entry many <- one item_template entry
disenchant_loot_template entry many <- many item_template DisenchantID
prospecting_loot_template entry many <- one item_template entry
milling_loot_template entry many <- one item_template entry
pickpocketing_loot_template entry many <- many creature_template pickpocketloot
skinning_loot_template entry many <- many creature_template skinloot Can also store minable/herbable items gathered from creatures
quest_mail_loot_template entry quest_template RewMailTemplateId
reference_loot_template entry many <- many *_loot_template reference
*/
/* 4.3 loot-example
template: 'item',
id: 'drops',
name: LANG.tab_drops,
tabs: tabsRelated,
parent: 'lkljbjkb574',
extraCols: [Listview.extraCols.count, Listview.extraCols.percent],
sort:['-percent', 'name'],
_totalCount: 448092, // total # creature killed/looted
computeDataFunc: Listview.funcBox.initLootTable,
onAfterCreate: Listview.funcBox.addModeIndicator,
data: [
{
"classs":15, // Tab Type
"commondrop":true, // loot filtered as "not noteworthy"
"id":25445,
"level":1,
"name":"7Wretched Ichor",
"slot":0,
"source":[2],
"sourcemore":[{"z":3520}],
"subclass":0, // Tab:Type
modes:{
"mode":4, // &1: heroic; &4: noteworthy(?); &8: reg10; &16: reg25; &32: hc10; &64: hc25; &128: RaidFinder
"4":{"count":363318,"outof":448092} // calculate pct chance
},
count:363318,
stack:[1,1],
pctstack:'{1: 50.0123, 2: 49.9877}'
}
]
});
*/
class Loot
{
public $jsGlobals = [];
public $extraCols = [];
private $entry = 0; // depending on the lookup itemId oder templateId
private $results = [];
private $chanceMods = [];
private $lootTemplates = array(
LOOT_REFERENCE, // internal
LOOT_ITEM, // item
LOOT_DISENCHANT, // item
LOOT_PROSPECTING, // item
LOOT_MILLING, // item
LOOT_CREATURE, // npc
LOOT_PICKPOCKET, // npc
LOOT_SKINNING, // npc (see its flags for mining, herbing, salvaging or actual skinning)
LOOT_FISHING, // zone
LOOT_GAMEOBJECT, // object (see its lockType for mining, herbing, fishing or generic looting)
LOOT_MAIL, // quest + achievement
LOOT_SPELL // spell
);
public function &iterate() : iterable
{
reset($this->results);
foreach ($this->results as $k => [, $tabData])
if ($tabData['data']) // only yield tabs with content
yield $k => $this->results[$k];
}
public function getResult() : array
{
return $this->results;
}
private function createStack(array $l) : string // issue: TC always has an equal distribution between min/max
{
if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min'])
return '';
$stack = [];
for ($i = $l['min']; $i <= $l['max']; $i++)
$stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3);
// yes, it wants a string .. how weired is that..
return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON !
}
private function storeJSGlobals(array $data) : void
{
foreach ($data as $type => $jsData)
{
foreach ($jsData as $k => $v)
{
// was already set at some point with full data
if (isset($this->jsGlobals[$type][$k]) && is_array($this->jsGlobals[$type][$k]))
continue;
$this->jsGlobals[$type][$k] = $v;
}
}
}
private function calcChance(array $refs, array $parents = []) : array
{
$retData = [];
$retKeys = [];
foreach ($refs as $rId => $ref)
{
// check for possible database inconsistencies
if (!$ref['chance'] && !$ref['isGrouped'])
trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING);
if ($ref['isGrouped'] && $ref['sumChance'] > 100)
trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING);
if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance'])
trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING);
$chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100;
// apply inherited chanceMods
if (isset($this->chanceMods[$ref['item']]))
{
$chance *= $this->chanceMods[$ref['item']][0];
$chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]);
}
// save chance for parent-ref
$this->chanceMods[$rId] = [$chance, $ref['multiplier']];
// refTemplate doesn't point to a new ref -> we are done
if (!in_array($rId, $parents))
{
$data = array(
'percent' => $chance,
'stack' => [$ref['min'], $ref['max']],
'count' => 1 // ..and one for the sort script
);
if ($_ = self::createStack($ref))
$data['pctstack'] = $_;
// sort highest chances first
$i = 0;
for (; $i < count($retData); $i++)
if ($retData[$i]['percent'] < $data['percent'])
break;
array_splice($retData, $i, 0, [$data]);
array_splice($retKeys, $i, 0, [$rId]);
}
}
return array_combine($retKeys, $retData);
}
private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : array
{
$loot = [];
$rawItems = [];
if (!$tableName || !$lootId)
return [null, null];
$rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP);
if (!$rows)
return [null, null];
$groupChances = [];
$nGroupEquals = [];
$cnd = new Conditions();
foreach ($rows as $entry)
{
$set = array(
'quest' => $entry['QuestRequired'],
'group' => $entry['GroupId'],
'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0,
'realChanceMod' => $baseChance,
'groupChance' => 0
);
if ($entry['QuestRequired'])
foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0',
$entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId)
$cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true);
// if ($entry['LootMode'] > 1)
// {
$buff = [];
for ($i = 0; $i < 8; $i++)
if ($entry['LootMode'] & (1 << $i))
$buff[] = $i + 1;
$set['mode'] = implode(', ', $buff);
// }
// else
// $set['mode'] = 0;
/*
modes:{"mode":8,"4":{"count":7173,"outof":17619},"8":{"count":7173,"outof":10684}}
ignore lootmodes from sharedDefines.h use different creatures/GOs from each template
modes.mode = b6543210
||||||'dungeon heroic
|||||'dungeon normal
||||'<empty>
|||'10man normal
||'25man normal
|'10man heroic
'25man heroic
*/
if ($entry['Reference'])
{
// bandaid.. remove when propperly handling lootmodes
if (!in_array($entry['Reference'], $handledRefs))
{ // todo (high): find out, why i used this in the first place. (don't do drugs, kids)
[$data, $raw] = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100);
$handledRefs[] = $entry['Reference'];
$loot = array_merge($loot, $data);
$rawItems = array_merge($rawItems, $raw);
}
$set['reference'] = $entry['Reference'];
$set['multiplier'] = $entry['MaxCount'];
}
else
{
$rawItems[] = $entry['Item'];
$set['content'] = $entry['Item'];
$set['min'] = $entry['MinCount'];
$set['max'] = $entry['MaxCount'];
}
if (!isset($groupChances[$entry['GroupId']]))
{
$groupChances[$entry['GroupId']] = 0;
$nGroupEquals[$entry['GroupId']] = 0;
}
if ($set['quest'] || !$set['group'])
$set['groupChance'] = $entry['Chance'];
else if ($entry['GroupId'] && !$entry['Chance'])
{
$nGroupEquals[$entry['GroupId']]++;
$set['groupChance'] = &$groupChances[$entry['GroupId']];
}
else if ($entry['GroupId'] && $entry['Chance'])
{
$set['groupChance'] = $entry['Chance'];
if (!$entry['Reference'])
{
if (empty($groupChances[$entry['GroupId']]))
$groupChances[$entry['GroupId']] = 0;
$groupChances[$entry['GroupId']] += $entry['Chance'];
}
}
else // shouldn't have happened
{
trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING);
continue;
}
$loot[] = $set;
}
foreach (array_keys($nGroupEquals) as $k)
{
$sum = $groupChances[$k];
if (!$sum)
$sum = 0;
else if ($sum >= 100.01)
{
trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING);
$sum = 100;
}
// is applied as backReference to items with 0-chance
$groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1);
}
if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare())
{
self::storeJSGlobals($cnd->getJsGlobals());
$cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content');
}
return [$loot, array_unique($rawItems)];
}
public function getByContainer(string $table, int $entry): bool
{
$this->entry = intVal($entry);
if (!in_array($table, $this->lootTemplates) || !$this->entry)
return false;
/*
// if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT])
// iterate over the 4 available difficulties and assign modes
modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
*/
$handledRefs = [];
[$lootRows, $itemIds] = self::getByContainerRecursive($table, $this->entry, $handledRefs);
if (!$lootRows)
return false;
$items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE')));
self::storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$foo = $items->getListviewData();
// assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
foreach ($lootRows as $loot)
{
$base = array(
'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3),
'group' => $loot['group'],
'quest' => $loot['quest'],
'count' => 1 // satisfies the sort-script
);
if ($_ = $loot['mode'])
$base['mode'] = $_;
if ($_ = $loot['parentRef'])
$base['reference'] = $_;
if (isset($loot['condition']))
$base['condition'] = $loot['condition'];
if ($_ = self::createStack($loot))
$base['pctstack'] = $_;
if (empty($loot['reference'])) // regular drop
{
if (!isset($foo[$loot['content']]))
{
trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING);
continue;
}
if (!User::isInGroup(U_GROUP_EMPLOYEE))
{
if (!isset($this->results[$loot['content']]))
$this->results[$loot['content']] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]);
else
$this->results[$loot['content']]['percent'] += $base['percent'];
}
else // in case of limited trash loot, check if $foo[<itemId>] exists
$this->results[] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]);
}
else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop
{
$data = array(
'id' => $loot['reference'],
'name' => '@REFERENCE: '.$loot['reference'],
'icon' => 'trade_engineering',
'stack' => [$loot['multiplier'], $loot['multiplier']]
);
$this->results[] = array_merge($base, $data);
$this->jsGlobals[Type::ITEM][$loot['reference']] = $data;
}
}
// move excessive % to extra loot
if (!User::isInGroup(U_GROUP_EMPLOYEE))
{
foreach ($this->results as &$_)
{
if ($_['percent'] <= 100)
continue;
while ($_['percent'] > 200)
{
$_['stack'][0]++;
$_['stack'][1]++;
$_['percent'] -= 100;
}
$_['stack'][1]++;
$_['percent'] = 100;
}
}
else
{
$fields = ['mode', 'reference'];
$base = [];
$set = 0;
foreach ($this->results as $foo)
{
foreach ($fields as $idx => $field)
{
$val = isset($foo[$field]) ? $foo[$field] : 0;
if (!isset($base[$idx]))
$base[$idx] = $val;
else if ($base[$idx] != $val)
$set |= 1 << $idx;
}
if ($set == (pow(2, count($fields)) - 1))
break;
}
$this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')";
foreach ($fields as $idx => $field)
if ($set & (1 << $idx))
$this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')";
}
return true;
}
public function getByItem(int $entry, int $maxResults = -1, array $lootTableList = []) : bool
{
$this->entry = $entry;
if (!$this->entry)
return false;
if ($maxResults < 0)
$maxResults = Cfg::get('SQL_LIMIT_DEFAULT');
// [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols]
$tabsFinal = array(
[Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
[Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
[Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
[Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
[Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
[Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
[Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
[Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
[Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
[Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
[Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
[Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
[Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
[Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
[Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []],
[Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
);
$refResults = [];
$query = 'SELECT
lt1.entry AS ARRAY_KEY,
IF(lt1.reference = 0, lt1.item, lt1.reference) AS item,
lt1.chance,
SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems,
SUM(IF(lt2.reference = 0, lt2.chance, 0)) AS sumChance,
IF(lt1.groupid > 0, 1, 0) AS isGrouped,
IF(lt1.reference = 0, lt1.mincount, 1) AS min,
IF(lt1.reference = 0, lt1.maxcount, 1) AS max,
IF(lt1.reference > 0, lt1.maxcount, 1) AS multiplier
FROM
?# lt1
LEFT JOIN
?# lt2 ON lt1.entry = lt2.entry AND lt1.groupid = lt2.groupid
WHERE
%s
GROUP BY lt2.entry, lt2.groupid';
/*
get references containing the item
*/
$newRefs = DB::World()->select(
sprintf($query, 'lt1.item = ?d AND lt1.reference = 0'),
LOOT_REFERENCE, LOOT_REFERENCE,
$this->entry
);
/* i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions.
if ($newRefs)
{
$cnd = new Conditions();
if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE))
if ($cnd->toListviewColumn($newRefs, $x, $this->entry))
self::storejsGlobals($cnd->getJsGlobals());
}
*/
while ($newRefs)
{
$curRefs = $newRefs;
$newRefs = DB::World()->select(
sprintf($query, 'lt1.reference IN (?a)'),
LOOT_REFERENCE, LOOT_REFERENCE,
array_keys($curRefs)
);
$refResults += $this->calcChance($curRefs, array_column($newRefs, 'item'));
}
/*
search the real loot-templates for the itemId and gathered refds
*/
foreach ($this->lootTemplates as $lootTemplate)
{
if ($lootTableList && !in_array($lootTemplate, $lootTableList))
continue;
if ($lootTemplate == LOOT_REFERENCE)
continue;
$result = $this->calcChance(DB::World()->select(
sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'),
$lootTemplate, $lootTemplate,
$refResults ? array_keys($refResults) : DBSIMPLE_SKIP,
$this->entry
));
// do not skip here if $result is empty. Additional loot for spells and quest is added separately
// format for actual use
foreach ($result as $k => $v)
{
unset($result[$k]);
$v['percent'] = round($v['percent'] * 100, 3);
$result[abs($k)] = $v;
}
// cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first
// screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather)
$ids = array_slice(array_keys($result), 0, $maxResults);
switch ($lootTemplate)
{
case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break;
case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break;
case LOOT_SKINNING: $field = 'skinLootId'; $tabId = -6; break; // assigned later
case LOOT_PROSPECTING: $field = 'id'; $tabId = 2; break;
case LOOT_MILLING: $field = 'id'; $tabId = 3; break;
case LOOT_ITEM: $field = 'id'; $tabId = 0; break;
case LOOT_DISENCHANT: $field = 'disenchantId'; $tabId = 1; break;
case LOOT_FISHING: $field = 'id'; $tabId = 11; break; // subAreas are currently ignored
case LOOT_GAMEOBJECT:
if (!$ids)
continue 2;
$srcObj = new GameObjectList(array(['lootId', $ids]));
if ($srcObj->error)
continue 2;
$srcData = $srcObj->getListviewData();
foreach ($srcObj->iterate() as $curTpl)
{
switch ($curTpl['typeCat'])
{
case 25: $tabId = 15; break; // fishing node
case -3: $tabId = 14; break; // herb
case -4: $tabId = 13; break; // vein
default: $tabId = 12; break; // general chest loot
}
$tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]);
$tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
if ($tabId != 15)
$tabsFinal[$tabId][6][] = 'skill';
}
continue 2;
case LOOT_MAIL:
// quest part
$conditions = array(['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry],
['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry],
'OR');
if ($ids)
$conditions[] = ['rewardMailTemplateId', $ids];
$srcObj = new QuestList($conditions);
if (!$srcObj->error)
{
self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
$srcData = $srcObj->getListviewData();
foreach ($srcObj->iterate() as $_)
$tabsFinal[10][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
}
// achievement part
$conditions = array(['itemExtra', $this->entry]);
if ($ar = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE ItemID = ?d{ OR MailTemplateID IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP))
array_push($conditions, ['id', $ar], 'OR');
$srcObj = new AchievementList($conditions);
if (!$srcObj->error)
{
self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
$srcData = $srcObj->getListviewData();
foreach ($srcObj->iterate() as $_)
$tabsFinal[17][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
$tabsFinal[17][5][] = 'rewards';
$tabsFinal[17][6][] = 'category';
}
continue 2;
case LOOT_SPELL:
$conditions = array(
'OR',
['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]],
['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]],
['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]],
);
if ($ids)
$conditions[] = ['id', $ids];
$srcObj = new SpellList($conditions);
if (!$srcObj->error)
{
self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$srcData = $srcObj->getListviewData();
if (!empty($result))
$tabsFinal[16][4][] = '$Listview.extraCols.percent';
if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'))
$tabsFinal[16][6][] = 'reagents';
foreach ($srcObj->iterate() as $_)
$tabsFinal[16][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]);
}
continue 2;
}
if (!$ids)
continue;
$parentData = [];
switch ($tabsFinal[abs($tabId)][0])
{
case TYPE::NPC: // new CreatureList
if ($baseIds = DB::Aowow()->selectCol(
'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry1 IN (?a) UNION
SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry2 IN (?a) UNION
SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry3 IN (?a)',
$ids, $ids, $ids))
{
$parentObj = new CreatureList(array(['id', $baseIds]));
if (!$parentObj->error)
{
self::storeJSGlobals($parentObj->getJSGlobals());
$parentData = $parentObj->getListviewData();
$ids = array_diff($ids, $baseIds);
}
}
case Type::ITEM: // new ItemList
case Type::ZONE: // new ZoneList
$srcObj = Type::newList($tabsFinal[abs($tabId)][0], array([$field, $ids]));
if (!$srcObj->error)
{
$srcData = $srcObj->getListviewData();
self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
foreach ($srcObj->iterate() as $curTpl)
{
if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM)
$tabId = 9;
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING)
$tabId = 8;
else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING)
$tabId = 7;
else if ($tabId < 0)
$tabId = abs($tabId); // general case (skinning)
if (($p = $srcObj->getField('parentId')) && ($d = $parentData[$p] ?? null))
$tabsFinal[$tabId][1][] = array_merge($d, $result[$srcObj->getField($field)]);
else
$tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]);
$tabsFinal[$tabId][4][] = '$Listview.extraCols.percent';
}
}
break;
}
}
foreach ($tabsFinal as $tabId => $data)
{
$tabData = array(
'data' => $data[1],
'name' => $data[2],
'id' => $data[3]
);
if ($data[4])
$tabData['extraCols'] = array_unique($data[4]);
if ($data[5])
$tabData['hiddenCols'] = array_unique($data[5]);
if ($data[6])
$tabData['visibleCols'] = array_unique($data[6]);
$this->results[$tabId] = [Type::getFileString($data[0]), $tabData];
}
return true;
}
}
?>

View File

@@ -0,0 +1,71 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
/* NOTE!
*
* TrinityCore uses "mode", a bitmask in a loot template, to distinguish dynamic properties of the template. i.e. entries get enabled if the boss fight progresses in a certain way.
* WH/we uses "mode", different loot templates, to describe the raid/dungeon difficulty
*
* try to not mix this shit up
*/
abstract class Loot
{
// Loot handles
public const FISHING = 'fishing_loot_template'; // fishing_loot_template (no relation entry is linked with ID of the fishing zone or area)
public const CREATURE = 'creature_loot_template'; // creature_loot_template entry many <- many creature_template lootid
public const GAMEOBJECT = 'gameobject_loot_template'; // gameobject_loot_template entry many <- many gameobject_template data1 (see its lockType for mining, herbing, fishing or generic looting)
public const ITEM = 'item_loot_template'; // item_loot_template entry many <- one item_template entry
public const DISENCHANT = 'disenchant_loot_template'; // disenchant_loot_template entry many <- many item_template DisenchantID
public const PROSPECTING = 'prospecting_loot_template'; // prospecting_loot_template entry many <- one item_template entry
public const MILLING = 'milling_loot_template'; // milling_loot_template entry many <- one item_template entry
public const PICKPOCKET = 'pickpocketing_loot_template'; // pickpocketing_loot_template entry many <- many creature_template pickpocketloot
public const SKINNING = 'skinning_loot_template'; // skinning_loot_template entry many <- many creature_template skinloot (see the creatures flags for mining, herbing, salvaging or actual skinning)
public const MAIL = 'mail_loot_template'; // mail_loot_template entry quest_template RewMailTemplateId (quest + achievement)
public const SPELL = 'spell_loot_template'; // spell_loot_template entry many <- one spell.dbc id
public const REFERENCE = 'reference_loot_template'; // reference_loot_template entry many <- many *_loot_template reference
protected const TEMPLATES = [self::REFERENCE, self::ITEM, self::DISENCHANT, self::PROSPECTING, self::CREATURE, self::MILLING, self::PICKPOCKET, self::SKINNING, self::FISHING, self::GAMEOBJECT, self::MAIL, self::SPELL];
public array $jsGlobals = [];
protected array $results = [];
/**
* builds stack info string for listview rows
* issue: TC always has an equal distribution between min/max
* and yes, it wants a string .. how weired is that..
*
* @param int $min min amount
* @param int $max max amount
* @return ?string stack info or null on error
*/
protected static function buildStack(int $min, int $max) : ?string
{
if (!$min || !$max || $max <= $min)
return null;
$stack = [];
for ($i = $min; $i <= $max; $i++)
$stack[$i] = round(100 / (1 + $max - $min), 3);
// do not replace with Util::toJSON !
return json_encode($stack, JSON_NUMERIC_CHECK);
}
/**
* @param array $data js global data to store
* @return void
*/
protected function storeJSGlobals(array $data) : void
{
Util::mergeJsGlobals($this->jsGlobals, $data);
}
}
?>

View File

@@ -0,0 +1,329 @@
<?php
namespace Aowow;
use stdClass;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class LootByContainer extends Loot
{
public array $extraCols = [];
private array $knownRefs = []; // known ref loot results (can be reused)
/**
* @return array found loot result
*/
public function getResult() : array
{
return $this->results;
}
/**
* recurse through reference loot while applying modifiers from parent container
*
* @param string $tableName a known loot template table name
* @param int $lootId a loot template entry
* @param int $groupId [optional] limit result to provided loot group
* @param float $baseChance [optional] chance multiplier passed down from parent container
* @return array [[<array>lootRows], [<int>itemIds]]
*/
private function getByContainerRecursive(string $tableName, int $lootId, int $groupId = 0, float $baseChance = 1.0) : array
{
$loot = [];
$rawItems = [];
if (!$tableName || !$lootId)
return [null, null];
$rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP);
if (!$rows)
return [null, null];
$groupChances = [];
$nGroupEquals = [];
$cnd = new Conditions();
foreach ($rows as $entry)
{
$set = array(
'quest' => $entry['QuestRequired'],
'group' => $entry['GroupId'],
'parentRef' => $tableName == self::REFERENCE ? $lootId : 0,
'realChanceMod' => $baseChance,
'groupChance' => 0
);
if ($entry['QuestRequired'])
foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0',
$entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId)
$cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true);
// TC 'mode' (dynamic loot modifier)
$buff = [];
for ($i = 0; $i < 8; $i++)
if ($entry['LootMode'] & (1 << $i))
$buff[] = $i + 1;
$set['mode'] = implode(', ', $buff);
if ($entry['Reference'])
{
if (!in_array($entry['Reference'], $this->knownRefs))
$this->knownRefs[$entry['Reference']] = $this->getByContainerRecursive(self::REFERENCE, $entry['Reference'], 0, $entry['Chance'] / 100);
[$data, $raw] = $this->knownRefs[$entry['Reference']];
$loot = array_merge($loot, $data);
$rawItems = array_merge($rawItems, $raw);
$set['reference'] = $entry['Reference'];
$set['multiplier'] = $entry['MaxCount'];
}
else
{
$rawItems[] = $entry['Item'];
$set['content'] = $entry['Item'];
$set['min'] = $entry['MinCount'];
$set['max'] = $entry['MaxCount'];
}
if (!isset($groupChances[$entry['GroupId']]))
{
$groupChances[$entry['GroupId']] = 0;
$nGroupEquals[$entry['GroupId']] = 0;
}
if ($set['quest'] || !$set['group'])
$set['groupChance'] = $entry['Chance'];
else if ($entry['GroupId'] && !$entry['Chance'])
{
$nGroupEquals[$entry['GroupId']]++;
$set['groupChance'] = &$groupChances[$entry['GroupId']];
}
else if ($entry['GroupId'] && $entry['Chance'])
{
$set['groupChance'] = $entry['Chance'];
if (!$entry['Reference'])
{
if (empty($groupChances[$entry['GroupId']]))
$groupChances[$entry['GroupId']] = 0;
$groupChances[$entry['GroupId']] += $entry['Chance'];
}
}
else // shouldn't have happened
{
trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING);
continue;
}
$loot[] = $set;
}
foreach (array_keys($nGroupEquals) as $k)
{
$sum = $groupChances[$k];
if (!$sum)
$sum = 0;
else if ($sum >= 100.01)
{
trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING);
$sum = 100;
}
// is applied as backReference to items with 0-chance
$groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1);
}
if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare())
{
$this->storeJSGlobals($cnd->getJsGlobals());
$cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content');
}
return [$loot, array_unique($rawItems)];
}
/**
* fetch loot for given loot container and optionally merge multiple container while adding mode info.
* If difficultyBit is 0, no merge will occur
*
* @param string $table a known loote template table name
* @param array $lootEntries array of [difficultyBit => entry].
* @return bool success and found loot
*/
public function getByContainer(string $table, array $lootEntries): bool
{
if (!in_array($table, self::TEMPLATES))
return false;
foreach ($lootEntries as $modeBit => $entry)
{
if (!$entry)
continue;
[$lootRows, $itemIds] = $this->getByContainerRecursive($table, $entry);
if (!$lootRows)
continue;
$items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE')));
$this->storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$itemRows = $items->getListviewData();
// assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times
foreach ($lootRows as $loot)
{
$count = ceil($loot['groupChance'] * $loot['realChanceMod'] * 100);
/* on modes...
* modes.mode is the (masked) sum of all modes where this item has been seen
* modes.mode & 1 dungeon normal
* modes.mode & 2 dungeon heroic
* modes.mode & 4 generic case (never included in mask for instanced creatures/gos or always === 4 for non-instanced creatures/gos)
* modes.mode & 8 raid 10 nh
* modes.mode & 16 raid 25 nh
* modes.mode & 32 raid 10 hc
* modes.mode & 64 raid 25 hc
*
* modes[4] is _always_ included and is the sum total over all modes:
* ex: modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}}
*/
if ($modeBit)
{
$modes = array( // emulate 'percent' with precision: 2
'mode' => $modeBit,
$modeBit => ['count' => $count, 'outof' => 10000]
);
if ($modeBit != 4)
$modes[4] = $modes[$modeBit];
// unsure: force display as noteworthy
// if (!empty($loot['content']) && !empty($itemRows[$loot['content']]) && $itemRows[$loot['content']]['name'][0] == 7 - ITEM_QUALITY_POOR)
// $modes['mode'] = 4;
// else if ($count < 100) // chance < 1%
// $modes['mode'] = 4;
// existing result row; merge modes and move on
if (!is_null($k = array_find_key($this->results, function($x) use ($loot) {
if (!empty($loot['reference']))
return $x['id'] == $loot['reference'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group'] && $x['stack'] == [$loot['multiplier'], $loot['multiplier']];
else
return $x['id'] == $loot['content'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group'];
})))
{
$this->results[$k]['modes']['mode'] |= $modes['mode'];
$this->results[$k]['modes'][$modeBit] = $modes[$modeBit];
$this->results[$k]['modes'][4]['count'] = max($modes[4]['count'], $this->results[$k]['modes'][4]['count']);
continue;
}
}
$base = array(
'count' => $count,
'outof' => 10000,
'group' => $loot['group'],
'quest' => $loot['quest'],
'mode' => $loot['mode'] ?: null, // dyn loot mode
'modes' => $modes ?? null, // difficulties
'reference' => $loot['parentRef'] ?: null,
'condition' => $loot['condition'] ?? null,
'pctstack' => self::buildStack($loot['min'] ?? 0, $loot['max'] ?? 0)
);
$base = array_filter($base, fn($x) => $x !== null);
if (empty($loot['reference'])) // regular drop
{
if ($itemRow = $itemRows[$loot['content']] ?? null)
{
$extra = ['stack' => [$loot['min'], $loot['max']]];
// unsure if correct - tag item as trash if chance < 1% and tagged as having many sources
if ($base['count'] < 100 && $items->getEntry($loot['content'])['moreMask'] & SRC_FLAG_COMMON)
$extra['commondrop'] = 1;
if (!User::isInGroup(U_GROUP_EMPLOYEE))
{
if (!isset($this->results[$loot['content']]))
$this->results[$loot['content']] = array_merge($itemRow, $base, $extra);
else
$this->results[$loot['content']]['count'] += $base['count'];
}
else
$this->results[] = array_merge($itemRow, $base, $extra);
}
else
trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING);
}
else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop
{
$data = array(
'id' => $loot['reference'],
'name' => '@REFERENCE: '.$loot['reference'],
'icon' => 'trade_engineering',
'stack' => [$loot['multiplier'], $loot['multiplier']]
);
$this->results[] = array_merge($base, $data);
$this->jsGlobals[Type::ITEM][$loot['reference']] = $data;
}
}
}
// move excessive % to extra loot
if (!User::isInGroup(U_GROUP_EMPLOYEE))
{
foreach ($this->results as &$_)
{
// remember 'count' is always relative to a base of 10000
if ($_['count'] <= 10000)
continue;
while ($_['count'] > 20000)
{
$_['stack'][0]++;
$_['stack'][1]++;
$_['count'] -= 10000;
}
$_['stack'][1]++;
$_['count'] = 10000;
}
}
else
{
$fields = [['mode', 'Dyn. Mode'], ['reference', 'Reference']];
$base = [];
$set = 0;
foreach ($this->results as $foo)
{
foreach ($fields as $idx => [$field, $title])
{
$val = $foo[$field] ?? 0;
if (!isset($base[$idx]))
$base[$idx] = $val;
else if ($base[$idx] != $val)
$set |= 1 << $idx;
}
if ($set == (pow(2, count($fields)) - 1))
break;
}
$this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')";
foreach ($fields as $idx => [$field, $title])
if ($set & (1 << $idx))
$this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".$title."', '7%', '".$field."')";
}
return !empty($this->results);
}
}
?>

View File

@@ -0,0 +1,439 @@
<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class LootByItem extends Loot
{
public const /* int */ ITEM_CONTAINED = 0;
public const /* int */ ITEM_DISENCHANTED = 1;
public const /* int */ ITEM_PROSPECTED = 2;
public const /* int */ ITEM_MILLED = 3;
public const /* int */ NPC_DROPPED = 4;
public const /* int */ NPC_PICKPOCKETED = 5;
public const /* int */ NPC_SKINNED = 6;
public const /* int */ NPC_MINED = 7;
public const /* int */ NPC_SALVAGED = 8;
public const /* int */ NPC_GATHERED = 9;
public const /* int */ QUEST_REWARD = 10;
public const /* int */ ZONE_FISHED = 11;
public const /* int */ OBJECT_CONTAINED = 12;
public const /* int */ OBJECT_MINED = 13;
public const /* int */ OBJECT_GATHERED = 14;
public const /* int */ OBJECT_FISHED = 15;
public const /* int */ SPELL_CREATED = 16;
public const /* int */ ACHIEVEMENT_REWARD = 17;
private array $chanceMods = [];
private array $listviewTabs = array( // order here determines tab order on page
// [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols]
self::NPC_DROPPED => [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []],
self::QUEST_REWARD => [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []],
self::ITEM_CONTAINED => [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []],
self::OBJECT_CONTAINED => [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []],
self::NPC_PICKPOCKETED => [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []],
self::NPC_SKINNED => [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []],
self::ITEM_DISENCHANTED => [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []],
self::ITEM_PROSPECTED => [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []],
self::ITEM_MILLED => [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []],
self::NPC_MINED => [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []],
self::NPC_SALVAGED => [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []],
self::NPC_GATHERED => [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []],
self::OBJECT_MINED => [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []],
self::OBJECT_GATHERED => [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []],
self::ZONE_FISHED => [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []],
self::OBJECT_FISHED => [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []],
self::SPELL_CREATED => [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []],
self::ACHIEVEMENT_REWARD => [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []]
);
private string $queryTemplate =
'SELECT lt1.`entry` AS ARRAY_KEY,
IF(lt1.`reference` = 0, lt1.`item`, lt1.`reference`) AS "item",
lt1.`chance` AS "chance",
SUM(IF(lt2.`chance` = 0, 1, 0)) AS "nZeroItems",
SUM(IF(lt2.`reference` = 0, lt2.`chance`, 0)) AS "sumChance",
IF(lt1.`groupid` > 0, 1, 0) AS "isGrouped",
IF(lt1.`reference` = 0, lt1.`mincount`, 1) AS "min",
IF(lt1.`reference` = 0, lt1.`maxcount`, 1) AS "max",
IF(lt1.`reference` > 0, lt1.`maxcount`, 1) AS "multiplier"
FROM ?# lt1
LEFT JOIN ?# lt2 ON lt1.`entry` = lt2.`entry` AND lt1.`groupid` = lt2.`groupid`
WHERE %s
GROUP BY lt2.`entry`, lt2.`groupid`';
/**
* @param int $entry item id to find loot container for
* @return void
*/
public function __construct(private int $entry)
{
}
/**
* iterate over result set
*
* @return iterable [tabIdx => [lvTemplate, lvData]]
*/
public function &iterate() : \Generator
{
reset($this->results);
foreach ($this->results as $k => [, $tabData])
if ($tabData['data']) // only yield tabs with content
yield $k => $this->results[$k];
}
/**
* calculate chance and stack info and apply to loot rows
*
* @param array $refs loot rows to apply chance + stack info to
* @param array $parents [optional] ref loot ids this call is derived from
* @return array [entry => stack+chance-info]
*/
private function calcChance(array $refs, array $parents = []) : array
{
$result = [];
foreach ($refs as $rId => $ref)
{
// check for possible database inconsistencies
if (!$ref['chance'] && !$ref['isGrouped'])
trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING);
if ($ref['isGrouped'] && $ref['sumChance'] > 100)
trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING);
if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance'])
trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING);
$chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100;
// apply inherited chanceMods
if (isset($this->chanceMods[$ref['item']]))
{
$chance *= $this->chanceMods[$ref['item']][0];
$chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]);
}
// save chance for parent-ref
$this->chanceMods[$rId] = [$chance, $ref['multiplier']];
// refTemplate doesn't point to a new ref -> we are done
if (in_array($rId, $parents))
continue;
$result[$rId] = array(
'percent' => $chance,
'stack' => [$ref['min'], $ref['max']],
'count' => 1, // ..and one for the sort script
'pctstack' => self::buildStack($ref['min'], $ref['max'])
);
}
// sort by % DESC
uasort($result, fn($a, $b) => $b['percent'] <=> $a['percent']);
return $result;
}
/**
* fetch loot container for item provided to __construct
*
* @param int $maxResults [optional] SQL_LIMIT override
* @param array $lootTableList [optional] limit lookup to provided loot template table names
* @return bool success
*/
public function getByItem(int $maxResults = -1, array $lootTableList = []) : bool
{
if (!$this->entry)
return false;
if ($maxResults < 0)
$maxResults = Cfg::get('SQL_LIMIT_DEFAULT');
$refResults = [];
/*
get references containing the item
*/
$newRefs = DB::World()->select(
sprintf($this->queryTemplate, 'lt1.`item` = ?d AND lt1.`reference` = 0'),
Loot::REFERENCE, Loot::REFERENCE,
$this->entry
);
/*
i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions.
if ($newRefs)
{
$cnd = new Conditions();
if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE))
if ($cnd->toListviewColumn($newRefs, $x, $this->entry))
$this->storejsGlobals($cnd->getJsGlobals());
}
*/
while ($newRefs)
{
$curRefs = $newRefs;
$newRefs = DB::World()->select(
sprintf($this->queryTemplate, 'lt1.`reference` IN (?a)'),
Loot::REFERENCE, Loot::REFERENCE,
array_keys($curRefs)
);
$refResults += $this->calcChance($curRefs, array_column($newRefs, 'item'));
}
/*
search the real loot-templates for the itemId and gathered refs
*/
foreach (self::TEMPLATES as $lootTemplate)
{
if ($lootTableList && !in_array($lootTemplate, $lootTableList))
continue;
if ($lootTemplate == Loot::REFERENCE)
continue;
$result = $this->calcChance(DB::World()->select(
sprintf($this->queryTemplate, '{lt1.`reference` IN (?a) OR }(lt1.`reference` = 0 AND lt1.`item` = ?d)'),
$lootTemplate, $lootTemplate,
$refResults ? array_keys($refResults) : DBSIMPLE_SKIP,
$this->entry
));
// do not skip here if $result is empty. Additional loot for spells and quest is added separately
// format for actual use
foreach ($result as $k => $v)
{
unset($result[$k]);
$v['percent'] = round($v['percent'] * 100, 3);
$result[abs($k)] = $v;
}
// cap fetched entries to the sql-limit to guarantee that the highest chance items get selected first
// screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) and (herb, ore, leather)
$ids = array_slice(array_keys($result), 0, $maxResults);
// fill ListviewTabs
match ($lootTemplate)
{
Loot::GAMEOBJECT => $this->handleObjectLoot( $ids, $result),
Loot::MAIL => $this->handleMailLoot( $ids, $result),
Loot::SPELL => $this->handleSpellLoot( $ids, $result),
Loot::CREATURE => $this->handleNpcLoot( $ids, $result, self::NPC_DROPPED, 'lootId'),
Loot::PICKPOCKET => $this->handleNpcLoot( $ids, $result, self::NPC_PICKPOCKETED, 'pickpocketLootId'),
Loot::SKINNING => $this->handleNpcLoot( $ids, $result, self::NPC_SKINNED, 'skinLootId'), // tabId < 0: assigned real id later
Loot::PROSPECTING => $this->handleGenericLoot($ids, $result, self::ITEM_PROSPECTED, 'id'),
Loot::MILLING => $this->handleGenericLoot($ids, $result, self::ITEM_MILLED, 'id'),
Loot::ITEM => $this->handleGenericLoot($ids, $result, self::ITEM_CONTAINED, 'id'),
Loot::DISENCHANT => $this->handleGenericLoot($ids, $result, self::ITEM_DISENCHANTED, 'disenchantId'),
Loot::FISHING => $this->handleGenericLoot($ids, $result, self::ZONE_FISHED, 'id') // subAreas are currently ignored
};
}
// finalize tabs
foreach ($this->listviewTabs as $idx => [$type, $data, $name, $id, $extraCols, $hiddenCols, $visibleCols])
{
$tabData = array(
'data' => $data,
'name' => $name,
'id' => $id
);
if ($extraCols)
$tabData['extraCols'] = array_unique($extraCols);
if ($hiddenCols)
$tabData['hiddenCols'] = array_unique($hiddenCols);
if ($visibleCols)
$tabData['visibleCols'] = array_unique($visibleCols);
$this->results[$idx] = [Type::getFileString($type), $tabData];
}
return true;
}
private function handleGenericLoot(array $ids, array $result, int $tabId, string $dbField) : bool
{
if (!$ids)
return false;
[$type, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId];
$srcObj = Type::newList($type, array([$dbField, $ids]));
if (!$srcObj || $srcObj->error)
return false;
$srcData = $srcObj->getListviewData();
$this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$extraCols[] = '$Listview.extraCols.percent';
foreach ($srcObj->iterate() as $__)
$data[] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($dbField)]);
return true;
}
private function handleNpcLoot(array $ids, array $result, int $tabId, string $dbField) : bool
{
if (!$ids)
return false;
if ($baseIds = DB::Aowow()->selectCol(
'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE `difficultyEntry1` IN (?a) UNION
SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE `difficultyEntry2` IN (?a) UNION
SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE `difficultyEntry3` IN (?a)',
$ids, $ids, $ids
))
{
$parentObj = new CreatureList(array(['id', $baseIds]));
if (!$parentObj->error)
{
$this->storeJSGlobals($parentObj->getJSGlobals());
$parentData = $parentObj->getListviewData();
$ids = array_diff($ids, $baseIds);
}
}
$npc = new CreatureList(array([$dbField, $ids]));
if ($npc->error)
return false;
$srcData = $npc->getListviewData();
$this->storeJSGlobals($npc->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
[, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId];
foreach ($npc->iterate() as $__)
{
if ($tabId == self::NPC_SKINNED)
{
if ($npc->isMineable())
$tabId = self::NPC_MINED;
else if ($npc->isGatherable())
$tabId = self::NPC_GATHERED;
else if ($npc->isSalvageable())
$tabId = self::NPC_SALVAGED;
}
$p = $npc->getField('parentId');
$data[] = array_merge($parentData[$p] ?? $srcData[$npc->id], $result[$npc->getField($dbField)]);
$extraCols[] = '$Listview.extraCols.percent';
}
return true;
}
private function handleSpellLoot(array $ids, array $result) : bool
{
$conditions = array(
'OR',
['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]],
['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]],
['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]],
);
if ($ids)
$conditions[] = ['id', $ids];
$srcObj = new SpellList($conditions);
if ($srcObj->error)
return false;
$this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
[, &$data, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[self::SPELL_CREATED];
if (!empty($result))
$extraCols[] = '$Listview.extraCols.percent';
if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8'))
$visibleCols[] = 'reagents';
foreach ($srcObj->getListviewData() as $id => $row)
$data[] = array_merge($row, $result[$id] ?? ['percent' => -1]);
return true;
}
private function handleMailLoot(array $ids, array $result) : bool
{
// quest part
$conditions = array('OR',
['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry],
['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry]
);
if ($ids)
$conditions[] = ['rewardMailTemplateId', $ids];
$quests = new QuestList($conditions);
if (!$quests->error)
{
$this->storeJSGlobals($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
[, &$qData, , , , , ] = $this->listviewTabs[self::QUEST_REWARD];
foreach ($quests->getListviewData() as $id => $row)
$qData[] = array_merge($row, $result[$id] ?? ['percent' => -1]);
}
// achievement part
$conditions = array(['itemExtra', $this->entry]);
if ($ar = DB::World()->selectCol('SELECT `ID` FROM achievement_reward WHERE `ItemID` = ?d{ OR `MailTemplateID` IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP))
array_push($conditions, ['id', $ar], 'OR');
$achievements = new AchievementList($conditions);
if (!$achievements->error)
{
$this->storeJSGlobals($achievements->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS));
[, &$aData, , , , &$hiddenCols, &$visibleCols] = $this->listviewTabs[self::ACHIEVEMENT_REWARD];
foreach ($achievements->getListviewData() as $id => $row)
$aData[] = array_merge($row, $result[$id] ?? ['percent' => -1]);
$hiddenCols[] = 'rewards';
$visibleCols[] = 'category';
}
return !$quests->error || !$achievements->error;
}
private function handleObjectLoot(array $ids, array $result) : bool
{
if (!$ids)
return false;
$srcObj = new GameObjectList(array(['lootId', $ids]));
if ($srcObj->error)
return false;
foreach ($srcObj->getListviewData() as $id => $row)
{
$tabId = match($row['type'])
{
25 => self::OBJECT_FISHED, // fishing node
-3 => self::OBJECT_GATHERED, // herb
-4 => self::OBJECT_MINED, // vein
default => self::OBJECT_CONTAINED // general chest loot
};
[, &$tabData, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[$tabId];
$tabData[] = array_merge($row, $result[$srcObj->getEntry($id)['lootId']]);
$extraCols[] = '$Listview.extraCols.percent';
if ($tabId != 15)
$visibleCols[] = 'skill';
}
return true;
}
}
?>

View File

@@ -54,6 +54,8 @@ spl_autoload_register(function (string $class) : void
require_once 'includes/game/chrstatistics.php';
else if (file_exists('includes/game/'.strtolower($class).'.class.php'))
require_once 'includes/game/'.strtolower($class).'.class.php';
else if (file_exists('includes/game/loot/'.strtolower($class).'.class.php'))
require_once 'includes/game/loot/'.strtolower($class).'.class.php';
});
// our site components

View File

@@ -6,6 +6,27 @@ if (!defined('AOWOW_REVISION'))
die('illegal access');
// PHP 8.4 polyfill
if (version_compare(PHP_VERSION, '8.4.0') < 0)
{
function array_find(array $array, callable $callback) : mixed
{
foreach ($array as $k => $v)
if ($callback($v, $k))
return $array[$k];
return null;
}
function array_find_key(array $array, callable $callback) : mixed
{
foreach ($array as $k => $v)
if ($callback($v, $k))
return $k;
return null;
}
}
class SimpleXML extends \SimpleXMLElement
{
public function addCData(string $cData) : \SimpleXMLElement

View File

@@ -397,7 +397,11 @@ $lang = array(
'phases' => "Phasen",
'mode' => "Modus: ",
'modes' => [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"],
'modes' => array(
[-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"],
["Normal", "Heroisch"],
["Normal 10", "Normal 25", "Heroisch 10", "Heroisch 25"]
),
'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"],
'stats' => ["Stärke", "Beweglichkeit", "Ausdauer", "Intelligenz", "Willenskraft"],
'timeAbbrev' => array(
@@ -1208,10 +1212,6 @@ $lang = array(
'mechanicimmune'=> 'Nicht anfällig für Mechanik: %s',
'_extraFlags' => 'Extra Flags: ',
'versions' => 'Schwierigkeitsgrade: ',
'modes' => array(
1 => ["Normal", "Heroisch"],
2 => ["10-Spieler Normal", "25-Spieler Normal", "10-Spieler Heroisch", "25-Spieler Heroisch"]
),
'cat' => array(
"Nicht kategorisiert", "Wildtiere", "Drachkin", "Dämonen", "Elementare", "Riesen", "Untote", "Humanoide",
"Tiere", "Mechanisch", "Nicht spezifiziert", "Totems", "Haustiere", "Gaswolken"

View File

@@ -397,7 +397,11 @@ $lang = array(
'phases' => "Phases",
'mode' => "Mode: ",
'modes' => [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"],
'modes' => array(
[-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"],
["Normal", "Heroic"],
["Normal 10", "Normal 25", "Heroic 10", "Heroic 25"]
),
'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"],
'stats' => ["Strength", "Agility", "Stamina", "Intellect", "Spirit"],
'timeAbbrev' => array( // <time>S_ABBR
@@ -1208,10 +1212,6 @@ $lang = array(
'mechanicimmune'=> 'Not affected by mechanic: %s',
'_extraFlags' => 'Extra Flags: ',
'versions' => 'Difficulty Versions: ',
'modes' => array(
1 => ["Normal", "Heroic"],
2 => ["10-player Normal", "25-player Normal", "10-player Heroic", "25-player Heroic"]
),
'cat' => array(
"Uncategorized", "Beasts", "Dragonkins", "Demons", "Elementals", "Giants", "Undead", "Humanoids",
"Critters", "Mechanicals", "Not specified", "Totems", "Non-combat Pets", "Gas Clouds"

View File

@@ -397,7 +397,11 @@ $lang = array(
'phases' => "Fases",
'mode' => "Modo: ",
'modes' => [-1 => "Cualquiera", "Normal / Normal 10", "Heroico / Normal 25", "Heróico 10", "Heróico 25"],
'modes' => array(
[-1 => "Cualquiera", "Normal / Normal 10", "Heroico / Normal 25", "Heróico 10", "Heróico 25"],
["Normal", "Heroico"],
["Normal 10", "Normal 25", "Heróico 10", "Heróico 25"],
),
'expansions' => ["World of Warcraft", "The Burning Crusade", "Wrath of the Lich King"],
'stats' => ["Fuerza", "Agilidad", "Aguante", "Intelecto", "Espíritu"],
'timeAbbrev' => array(
@@ -1208,10 +1212,6 @@ $lang = array(
'mechanicimmune'=> 'No afectado por la mecánica: %s',
'_extraFlags' => 'Banderas extra: ',
'versions' => 'Versiones de dificultad: ',
'modes' => array(
1 => ["Normal", "Heroico"],
2 => ["10 jugadores Normal", "25 jugadores Normal", "10 jugadores Heroico", "25 jugadores Heroico"]
),
'cat' => array(
"Sin categoría", "Bestia", "Dragonante", "Demonio", "Elemental", "Gigante", "No-muerto", "Humanoide",
"Alimaña", "Mecánico", "Sin especificar", "Tótem", "Mascota mansa", "Nube de gas"

View File

@@ -397,7 +397,11 @@ $lang = array(
'phases' => "Phases",
'mode' => "Mode : ",
'modes' => [-1 => "Tout", "Standard / Normal 10", "Héroïque / Normal 25", "10 héroïque", "25 héroïque"],
'modes' => array(
[-1 => "Tout", "Standard / Normal 10", "Héroïque / Normal 25", "10 Héroïque", "25 Héroïque"],
["Normal", "Héroïque"],
["10 Normal", "25 Normal", "10 Héroïque", "25 Héroïque"],
),
'expansions' => ["Classique", "The Burning Crusade", "Wrath of the Lich King"],
'stats' => ["Force", "Agilité", "Endurance", "Intelligence", "Esprit"],
'timeAbbrev' => array(
@@ -1208,10 +1212,6 @@ $lang = array(
'mechanicimmune'=> '[Not affected by mechanic] : %s',
'_extraFlags' => '[Extra Flags] : ',
'versions' => '[Difficulty Versions] : ',
'modes' => array(
1 => ["Normal", "Héroïque"],
2 => ["10-joueurs Normal", "25-joueurs Normal", "10-joueurs Héroïque", "25-joueurs Héroïque"]
),
'cat' => array(
"Non classés", "Bêtes", "Draconien", "Démons", "Élémentaires", "Géants", "Mort-vivant", "Humanoïdes",
"Bestioles", "Mécaniques", "Non spécifié", "Totems", "Familier pacifique", "Nuages de gaz"

View File

@@ -397,7 +397,11 @@ $lang = array(
'phases' => "Фазы",
'mode' => "Режим: ",
'modes' => [-1 => "Все", "Обычный / 10-норм.", "Героический / 25-норм.", "10-героич", "25-героич"],
'modes' => array(
[-1 => "Все", "Обычный / 10-норм.", "Героический / 25-норм.", "10-героич", "25-героич"],
["Обычный", "Героический"],
["10-нормал", "25-нормал", "10-героич", "25-героич"],
),
'expansions' => array("World of Warcraft", "The Burning Crusade", "Wrath of the Lich King"),
'stats' => array("к силе", "к ловкости", "к выносливости", "к интеллекту", "к духу"),
'timeAbbrev' => array(
@@ -1208,10 +1212,6 @@ $lang = array(
'mechanicimmune'=> '[Not affected by mechanic]: %s',
'_extraFlags' => '[Extra Flags]: ',
'versions' => '[Difficulty Versions]: ',
'modes' => array(
1 => ["Обычный", "Героический"],
2 => ["10 нормал.", "25 нормал.", "10 героич.", "25 героич."]
),
'cat' => array(
"Разное", "Животные", "Дракон", "Демоны", "Элементали", "Великаны", "Нежить", "Гуманоиды",
"Существа", "Механизмы", "Не указано", "Тотемы", "Спутники", "Облака газа"

View File

@@ -397,7 +397,11 @@ $lang = array(
'phases' => "阶段",
'mode' => "模式:",
'modes' => [-1 => "任何", "普通 / 普通 10人", "英雄 / 普通 25人", "英雄 10人", "英雄 25人"],
'modes' => array(
[-1 => "任何", "普通 / 普通 10人", "英雄 / 普通 25人", "英雄 10人", "英雄 25人"],
["普通", "英雄"],
["普通 10人", "普通 25人", "英雄 10人", "英雄 25人"],
),
'expansions' => ["经典旧世", "燃烧的远征", "巫妖王之怒"],
'stats' => ["力量", "敏捷", "耐力", "智力", "精神"],
'timeAbbrev' => array(
@@ -1208,10 +1212,6 @@ $lang = array(
'mechanicimmune'=> '[Not affected by mechanic]%s',
'_extraFlags' => '[Extra Flags]',
'versions' => '[Difficulty Versions]',
'modes' => array(
1 => ["普通", "英雄"],
2 => ["10人普通", "25人普通", "10人英雄", "25人英雄"]
),
'cat' => array(
"未分类", '野兽', '龙类', '恶魔', '元素生物', '巨人', '亡灵',
'人型生物', '小动物', '机械', '未指定', '图腾', '非战斗宠物', '气体云雾'

View File

@@ -1571,6 +1571,26 @@ CREATE TABLE `aowow_mails` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `aowow_objectdifficulty`
--
DROP TABLE IF EXISTS `aowow_objectdifficulty`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `aowow_objectdifficulty` (
`normal10` mediumint(8) unsigned NOT NULL,
`normal25` mediumint(8) unsigned NOT NULL,
`heroic10` mediumint(8) unsigned NOT NULL,
`heroic25` mediumint(8) unsigned NOT NULL,
`mapType` tinyint(3) unsigned NOT NULL,
KEY `normal10` (`normal10`),
KEY `normal25` (`normal25`),
KEY `heroic10` (`heroic10`),
KEY `heroic25` (`heroic25`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `aowow_objects`
--
@@ -2813,6 +2833,7 @@ CREATE TABLE `aowow_spelldifficulty` (
`normal25` mediumint(8) unsigned NOT NULL,
`heroic10` mediumint(8) unsigned NOT NULL,
`heroic25` mediumint(8) unsigned NOT NULL,
`mapType` tinyint(3) unsigned NOT NULL,
KEY `normal10` (`normal10`),
KEY `normal25` (`normal25`),
KEY `heroic10` (`heroic10`),

View File

@@ -115,6 +115,17 @@ INSERT INTO `aowow_loot_link` VALUES (19710,184465,1,0,0),(19218,184465,1,1,0),(
/*!40000 ALTER TABLE `aowow_loot_link` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `aowow_objectdifficulty`
--
LOCK TABLES `aowow_objectdifficulty` WRITE;
/*!40000 ALTER TABLE `aowow_objectdifficulty` DISABLE KEYS */;
INSERT INTO `aowow_objectdifficulty` VALUES (181366,193426,0,0,2),(193905,193967,0,0,2),(194307,194308,194200,194201,2),(194312,194314,194313,194315,2),(194324,194328,194327,194331,2),(194789,194956,194957,194958,2),(194821,194822,0,0,2),(195046,195047,0,0,2),(195631,195632,195633,195635,2),(202178,202180,202177,202179,2),(202239,202240,202238,202241,2),(201959,202339,202338,202340,2),(0,0,195668,195672,2),(0,0,195667,195671,2),(0,0,195666,195670,2),(0,0,195665,195669,2),(185168,185169,0,0,1),(184465,184849,0,0,1),(190586,193996,0,0,1),(190663,193597,0,0,1),(191349,193603,0,0,1),(195709,195710,0,0,1),(195323,195324,0,0,1),(195374,195375,0,0,1),(201710,202336,0,0,1);
/*!40000 ALTER TABLE `aowow_objectdifficulty` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
--
-- Dumping data for table `aowow_profiler_excludes`
--

View File

@@ -0,0 +1,42 @@
DROP TABLE IF EXISTS `aowow_objectdifficulty`;
CREATE TABLE `aowow_objectdifficulty` (
`normal10` mediumint(8) unsigned NOT NULL,
`normal25` mediumint(8) unsigned NOT NULL,
`heroic10` mediumint(8) unsigned NOT NULL,
`heroic25` mediumint(8) unsigned NOT NULL,
`mapType` tinyint(3) unsigned NOT NULL,
KEY `normal10` (`normal10`),
KEY `normal25` (`normal25`),
KEY `heroic10` (`heroic10`),
KEY `heroic25` (`heroic25`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `aowow_objectdifficulty` VALUES
(181366, 193426, 0, 0 , 2), -- naxxramas: four horsemen chest
(193905, 193967, 0, 0 , 2), -- eoe: alexstrasza's gift
(194307, 194308, 194200, 194201, 2), -- ulduar: cache of winter
(194312, 194314, 194313, 194315, 2), -- ulduar: cache of storms
(194324, 194328, 194325, 194329, 2), -- ulduar: freya's gift +1 elder
(194324, 194328, 194326, 194330, 2), -- ulduar: freya's gift +2 elder
(194324, 194328, 194327, 194331, 2), -- ulduar: freya's gift +3 elder
(194789, 194956, 194957, 194958, 2), -- ulduar: cache of innovation
(194821, 194822, 0, 0 , 2), -- ulduar: gift of the observer
(195046, 195047, 0, 0 , 2), -- ulduar: cache of living stone
(195631, 195632, 195633, 195635, 2), -- toc25: champions' cache
(202178, 202180, 202177, 202179, 2), -- icc: gunship armory (horde)
(201873, 201874, 201872, 201875, 2), -- icc: gunship armory (alliance)
(202239, 202240, 202238, 202241, 2), -- icc: deathbringer's cache
(201959, 202339, 202338, 202340, 2), -- icc: cache of the dreamwalker
(0, 0, 195668, 195672, 2), -- toc25: argent crusade tribute chest 1TL
(0, 0, 195667, 195671, 2), -- toc25: argent crusade tribute chest 25TL
(0, 0, 195666, 195670, 2), -- toc25: argent crusade tribute chest 45TL
(0, 0, 195665, 195669, 2), -- toc25: argent crusade tribute chest 50TL
(185168, 185169, 0, 0 , 1), -- hellfire ramparts: reinforced fel iron chest
(184465, 184849, 0, 0 , 1), -- mechanar: cache of the legion
(190586, 193996, 0, 0 , 1), -- halls of stone: tribunal chest
(190663, 193597, 0, 0 , 1), -- cot - cos: dark runed chest
(191349, 193603, 0, 0 , 1), -- oculus: cache of eregos
(195709, 195710, 0, 0 , 1), -- toc5: champion's cache
(195323, 195324, 0, 0 , 1), -- toc5: confessor's cache
(195374, 195375, 0, 0 , 1), -- toc5: eadric's cache
(201710, 202336, 0, 0 , 1); -- hor: captain's chest

View File

@@ -0,0 +1,18 @@
ALTER TABLE `aowow_spelldifficulty`
ADD COLUMN `mapType` tinyint(3) unsigned NOT NULL AFTER `heroic25`
;
-- move linked chest for icc: gunship battle. duplicate saurfang to muradin
DELETE FROM `aowow_loot_link` WHERE `npcId` IN (36939, 38156, 38637, 38638, 36948, 38157, 38639, 38640);
INSERT INTO `aowow_loot_link` (`npcId`, `objectId`, `difficulty`, `priority`, `encounterId`) VALUES
(36939, 201873, 1, 0, 847),
(38156, 201874, 2, 0, 847),
(38637, 201872, 3, 0, 847),
(38638, 201875, 4, 0, 847),
(36948, 202178, 1, 0, 847),
(38157, 202180, 2, 0, 847),
(38639, 202177, 3, 0, 847),
(38640, 202179, 4, 0, 847)
;
UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source spelldifficulty');

View File

@@ -26,7 +26,20 @@ CLISetup::registerSetup("sql", new class extends SetupScript
private array $disables = [];
private const /* array */ PVP_MONEY = [26045, 24581, 24579, 43589, 37836]; // Nagrand, Hellfire Pen. H, Hellfire Pen. A, Wintergrasp, Grizzly Hills
private const /* int */ COMMON_THRESHOLD = 100;
private const /* int */ COMMON_THRESHOLD = 30; // if an item has more than X sources it gets filtered by default in loot listviews; ancient WH versions have chance < 1% instead of checking for commonloot property.
// but that would include the super rare vanity pet drops etc, so.. idk? Make it depend of item class and/or quality? That sounds like pain. :<
private const /* array */ FAKE_CHESTS = array( // special cases where multiple chests share the same loot and are spawned for the same encounter
// icc - gunship armory // if we process it like normal the contained items show up as zone drop because technically they have multiple sources. So lets avoid that!
201873 => 202178, // A -> H
201874 => 202180,
201872 => 202177,
201875 => 202179,
// ulduar freya's gift - point +1 and +2 hardmode chests to max chest
194329 => 194331, // 25
194330 => 194331,
194325 => 194327, // 10
194326 => 194327
);
public function generate(array $ids = []) : bool
{
@@ -41,10 +54,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript
);
$this->dummyGOs = DB::Aowow()->select(
'SELECT l1.`objectId` AS ARRAY_KEY, BIT_OR(l1.`difficulty`) AS "0", IFNULL(l2.`npcId`, l1.`npcId`) AS "1"
FROM ?_loot_link l1
LEFT JOIN ?_loot_link l2 ON l1.`objectId` = l2.`objectId` AND l2.`priority` = 1
GROUP BY l1.`objectid`'
'SELECT `normal10` AS ARRAY_KEY, 1 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ?_objectdifficulty WHERE `normal10` > 0 UNION
SELECT `normal25` AS ARRAY_KEY, 2 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ?_objectdifficulty WHERE `normal25` > 0 UNION
SELECT `heroic10` AS ARRAY_KEY, 4 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ?_objectdifficulty WHERE `heroic10` > 0 UNION
SELECT `heroic25` AS ARRAY_KEY, 8 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ?_objectdifficulty WHERE `heroic25` > 0'
);
$this->disables = DB::World()->selectCol(
@@ -142,23 +155,27 @@ CLISetup::registerSetup("sql", new class extends SetupScript
$this->itemset(); # Meta category .. inherit from items #
$t = new Timer(500);
$d = new Timer(500);
foreach ($this->srcBuffer as $type => $data)
{
$j = 0;
$rows = [];
$sum = count($data);
foreach ($data as $d)
foreach ($data as [$t, $ti, $mt, $mti, $mz, $mFlags, $modes, $sumSources])
{
$rows[++$j] = array_slice($d, 0, 6);
for ($i = 1; $i < 25; $i++)
$rows[$j][] = $d[6][$i] ?? 0;
// can only ever be either/or .. unset if both
if (($mFlags & (SRC_FLAG_RAID_DROP | SRC_FLAG_DUNGEON_DROP)) == (SRC_FLAG_RAID_DROP | SRC_FLAG_DUNGEON_DROP))
$mFlags &= ~(SRC_FLAG_RAID_DROP | SRC_FLAG_DUNGEON_DROP);
if ($d[7] > self::COMMON_THRESHOLD)
$rows[++$j] = [$t, $ti, $mt, $mti, $mz, $mFlags];
for ($i = 1; $i < 25; $i++)
$rows[$j][] = $modes[$i] ?? 0;
if ($sumSources > self::COMMON_THRESHOLD)
$rows[$j][5] |= SRC_FLAG_COMMON;
if ($t->update())
CLI::write('[source] - Inserting... (['.$type.'] '.$j.' / '.$sum.')', CLI::LOG_BLANK, true, true);
if ($d->update())
CLI::write('[source] - Inserting... (['.Type::getFileString($type).'] '.$j.' / '.$sum.')', CLI::LOG_BLANK, true, true);
if (!($j % 300))
$this->insert($rows);
@@ -196,7 +213,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
return true;
}
private function pushBuffer(int $type, int $typeId, int $srcId, int $srcBit = 1, int $mType = 0, int $mTypeId = 0, ?int $mZoneId = null, int $mMask = 0x0, int $qty = 1) : void
private function pushBuffer(int $type, int $typeId, int $srcId, int $srcBit = 1, int $mType = 0, int $mTypeId = 0, ?int $mZoneId = null, ?int $mMask = null, int $qty = 1) : void
{
if (!isset($this->srcBuffer[$type]))
$this->srcBuffer[$type] = [];
@@ -207,19 +224,26 @@ CLISetup::registerSetup("sql", new class extends SetupScript
return;
}
$b = &$this->srcBuffer[$type][$typeId];
[, , &$bType, &$bTypeId, &$bZone, &$bFlags, &$bSrc, &$bQty] = $this->srcBuffer[$type][$typeId];
if ($mType != $b[2] || $mTypeId != $b[3])
$b[2] = $b[3] = null;
if ($mType != $bType || $mTypeId != $bTypeId)
$bType = $bTypeId = null;
if ($mZoneId && $b[4] === null)
$b[4] = $mZoneId;
else if ($mZoneId && $b[4] && $mZoneId != $b[4])
$b[4] = 0;
if ($bZone === null)
$bZone = $mZoneId;
else if ($mZoneId !== null && $mZoneId != $bZone)
$bZone = 0;
$b[5] = ($b[5] ?? 0) & $mMask; // only bossdrop for now .. remove flag if regular source is available
$b[6][$srcId] = ($b[6][$srcId] ?? 0) | $srcBit; // SIDE_X for quests, modeMask for drops, subSrc for pvp, else: 1
$b[7] += $qty;
if ($bFlags === null) // bossdrop, raid drop, dungeon drop .. remove flag if regular source is available
$bFlags = $mMask;
else if ($mMask !== null)
{
$bFlags &= ($mMask & SRC_FLAG_BOSSDROP);
$bFlags |= ($mMask & (SRC_FLAG_DUNGEON_DROP | SRC_FLAG_RAID_DROP));
}
$bSrc[$srcId] = ($bSrc[$srcId] ?? 0) | $srcBit; // SIDE_X for quests, modeMask for drops, subSrc for pvp, else: 1
$bQty += $qty;
}
private function insert(array &$rows) : void
@@ -293,7 +317,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #2 Drop [NPC]', CLI::LOG_BLANK, true, true);
$creatureLoot = DB::World()->select(
'SELECT IF(clt.`Reference` > 0, -clt.`Reference`, clt.`Item`) AS "refOrItem", ct.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT IF(clt.`Reference` > 0, -clt.`Reference`, clt.`Item`) AS "refOrItem", ct.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT clt.`Reference`) AS "qty"
FROM creature_loot_template clt
JOIN creature_template ct ON clt.`entry` = ct.`lootid`
LEFT JOIN item_template it ON it.`entry` = clt.`Item` AND clt.`Reference` <= 0
@@ -301,7 +325,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript
GROUP BY `refOrItem`, ct.`entry`'
);
$npcSpawns = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ?_spawns s JOIN ?_zones z ON z.`id` = s.`areaId` WHERE s.`type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_merge(array_column($this->dummyGOs, 1), array_filter(array_column($creatureLoot, 'entry'))));
$linkedNpcs = DB::Aowow()->selectCol('SELECT l1.`objectId` AS ARRAY_KEY, IFNULL(l2.`npcId`, l1.`npcId`) FROM ?_loot_link l1 LEFT JOIN ?_loot_link l2 ON l1.`objectId` = l2.`objectId` AND l2.`priority` = 1 GROUP BY l1.`objectid`');
$npcSpawns = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ?_spawns s JOIN ?_zones z ON z.`id` = s.`areaId` WHERE s.`type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::NPC, array_merge($linkedNpcs, array_filter(array_column($creatureLoot, 'entry'))));
$bosses = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, IF(`cuFlags` & ?d, 1, IF(`typeFlags` & 0x4 AND `rank` > 0, 1, 0)) FROM ?_creature WHERE `id` IN (?a)', NPC_CU_INSTANCE_BOSS, array_filter(array_column($creatureLoot, 'entry')));
foreach ($creatureLoot as $l)
@@ -309,12 +334,12 @@ CLISetup::registerSetup("sql", new class extends SetupScript
$roi = $l['refOrItem'];
$entry = $l['entry'];
$mode = 1;
$zoneId = 0;
$zoneId = null;
$mMask = 0x0;
if (isset($this->dummyNPCs[$l['entry']]))
[$mode, $entry] = $this->dummyNPCs[$l['entry']];
if (isset($bosses[$entry]) && $bosses[$entry]) // can be empty...?
if (!empty($bosses[$entry]))
$mMask |= SRC_FLAG_BOSSDROP;
if (isset($npcSpawns[$entry]))
@@ -355,10 +380,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript
$this->pushBuffer(Type::ITEM, $roi, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::NPC, $entry, $zoneId, $mMask, $l['qty']);
}
CLI::write('[source] * #2 Drop [Object]', CLI::LOG_BLANK, true, true);
$objectLoot = DB::World()->select(
'SELECT IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "refOrItem", gt.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "refOrItem", gt.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT glt.`Reference`) AS "qty"
FROM gameobject_loot_template glt
JOIN gameobject_template gt ON glt.`entry` = gt.`data1`
LEFT JOIN item_template it ON it.`entry` = glt.`Item` AND glt.`Reference` <= 0
@@ -373,23 +399,25 @@ CLISetup::registerSetup("sql", new class extends SetupScript
foreach ($objectLoot as $l)
{
$roi = $l['refOrItem'];
$entry = $l['entry'];
$mode = 1 | ($this->dummyGOs[$entry][0] ?? 0);
$zoneId = 0;
$entry = self::FAKE_CHESTS[$l['entry']] ?? $l['entry'];
$mode = 1;
$zoneId = null;
$mMask = 0x0;
$spawn = [];
if (isset($this->dummyGOs[$entry])) // we know these are all boss drops
$mMask |= SRC_FLAG_BOSSDROP;
if (isset($goSpawns[$entry]))
$spawn = $goSpawns[$entry];
else if (isset($this->dummyGOs[$entry]) && isset($npcSpawns[$this->dummyGOs[$entry][1]]))
$spawn = $npcSpawns[$this->dummyGOs[$entry][1]];
if ($spawn)
if ([$modeBit, $baseEntry, $mapType] = ($this->dummyGOs[$entry] ?? null))
{
switch ($spawn['type'])
$mMask |= SRC_FLAG_BOSSDROP; // we know these are all boss drops
$mode = $modeBit;
$entry = $baseEntry ?: $entry;
if ($mapType == 1)
$mMask |= SRC_FLAG_DUNGEON_DROP;
if ($mapType == 2)
$mMask |= SRC_FLAG_RAID_DROP;
}
else if (isset($goSpawns[$entry]))
{
switch ($goSpawns[$entry]['type'])
{
case MAP_TYPE_DUNGEON_HC:
$mMask |= SRC_FLAG_DUNGEON_DROP; break;
@@ -397,8 +425,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript
case MAP_TYPE_MMODE_RAID_HC:
$mMask |= SRC_FLAG_RAID_DROP; break;
}
}
$zoneId = $spawn['areaId'];
if (isset($goSpawns[$entry]))
$zoneId = $goSpawns[$entry]['areaId'];
else if (isset($linkedNpcs[$entry]))
{
if (!empty($bosses[$linkedNpcs[$entry]]))
$mMask |= SRC_FLAG_BOSSDROP;
if (isset($npcSpawns[$linkedNpcs[$entry]]))
$zoneId = $npcSpawns[$linkedNpcs[$entry]]['areaId'];
}
if ($roi < 0 && !empty($this->refLoot[-$roi]))
@@ -406,26 +442,27 @@ CLISetup::registerSetup("sql", new class extends SetupScript
foreach ($this->refLoot[-$roi] as $iId => $r)
{
if ($_ = $this->taughtSpell($r))
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']);
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']);
$objectOT[] = $iId;
$this->pushBuffer(Type::ITEM, $iId, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']);
$this->pushBuffer(Type::ITEM, $iId, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']);
}
continue;
}
if ($_ = $this->taughtSpell($l))
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']);
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']);
$objectOT[] = $roi;
$this->pushBuffer(Type::ITEM, $roi, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']);
$this->pushBuffer(Type::ITEM, $roi, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']);
}
CLI::write('[source] * #2 Drop [Item]', CLI::LOG_BLANK, true, true);
$itemLoot = DB::World()->select(
'SELECT IF(ilt.`Reference` > 0, -ilt.`Reference`, ilt.`Item`) AS ARRAY_KEY, itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT IF(ilt.`Reference` > 0, -ilt.`Reference`, ilt.`Item`) AS ARRAY_KEY, itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT ilt.`Reference`) AS "qty"
FROM item_loot_template ilt
JOIN item_template itA ON ilt.`entry` = itA.`entry`
LEFT JOIN item_template itB ON itB.`entry` = ilt.`Item` AND ilt.`Reference` <= 0
@@ -441,23 +478,25 @@ CLISetup::registerSetup("sql", new class extends SetupScript
foreach ($this->refLoot[-$roi] as $iId => $r)
{
if ($_ = $this->taughtSpell($r))
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']);
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']);
$itemOT[] = $iId;
$this->pushBuffer(Type::ITEM, $iId, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']);
$this->pushBuffer(Type::ITEM, $iId, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']);
}
continue;
}
if ($_ = $this->taughtSpell($l))
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']);
$this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']);
$itemOT[] = $roi;
$this->pushBuffer(Type::ITEM, $roi, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']);
$this->pushBuffer(Type::ITEM, $roi, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']);
}
if ($itemOT)
DB::Aowow()->query('UPDATE ?_items SET `cuFLags` = `cuFlags` | ?d WHERE `id` IN (?a)', ITEM_CU_OT_ITEMLOOT, $itemOT);
if ($objectOT)
DB::Aowow()->query('UPDATE ?_items SET `cuFLags` = `cuFlags` | ?d WHERE `id` IN (?a)', ITEM_CU_OT_OBJECTLOOT, $objectOT);
}
@@ -537,7 +576,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
}
$mailLoot = DB::World()->select(
'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS ARRAY_KEY, qt.`Id` AS "entry", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone", BIT_OR(IF(qt.`AllowableRaces` & ?d AND NOT (qt.`AllowableRaces` & ?d), ?d, IF(qt.`AllowableRaces` & ?d AND NOT (qt.`AllowableRaces` & ?d), ?d, ?d))) AS "side"
'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS ARRAY_KEY, qt.`Id` AS "entry", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT mlt.`Reference`) AS "qty", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone", BIT_OR(IF(qt.`AllowableRaces` & ?d AND NOT (qt.`AllowableRaces` & ?d), ?d, IF(qt.`AllowableRaces` & ?d AND NOT (qt.`AllowableRaces` & ?d), ?d, ?d))) AS "side"
FROM mail_loot_template mlt
JOIN quest_template_addon qta ON qta.`RewardMailTemplateId` = mlt.`entry`
JOIN quest_template qt ON qt.`ID` = qta.`ID`
@@ -659,7 +698,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #15 Disenchanted', CLI::LOG_BLANK, true, true);
$deLoot = DB::World()->select(
'SELECT IF(dlt.`Reference` > 0, -dlt.`Reference`, dlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT IF(dlt.`Reference` > 0, -dlt.`Reference`, dlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT dlt.`Reference`) AS "qty"
FROM disenchant_loot_template dlt
JOIN item_template itA ON dlt.`entry` = itA.`DisenchantId`
LEFT JOIN item_template itB ON itB.`entry` = dlt.`Item` AND dlt.`Reference` <= 0
@@ -696,7 +735,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #16 Fished', CLI::LOG_BLANK, true, true);
$fishLoot = DB::World()->select(
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", IF(COUNT(DISTINCT `zone`) > 2, 0, MAX(`zone`)) AS "zone"
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty", IF(COUNT(DISTINCT `zone`) > 2, 0, MAX(`zone`)) AS "zone"
FROM (SELECT 0 AS "entry", IF(flt.`Reference` > 0, -flt.`Reference`, flt.`Item`) AS "itemOrRef", `entry` AS "zone" FROM fishing_loot_template flt UNION
SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "itemOrRef", 0 AS "zone" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE `type` = ?d AND gt.`data1` > 0) src
LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry`
@@ -717,7 +756,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
{
$roi = $l['refOrItem'];
$l['zone'] = $areaParent[$l['zone']] ?? $l['zone'];
$zoneId = $goSpawns[$l['entry']] ?? 0;
$zoneId = $goSpawns[$l['entry']] ?? null;
if ($l['zone'] != $zoneId)
$zoneId = 0;
@@ -746,7 +785,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #17 Gathered', CLI::LOG_BLANK, true, true);
$herbLoot = DB::World()->select(
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", src.`srcType`
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty", src.`srcType`
FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef`, ?d AS "srcType" FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) AND ct.`skinloot` > 0 UNION
SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) `itemOrRef`, ?d AS "srcType" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE gt.`type` = ?d AND gt.`data1` > 0 AND gt.`data0` IN (?a)) src
LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.`itemOrRef` = it.`entry`
@@ -761,8 +800,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript
return;
}
$spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($herbLoot, function($x) { return $x['srcType'] == Type::OBJECT; }), 'entry'));
$spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::NPC, array_column(array_filter($herbLoot, function($x) { return $x['srcType'] == Type::NPC; }), 'entry'));
$spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($herbLoot, fn($x) => $x['srcType'] == Type::OBJECT), 'entry'));
$spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::NPC, array_column(array_filter($herbLoot, fn($x) => $x['srcType'] == Type::NPC), 'entry'));
foreach ($herbLoot as $l)
{
@@ -772,7 +811,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
if (isset($this->dummyNPCs[$l['entry']]) && $l['srcType'] == Type::NPC)
[$mode, $entry] = $this->dummyNPCs[$l['entry']];
$zoneId = $spawns[$l['srcType']][$l['entry']] ?? 0;
$zoneId = $spawns[$l['srcType']][$l['entry']] ?? null;
if ($roi < 0 && !empty($this->refLoot[-$roi]))
{
@@ -799,7 +838,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #18 Milled', CLI::LOG_BLANK, true, true);
$millLoot = DB::World()->select(
'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT mlt.`Reference`) AS "qty"
FROM milling_loot_template mlt
JOIN item_template itA ON mlt.`entry` = itA.`entry`
LEFT JOIN item_template itB ON itB.`entry` = mlt.`Item` AND mlt.`Reference` <= 0
@@ -835,7 +874,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #19 Mined', CLI::LOG_BLANK, true, true);
$mineLoot = DB::World()->select(
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", src.`srcType`
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty", src.`srcType`
FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef`, ?d AS "srcType" FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) AND ct.`skinloot` > 0 UNION
SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) `itemOrRef`, ?d AS "srcType" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE gt.`type` = ?d AND gt.`data1` > 0 AND gt.`data0` IN (?a)) src
LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.`itemOrRef` = it.`entry`
@@ -850,8 +889,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript
return;
}
$spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($mineLoot, function($x) { return $x['srcType'] == Type::OBJECT; }), 'entry'));
$spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_column(array_filter($mineLoot, function($x) { return $x['srcType'] == Type::NPC; }), 'entry'));
$spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($mineLoot, fn($x) => $x['srcType'] == Type::OBJECT), 'entry'));
$spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_column(array_filter($mineLoot, fn($x) => $x['srcType'] == Type::NPC), 'entry'));
foreach ($mineLoot as $l)
{
@@ -861,7 +900,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
if (isset($this->dummyNPCs[$l['entry']]) && $l['srcType'] == Type::NPC)
[$mode, $entry] = $this->dummyNPCs[$l['entry']];
$zoneId = $spawns[$l['srcType']][$l['entry']] ?? 0;
$zoneId = $spawns[$l['srcType']][$l['entry']] ?? null;
if ($roi < 0 && !empty($this->refLoot[-$roi]))
{
@@ -888,7 +927,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #20 Prospected', CLI::LOG_BLANK, true, true);
$prospectLoot = DB::World()->select(
'SELECT IF(plt.`Reference` > 0, -plt.`Reference`, plt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT IF(plt.`Reference` > 0, -plt.`Reference`, plt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT plt.`Reference`) AS "qty"
FROM prospecting_loot_template plt
JOIN item_template itA ON plt.`entry` = itA.`entry`
LEFT JOIN item_template itB ON itB.`entry` = plt.`Item` AND plt.`Reference` <= 0
@@ -924,7 +963,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #21 Pickpocket', CLI::LOG_BLANK, true, true);
$theftLoot = DB::World()->select(
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty"
FROM (SELECT ct.`entry`, IF(plt.`Reference` > 0, -plt.`Reference`, plt.`Item`) `itemOrRef` FROM creature_template ct JOIN pickpocketing_loot_template plt ON plt.`entry` = ct.`pickpocketloot` WHERE ct.`pickpocketloot` > 0) src
LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry`
GROUP BY `refOrItem`, src.`entry`'
@@ -946,7 +985,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
if (isset($this->dummyNPCs[$l['entry']]))
[$mode, $entry] = $this->dummyNPCs[$l['entry']];
$zoneId = $spawns[$l['entry']] ?? 0;
$zoneId = $spawns[$l['entry']] ?? null;
if ($roi < 0 && !empty($this->refLoot[-$roi]))
{
@@ -973,7 +1012,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #22 Salvaged', CLI::LOG_BLANK, true, true);
$salvageLoot = DB::World()->select(
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty"
FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef` FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) AND ct.`skinloot` > 0) src
LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry`
GROUP BY `refOrItem`, src.`entry`',
@@ -996,7 +1035,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
if (isset($this->dummyNPCs[$l['entry']]))
[$mode, $entry] = $this->dummyNPCs[$l['entry']];
$zoneId = $spawns[$l['entry']] ?? 0;
$zoneId = $spawns[$l['entry']] ?? null;
if ($roi < 0 && !empty($this->refLoot[-$roi]))
{
@@ -1023,7 +1062,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
CLI::write('[source] * #23 Skinned', CLI::LOG_BLANK, true, true);
$skinLoot = DB::World()->select(
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty"
'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty"
FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef` FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) = 0 AND ct.`skinloot` > 0 AND ct.`type` <> 13) src
LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry`
GROUP BY `refOrItem`, src.`entry`',
@@ -1046,7 +1085,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript
if (isset($this->dummyNPCs[$l['entry']]))
[$mode, $entry] = $this->dummyNPCs[$l['entry']];
$zoneId = $spawns[$l['entry']] ?? 0;
$zoneId = $spawns[$l['entry']] ?? null;
if ($roi < 0 && !empty($this->refLoot[-$roi]))
{

View File

@@ -17,17 +17,64 @@ CLISetup::registerSetup("sql", new class extends SetupScript
protected $dbcSourceFiles = ['spelldifficulty'];
protected $worldDependency = ['spelldifficulty_dbc'];
protected $setupAfter = [['creature', 'spawns'], []];
public function generate(array $ids = []) : bool
{
DB::Aowow()->query('TRUNCATE TABLE ?_spelldifficulty');
DB::Aowow()->query('INSERT INTO ?_spelldifficulty SELECT GREATEST(`normal10`, 0), GREATEST(`normal25`, 0), GREATEST(`heroic10`, 0), GREATEST(`heroic25`, 0) FROM dbc_spelldifficulty');
DB::Aowow()->query('INSERT INTO ?_spelldifficulty SELECT GREATEST(`normal10`, 0), GREATEST(`normal25`, 0), GREATEST(`heroic10`, 0), GREATEST(`heroic25`, 0), IF(`heroic10` > 0, 2, 0) FROM dbc_spelldifficulty');
$rows = DB::World()->select('SELECT `spellid0`, `spellid1`, `spellid2`, `spellid3` FROM spelldifficulty_dbc');
$rows = DB::World()->select('SELECT `spellid0`, `spellid1`, `spellid2`, `spellid3`, IF(`spellid2` > 0, 2, 0) FROM spelldifficulty_dbc');
foreach ($rows as $r)
DB::Aowow()->query('INSERT INTO ?_spelldifficulty VALUES (?a)', array_values($r));
CLI::write('[spelldifficulty] - trying to assign map type by traversing creature spells > spawns');
// try to update mode of ambiguous entries
$baseSpells = DB::Aowow()->selectCol('SELECT `normal10` FROM ?_spelldifficulty WHERE `heroic10` = 0 AND `heroic25` = 0');
for ($i = 1; $i < 9; $i++)
DB::Aowow()->query(
'UPDATE ?_spelldifficulty sd,
(SELECT c.?# AS "spell", BIT_OR(CASE WHEN z.`type` = ?d THEN 1 WHEN z.`type` = ?d THEN 2 WHEN z.`type` = ?d THEN 2 ELSE 0 END) AS "mapType"
FROM ?_creature c
JOIN ?_spawns s ON c.id = s.typeId AND s.type = ?d
JOIN ?_zones z ON z.id = s.areaId
WHERE c.?# IN (?a)
GROUP BY c.?#
HAVING c.?# <> 0) x
SET sd.`mapType` = x.`mapType`
WHERE sd.`normal10` = x.`spell`',
'spell'.$i, MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC,
Type::NPC, 'spell'.$i, $baseSpells, 'spell'.$i, 'spell'.$i
);
CLI::write('[spelldifficulty] - trying to assign map type by traversing smart_scripts > spawns');
$smartCaster = [];
foreach ($baseSpells as $bs)
if ($owner = SmartAI::getOwnerOfSpellCast($bs))
foreach ($owner as $type => $caster)
$smartCaster[$type][$bs] = $caster;
foreach ($smartCaster as $type => $spells)
foreach ($spells as $spellId => $casterEntries)
DB::Aowow()->query(
'UPDATE ?_spelldifficulty sd,
(SELECT BIT_OR(CASE WHEN z.`type` = ?d THEN 1 WHEN z.`type` = ?d THEN 2 WHEN z.`type` = ?d THEN 2 ELSE 0 END) AS "mapType"
FROM ?_spawns s
JOIN ?_zones z ON z.id = s.areaId
WHERE s.type = ?d AND s.typeId IN (?a) ) sp
SET sd.`mapType` = IF(sp.`mapType` > 2, 0, sp.`mapType`)
WHERE sd.`normal10` = ?d',
MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC,
$type, $casterEntries,
$spellId
);
return true;
}
});

View File

@@ -1651,7 +1651,7 @@ span.icon-instance8 {
.listview-note {
line-height: 16px;
clear: left;
/* clear: left; */
}
.listview table {