Files
aowow/endpoints/spell/spell.php
Sarjuuk 6eb5a67add Profiler/Optimization
* move searchable flags to their own db cols to speed up lookups
 * don't cast profile name to LOWER in SQL when displaying tooltips.
2025-11-09 16:20:52 +01:00

2500 lines
112 KiB
PHP

<?php
namespace Aowow;
if (!defined('AOWOW_REVISION'))
die('illegal access');
class SpellBaseResponse extends TemplateResponse implements ICache
{
use TrDetailPage, TrCache;
private const MOD_AURAS = [SPELL_AURA_ADD_FLAT_MODIFIER, SPELL_AURA_ADD_PCT_MODIFIER, SPELL_AURA_NO_REAGENT_USE,
SPELL_AURA_ABILITY_PERIODIC_CRIT, SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL, SPELL_AURA_ABILITY_IGNORE_AURASTATE,
SPELL_AURA_ALLOW_ONLY_ABILITY, SPELL_AURA_IGNORE_MELEE_RESET, SPELL_AURA_ABILITY_CONSUME_NO_AMMO,
SPELL_AURA_MOD_IGNORE_SHAPESHIFT, SPELL_AURA_PERIODIC_HASTE, SPELL_AURA_OVERRIDE_CLASS_SCRIPTS,
SPELL_AURA_MOD_DAMAGE_FROM_CASTER, SPELL_AURA_ADD_TARGET_TRIGGER, SPELL_AURA_IGNORE_COMBAT_RESULT, /* SPELL_AURA_DUMMY ? */];
protected int $cacheType = CACHE_TYPE_DETAIL_PAGE;
protected string $template = 'spell';
protected string $pageName = 'spell';
protected ?int $activeTab = parent::TAB_DATABASE;
protected array $breadcrumb = [0, 1];
public int $type = Type::SPELL;
public int $typeId = 0;
public array $reagents = [false, null];
public string $items = '';
public array $tools = [];
public array $effects = [];
public array $attributes = [];
public string $powerCost = '';
public string $castTime = '';
public string $level = '';
public string $rangeName = '';
public string $range = '';
public string $gcd = '';
public string $gcdCat = '';
public string $school = '';
public ?string $dispel = null;
public ?string $mechanic = null;
public string $stances = '';
public string $cooldown = '';
public string $duration = '';
public array $tooltip = [];
private SpellList $subject;
private int $firstRank = 0;
private array $modelInfo = [];
private array $difficulties = [];
public function __construct(string $id)
{
parent::__construct($id);
$this->typeId = intVal($id);
$this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE;
}
protected function generate() : void
{
$this->subject = new SpellList(array(['id', $this->typeId]));
if ($this->subject->error)
$this->generateNotFound(Lang::game('spell'), Lang::spell('notFound'));
if ($jsg = $this->subject->getJSGlobals(GLOBALINFO_ANY, $extra))
$this->extendGlobalData($jsg, $extra);
$this->modelInfo = $this->subject->getModelInfo($this->typeId);
$this->difficulties = DB::Aowow()->selectRow( // has difficulty versions of itself
'SELECT `normal10` AS "0", `normal25` AS "1",
`heroic10` AS "2", `heroic25` AS "3"
FROM ?_spelldifficulty
WHERE `normal10` = ?d OR `normal25` = ?d OR
`heroic10` = ?d OR `heroic25` = ?d',
$this->typeId, $this->typeId, $this->typeId, $this->typeId
);
// returns self or firstRank
if ($fr = DB::World()->selectCell('SELECT `first_spell_id` FROM spell_ranks WHERE `spell_id` = ?d', $this->typeId))
$this->firstRank = $fr;
else
$this->firstRank = DB::Aowow()->selectCell(
'SELECT IF(s1.`RankNo` <> 1 AND s2.`id`, s2.`id`, s1.`id`)
FROM ?_spell s1
LEFT JOIN ?_spell s2
ON s1.`SpellFamilyId` = s2.`SpelLFamilyId` AND s1.`SpellFamilyFlags1` = s2.`SpelLFamilyFlags1` AND
s1.`SpellFamilyFlags2` = s2.`SpellFamilyFlags2` AND s1.`SpellFamilyFlags3` = s2.`SpellFamilyFlags3` AND
s1.`name_loc0` = s2.`name_loc0` AND s2.`RankNo` = 1
WHERE s1.`id` = ?d',
$this->typeId
);
$this->h1 = Util::htmlEscape($this->subject->getField('name', true));
$this->gPageInfo += array(
'type' => $this->type,
'typeId' => $this->typeId,
'name' => $this->subject->getField('name', true)
);
/*************/
/* Menu Path */
/*************/
$this->generatePath();
/**************/
/* Page Title */
/**************/
array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('spell')));
/***********/
/* Infobox */
/***********/
$this->createInfobox();
/***************/
/* Red Buttons */
/***************/
$this->redButtons = array(
BUTTON_VIEW3D => false,
BUTTON_WOWHEAD => true,
BUTTON_LINKS => array(
'linkColor' => 'ff71d5ff',
'linkId' => Type::getFileString(Type::SPELL).':'.$this->typeId,
'linkName' => $this->subject->getField('name', true),
'type' => $this->type,
'typeId' => $this->typeId
)
);
// could have multiple models set, one per effect
foreach ($this->modelInfo as $mI)
{
$this->redButtons[BUTTON_VIEW3D] = ['type' => $mI['type'], 'displayId' => $mI['displayId']];
if (isset($mI['humanoid']))
{
$this->redButtons[BUTTON_VIEW3D]['typeId'] = $mI['typeId'];
$this->redButtons[BUTTON_VIEW3D]['humanoid'] = 1;
}
break;
}
/*******************/
/* Reagent Listing */
/*******************/
$this->createReagentList();
/******************/
/* Required Items */
/******************/
$this->createRequiredItems();
/*************************/
/* Required Tools/Totems */
/*************************/
// prepare Tools
foreach ($this->subject->getToolsForCurrent() as $tool)
$this->tools[] = new IconElement(
Type::ITEM,
$tool['itemId'] ?? 0,
$tool['name'],
quality: ITEM_QUALITY_NORMAL,
size: IconElement::SIZE_SMALL,
url: !isset($tool['itemId']) ? '?items&filter=cr=91;crs='.$tool['id'].';crv=0' : '',
element: 'iconlist-icon'
);
/**********************/
/* Spell Effect Block */
/**********************/
$this->createEffects();
/**************************************************/
/* Spell Attributes listing and SpellFilter links */
/**************************************************/
$this->createAttributesList();
/*************************/
/* Factionchange pendant */
/*************************/
// factionchange-equivalent
if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_spells WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId))
{
$altSpell = new SpellList(array(['id', abs($pendant)]));
if (!$altSpell->error)
{
$this->transfer = Lang::spell('_transfer', array(
$altSpell->id,
ITEM_QUALITY_NORMAL,
$altSpell->getField('iconString'),
$altSpell->getField('name', true),
$pendant > 0 ? 'alliance' : 'horde',
$pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE)
));
}
}
/****************/
/* Main Content */
/****************/
$this->powerCost = $this->subject->createPowerCostForCurrent();
$this->castTime = $this->subject->createCastTimeForCurrent(false, false);
$this->level = $this->subject->getField('spellLevel');
$this->rangeName = $this->subject->getField('rangeText', true);
$this->gcd = Util::formatTime($this->subject->getField('startRecoveryTime'));
$this->school = $this->fmtStaffTip(Lang::getMagicSchools($this->subject->getField('schoolMask')), Util::asHex($this->subject->getField('schoolMask')));
$this->dispel = $this->subject->getField('dispelType') ? Lang::game('dt', $this->subject->getField('dispelType')) : null;
$this->mechanic = $this->subject->getField('mechanic') ? Lang::game('me', $this->subject->getField('mechanic')) : null;
$this->tooltip = array(
$this->subject->getField('iconString'),
$this->subject->getField('stackAmount') ?: ($this->subject->getField('procCharges') > 1 ? $this->subject->getField('procCharges') : 0),
$this->subject->getField('buff', true, true) ? 1 : 0
);
$this->gcdCat = match((int)$this->subject->getField('startRecoveryCategory'))
{
133 => Lang::spell('normal'),
330, // Mounts
1156, // Heart of the Phoenix
1159, // Ignis Grab and Slag Pot
1164, // Kessel Run Elek
1173, // Birmingham Test Spells
1178, // Stealth (Druid Cat, Rogue, Hunter Cat Pets) + Charge (Warrior)
1244 => Lang::spell('special'), // Argent Tournament Vehcile Jousting Abilities
default => '' // n/a
};
$this->range = $this->subject->getField('rangeMaxHostile');
if ($_ = $this->subject->getField('rangeMinHostile'))
$this->range = $_.' - '.$this->range;
if (!($this->subject->getField('attributes2') & SPELL_ATTR2_NOT_NEED_SHAPESHIFT))
$this->stances = Lang::getStances($this->subject->getField('stanceMask'));
if (($_ = $this->subject->getField('recoveryTime')) && $_ > 0)
$this->cooldown = Util::formatTime($_);
else if (($_ = $this->subject->getField('recoveryCategory')) && $_ > 0)
$this->cooldown = Util::formatTime($_);
if (($_ = $this->subject->getField('duration')) && $_ > 0)
$this->duration = Util::formatTime($_);
/**************/
/* Extra Tabs */
/**************/
$this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true);
$ubSAI = SmartAI::getOwnerOfSpellCast($this->typeId);
// tab: abilities [of shapeshift form]
$formSpells = [];
for ($i = 1; $i < 4; $i++)
if ($this->subject->getField('effect'.$i.'AuraId') == SPELL_AURA_MOD_SHAPESHIFT)
if ($_ = DB::Aowow()->selectRow('SELECT `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5`, `spellId6`, `spellId7`, `spellId8` FROM ?_shapeshiftforms WHERE `id` = ?d', $this->subject->getField('effect'.$i.'MiscValue')))
$formSpells = array_merge($formSpells, $_);
if ($formSpells)
{
$abilities = new SpellList(array(['id', $formSpells]));
if (!$abilities->error)
{
$tabData = array(
'data' => $abilities->getListviewData(),
'id' => 'controlledabilities',
'name' => '$LANG.tab_controlledabilities',
'visibleCols' => ['level'],
);
if (!$abilities->hasSetFields('skillLines'))
$tabData['hiddenCols'] = ['skill'];
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
$this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF));
}
}
// tab: [$this] modifies
$sub = [];
$conditions = [
['s.typeCat', [-9], '!'], // GM (-9); also include uncategorized (0), NPC-Spell (-8)?; NPC includes totems, lightwell and others :/
['s.spellFamilyId', $this->subject->getField('spellFamilyId')],
&$sub
];
$modifiesData = [];
$hideSkillCol = true;
for ($i = 1; $i < 4; $i++)
{
if (!in_array($this->subject->getField('effect'.$i.'AuraId'), self::MOD_AURAS))
continue;
$m1 = $this->subject->getField('effect'.$i.'SpellClassMaskA');
$m2 = $this->subject->getField('effect'.$i.'SpellClassMaskB');
$m3 = $this->subject->getField('effect'.$i.'SpellClassMaskC');
if (!$m1 && !$m2 && !$m3)
continue;
$classSpells = $miscSpells = [];
$this->effects[$i]['modifies'] = [&$classSpells, &$miscSpells];
$sub = ['OR', ['s.spellFamilyFlags1', $m1, '&'], ['s.spellFamilyFlags2', $m2, '&'], ['s.spellFamilyFlags3', $m3, '&']];
$modSpells = new SpellList($conditions);
if (!$modSpells->error)
{
foreach ($modSpells->iterate() as $id => $__)
{
if (in_array($modSpells->getField('typeCat'), [-2, 7]))
$classSpells[$id] = [new IconElement(Type::SPELL, $id, $modSpells->getField('name', true), size: IconElement::SIZE_SMALL), []];
else
$miscSpells[$id] = [new IconElement(Type::SPELL, $id, $modSpells->getField('name', true), size: IconElement::SIZE_SMALL), []];
}
if ($classSpells)
{
foreach (DB::World()->select('SELECT `spell_id` AS ARRAY_KEY, `first_spell_id` AS "0", `rank` AS "1" FROM spell_ranks WHERE `spell_id` IN (?a)', array_keys($classSpells)) as $spellId => [$firstSpellId, $rank])
{
$classSpells[$firstSpellId][1][0] = min($classSpells[$firstSpellId][1][0] ?? $rank, $rank);
$classSpells[$firstSpellId][1][1] = max($classSpells[$firstSpellId][1][1] ?? $rank, $rank);
if ($spellId != $firstSpellId)
unset($classSpells[$spellId]);
}
array_walk($classSpells, function(&$x) {
if ($x[1] && $x[1][0] == $x[1][1]) // only one rank => unset
$x[1] = null;
});
}
$modifiesData += $modSpells->getListviewData();
if ($modSpells->hasSetFields('skillLines'))
$hideSkillCol = false;
$this->extendGlobalData($modSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
$classSpells = array_values($classSpells);
$miscSpells = array_values($miscSpells);
unset($classSpells, $miscSpells);
}
if ($modifiesData)
{
$tabData = array(
'data' => $modifiesData,
'id' => 'modifies',
'name' => '$LANG.tab_modifies',
'visibleCols' => ['level'],
);
if ($hideSkillCol)
$tabData['hiddenCols'] = ['skill'];
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
}
// tab: [$this is] modified by
$sub = ['OR'];
$conditions = [
['s.typeCat', [-9], '!'], // GM (-9); also include uncategorized (0), NPC-Spell (-8)?; NPC includes totems, lightwell and others :/
['s.spellFamilyId', $this->subject->getField('spellFamilyId')],
&$sub
];
for ($i = 1; $i < 4; $i++)
{
$m1 = $this->subject->getField('spellFamilyFlags1');
$m2 = $this->subject->getField('spellFamilyFlags2');
$m3 = $this->subject->getField('spellFamilyFlags3');
if (!$m1 && !$m2 && !$m3)
continue;
$sub[] = array(
'AND',
['s.effect'.$i.'AuraId', self::MOD_AURAS],
[
'OR',
['s.effect'.$i.'SpellClassMaskA', $m1, '&'],
['s.effect'.$i.'SpellClassMaskB', $m2, '&'],
['s.effect'.$i.'SpellClassMaskC', $m3, '&']
]
);
}
if (count($sub) > 1)
{
$modsSpell = new SpellList($conditions);
if (!$modsSpell->error)
{
$tabData = array(
'data' => $modsSpell->getListviewData(),
'id' => 'modified-by',
'name' => '$LANG.tab_modifiedby',
'visibleCols' => ['level'],
);
if (!$modsSpell->hasSetFields('skillLines'))
$tabData['hiddenCols'] = ['skill'];
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
$this->extendGlobalData($modsSpell->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
}
// tab: see also
$conditions = array(
['s.schoolMask', $this->subject->getField('schoolMask')],
['s.effect1Id', $this->subject->getField('effect1Id')],
['s.effect2Id', $this->subject->getField('effect2Id')],
['s.effect3Id', $this->subject->getField('effect3Id')],
['s.id', $this->typeId, '!'],
['s.name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)]
);
$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?
{
$saE = ['$Listview.extraCols.mode'];
foreach ($data as $id => &$d)
{
$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;
}
}
$tabData = array(
'data' => $data,
'id' => 'see-also',
'name' => '$LANG.tab_seealso',
'visibleCols' => ['level'],
);
if (!$saSpells->hasSetFields('skillLines'))
$tabData['hiddenCols'] = ['skill'];
if (isset($saE))
$tabData['extraCols'] = $saE;
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
$this->extendGlobalData($saSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
// tab: shared cooldown
if ($this->subject->getField('recoveryCategory'))
{
$conditions = array(
['id', $this->typeId, '!'],
['category', $this->subject->getField('category')],
['recoveryCategory', 0, '>'],
);
// limit shared cooldowns to same player class for regulat users
if (!User::isInGroup(U_GROUP_STAFF) && $this->subject->getField('spellFamilyId'))
$conditions[] = ['spellFamilyId', $this->subject->getField('spellFamilyId')];
$cdSpells = new SpellList($conditions);
if (!$cdSpells->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $cdSpells->getListviewData(),
'name' => '$LANG.tab_sharedcooldown',
'id' => 'shared-cooldown'
), SpellList::$brickFile));
$this->extendGlobalData($cdSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
}
// tab: used by - spell
if ($so = DB::Aowow()->selectCell('SELECT `id` FROM ?_spelloverride WHERE `spellId1` = ?d OR `spellId2` = ?d OR `spellId3` = ?d OR `spellId4` = ?d OR `spellId5` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId))
{
$conditions = array(
'OR',
['AND', ['effect1AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect1MiscValue', $so]],
['AND', ['effect2AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect2MiscValue', $so]],
['AND', ['effect3AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect3MiscValue', $so]]
);
$ubSpells = new SpellList($conditions);
if (!$ubSpells->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $ubSpells->getListviewData(),
'id' => 'used-by-spell',
'name' => '$LANG.tab_usedby'
), SpellList::$brickFile));
$this->extendGlobalData($ubSpells->getJSGlobals(GLOBALINFO_SELF));
}
}
// tab: used by - itemset
$conditions = array(
'OR',
['spell1', $this->typeId], ['spell2', $this->typeId], ['spell3', $this->typeId], ['spell4', $this->typeId],
['spell5', $this->typeId], ['spell6', $this->typeId], ['spell7', $this->typeId], ['spell8', $this->typeId]
);
$ubSets = new ItemsetList($conditions);
if (!$ubSets->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $ubSets->getListviewData(),
'id' => 'used-by-itemset',
'name' => '$LANG.tab_usedby'
), ItemsetList::$brickFile));
$this->extendGlobalData($ubSets->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
// tab: used by - item
$conditions = array(
'OR',
['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN, '!'], ['spellId1', $this->typeId]],
['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN, '!'], ['spellId2', $this->typeId]],
['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN, '!'], ['spellId3', $this->typeId]],
['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN, '!'], ['spellId4', $this->typeId]],
['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN, '!'], ['spellId5', $this->typeId]]
);
$ubItems = new ItemList($conditions);
if (!$ubItems->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $ubItems->getListviewData(),
'id' => 'used-by-item',
'name' => '$LANG.tab_usedby'
), ItemList::$brickFile));
$this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF));
}
// tab: used by - object
$conditions = array(
'OR',
['onUseSpell', $this->typeId], ['onSuccessSpell', $this->typeId],
['auraSpell', $this->typeId], ['triggeredSpell', $this->typeId]
);
if (!empty($ubSAI[Type::OBJECT]))
$conditions[] = ['id', $ubSAI[Type::OBJECT]];
$ubObjects = new GameObjectList($conditions);
if (!$ubObjects->error)
{
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $ubObjects->getListviewData(),
'id' => 'used-by-object',
'name' => '$LANG.tab_usedby'
), GameObjectList::$brickFile));
$this->extendGlobalData($ubObjects->getJSGlobals());
}
// tab: used by - areatrigger
if (User::isInGroup(U_GROUP_EMPLOYEE))
{
if (!empty($ubSAI[Type::AREATRIGGER]))
{
$ubTriggers = new AreaTriggerList(array(['id', $ubSAI[Type::AREATRIGGER]]));
if (!$ubTriggers->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $ubTriggers->getListviewData(),
'id' => 'used-by-areatrigger',
'name' => '$LANG.tab_usedby'
), AreaTriggerList::$brickFile, 'areatrigger'));
}
}
}
// tab: criteria of
$conditions = array(
['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET, ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2, ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL,
ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL2, ACHIEVEMENT_CRITERIA_TYPE_LEARN_SPELL]
],
['ac.value1', $this->typeId]
);
$coAchievemnts = new AchievementList($conditions);
if (!$coAchievemnts->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $coAchievemnts->getListviewData(),
'id' => 'criteria-of',
'name' => '$LANG.tab_criteriaof'
), AchievementList::$brickFile));
$this->extendGlobalData($coAchievemnts->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
// tab: contains
// spell_loot_template
$spellLoot = new Loot();
if ($spellLoot->getByContainer(LOOT_SPELL, $this->typeId))
{
$this->extendGlobalData($spellLoot->jsGlobals);
$extraCols = $spellLoot->extraCols;
$extraCols[] = '$Listview.extraCols.percent';
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $spellLoot->getResult(),
'name' => '$LANG.tab_contains',
'id' => 'contains',
'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'],
'extraCols' => array_unique($extraCols)
), ItemList::$brickFile));
}
// tab: bonus loot
if ($extraItemData = DB::World()->select('SELECT `spellId` AS ARRAY_KEY, `additionalCreateChance` AS "0", `additionalMaxNum` AS "1" FROM skill_extra_item_template WHERE `requiredSpecialization` = ?d', $this->typeId))
{
$extraSpells = new SpellList(array(['id', array_keys($extraItemData)]));
if (!$extraSpells->error)
{
$this->extendGlobalData($extraSpells->getJSGlobals(GLOBALINFO_RELATED));
$lvItems = $extraSpells->getListviewData();
foreach ($lvItems as $iId => $data)
{
[$chance, $maxItr] = $extraItemData[$iId];
$lvItems[$iId]['count'] = 1; // expected by js or the pct-col becomes unsortable
$lvItems[$iId]['percent'] = $chance;
$lvItems[$iId]['pctstack'] = $this->buildPctStack($chance / 100, $maxItr, $data['creates'][1]);
$lvItems[$iId]['creates'][2] *= $maxItr;
}
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $lvItems,
'name' => '$LANG.tab_bonusloot',
'id' => 'bonusloot',
'hiddenCols' => ['side', 'reqlevel'],
'extraCols' => ['$Listview.extraCols.percent']
), SpellList::$brickFile));
}
}
// tab: exclusive with
if ($this->firstRank && DB::World()->selectCell('SELECT 1 FROM spell_group WHERE `spell_id` = ?d', $this->firstRank))
{
$groups = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, `spell_id` AS ARRAY_KEY2, `spell_id` FROM spell_group');
// unpack recursion
foreach ($groups as $i => $group)
{
foreach ($group as $j => $g)
{
if ($g > 0)
continue;
foreach ($groups[-$g] ?? [] as $new)
$groups[$i][] = $new;
unset($group[$j]);
}
}
// find ourselves
if ($filtered = array_filter($groups, fn($x) => in_array($this->firstRank, $x)))
{
// get rule set
$rules = DB::World()->selectCol('SELECT `group_id` AS ARRAY_KEY, `stack_rule` FROM spell_group_stack_rules WHERE `group_id` IN (?a)', array_keys($filtered));
// only use groups that have rules set
if ($filtered = array_intersect_key($filtered, $rules))
{
$cnd = ['OR'];
foreach ($filtered as $gr)
$cnd[] = ['s.id', $gr];
$stacks = new SpellList($cnd);
if (!$stacks->error)
{
$lvData = $stacks->getListviewData();
$this->extendGlobalData($stacks->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
if (!$stacks->hasSetFields('skillLines'))
$sH = ['skill'];
foreach ($filtered as $gId => $spellIds)
{
$data = [];
foreach ($spellIds as $id)
if (isset($lvData[$id]) && $id != $this->firstRank)
$data[] = array_merge($lvData[$id], ['stackRule' => $rules[$gId]]);
if (!$data)
continue;
$tabData = array(
'data' => $data,
'id' => 'spell-group-stack-'.$rules[$gId],
'name' => Lang::spell('stackGroup'),
'visibleCols' => ['stackRules']
);
if (isset($sH))
$tabData['hiddenCols'] = $sH;
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
}
}
}
}
}
// tab: linked with
$rows = DB::World()->select(
'SELECT `spell_trigger` AS "trigger", `spell_effect` AS "effect", `type`, IF(ABS(`spell_effect`) = ?d, ABS(`spell_trigger`), ABS(`spell_effect`)) AS "related"
FROM spell_linked_spell
WHERE ABS(`spell_effect`) = ?d OR ABS(`spell_trigger`) = ?d',
$this->typeId, $this->typeId, $this->typeId
);
$related = [];
foreach ($rows as $row)
$related[] = $row['related'];
if ($related)
$linked = new SpellList(array(['s.id', $related]));
if (isset($linked) && !$linked->error)
{
$lv = $linked->getListviewData();
$data = [];
foreach ($rows as $r)
{
foreach ($lv as $dk => $d)
{
if ($r['related'] != $dk)
continue;
$lv[$dk]['linked'] = [$r['trigger'], $r['effect'], $r['type']];
$data[] = $lv[$dk];
break;
}
}
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $data,
'id' => 'spell-link',
'name' => Lang::spell('linkedWith'),
'hiddenCols' => ['skill', 'name'],
'visibleCols' => ['linkedTrigger', 'linkedEffect']
), SpellList::$brickFile));
$this->extendGlobalData($linked->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
// tab: triggered by
$conditions = array(
'OR',
['AND', ['OR', ['effect1Id', SpellList::EFFECTS_TRIGGER], ['effect1AuraId', SpellList::AURAS_TRIGGER]], ['effect1TriggerSpell', $this->typeId]],
['AND', ['OR', ['effect2Id', SpellList::EFFECTS_TRIGGER], ['effect2AuraId', SpellList::AURAS_TRIGGER]], ['effect2TriggerSpell', $this->typeId]],
['AND', ['OR', ['effect3Id', SpellList::EFFECTS_TRIGGER], ['effect3AuraId', SpellList::AURAS_TRIGGER]], ['effect3TriggerSpell', $this->typeId]],
);
$trigger = new SpellList($conditions);
if (!$trigger->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $trigger->getListviewData(),
'id' => 'triggered-by',
'name' => '$LANG.tab_triggeredby'
), SpellList::$brickFile));
$this->extendGlobalData($trigger->getJSGlobals(GLOBALINFO_SELF));
}
// tab: used by - creature
$conditions = array(
'OR',
['spell1', $this->typeId], ['spell2', $this->typeId], ['spell3', $this->typeId], ['spell4', $this->typeId],
['spell5', $this->typeId], ['spell6', $this->typeId], ['spell7', $this->typeId], ['spell8', $this->typeId]
);
if (!empty($ubSAI[Type::NPC]))
$conditions[] = ['id', $ubSAI[Type::NPC]];
if ($auras = DB::World()->selectCol('SELECT `entry` FROM creature_template_addon WHERE `auras` REGEXP ?', '\\b'.$this->typeId.'\\b'))
$conditions[] = ['id', $auras];
$ubCreature = new CreatureList($conditions);
if (!$ubCreature->error)
{
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $ubCreature->getListviewData(),
'id' => 'used-by-npc',
'name' => '$LANG.tab_usedby'
), CreatureList::$brickFile));
$this->extendGlobalData($ubCreature->getJSGlobals(GLOBALINFO_SELF));
}
// tab: zone
if ($areaSpells = DB::World()->select('SELECT `area` AS ARRAY_KEY, `aura_spell` AS "0", `quest_start` AS "1", `quest_end` AS "2", `quest_start_status` AS "3", `quest_end_status` AS "4", `racemask` AS "5", `gender` AS "6" FROM spell_area WHERE `spell` = ?d', $this->typeId))
{
$zones = new ZoneList(array(['id', array_keys($areaSpells)]));
if (!$zones->error)
{
$lvZones = $zones->getListviewData();
$this->extendGlobalData($zones->getJSGlobals());
$resultLv = [];
$parents = [];
$extraCols = [];
foreach ($areaSpells as $areaId => $condition)
{
if (empty($lvZones[$areaId]))
continue;
$row = $lvZones[$areaId];
// attach to lv row and evaluate after merging
$row['__condition'] = $condition;
// merge subzones, into one row, if: spell_area data is identical && parentZone is shared
if ($p = $zones->getEntry($areaId)['parentArea'])
{
$parents[] = $p;
$row['__parent'] = $p;
$row['subzones'] = [$areaId];
}
else
$row['__parent'] = 0;
$set = false;
foreach ($resultLv as &$v)
{
if ($v['__parent'] != $row['__parent'] && $v['id'] != $row['__parent'])
continue;
if ($v['__condition'] != $row['__condition'])
continue;
if (!$row['__parent'] && $v['id'] != $row['__parent'])
continue;
$set = true;
$v['subzones'][] = $row['id'];
break;
}
// add self as potential subzone; IF we are a parentZone without added children, we get filtered in JScript
if (!$set)
{
$row['subzones'] = [$row['id']];
$resultLv[] = $row;
}
}
// overwrite lvData with parent-lvData (condition and subzones are kept)
if ($parents)
{
$parents = (new ZoneList(array(['id', $parents])))->getListviewData();
foreach ($resultLv as &$_)
if (isset($parents[$_['__parent']]))
$_ = array_merge($_, $parents[$_['__parent']]);
}
$cnd = new Conditions();
foreach ($resultLv as $idx => $lv)
{
[$auraSpell, $questStart, $questEnd, $questStartState, $questEndState, $raceMask, $gender] = $lv['__condition'];
if ($auraSpell)
$cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [$auraSpell > 0 ? Conditions::AURA : -Conditions::AURA, abs($auraSpell)]);
if ($questStart)
$cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::QUESTSTATE, $questStart, $questStartState]);
if ($questEnd && $questEnd != $questStart)
$cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::QUESTSTATE, $questEnd, $questEndState]);
if ($raceMask)
$cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::CHR_RACE, $raceMask]);
if ($gender != 2) // 2: both
$cnd->addExternalCondition(Conditions::SRC_NONE, $lv['id'], [Conditions::GENDER, $gender]);
// remove temp storage from result
unset($resultLv[$idx]['__condition']);
unset($resultLv[$idx]['__parent']);
}
if ($cnd->toListviewColumn($resultLv, $extraCols))
$this->extendGlobalData($cnd->getJsGlobals());
$tabData = ['data' => $resultLv];
if ($extraCols)
{
$tabData['extraCols'] = $extraCols;
$tabData['hiddenCols'] = ['instancetype'];
}
$this->lvTabs->addListviewTab(new Listview($tabData, ZoneList::$brickFile));
}
}
// tab: teaches
if ($ids = Game::getTaughtSpells($this->subject))
{
$teaches = new SpellList(array(['id', $ids]));
if (!$teaches->error)
{
$this->extendGlobalData($teaches->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
$vis = ['level', 'schools'];
foreach ($teaches->iterate() as $__)
{
if (!$teaches->canCreateItem())
continue;
$vis[] = 'reagents';
break;
}
$tabData = array(
'data' => $teaches->getListviewData(),
'id' => 'teaches-spell',
'name' => '$LANG.tab_teaches',
'visibleCols' => $vis,
);
if (!$teaches->hasSetFields('skillLines'))
$tabData['hiddenCols'] = ['skill'];
$this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile));
}
}
// tab: taught by npc
if ($this->subject->getSources($s) && in_array(SRC_TRAINER, $s))
{
$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"
FROM creature_default_trainer cdt
JOIN trainer_spell ts ON ts.`TrainerId` = cdt.`TrainerId`
WHERE ts.`SpellId` = ?d',
$this->typeId
);
if ($trainers)
{
$tbTrainer = new CreatureList(array(Cfg::get('SQL_LIMIT_NONE'), ['ct.id', array_keys($trainers)], ['s.guid', null, '!'], ['ct.npcflag', NPC_FLAG_TRAINER, '&']));
if (!$tbTrainer->error)
{
$this->extendGlobalData($tbTrainer->getJSGlobals());
$cnd = new Conditions();
$skill = $this->subject->getField('skillLines');
foreach ($trainers as $tId => $train)
{
if ($_ = $train['reqLevel'])
$cnd->addExternalCondition(Conditions::SRC_NONE, $tId, [Conditions::LEVEL, $_, Conditions::OP_GT_E]);
if ($_ = $train['reqSkillId'])
if (count($skill) == 1 && $_ != $skill[0])
$cnd->addExternalCondition(Conditions::SRC_NONE, $tId, [Conditions::SKILL, $_, $train['reqSkillValue']]);
for ($i = 1; $i < 3; $i++)
if ($_ = $train['reqSpellId'.$i])
$cnd->addExternalCondition(Conditions::SRC_NONE, $tId, [Conditions::SPELL, $_]);
}
$lvData = $tbTrainer->getListviewData();
$extraCols = [];
if ($cnd->toListviewColumn($lvData, $extraCols))
$this->extendGlobalData($cnd->getJsGlobals());
$tabData = array(
'data' => $lvData,
'id' => 'taught-by-npc',
'name' => '$LANG.tab_taughtby',
);
if ($extraCols)
$tabData['extraCols'] = $extraCols;
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile));
}
}
}
// tab: taught by spell
$conditions = array(
'OR',
['AND', ['effect1Id', SpellList::EFFECTS_TEACH], ['effect1TriggerSpell', $this->typeId]],
['AND', ['effect2Id', SpellList::EFFECTS_TEACH], ['effect2TriggerSpell', $this->typeId]],
['AND', ['effect3Id', SpellList::EFFECTS_TEACH], ['effect3TriggerSpell', $this->typeId]],
);
$tbSpell = new SpellList($conditions);
$tbsData = [];
if (!$tbSpell->error)
{
$tbsData = $tbSpell->getFoundIDs();
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $tbSpell->getListviewData(),
'id' => 'taught-by-spell',
'name' => '$LANG.tab_taughtby'
), SpellList::$brickFile));
$this->extendGlobalData($tbSpell->getJSGlobals(GLOBALINFO_SELF));
}
// tab: taught by quest
$conditions = array(
'OR',
['sourceSpellId', $this->typeId],
['rewardSpell', $this->typeId],
['rewardSpellCast', $this->typeId]
);
if ($tbsData)
array_push($conditions, ['rewardSpell', $tbsData], ['rewardSpellCast', $tbsData]);
$tbQuest = new QuestList($conditions);
if (!$tbQuest->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $tbQuest->getListviewData(),
'id' => 'reward-from-quest',
'name' => '$LANG.tab_rewardfrom'
), QuestList::$brickFile));
$this->extendGlobalData($tbQuest->getJSGlobals());
}
// tab: taught by item (i'd like to precheck $this->subject->sources, but there is no source:item only complicated crap like "drop" and "vendor")
$conditions = array(
'OR',
['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN], ['spellId1', $this->typeId]],
['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN], ['spellId2', $this->typeId]],
['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN], ['spellId3', $this->typeId]],
['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN], ['spellId4', $this->typeId]],
['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN], ['spellId5', $this->typeId]],
);
$tbItem = new ItemList($conditions);
if (!$tbItem->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $tbItem->getListviewData(),
'id' => 'taught-by-item',
'name' => '$LANG.tab_taughtby'
), ItemList::$brickFile));
$this->extendGlobalData($tbItem->getJSGlobals(GLOBALINFO_SELF));
}
// tab: enchantments
$conditions = array(
'OR',
['AND', ['type1', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object1', $this->typeId]],
['AND', ['type2', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object2', $this->typeId]],
['AND', ['type3', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object3', $this->typeId]]
);
$enchList = new EnchantmentList($conditions);
if (!$enchList->error)
{
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $enchList->getListviewData(),
'name' => Util::ucFirst(Lang::game('enchantments'))
), EnchantmentList::$brickFile, 'enchantment'));
$this->extendGlobalData($enchList->getJSGlobals());
}
// tab: sounds
$data = [];
$seSounds = [];
for ($i = 1; $i < 4; $i++) // sounds from screen effect
if ($this->subject->getField('effect'.$i.'AuraId') == SPELL_AURA_SCREEN_EFFECT)
$seSounds = DB::Aowow()->selectRow('SELECT `ambienceDay`, `ambienceNight`, `musicDay`, `musicNight` FROM ?_screeneffect_sounds WHERE `id` = ?d', $this->subject->getField('effect'.$i.'MiscValue'));
$activitySounds = DB::Aowow()->selectRow('SELECT * FROM ?_spell_sounds WHERE `id` = ?d', $this->subject->getField('spellVisualId'));
array_shift($activitySounds); // remove id-column
if ($soundIDs = $activitySounds + $seSounds)
{
$sounds = new SoundList(array(['id', $soundIDs]));
if (!$sounds->error)
{
$data = $sounds->getListviewData();
foreach ($activitySounds as $activity => $id)
if (isset($data[$id]))
$data[$id]['activity'] = $activity; // no index, js wants a string :(
$tabData = ['data' => $data];
if ($activitySounds)
$tabData['visibleCols'] = ['activity'];
$this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF));
$this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile));
}
}
// tab: unlocks (object or item)
$lockIds = DB::Aowow()->selectCol(
'SELECT `id` FROM ?_lock WHERE (`type1` = ?d AND `properties1` = ?d) OR
(`type2` = ?d AND `properties2` = ?d) OR (`type3` = ?d AND `properties3` = ?d) OR
(`type4` = ?d AND `properties4` = ?d) OR (`type5` = ?d AND `properties5` = ?d)',
LOCK_TYPE_SPELL, $this->typeId, LOCK_TYPE_SPELL, $this->typeId,
LOCK_TYPE_SPELL, $this->typeId, LOCK_TYPE_SPELL, $this->typeId,
LOCK_TYPE_SPELL, $this->typeId
);
// we know this spell effect is only in use on index 1
if ($this->subject->getField('effect1Id') == SPELL_EFFECT_OPEN_LOCK && ($lockId = $this->subject->getField('effect1MiscValue')))
$lockIds += DB::Aowow()->selectCol(
'SELECT `id` FROM ?_lock WHERE (`type1` = ?d AND `properties1` = ?d) OR
(`type2` = ?d AND `properties2` = ?d) OR (`type3` = ?d AND `properties3` = ?d) OR
(`type4` = ?d AND `properties4` = ?d) OR (`type5` = ?d AND `properties5` = ?d)',
LOCK_TYPE_SKILL, $lockId, LOCK_TYPE_SKILL, $lockId,
LOCK_TYPE_SKILL, $lockId, LOCK_TYPE_SKILL, $lockId,
LOCK_TYPE_SKILL, $lockId
);
if ($lockIds)
{
// objects
$lockedObj = new GameObjectList(array(Cfg::get('SQL_LIMIT_NONE'), ['lockId', $lockIds]));
if (!$lockedObj->error)
{
$this->addDataLoader('zones');
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $lockedObj->getListviewData(),
'name' => '$LANG.tab_unlocks',
'id' => 'unlocks-object',
'visibleCols' => $lockedObj->hasSetFields('reqSkill') ? ['skill'] : null
), GameObjectList::$brickFile));
}
$lockedItm = new ItemList(array(Cfg::get('SQL_LIMIT_NONE'), ['lockId', $lockIds]));
if (!$lockedItm->error)
{
$this->extendGlobalData($lockedItm->getJSGlobals(GLOBALINFO_SELF));
$this->lvTabs->addListviewTab(new Listview(array(
'data' => $lockedItm->getListviewData(),
'name' => '$LANG.tab_unlocks',
'id' => 'unlocks-item'
), ItemList::$brickFile));
}
}
// find associated NPC, Item and merge results
// taughtbypets (unused..?)
// taughtbyquest (usually the spell casted as quest reward teaches something; exclude those seplls from taughtBySpell)
// taughtbytrainers
// taughtbyitem
// tab: conditions
$cnd = new Conditions();
$cnd->getBySourceEntry($this->typeId, Conditions::SRC_SPELL_IMPLICIT_TARGET, Conditions::SRC_SPELL, Conditions::SRC_SPELL_CLICK_EVENT, Conditions::SRC_VEHICLE_SPELL, Conditions::SRC_SPELL_PROC)
->getByCondition(Type::SPELL, $this->typeId)
->prepare();
if ($tab = $cnd->toListviewTab())
{
$this->extendGlobalData($cnd->getJsGlobals());
$this->lvTabs->addDataTab(...$tab);
}
parent::generate();
}
/******************************************/
/* SpellLoot recursive dropchance builder */
/******************************************/
private function buildPctStack(float $baseChance, int $maxStack, int $baseCount = 1) : string
{
// note: pctStack does not contain absolute values but chances relative to the overall drop chance
// e.g.: dropChance is 17% then [1 => 50, 2 => 25, 3 => 25] displays > Stack of 1: 8%; Stack of 2: 4%; Stack of 3: 4%
$maxStack = $maxStack ?: 1;
$pctStack = [];
for ($i = 1; $i <= $maxStack; $i++)
{
$pctStack[$i] = (($baseChance ** $i) * 100) / $baseChance;
// remove chance from previous stacks
if ($i > 1)
$pctStack[$i-1] -= $pctStack[$i];
}
// cleanup rounding errors
$pctStack = array_map(fn($x) => round($x, 3), $pctStack);
// cleanup tiny fractions
$pctStack = array_filter($pctStack, fn($x) => ($x * $baseChance) >= 0.01);
if ($baseCount > 1)
$pctStack = array_combine(array_map(fn($x) => $x * $baseCount, array_keys($pctStack)), $pctStack);
return json_encode($pctStack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON !
}
/**********************************/
/* recursive reagent list builder */
/**********************************/
private function appendReagentItem(array &$reagentResult, int $itemId, int $qty, int $mult, int $level, string $path, array $alreadyUsed, int $fromSpell = 0) : bool
{
if (in_array($itemId, $alreadyUsed))
return false;
$item = DB::Aowow()->selectRow(
'SELECT `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, i.`id`, ic.`name` AS `iconString`, `quality`, `spellId1`, `spellCharges1`
FROM ?_items i
LEFT JOIN ?_icons ic ON ic.`id` = i.`iconId`
WHERE i.`id` = ?d',
$itemId
);
if (!$item)
return false;
$this->extendGlobalIds(Type::ITEM, $item['id']);
// the spell calling this is also on the item and triggering it destroys the item
// so effectively we need one more. (see elemental particles and enchantment essences)
if ($fromSpell && $fromSpell == $item['spellId1'] && $item['spellCharges1'] == -1)
$qty++;
$level++;
$data = array(
'path' => $path.'.'.Type::ITEM.'-'.$item['id'],
'level' => $level,
'final' => false,
'typeStr' => Type::getFileString(Type::ITEM),
'icon' => new IconElement(Type::ITEM, $item['id'], Util::localizedString($item, 'name'), $qty * $mult, '', $item['quality'], IconElement::SIZE_SMALL, align: 'right', element: 'iconlist-icon')
);
$idx = count($reagentResult);
$reagentResult[] = $data;
$alreadyUsed[] = $item['id'];
if (!$this->appendReagentSpell($reagentResult, $item['id'], $qty * $mult, $data['level'], $data['path'], $alreadyUsed))
$reagentResult[$idx]['final'] = true;
return true;
}
private function appendReagentSpell(array &$reagentResult, int $itemId, int $qty, int $level, string $path, array $alreadyUsed) : bool
{
$level++;
// assume that tradeSpells only use the first index to create items, so this runs somewhat efficiently >.<
$spells = DB::Aowow()->select(
'SELECT `reagent1`, `reagent2`, `reagent3`, `reagent4`, `reagent5`, `reagent6`, `reagent7`, `reagent8`,
`reagentCount1`, `reagentCount2`, `reagentCount3`, `reagentCount4`, `reagentCount5`, `reagentCount6`, `reagentCount7`, `reagentCount8`,
`name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`,
`iconIdBak`,
s.`id` AS ARRAY_KEY, ic.`name` AS `iconString`
FROM ?_spell s
JOIN ?_icons ic ON s.`iconId` = ic.`id`
WHERE (`effect1CreateItemId` = ?d AND `effect1Id` = ?d)',// OR
// (`effect2CreateItemId` = ?d AND `effect2Id` = ?d) OR
// (`effect3CreateItemId` = ?d AND `effect3Id` = ?d)',
$itemId, SPELL_EFFECT_CREATE_ITEM //, $itemId, SPELL_EFFECT_CREATE_ITEM, $itemId, SPELL_EFFECT_CREATE_ITEM
);
if (!$spells)
return false;
$didAppendSomething = false;
foreach ($spells as $sId => $row)
{
if (in_array(-$sId, $alreadyUsed))
continue;
$this->extendGlobalIds(Type::SPELL, $sId);
$data = array(
'path' => $path.'.'.Type::SPELL.'-'.$sId,
'level' => $level,
'final' => false,
'typeStr' => Type::getFileString(Type::SPELL),
'icon' => new IconElement(Type::SPELL, $sId, Util::localizedString($row, 'name'), $qty, size: IconElement::SIZE_SMALL, align: 'right', element: 'iconlist-icon')
);
$reagentResult[] = $data;
$_aU = $alreadyUsed;
$_aU[] = -$sId;
$hasUnusedReagents = false;
for ($i = 1; $i < 9; $i++)
{
if ($row['reagent'.$i] <= 0 || $row['reagentCount'.$i] <= 0)
continue;
if ($this->appendReagentItem($reagentResult, $row['reagent'.$i], $row['reagentCount'.$i], $qty, $data['level'], $data['path'], $_aU, $sId))
{
$hasUnusedReagents = true;
$didAppendSomething = true;
}
}
if (!$hasUnusedReagents) // no reagents were added, remove spell from result set
array_pop($reagentResult);
}
return $didAppendSomething;
}
private function createReagentList() : void
{
$reagentResult = [];
$enhanced = false;
$reagents = $this->subject->getReagentsForCurrent();
if (!$reagents)
return;
foreach ($reagents as [$iId, $num])
{
$relItem = $this->subject->relItems->getEntry($iId);
$data = array(
'path' => Type::ITEM.'-'.$iId, // id of the html-element
'level' => 0, // depths in array, used for indentation
'final' => false,
'typeStr' => Type::getFileString(Type::ITEM),
'icon' => new IconElement(
Type::ITEM,
is_null($relItem) ? 0 : $iId,
is_null($relItem) ? 'Item #'.$iId : $this->subject->relItems->getField('name', true),
$num,
quality: $relItem['quality'] ?? 'q',
size: IconElement::SIZE_SMALL,
link: !is_null($relItem),
align: 'right',
element: 'iconlist-icon'
)
);
$idx = count($reagentResult);
$reagentResult[] = $data;
// start with self and current original item in usedEntries (spell < 0; item > 0)
if ($this->appendReagentSpell($reagentResult, $iId, $reagents[$iId][1], 0, $data['path'], [-$this->typeId, $iId]))
$enhanced = true;
else
$reagentResult[$idx]['final'] = true;
}
// increment all indizes (by prepending null and removing it again)
array_unshift($reagentResult, null);
unset($reagentResult[0]);
$this->reagents = [$enhanced, $reagentResult];
}
private function calculateEffectScaling() : array // calculation mostly like seen in TC
{
if ($this->subject->getField('attributes3') & SPELL_ATTR3_NO_DONE_BONUS)
return [0, 0, 0, 0];
if (!$this->subject->isScalableDamagingSpell() && !$this->subject->isScalableHealingSpell())
return [0, 0, 0, 0];
$scaling = [0, 0, 0, 0];
$pMask = $this->subject->periodicEffectsMask();
$allDoTs = true;
for ($i = 1; $i < 4; $i++)
{
if (!$this->subject->getField('effect'.$i.'Id'))
continue;
if ($pMask & 1 << ($i - 1))
{
$scaling[1] = $this->subject->getField('effect'.$i.'BonusMultiplier');
continue;
}
else if ($this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_MAGIC)
$scaling[0] = $this->subject->getField('effect'.$i.'BonusMultiplier');
$allDoTs = false;
}
if ($s = DB::World()->selectRow('SELECT `direct_bonus` AS "0", `dot_bonus` AS "1", `ap_bonus` AS "2", `ap_dot_bonus` AS "3" FROM spell_bonus_data WHERE `entry` = ?d', $this->firstRank))
$scaling = $s;
if (!in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7]) || $this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_NONE)
return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling);
foreach ($scaling as $k => $v)
{
// recalculate if spell_bonus_data says so
if ($v != -1)
continue;
// no known calculation for physical abilities
if (in_array($k, [2, 3])) // [direct AP, DoT AP]
continue;
// dont use spellPower to scale physical Abilities
if ($this->subject->getField('schoolMask') == (1 << SPELL_SCHOOL_NORMAL) && in_array($k, [0, 1]))
continue;
$isDOT = false;
if (in_array($k, [1, 3])) // [DoT SP, DoT AP]
{
if ($pMask)
$isDOT = true;
else
continue;
}
else if ($allDoTs) // if all used effects are periodic, dont calculate direct component
continue;
// damage over time spells bonus calculation
$dotFactor = 1.0;
if ($isDOT)
{
$dotDuration = $this->subject->getField('duration');
// 200% limit
if ($dotDuration > 0)
{
if ($dotDuration > 30000)
$dotDuration = 30000;
if (!$this->subject->isChanneledSpell())
$dotFactor = $dotDuration / 15000;
}
}
// distribute damage over multiple effects, reduce by AoE
$castingTime = $this->subject->getCastingTimeForBonus($isDOT);
// 50% for damage and healing spells for leech spells from damage bonus and 0% from healing
for ($j = 1; $j < 4; ++$j)
{
if ($this->subject->getField('effectId'.$j) == SPELL_EFFECT_HEALTH_LEECH || $this->subject->getField('effect'.$j.'AuraId') == SPELL_AURA_PERIODIC_LEECH)
{
$castingTime /= 2;
break;
}
}
if ($this->subject->isScalableHealingSpell())
$castingTime *= 1.88;
// SPELL_SCHOOL_MASK_NORMAL
if ($this->subject->getField('schoolMask') != (1 << SPELL_SCHOOL_NORMAL))
$scaling[$k] = ($castingTime / 3500.0) * $dotFactor;
else
$scaling[$k] = 0; // would be 1 ($dotFactor), but we dont want it to be displayed
}
return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling);
}
private function createRequiredItems() : void
{
// parse itemClass & itemSubClassMask
$class = $this->subject->getField('equippedItemClass');
$subClass = $this->subject->getField('equippedItemSubClassMask');
$invType = $this->subject->getField('equippedItemInventoryTypeMask');
if ($class <= 0)
return;
$tip = 'Class: '.$class.'<br />SubClass: '.Util::asHex($subClass);
$text = Lang::getRequiredItems($class, $subClass, false);
if ($invType)
{
// remap some duplicated strings 'Off Hand' and 'Shield' are never used simultaneously
if ($invType & (1 << INVTYPE_ROBE)) // Robe => Chest
{
$invType &= ~(1 << INVTYPE_ROBE);
$invType |= (1 << INVTYPE_CHEST);
}
if ($invType & (1 << INVTYPE_RANGEDRIGHT)) // Ranged2 => Ranged
{
$invType &= ~(1 << INVTYPE_RANGEDRIGHT);
$invType |= (1 << INVTYPE_RANGED);
}
$_ = [];
$strs = Lang::item('inventoryType');
foreach ($strs as $k => $str)
if ($invType & 1 << $k && $str)
$_[] = $str;
$tip .= '<br />'.Lang::item('slot').Lang::main('colon').Util::asHex($invType);
$text .= ' '.Lang::spell('_inSlot').implode(', ', $_);
}
$this->items = $this->fmtStaffTip($text, $tip);
}
private function createEffects() : void
{
// proc data .. maybe use more information..?
$procData = array(
'chance' => $this->subject->getField('procChance'),
'cooldown' => 0
);
if ($sp = DB::World()->selectRow('SELECT IF(`ProcsPerMinute` > 0, -`ProcsPerMinute`, `Chance`) AS "chance", `Cooldown` AS "cooldown" FROM `spell_proc` WHERE ABS(`SpellId`) = ?d', $this->firstRank))
{
$procData['chance'] = $sp['chance'] ?: $procData['chance'];
$procData['cooldown'] = $sp['cooldown'] ?: $procData['cooldown'];
}
$effects = [];
$spellIdx = array_unique(array_merge($this->subject->canTriggerSpell(), $this->subject->canTeachSpell()));
$itemIdx = $this->subject->canCreateItem();
$perfItem = DB::World()->selectRow('SELECT `perfectItemType` AS "itemId", `requiredSpecialization` AS "reqSpellId", `perfectCreateChance` AS "chance" FROM skill_perfect_item_template WHERE `spellId` = ?d', $this->typeId);
$scaling = $this->calculateEffectScaling();
// Iterate through all effects:
for ($i = 1; $i < 4; $i++)
{
if ($this->subject->getField('effect'.$i.'Id') <= 0)
continue;
$effId = $this->subject->getField('effect'.$i.'Id');
$effMV = $this->subject->getField('effect'.$i.'MiscValue');
$effMVB = $this->subject->getField('effect'.$i.'MiscValueB');
$effBP = $this->subject->getField('effect'.$i.'BasePoints');
$effDS = $this->subject->getField('effect'.$i.'DieSides');
$effRPPL = $this->subject->getField('effect'.$i.'RealPointsPerLevel');
$effPPCP = $this->subject->getField('effect'.$i.'PointsPerComboPoint');
$effAura = $this->subject->getField('effect'.$i.'AuraId');
/* Effect Format
*
* EffectName<: AuraName>< (effMV)>< [effMVB]>
* Value: A<, plus y per level>
* Radius: Byd
* Interval: Cms
* Mechanic: D
* Proc Chance: E% || (E procs per minute)
* Cooldown: Fs
* < formated markup >
* < iconlist >
* < icon >
*/
$_nameEffect = $_nameAura = $_nameMV = $_nameMVB = $_markup = '';
$_icon = $_perfItem = $_footer = [];
$_footer['value'] = [0, 0];
$valueFmt = '%s';
// Icons:
// .. from item
if (in_array($i, $itemIdx))
{
if ($itemId = $this->subject->getField('effect'.$i.'CreateItemId'))
{
$itemEntry = $this->subject->relItems->getEntry($itemId);
$_icon = new IconElement(
Type::ITEM,
$itemId,
$itemEntry ? $this->subject->relItems->getField('name', true) : Util::ucFirst(Lang::game('item')).' #'.$itemId,
$this->createNumRange($effBP, $effDS),
quality: $itemEntry ? $this->subject->relItems->getField('quality') : '',
link: !empty($itemEntry)
);
}
// perfect Items
if ($perfItem && $this->subject->relItems->getEntry($perfItem['itemId']))
{
$cndSpell = new SpellList(array(['id', $perfItem['reqSpellId']]));
if (!$cndSpell->error)
{
$_perfItem = array(
'spellId' => $cndSpell->id,
'spellName' => $cndSpell->getField('name', true),
'icon' => $cndSpell->getField('iconString'),
'chance' => $perfItem['chance'],
'item' => new IconElement(
Type::ITEM,
$perfItem['itemId'],
$this->subject->relItems->getField('name', true),
quality: $this->subject->relItems->getField('quality')
)
);
}
}
else if ($extraItem = DB::World()->selectRow('SELECT * FROM skill_extra_item_template WHERE `spellid` = ?d', $this->typeId))
{
$cndSpell = new SpellList(array(['id', $extraItem['requiredSpecialization']]));
if (!$cndSpell->error)
{
$_perfItem = array(
'spellId' => $cndSpell->id,
'spellName' => $cndSpell->getField('name', true),
'icon' => $cndSpell->getField('iconString'),
'chance' => $extraItem['additionalCreateChance'],
'item' => new IconElement(
Type::ITEM,
$this->subject->relItems->id,
$this->subject->relItems->getField('name', true),
num: '+'.$this->createNumRange($effBP, $effDS, $extraItem['additionalMaxNum']),
quality: $this->subject->relItems->getField('quality')
)
);
}
}
}
// .. from spell
else if (in_array($i, $spellIdx) || $effId == SPELL_EFFECT_UNLEARN_SPECIALIZATION)
{
if ($effId == SPELL_EFFECT_TITAN_GRIP)
$triggeredSpell = $effMV;
else
$triggeredSpell = $this->subject->getField('effect'.$i.'TriggerSpell');
if ($triggeredSpell > 0) // Dummy Auras are probably scripted
{
$trig = new SpellList(array(['s.id', (int)$triggeredSpell]));
$_icon = new IconElement(
Type::SPELL,
$triggeredSpell,
$trig->error ? Util::ucFirst(Lang::game('spell')).' #'.$triggeredSpell : $trig->getField('name', true),
link: !$trig->error
);
$this->extendGlobalData($trig->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED));
}
}
// small text under effect name and resolved links .. order of adding to _footer determines order of output.
// cases where we dont want 'Value' to be displayed [min, max, staffTT]
if (!in_array($i, $itemIdx) && !in_array($effAura, [SPELL_AURA_MOD_TAUNT, SPELL_AURA_MOD_STUN, SPELL_AURA_MOD_SHAPESHIFT, SPELL_AURA_MECHANIC_IMMUNITY]) && !in_array($effId, [SPELL_EFFECT_PLAY_MUSIC]) && ($effBP + $effDS) != 0)
$_footer['value'] = [$effBP + ($effDS ? 1 : 0), $effBP + $effDS];
if ($this->subject->getField('effect'.$i.'RadiusMax') > 0)
$_footer['radius'] = Lang::spell('_radius').$this->subject->getField('effect'.$i.'RadiusMax').' '.Lang::spell('_distUnit');
if ($this->subject->getField('effect'.$i.'Periode') > 0)
$_footer['interval'] = Lang::spell('_interval').Util::formatTime($this->subject->getField('effect'.$i.'Periode'));
if ($_ = $this->subject->getField('effect'.$i.'Mechanic'))
$_footer['mechanic'] = Lang::game('mechanic').Lang::main('colon').Lang::game('me', $_);
if (in_array($i, $this->subject->canTriggerSpell()) && $procData['chance'] && $procData['chance'] < 100)
{
$_footer['proc'] = $procData['chance'] < 0 ? Lang::spell('ppm', [-$procData['chance']]) : Lang::spell('procChance', [$procData['chance']]);
if ($procData['cooldown'])
$_footer['procCD'] = Lang::game('cooldown', [Util::formatTime($procData['cooldown'], true)]);
}
// Effect Name
if ($_ = Lang::spell('effects', $effId))
$_nameEffect = '<a href="?spells&filter=cr=109;crs='.$effId.';crv=0">'.$this->fmtStaffTip($_, 'EffectId: '.$effId).'</a>';
else
$_nameEffect = Lang::spell('unkEffect', [$effId]);
// parse masks and indizes
switch ($effId)
{
case SPELL_EFFECT_ENERGIZE_PCT:
$valueFmt = '%s%%';
case SPELL_EFFECT_POWER_DRAIN:
case SPELL_EFFECT_ENERGIZE:
case SPELL_EFFECT_POWER_BURN:
if ($_ = Lang::spell('powerTypes', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
if ($effMV == POWER_RAGE || $effMV == POWER_RUNIC_POWER)
array_walk($_footer['value'], fn(&$x) => $x /= 10);
break;
case SPELL_EFFECT_BIND:
if ($effMV <= 0)
$_nameMV = $this->fmtStaffTip(Lang::spell('currentArea'), 'MiscValue: '.$effMV);
else if ($a = ZoneList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('zone')).' #'.$effMV;
break;
case SPELL_EFFECT_QUEST_COMPLETE:
case SPELL_EFFECT_CLEAR_QUEST:
case SPELL_EFFECT_QUEST_FAIL:
if ($a = QuestList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('quest')).' #'.$effMV;
break;
case SPELL_EFFECT_SUMMON_PET:
$effMVB = 67; // TC uses hardcoded summon property 67
// DO NOT BREAK !
case SPELL_EFFECT_SUMMON:
if (($sp = DB::Aowow()->selectRow('SELECT `control`, `slot` FROM ?_summonproperties WHERE `id` = ?d', $effMVB)))
$_nameMVB = $this->fmtStaffTip(Lang::spell('summonControl', $sp['control']).' &ndash; '.Lang::spell('summonSlot', $sp['slot']) , 'SummonProperty: '.$effMVB);
// DO NOT BREAK !
case SPELL_EFFECT_SUMMON_DEMON:
case SPELL_EFFECT_KILL_CREDIT:
case SPELL_EFFECT_KILL_CREDIT2:
if ($a = CreatureList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('npc')).' #'.$effMV;
break;
case SPELL_EFFECT_OPEN_LOCK:
if ($effMV && ($_ = Lang::spell('lockType', $effMV)))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_ENCHANT_ITEM:
case SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY:
case SPELL_EFFECT_ENCHANT_HELD_ITEM:
case SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC:
if ($a = EnchantmentList::makeLink($effMV, cssClass: 'q2'))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('enchantment')).' #'.$effMV;
break;
case SPELL_EFFECT_DISPEL:
case SPELL_EFFECT_STEAL_BENEFICIAL_BUFF:
if ($effMV && ($_ = Lang::game('dt', $effMV)))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_LANGUAGE:
if ($effMV && ($_ = Lang::game('languages', $effMV)))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_TRANS_DOOR:
case SPELL_EFFECT_SUMMON_OBJECT_WILD:
case SPELL_EFFECT_SUMMON_OBJECT_SLOT1:
case SPELL_EFFECT_SUMMON_OBJECT_SLOT2:
case SPELL_EFFECT_SUMMON_OBJECT_SLOT3:
case SPELL_EFFECT_SUMMON_OBJECT_SLOT4:
if ($a = GameobjectList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('object')).' #'.$effMV;
break;
case SPELL_EFFECT_ACTIVATE_OBJECT:
if ($_ = Lang::gameObject('actions', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_APPLY_GLYPH:
if ($_ = DB::Aowow()->selectCell('SELECT `spellId` FROM ?_glyphproperties WHERE `id` = ?d', $effMV))
{
if ($a = SpellList::makeLink($_))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('spell')).' #'.$_;
}
break;
case SPELL_EFFECT_SKINNING:
$_ = match ($effMV)
{
0 => Lang::game('ct', 1).', '.Lang::game('ct', 2), // Skinning > Beast, Dragonkin
1, 2 => Lang::game('ct', 4), // Gathering, Mining > Elemental
3 => Lang::game('ct', 9), // Dismantling > Mechanic
default => ''
};
if ($_)
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_DISPEL_MECHANIC:
if ($_ = Lang::game('me', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_SKILL_STEP:
case SPELL_EFFECT_SKILL:
if ($a = SkillList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('skill')).' #'.$effMV;
break;
case SPELL_EFFECT_ACTIVATE_RUNE:
if ($_ = Lang::spell('powerRunes', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_PLAY_SOUND:
case SPELL_EFFECT_PLAY_MUSIC:
if (DB::Aowow()->selectCell('SELECT 1 FROM ?_sounds WHERE `id` = ?d', $effMV))
{
$_markup = '[sound='.$effMV.']';
$effMV = 0; // prevent default display
}
else
$_nameMV = Util::ucFirst(Lang::game('sound')).' #'.$effMV;
break;
case SPELL_EFFECT_REPUTATION:
if ($a = FactionList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('faction')).' #'.$effMV;
// apply custom reward rated
if ($cuRate = DB::World()->selectCell('SELECT `spell_rate` FROM reputation_reward_rate WHERE `spell_rate` <> 1 AND `faction` = ?d', $effMV))
$_footer['value'][2] = sprintf(Util::$dfnString, Lang::faction('customRewRate'), ' ('.(($cuRate < 1 ? '-' : '+').intVal(($cuRate - 1) * $_footer['value'][0])).')');
break;
case SPELL_EFFECT_SEND_TAXI:
$_ = DB::Aowow()->selectRow(
'SELECT tn1.`name_loc0` AS `start_loc0`, tn1.name_loc?d AS start_loc?d, tn2.`name_loc0` AS `end_loc0`, tn2.name_loc?d AS end_loc?d
FROM ?_taxipath tp
JOIN ?_taxinodes tn1 ON tp.`startNodeId` = tn1.`id`
JOIN ?_taxinodes tn2 ON tp.`endNodeId` = tn2.`id`
WHERE tp.`id` = ?d',
Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, $effMV
);
if ($_)
$_nameMV = $this->fmtStaffTip('<span class="breadcrumb-arrow">'.Util::localizedString($_, 'start').'</span>'.Util::localizedString($_, 'end'), 'MiscValue: '.$effMV);
break;
case SPELL_EFFECT_TITAN_GRIP:
$effMV = 0; // effMV is trigger spell and was handled earlier
break;
case SPELL_EFFECT_CAST_BUTTON:
$_nameMV = $effMV; // has a valid 0 value
break;
// Aura
case SPELL_EFFECT_APPLY_AURA:
case SPELL_EFFECT_PERSISTENT_AREA_AURA:
case SPELL_EFFECT_APPLY_AREA_AURA_PARTY:
case SPELL_EFFECT_APPLY_AREA_AURA_RAID:
case SPELL_EFFECT_APPLY_AREA_AURA_PET:
case SPELL_EFFECT_APPLY_AREA_AURA_FRIEND:
case SPELL_EFFECT_APPLY_AREA_AURA_ENEMY:
case SPELL_EFFECT_APPLY_AREA_AURA_OWNER:
{
if ($effAura > 0 && ($_ = Lang::spell('auras', $effAura)))
$_nameAura = '<a href="?spells&filter=cr=29;crs='.$effAura.';crv=0">'.$this->fmtStaffTip($_, 'AuraId: '.$effAura).'</a>';
else if ($effAura > 0)
$_nameAura = Lang::spell('unkAura', [$effAura]);
switch ($effAura)
{
case SPELL_AURA_MOD_STEALTH_DETECT:
if ($_ = Lang::spell('stealthType', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_INVISIBILITY:
case SPELL_AURA_MOD_INVISIBILITY_DETECT:
if ($_ = Lang::spell('invisibilityType', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_POWER_REGEN_PERCENT:
case SPELL_AURA_MOD_INCREASE_ENERGY_PERCENT:
case SPELL_AURA_OBS_MOD_POWER:
$valueFmt = '%s%%';
case SPELL_AURA_PERIODIC_ENERGIZE:
case SPELL_AURA_MOD_INCREASE_ENERGY:
case SPELL_AURA_MOD_POWER_REGEN:
if ($_ = Lang::spell('powerTypes', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
if ($effMV == POWER_RAGE || $effMV == POWER_RUNIC_POWER)
array_walk($_footer['value'], fn(&$x) => $x /= 10);
break;
case SPELL_AURA_MOD_PERCENT_STAT:
case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE:
case SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT:
case SPELL_AURA_MOD_RANGED_ATTACK_POWER_OF_STAT_PERCENT:
case SPELL_AURA_MOD_ATTACK_POWER_OF_STAT_PERCENT:
case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT:
$valueFmt = '%s%%';
case SPELL_AURA_MOD_STAT:
if ($effMV < 0)
$_nameMV = $this->fmtStaffTip(Lang::main('all'), 'MiscValue: '.$effMV);
else if ($_ = Lang::game('stats', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_SHAPESHIFT:
if ($_ = Lang::game('st', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_EFFECT_IMMUNITY:
if ($_ = Lang::spell('effects', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_STATE_IMMUNITY:
if ($_ = Lang::spell('auras', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_DEBUFF_RESISTANCE:
case SPELL_AURA_MOD_AURA_DURATION_BY_DISPEL:
case SPELL_AURA_MOD_AURA_DURATION_BY_DISPEL_NOT_STACK:
$valueFmt = '%s%%';
case SPELL_AURA_DISPEL_IMMUNITY:
if ($_ = Lang::game('dt', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_TRACK_CREATURES:
if ($_ = Lang::game('ct', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_TRACK_RESOURCES:
if ($_ = Lang::spell('lockType', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_LANGUAGE:
case SPELL_AURA_COMPREHEND_LANGUAGE:
if ($_ = Lang::game('languages', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_MECHANIC_DAMAGE_TAKEN_PERCENT:
case SPELL_AURA_MOD_MECHANIC_RESISTANCE:
case SPELL_AURA_MECHANIC_DURATION_MOD:
case SPELL_AURA_MECHANIC_DURATION_MOD_NOT_STACK:
case SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC:
$valueFmt = '%s%%';
case SPELL_AURA_MECHANIC_IMMUNITY:
if ($_ = Lang::game('me', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MECHANIC_IMMUNITY_MASK:
$_ = [];
foreach (Lang::game('me') as $k => $str)
if ($k && ($effMV & (1 << $k - 1)))
$_[] = $str;
if ($_ = implode(', ', $_))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV));
break;
case SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT:
case SPELL_AURA_MOD_RESISTANCE_OF_STAT_PERCENT:
if ($_ = Lang::game('stats', $effMVB))
$_nameMVB = $this->fmtStaffTip($_, 'MiscValueB: '.$effMVB);
// ! DO NOT BREAK !
case SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT:
case SPELL_AURA_SPLIT_DAMAGE_PCT:
case SPELL_AURA_MOD_RESISTANCE_PCT:
case SPELL_AURA_MOD_HEALING_PCT:
case SPELL_AURA_MOD_BASE_RESISTANCE_PCT:
case SPELL_AURA_MOD_INCREASES_SPELL_PCT_TO_HIT:
case SPELL_AURA_MOD_HOT_PCT:
case SPELL_AURA_SHARE_DAMAGE_PCT:
case SPELL_AURA_MOD_THREAT:
case SPELL_AURA_MOD_DAMAGE_PERCENT_DONE:
case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN:
case SPELL_AURA_MOD_HEALING_DONE_PERCENT:
case SPELL_AURA_MOD_WEAPON_CRIT_PERCENT:
case SPELL_AURA_MOD_CRITICAL_HEALING_AMOUNT:
case SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL:
case SPELL_AURA_REFLECT_SPELLS_SCHOOL:
case SPELL_AURA_REDUCE_PUSHBACK:
case SPELL_AURA_MOD_CRIT_DAMAGE_BONUS:
case SPELL_AURA_MOD_ATTACKER_SPELL_HIT_CHANCE:
case SPELL_AURA_MOD_TARGET_ABSORB_SCHOOL:
case SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL:
case SPELL_AURA_MOD_AOE_DAMAGE_AVOIDANCE:
case SPELL_AURA_MOD_DAMAGE_FROM_CASTER:
case SPELL_AURA_MOD_CREATURE_AOE_DAMAGE_AVOIDANCE:
case SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER:
case SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER:
case SPELL_AURA_MOD_SPELL_DAMAGE_FROM_HEALING: // ? Mod Spell & Healing Power by % of Int
$valueFmt = '%s%%';
case SPELL_AURA_SCHOOL_ABSORB:
case SPELL_AURA_MOD_DAMAGE_DONE:
case SPELL_AURA_MOD_DAMAGE_TAKEN:
case SPELL_AURA_MOD_RESISTANCE:
case SPELL_AURA_SCHOOL_IMMUNITY:
case SPELL_AURA_DAMAGE_IMMUNITY:
case SPELL_AURA_MOD_POWER_COST_SCHOOL:
case SPELL_AURA_MOD_BASE_RESISTANCE:
case SPELL_AURA_MANA_SHIELD:
case SPELL_AURA_MOD_HEALING:
case SPELL_AURA_MOD_TARGET_RESISTANCE:
case SPELL_AURA_MOD_HEALING_DONE:
case SPELL_AURA_MOD_RESISTANCE_EXCLUSIVE:
case SPELL_AURA_MOD_IMMUNE_AURA_APPLY_SCHOOL: // ? Cancel Aura Buffer at % of Caster Health
case SPELL_AURA_MOD_IGNORE_TARGET_RESIST:
case SPELL_AURA_MOD_ATTACK_POWER_OF_ARMOR: // ? Mod Attack Power by School Resistance
case SPELL_AURA_SCHOOL_HEAL_ABSORB:
if ($_ = Lang::getMagicSchools($effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV));
break;
case SPELL_AURA_MOD_SKILL: // temp
case SPELL_AURA_MOD_SKILL_TALENT: // perm
$valueFmt = '%+d';
if ($a = SkillList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('skill')).' #'.$effMV;
break;
case SPELL_AURA_ADD_PCT_MODIFIER:
$valueFmt = '%s%%';
case SPELL_AURA_ADD_FLAT_MODIFIER:
if ($_ = Lang::spell('spellModOp', $effMV))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV);
break;
case SPELL_AURA_MOD_RATING_FROM_STAT:
$valueFmt = '%s%%';
if ($_ = Lang::game('stats', $effMVB))
$_nameMVB = $this->fmtStaffTip($_, 'MiscValueB: '.$effMVB);
// DO NOT BREAK !
case SPELL_AURA_MOD_RATING:
foreach (Lang::spell('combatRatingMask') as $m => $str)
{
if ($effMV != $m)
continue;
$_nameMV = $this->fmtStaffTip($str, 'MiscValue: '.Util::asHex($effMV));
break 2;
}
$_ = [];
foreach (Lang::spell('combatRating') as $k => $str)
if ((1 << $k) & $effMV)
$_[] = $str;
if ($_ = implode(', ', $_))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV));
break;
case SPELL_AURA_MOD_DAMAGE_DONE_VERSUS:
$valueFmt = '%s%%';
case SPELL_AURA_MOD_DAMAGE_DONE_CREATURE:
case SPELL_AURA_MOD_MELEE_ATTACK_POWER_VERSUS:
case SPELL_AURA_MOD_RANGED_ATTACK_POWER_VERSUS:
case SPELL_AURA_MOD_FLAT_SPELL_DAMAGE_VERSUS:
$_ = [];
foreach (Lang::game('ct') as $k => $str)
if ($k && ($effMV & (1 << $k - 1)))
$_[] = $str;
if ($_ = implode(', ', $_))
$_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV));
break;
case SPELL_AURA_CONVERT_RUNE:
$from = $effMV;
if ($_ = Lang::spell('powerRunes', $effMV))
$from = $_;
$to = $effMVB;
if ($_ = Lang::spell('powerRunes', $effMVB))
$to = $_;
$effMVB = 0; // prevent default display
$_nameMV = $this->fmtStaffTip('<span class="breadcrumb-arrow">'.$from.'</span>', 'MiscValue: '.$effMV).$this->fmtStaffTip($to, 'MiscValueB: '.$effMVB);
break;
case SPELL_AURA_MOUNTED:
case SPELL_AURA_TRANSFORM:
case SPELL_AURA_CHANGE_MODEL_FOR_ALL_HUMANOIDS:
case SPELL_AURA_X_RAY:
case SPELL_AURA_MOD_FAKE_INEBRIATE:
if ($effMV && $a = CreatureList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('npc')).' #'.$effMV;
break;
case SPELL_AURA_FORCE_REACTION:
$_footer['value'][1] = $this->fmtStaffTip(Lang::game('rep', $_footer['value'][1]), $_footer['value'][1]);
$_footer['value'][0] = null; // disable range here as the string replacement will fail the comparison at the end
// DO NOT BREAK !
case SPELL_AURA_MOD_FACTION_REPUTATION_GAIN:
if ($effAura == SPELL_AURA_MOD_FACTION_REPUTATION_GAIN)
$valueFmt = '%s%%';
if ($a = FactionList::makeLink($effMV))
$_nameMV = $a;
else
$_nameMV = Util::ucFirst(Lang::game('faction')).' #'.$effMV;
break; // also breaks for SPELL_AURA_FORCE_REACTION
case SPELL_AURA_OVERRIDE_SPELLS:
if ($so = DB::Aowow()->selectRow('SELECT `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5` FROM ?_spelloverride WHERE `id` = ?d', $effMV))
{
if ($so = array_filter($so))
{
$this->extendGlobalData([Type::SPELL => $so]);
$_markup = '[spell='.implode('], [spell=', $so).']';
$effMV = 0; // prevent default display
}
}
break;
case SPELL_AURA_MOD_COMBAT_RESULT_CHANCE:
$valueFmt = '%s%%';
case SPELL_AURA_IGNORE_COMBAT_RESULT:
$what = match ($effMV)
{
2 => Lang::spell('combatRating', 2), // Dodged
3 => Lang::spell('combatRating', 4), // Blocked
4 => Lang::spell('combatRating', 3), // Parried
default => '' // Evaded(0) Missed(1) Glanced(5) Crited'ed..ed(6) Crushed(7) Regular(8)
};
if ($what)
$_nameMV = $this->fmtStaffTip($what, 'MiscValue: '.$effMV);
else
trigger_error('unused case #'.$effMV.' found for aura #'.$effAura);
break;
case SPELL_AURA_SCREEN_EFFECT:
if ($ses = DB::Aowow()->selectRow('SELECT `name`, `ambienceDay` AS "0", IF(`ambienceNight` <> `ambienceDay`, `ambienceNight`, 0) AS "1", `musicDay` AS "2", IF(`musicNight` <> `musicDay`, `musicNight`, 0) AS "3" FROM ?_screeneffect_sounds WHERE `id` = ?d', $effMV))
{
$_nameMV = $this->fmtStaffTip($ses['name'], 'MiscValue: '.$effMV);
for ($j = 0; $j < 4; $j++)
if ($ses[$j])
$_markup .= '[sound='.$ses[$j].']';
}
break;
case SPELL_AURA_MOD_HEALTH_REGEN_PERCENT:
case SPELL_AURA_OBS_MOD_HEALTH:
case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT:
case SPELL_AURA_MOD_ARMOR_PENETRATION_PCT:
case SPELL_AURA_MOD_SCALE:
case SPELL_AURA_MOD_SCALE_2:
case SPELL_AURA_MOD_SPEED_ALWAYS:
case SPELL_AURA_MOD_SPEED_SLOW_ALL:
case SPELL_AURA_MOD_SPEED_NOT_STACK:
case SPELL_AURA_MOD_INCREASE_SPEED:
case SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED:
case SPELL_AURA_MOD_DECREASE_SPEED:
case SPELL_AURA_MOD_INCREASE_SWIM_SPEED:
case SPELL_AURA_MOD_PARRY_PERCENT:
case SPELL_AURA_MOD_DODGE_PERCENT:
case SPELL_AURA_MOD_BLOCK_PERCENT:
case SPELL_AURA_MOD_BLOCK_CRIT_CHANCE:
case SPELL_AURA_MOD_HIT_CHANCE:
case SPELL_AURA_MOD_CRIT_PCT:
case SPELL_AURA_MOD_SPELL_HIT_CHANCE:
case SPELL_AURA_MOD_SPELL_CRIT_CHANCE:
case SPELL_AURA_MOD_MELEE_RANGED_HASTE:
case SPELL_AURA_MOD_CASTING_SPEED_NOT_STACK:
$valueFmt = '%s%%';
break;
}
break;
}
case SPELL_EFFECT_RESURRECT:
case SPELL_EFFECT_SPIRIT_HEAL:
case SPELL_EFFECT_WEAPON_PERCENT_DAMAGE:
case SPELL_EFFECT_DURABILITY_DAMAGE_PCT:
case SPELL_EFFECT_MODIFY_THREAT_PERCENT:
case SPELL_EFFECT_REDIRECT_THREAT:
case SPELL_EFFECT_HEAL_PCT:
$valueFmt = '%s%%';
break;
}
if ($_footer['value'][1])
{
$buffer = Lang::spell('_value').Lang::main('colon').sprintf($valueFmt, $_footer['value'][0]);
if ($_footer['value'][0] != $_footer['value'][1])
$buffer .= Lang::game('valueDelim').sprintf($valueFmt, $_footer['value'][1]);
if ($effRPPL != 0)
$buffer .= Lang::spell('costPerLevel', [sprintf($valueFmt, $effRPPL)]);
if ($effPPCP != 0)
$buffer .= Lang::spell('pointsPerCP', [sprintf($valueFmt, $effPPCP)]);
if (isset($_footer['value'][2]))
$buffer .= $_footer['value'][2];
if (in_array($effId, SpellList::EFFECTS_SCALING_DAMAGE))
{
if ($scaling[2])
$buffer .= Lang::spell('apMod', [$scaling[2]]);
if ($scaling[0])
$buffer .= Lang::spell('spMod', [$scaling[0]]);
}
if (in_array($effAura, SpellList::AURAS_SCALING_DAMAGE))
{
if ($scaling[3])
$buffer .= Lang::spell('apMod', [$scaling[3]]);
if ($scaling[1])
$buffer .= Lang::spell('spMod', [$scaling[1]]);
}
$_footer['value'] = $buffer;
}
else
unset($_footer['value']);
$_header = $_nameEffect;
if ($_nameAura)
$_header .= Lang::main('colon').$_nameAura;
if (strlen($_nameMV))
$_header .= ' ('.$_nameMV.')';
else if ($effMV)
$_header .= ' ('.$effMV.')';
if (strlen($_nameMVB))
$_header .= ' ['.$_nameMVB.']';
else if ($effMVB)
$_header .= ' ['.$effMVB.']';
$effects[$i] = array(
'icon' => $_icon,
'perfectItem' => $_perfItem,
'name' => $_header,
'footer' => $_footer,
'markup' => $_markup,
'modifies' => [] // may be set later
);
}
$this->effects = $effects;
}
private function createAttributesList() : void
{
$list = [];
for ($i = 0; $i < 8; $i++)
{
$attributes = $this->subject->getField('attributes'.$i);
for ($j = 1; $j <= (1 << 31); $j <<= 1)
{
if (!($attributes & $j))
continue;
$listItem = Lang::spell('attributes'.$i, $j);
if (!$listItem && User::isInGroup(U_GROUP_STAFF))
$listItem = '<span class="q0">Unknown SpellAttribute'.$i.'</span>';
else if (!$listItem)
continue;
if ($crId = (SpellListFilter::$attributesFilter[$i][$j] ?? 0))
$listItem = sprintf('<a href="?spells&filter=cr=%2$d;crs=%3$d;crv=0">%1$s</a>', $listItem, abs($crId), $crId > 0 ? 1 : 2);
$list[] = $this->fmtStaffTip($listItem, 'Attributes'.$i.': '.Util::asHex($j));
}
}
$this->attributes = $list;
}
private function createNumRange(int $bp, int $ds, int $mult = 1) : string
{
return Util::createNumRange($bp + 1, ($bp + $ds) * $mult, '-');
}
private function generatePath()
{
$cat = $this->subject->getField('typeCat');
$cf = $this->subject->getField('cuFlags');
$sl = $this->subject->getField('skillLines');
$this->breadcrumb[] = $cat;
// reconstruct path
switch ($cat)
{
case -2:
case 7:
case -13:
if ($cl = $this->subject->getField('reqClassMask'))
$this->breadcrumb[] = ChrClass::fromMask($cl)[0];
else if ($sf = $this->subject->getField('spellFamilyId'))
foreach (ChrClass::cases() as $cl)
if ($cl->spellFamily() == $sf)
{
$this->breadcrumb[] = $cl->value;
break;
}
if ($cat == -13)
$this->breadcrumb[] = ($cf & (SPELL_CU_GLYPH_MAJOR | SPELL_CU_GLYPH_MINOR)) >> 6;
else if ($sl)
$this->breadcrumb[] = $sl[0];
break;
case 9:
case -3:
case 11:
if ($sl)
$this->breadcrumb[] = $sl[0];
if ($cat == 11)
if ($_ = $this->subject->getField('reqSpellId'))
$this->breadcrumb[] = $_;
break;
case -11:
foreach (SpellList::$skillLines as $line => $skills)
if (in_array($sl[0] ?? [], $skills))
$this->breadcrumb[] = $line;
break;
case -7: // only spells unique in skillLineAbility will always point to the right skillLine :/
if ($cf & SPELL_CU_PET_TALENT_TYPE0)
$this->breadcrumb[] = 411; // Ferocity
else if ($cf & SPELL_CU_PET_TALENT_TYPE1)
$this->breadcrumb[] = 409; // Tenacity
else if ($cf & SPELL_CU_PET_TALENT_TYPE2)
$this->breadcrumb[] = 410; // Cunning
break;
case -5:
if ($this->subject->getField('effect2AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED ||
$this->subject->getField('effect3AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED)
$this->breadcrumb[] = 2; // flying (also contains SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED, so checked first)
else if ($this->subject->getField('effect2AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED ||
$this->subject->getField('effect3AuraId') == SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED)
$this->breadcrumb[] = 1; // ground
else
$this->breadcrumb[] = 3; // misc
}
}
private function createInfobox() : void
{
$infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags'));
$typeCat = $this->subject->getField('typeCat');
$hasCompletion = in_array($typeCat, [-5, -6]) && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW);
// level
if (!in_array($typeCat, [-5, -6])) // not mount or vanity pet
{
if ($_ = $this->subject->getField('talentLevel'))
$infobox[] = (in_array($typeCat, [-2, 7, -13]) ? Lang::game('reqLevel', [$_]) : Lang::game('level').Lang::main('colon').$_);
else if ($_ = $this->subject->getField('spellLevel'))
$infobox[] = (in_array($typeCat, [-2, 7, -13]) ? Lang::game('reqLevel', [$_]) : Lang::game('level').Lang::main('colon').$_);
}
$jsg = [];
// races
if ($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $jsg, Lang::FMT_MARKUP))
{
$this->extendGlobalIds(Type::CHR_RACE, ...$jsg);
$t = count($jsg) == 1 ? Lang::game('race') : Lang::game('races');
$infobox[] = Util::ucFirst($t).Lang::main('colon').$_;
}
// classes
if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, Lang::FMT_MARKUP))
{
$this->extendGlobalIds(Type::CHR_CLASS, ...$jsg);
$t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes');
$infobox[] = Util::ucFirst($t).Lang::main('colon').$_;
}
// spell focus
if ($_ = $this->subject->getField('spellFocusObject'))
{
if ($sfObj = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE `id` = ?d', $_))
{
$n = Util::localizedString($sfObj, 'name');
if ($objId = DB::Aowow()->selectCell('SELECT `id` FROM ?_objects WHERE `spellFocusId` = ?d', $_))
$n = '[url=?object='.$objId.']'.$n.'[/url]';
$infobox[] = Lang::game('requires2').' '.$n;
}
}
// primary & secondary trades
if (in_array($typeCat, [9, 11]))
{
// skill
if ($_ = $this->subject->getField('skillLines'))
{
$this->extendGlobalIds(Type::SKILL, $_[0]);
$bar = Lang::game('requires', ['&nbsp;[skill='.$_[0].']']);
if ($_ = $this->subject->getField('learnedAt'))
$bar .= ' ('.$_.')';
$infobox[] = $bar;
}
// specialization
if ($_ = $this->subject->getField('reqSpellId'))
{
$this->extendGlobalIds(Type::SPELL, $_);
$infobox[] = Lang::game('requires2').' [spell='.$_.']';
}
// difficulty
if ($_ = $this->subject->getColorsForCurrent())
$infobox[] = Lang::formatSkillBreakpoints($_);
}
// accquisition.. 10: starter spell; 7: discovery
if ($this->subject->getSources($s))
{
if (in_array(SRC_STARTER, $s))
$infobox[] = Lang::spell('starter');
else if (in_array(SRC_DISCOVERY, $s))
$infobox[] = Lang::spell('discovered');
}
// training cost
if ($cost = $this->subject->getField('trainingCost'))
$infobox[] = Lang::spell('trainingCost').'[money='.$cost.']';
// id
$infobox[] = Lang::spell('id') . $this->typeId;
// icon
if ($_ = $this->subject->getField('iconId'))
{
$infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]';
$this->extendGlobalIds(Type::ICON, $_);
}
// profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view)
if (Cfg::get('PROFILER_ENABLE') && $hasCompletion)
{
$x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_completion_spells WHERE `spellId` = ?d', $this->typeId);
$y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_profiler_profiles WHERE `custom` = 0 AND `stub` = 0');
$infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]);
// completion row added by InfoboxMarkup
}
// original name
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', $n);
// Creature Type from Aura: Shapeshift
foreach ($this->modelInfo as $mI)
{
if (!isset($mI['creatureType']))
continue;
if ($mI['creatureType'] > 0)
$infobox[] = Lang::game('type').Lang::game('ct', $mI['creatureType']);
break;
}
// spell script
if (User::isInGroup(U_GROUP_STAFF))
if ($_ = DB::World()->selectCell('SELECT `ScriptName` FROM spell_script_names WHERE ABS(`spell_id`) = ?d', $this->firstRank))
$infobox[] = 'Script'.Lang::main('colon').$_;
$this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion);
// append glyph symbol if available
$glyphId = 0;
for ($i = 1; $i < 4; $i++)
if ($this->subject->getField('effect'.$i.'Id') == SPELL_EFFECT_APPLY_GLYPH)
$glyphId = $this->subject->getField('effect'.$i.'MiscValue');
if ($_ = DB::Aowow()->selectCell('SELECT ic.`name` FROM ?_glyphproperties gp JOIN ?_icons ic ON gp.`iconId` = ic.`id` WHERE gp.`spellId` = ?d { OR gp.`id` = ?d }', $this->typeId, $glyphId ?: DBSIMPLE_SKIP))
if (file_exists('static/images/wow/Interface/Spellbook/'.$_.'.png'))
$this->infobox->append('[img src='.Cfg::get('STATIC_URL').'/images/wow/Interface/Spellbook/'.$_.'.png border=0 float=center margin=15]');
}
}
?>