typeId = intVal($id); $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; } protected function generate() : void { $this->subject = new CreatureList(array(['id', $this->typeId])); if ($this->subject->error) $this->generateNotFound(Lang::game('npc'), Lang::npc('notFound')); $this->h1 = Util::htmlEscape($this->subject->getField('name', true)); $this->subname = $this->subject->getField('subname', true); $this->gPageInfo += array( 'type' => $this->type, 'typeId' => $this->typeId, 'name' => $this->subject->getField('name', true) ); $_typeFlags = $this->subject->getField('typeFlags'); $_altIds = []; /*************/ /* Menu Path */ /*************/ $this->breadcrumb[] = $this->subject->getField('type'); if ($_ = $this->subject->getField('family')) $this->breadcrumb[] = $_; /**************/ /* Page Title */ /**************/ array_unshift($this->title, $this->subject->getField('name', true), mb_strtoupper(Lang::game('npc'))); /***********************/ /* Difficulty versions */ /***********************/ if ($this->subject->getField('cuFlags') & NPC_CU_DIFFICULTY_DUMMY) $this->placeholder = [$this->subject->getField('parentId'), $this->subject->getField('parent', true)]; else { for ($i = 1; $i < 4; $i++) if ($_ = $this->subject->getField('difficultyEntry'.$i)) $_altIds[$_] = $i; if ($_altIds) $this->altNPCs = new CreatureList(array(['id', array_keys($_altIds)])); } if ($_ = DB::World()->selectCol('SELECT DISTINCT `entry` FROM vehicle_template_accessory WHERE `accessory_entry` = ?d', $this->typeId)) { $vehicles = new CreatureList(array(['id', $_])); foreach ($vehicles->iterate() as $id => $__) $this->accessory[] = [$id, $vehicles->getField('name', true)]; } /**********************/ /* Determine Map Type */ /**********************/ $mapType = 0; if ($maps = DB::Aowow()->selectCol('SELECT DISTINCT `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])) { // MAP_TYPE_DUNGEON, MAP_TYPE_DUNGEON_HC => 1, // MAP_TYPE_RAID, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC => 2, default => 0 }; } // npc is difficulty dummy: get max difficulty from parent npc if ($this->placeholder && ($mt = DB::Aowow()->selectCell('SELECT IF(`difficultyEntry1` = ?d, 1, 2) FROM ?_creature WHERE `difficultyEntry1` = ?d OR `difficultyEntry2` = ?d OR `difficultyEntry3` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId))) $mapType = max($mapType, $mt); // npc has difficulty dummys: 2+ dummies -> definitely raid (10/25 + hc); 1 dummy -> may be heroic (used here), but may also be 10/25-raid if ($_altIds) $mapType = max($mapType, count($_altIds) > 1 ? 2 : 1); // for event encounters a single npc may be reused over multiple difficulties but have different chests assigned if ($d = DB::Aowow()->selectCell('SELECT MAX(`difficulty`) FROM ?_loot_link WHERE `npcId` IN (?a)', array_merge($_altIds, [$this->typeId]))) $mapType = max($mapType, $d > 2 ? 2 : 1); /***********/ /* Infobox */ /***********/ $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // Event (ignore events, where the object only gets removed) if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_creature gec, creature c WHERE ge.`eventEntry` = gec.`eventEntry` AND c.`guid` = gec.`guid` AND c.`id` = ?d', $this->typeId)) { $this->extendGlobalIds(Type::WORLDEVENT, ...$_); $ev = []; foreach ($_ as $i => $e) $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; $infobox[] = Lang::game('eventShort', [implode(',', $ev)]); } // Level if ($this->subject->getField('rank') != NPC_RANK_BOSS) { $level = $this->subject->getField('minLevel'); $maxLvl = $this->subject->getField('maxLevel'); if ($level < $maxLvl) $level .= ' - '.$maxLvl; } else // Boss Level $level = '??'; $infobox[] = Lang::game('level').Lang::main('colon').$level; // Classification if ($_ = $this->subject->getField('rank')) // != NPC_RANK_NORMAL { $str = $this->subject->isBoss() ? '[span class=icon-boss]'.Lang::npc('rank', $_).'[/span]' : Lang::npc('rank', $_); $infobox[] = Lang::npc('classification', [$str]); } // Reaction $color = fn (int $r) : string => match($r) { 1 => 'q2', // q2 green -1 => 'q10', // q10 red default => 'q' // q yellow }; $infobox[] = Lang::npc('react', ['[color='.$color($this->subject->getField('A')).']A[/color] [color='.$color($this->subject->getField('H')).']H[/color]']); // Faction $this->extendGlobalIds(Type::FACTION, $this->subject->getField('factionId')); $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$this->subject->getField('factionId').']'; // Tameable if ($_typeFlags & NPC_TYPEFLAG_TAMEABLE) if ($_ = $this->subject->getField('family')) $infobox[] = Lang::npc('tameable', ['[url=pet='.$_.']'.Lang::game('fa', $_).'[/url]']); // Wealth if ($_ = intVal(($this->subject->getField('minGold') + $this->subject->getField('maxGold')) / 2)) $infobox[] = Lang::npc('worth', ['[tooltip=tooltip_avgmoneydropped][money='.$_.'][/tooltip]']); // is Vehicle if ($this->subject->getField('vehicleId')) $infobox[] = Lang::npc('vehicle'); // is visible as ghost (redundant to extraFlags) if ($this->subject->getField('npcflag') & (NPC_FLAG_SPIRIT_HEALER | NPC_FLAG_SPIRIT_GUIDE)) $infobox[] = Lang::npc('extraFlags', CREATURE_FLAG_EXTRA_GHOST_VISIBILITY); if (User::isInGroup(U_GROUP_EMPLOYEE)) { // AI if ($_ = $this->subject->getField('scriptName')) $infobox[] = 'Script'.Lang::main('colon').$_; else if ($_ = $this->subject->getField('aiName')) $infobox[] = 'AI'.Lang::main('colon').$_; // Mechanic immune if ($immuneMask = $this->subject->getField('mechanicImmuneMask')) { $buff = []; for ($i = 0; $i < 31; $i++) if ($immuneMask & (1 << $i)) $buff[] = (!fMod(count($buff), 3) ? "\n" : '').'[url=?spells&filter=me='.($i + 1).']'.Lang::game('me', $i + 1).'[/url]'; $infobox[] = Lang::npc('mechanicimmune', [implode(', ', $buff)]); } // extra flags if ($flagsExtra = $this->subject->getField('flagsExtra')) { $buff = []; foreach (Lang::npc('extraFlags') as $idx => $str) if ($flagsExtra & $idx) $buff[] = $str; if ($buff) $infobox[] = Lang::npc('_extraFlags').'[ul][li]'.implode('[/li][li]', $buff).'[/li][/ul]'; } // Mode dummy references if ($this->altNPCs) { $this->extendGlobalData($this->altNPCs->getJSGlobals()); $buff = Lang::npc('versions').'[ul]'; foreach ($this->altNPCs->iterate() as $id => $__) $buff .= '[li][npc='.$id.'][/li]'; $infobox[] = $buff.'[/ul]'; } } 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]'; if ($infobox) { $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); $this->extendGlobalData($this->infobox->getJsGlobals()); } /****************/ /* Main Content */ /****************/ // get spawns and path if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) { $this->addDataLoader('zones'); $this->map = array( ['parent' => 'mapper-generic'], // Mapper $spawns, // mapperData null, // ShowOnMap [Lang::npc('foundIn')] // foundIn ); foreach ($spawns as $areaId => $_) $this->map[3][$areaId] = ZoneList::getName($areaId); } // smart AI $sai = null; if ($this->subject->getField('aiName') == 'SmartAI') { $sai = new SmartAI(SmartAI::SRC_TYPE_CREATURE, $this->typeId); if (!$sai->prepare()) // no smartAI found .. check per guid { // at least one of many $guids = DB::World()->selectCol('SELECT `guid` FROM creature WHERE `id` = ?d', $this->typeId); while ($_ = array_pop($guids)) { $sai = new SmartAI(SmartAI::SRC_TYPE_CREATURE, -$_, ['baseEntry' => $this->typeId, 'title' => ' [small](for GUID: '.$_.')[/small]']); if ($sai->prepare()) break; } } if ($sai->prepare()) { $this->extendGlobalData($sai->getJSGlobals()); $this->smartAI = $sai->getMarkup(); } else trigger_error('Creature has SmartAI set in template but no SmartAI defined.'); } // consider pooled spawns $this->quotes = $this->getQuotes(); $this->reputation = $this->getOnKillRep($_altIds, $mapType); $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], BUTTON_VIEW3D => ['type' => Type::NPC, 'typeId' => $this->typeId, 'displayId' => $this->subject->getRandomModelId()] ); if ($this->subject->getField('humanoid')) $this->redButtons[BUTTON_VIEW3D]['humanoid'] = 1; /**************/ /* Extra Tabs */ /**************/ $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); // tab: abilities / tab_controlledabilities (dep: VehicleId) $tplSpells = []; $genSpells = []; $conditions = ['OR']; for ($i = 1; $i < 9; $i++) if ($_ = $this->subject->getField('spell'.$i)) $tplSpells[] = $_; if ($tplSpells) $conditions[] = ['id', $tplSpells]; if ($smartSpells = SmartAI::getSpellCastsForOwner($this->typeId, SmartAI::SRC_TYPE_CREATURE)) $genSpells = $smartSpells; if ($auras = DB::World()->selectCell('SELECT `auras` FROM creature_template_addon WHERE `entry` = ?d', $this->typeId)) { $auras = preg_replace('/[^\d ]/', ' ', $auras); // remove erroneous chars from string $genSpells = array_merge($genSpells, array_filter(explode(' ', $auras))); } if ($genSpells) $conditions[] = ['id', $genSpells]; // Pet-Abilities if (($_typeFlags & NPC_TYPEFLAG_TAMEABLE) && ($_ = $this->subject->getField('family'))) { $skill = 0; $mask = 0x0; foreach (Game::$skillLineMask[-1] as $idx => [$familyId, $skillLineId]) { if ($familyId != $_) continue; $skill = $skillLineId; $mask = 1 << $idx; break; } $conditions[] = [ 'AND', ['s.typeCat', -3], [ 'OR', ['skillLine1', $skill], ['AND', ['skillLine1', 0, '>'], ['skillLine2OrMask', $skill]], ['AND', ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] ] ]; } if (count($conditions) > 1) { $abilities = new SpellList($conditions); if (!$abilities->error) { $this->extendGlobalData($abilities->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); $controled = $abilities->getListviewData(); $normal = []; foreach ($controled as $id => $values) { if (in_array($id, $genSpells)) { $normal[$id] = $values; if (!in_array($id, $tplSpells)) unset($controled[$id]); } } $cnd = new Conditions(); $cnd->getBySourceGroup($this->typeId, Conditions::SRC_VEHICLE_SPELL)->prepare(); if ($cnd->toListviewColumn($controled, $extraCols, $this->typeId, 'id')) $this->extendGlobalData($cnd->getJsGlobals()); if ($normal) $this->lvTabs->addListviewTab(new Listview(array( 'data' => $normal, 'name' => '$LANG.tab_abilities', 'id' => 'abilities' ), SpellList::$brickFile)); if ($controled) $this->lvTabs->addListviewTab(new Listview(array( 'data' => $controled, 'name' => '$LANG.tab_controlledabilities', 'id' => 'controlled-abilities', 'extraCols' => $extraCols ?: null ), SpellList::$brickFile)); } } // tab: summoned by [spell] $conditions = array( 'OR', ['AND', ['effect1Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect3MiscValue', $this->typeId]] ); $sbSpell = new SpellList($conditions); if (!$sbSpell->error) { $this->extendGlobalData($sbSpell->getJSGlobals()); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $sbSpell->getListviewData(), 'name' => '$LANG.tab_summonedby', 'id' => 'summoned-by-spell' ), SpellList::$brickFile)); } // tab: summoned by [NPC] $sb = SmartAI::getOwnerOfNPCSummon($this->typeId); if (!empty($sb[Type::NPC])) { $sbNPC = new CreatureList(array(['id', $sb[Type::NPC]])); if (!$sbNPC->error) { $this->extendGlobalData($sbNPC->getJSGlobals()); $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $sbNPC->getListviewData(), 'name' => '$LANG.tab_summonedby', 'id' => 'summoned-by-npc' ), CreatureList::$brickFile)); } } // tab: summoned by [Object] if (!empty($sb[Type::OBJECT])) { $sbGO = new GameObjectList(array(['id', $sb[Type::OBJECT]])); if (!$sbGO->error) { $this->extendGlobalData($sbGO->getJSGlobals()); $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $sbGO->getListviewData(), 'name' => '$LANG.tab_summonedby', 'id' => 'summoned-by-object' ), GameObjectList::$brickFile)); } } // tab: teaches if ($this->subject->getField('npcflag') & NPC_FLAG_TRAINER) { $teachQuery = 'SELECT ts.`SpellId` AS ARRAY_KEY, ts.`MoneyCost` AS "cost", ts.`ReqSkillLine` AS "reqSkillId", ts.`ReqSkillRank` AS "reqSkillValue", ts.`ReqLevel` AS "reqLevel", ts.`ReqAbility1` AS "reqSpellId1", ts.`reqAbility2` AS "reqSpellId2" FROM trainer_spell ts JOIN creature_default_trainer cdt ON cdt.`TrainerId` = ts.`TrainerId` WHERE cdt.`Creatureid` = ?d'; if ($tSpells = DB::World()->select($teachQuery, $this->typeId)) { $teaches = new SpellList(array(['id', array_keys($tSpells)])); if (!$teaches->error) { $this->extendGlobalData($teaches->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); $data = $teaches->getListviewData(); $extraCols = []; $cnd = new Conditions(); foreach ($tSpells as $sId => $train) { if (empty($data[$sId])) continue; if ($_ = $train['reqSkillId']) if (count($data[$sId]['skill']) == 1 && $_ != $data[$sId]['skill'][0]) $cnd->addExternalCondition(Conditions::SRC_NONE, $sId, [Conditions::SKILL, $_, $train['reqSkillValue']]); for ($i = 1; $i < 3; $i++) if ($_ = $train['reqSpellId'.$i]) $cnd->addExternalCondition(Conditions::SRC_NONE, $sId, [Conditions::SPELL, $_]); if ($_ = $train['reqLevel']) { if (!isset($extraCols[1])) $extraCols[1] = "\$Listview.funcBox.createSimpleCol('reqLevel', LANG.tooltip_reqlevel, '7%', 'reqLevel')"; $data[$sId]['reqLevel'] = $_; } if ($_ = $train['cost']) $data[$sId]['trainingcost'] = $_; } if ($cnd->toListviewColumn($data, $extraCols)) $this->extendGlobalData($cnd->getJsGlobals()); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $data, 'name' => '$LANG.tab_teaches', 'id' => 'teaches', 'visibleCols' => ['trainingcost'], 'extraCols' => $extraCols ?: null ), SpellList::$brickFile)); } } else trigger_error('NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set', E_USER_WARNING); } // tab: sells if ($sells = DB::World()->selectCol( 'SELECT nv.`item` FROM npc_vendor nv WHERE nv.`entry` = ?d UNION SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` = ?d UNION SELECT genv.`item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` = ?d', $this->typeId, $this->typeId, $this->typeId) ) { $soldItems = new ItemList(array(['id', $sells])); if (!$soldItems->error) { $colAddIn = null; $extraCols = ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; $lvData = $soldItems->getListviewData(ITEMINFO_VENDOR, [Type::NPC => [$this->typeId]]); if (array_column($lvData, 'condition')) $extraCols[] = '$Listview.extraCols.condition'; if (array_filter(array_column($lvData, 'restock'))) { $extraCols[] = '$_'; $colAddIn = 'vendorRestockCol'; } $cnd = new Conditions(); if ($cnd->getBySourceGroup($this->typeId, Conditions::SRC_NPC_VENDOR)->prepare()) { $this->extendGlobalData($cnd->getJsGlobals()); $cnd->toListviewColumn($lvData, $extraCols, $this->typeId, 'id'); } $this->lvTabs->addListviewTab(new Listview(array( 'data' => $lvData, 'name' => '$LANG.tab_sells', 'id' => 'currency-for', 'extraCols' => array_unique($extraCols) ), ItemList::$brickFile, $colAddIn)); $this->extendGlobalData($soldItems->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); } } // 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}} */ $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'], ''] ); // 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)' ); if ($_altIds) { $sourceFor[0][2] = $mapType == 1 ? $langref[-1] : $langref[1]; 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]) { $creatureLoot = new Loot(); if ($creatureLoot->getByContainer($lootTpl, $lootId)) { $extraCols = $creatureLoot->extraCols; $extraCols[] = '$Listview.extraCols.percent'; $this->extendGlobalData($creatureLoot->jsGlobals); $tabData = array( 'data' => $creatureLoot->getResult(), 'name' => $tabName, 'id' => $tabId, 'extraCols' => array_unique($extraCols), 'hiddenCols' => $hiddenCols ?: null, 'sort' => ['-percent', 'name'] ); if ($note) $tabData['note'] = $note; else if ($lootTpl == LOOT_SKINNING) $tabData['note'] = ''.Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill($skinTab[2], $this->subject->getField('maxLevel') * 5), Lang::FMT_HTML).''; $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); } } // tab: starts quest // tab: ends quest $startEnd = new QuestList(array(['qse.type', Type::NPC], ['qse.typeId', $this->typeId])); if (!$startEnd->error) { $this->extendGlobalData($startEnd->getJSGlobals()); $lvData = $startEnd->getListviewData(); $start = $end = []; foreach ($startEnd->iterate() as $id => $__) { if ($startEnd->getField('method') & 0x1) $start[] = $lvData[$id]; if ($startEnd->getField('method') & 0x2) $end[] = $lvData[$id]; } if ($start) $this->lvTabs->addListviewTab(new Listview(array( 'data' => $start, 'name' => '$LANG.tab_starts', 'id' => 'starts' ), QuestList::$brickFile)); if ($end) $this->lvTabs->addListviewTab(new Listview(array( 'data' => $end, 'name' => '$LANG.tab_ends', 'id' => 'ends' ), QuestList::$brickFile)); } // tab: objective of quest $conditions = array( 'OR', ['AND', ['reqNpcOrGo1', $this->typeId], ['reqNpcOrGoCount1', 0, '>']], ['AND', ['reqNpcOrGo2', $this->typeId], ['reqNpcOrGoCount2', 0, '>']], ['AND', ['reqNpcOrGo3', $this->typeId], ['reqNpcOrGoCount3', 0, '>']], ['AND', ['reqNpcOrGo4', $this->typeId], ['reqNpcOrGoCount4', 0, '>']], ); $objectiveOf = new QuestList($conditions); if (!$objectiveOf->error) { $this->extendGlobalData($objectiveOf->getJSGlobals()); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $objectiveOf->getListviewData(), 'name' => '$LANG.tab_objectiveof', 'id' => 'objective-of' ), QuestList::$brickFile)); } // tab: criteria of [ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE_TYPE have no data set to check for] $conditions = array( ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE, ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE]], ['ac.value1', $this->typeId] ); $crtOf = new AchievementList($conditions); if (!$crtOf->error) { $this->extendGlobalData($crtOf->getJSGlobals()); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $crtOf->getListviewData(), 'name' => '$LANG.tab_criteriaof', 'id' => 'criteria-of' ), AchievementList::$brickFile)); } // tab: passengers if ($_ = DB::World()->selectCol('SELECT `accessory_entry` AS ARRAY_KEY, GROUP_CONCAT(`seat_id` SEPARATOR ", ") FROM vehicle_template_accessory WHERE `entry` = ?d GROUP BY `accessory_entry`', $this->typeId)) { $passengers = new CreatureList(array(['id', array_keys($_)])); if (!$passengers->error) { $data = $passengers->getListviewData(); if (User::isInGroup(U_GROUP_STAFF)) foreach ($data as $id => &$d) $d['seat'] = $_[$id]; $this->extendGlobalData($passengers->getJSGlobals(GLOBALINFO_SELF)); $tabData = array( 'data' => $data, 'name' => Lang::npc('accessory'), 'id' => 'accessory' ); if (User::isInGroup(U_GROUP_STAFF)) $tabData['extraCols'] = ["\$Listview.funcBox.createSimpleCol('seat', '".Lang::npc('seat')."', '10%', 'seat')"]; $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); } } /* tab sounds: * activity sounds => CreatureDisplayInfo.dbc => (CreatureModelData.dbc => ) CreatureSoundData.dbc * AI => smart_scripts * Dialogue VO => creature_text * onClick VO => CreatureDisplayInfo.dbc => NPCSounds.dbc */ $this->soundIds = array_merge($this->soundIds, SmartAI::getSoundsPlayedForOwner($this->typeId, SmartAI::SRC_TYPE_CREATURE)); // up to 4 possible displayIds .. for the love of things betwixt, just use the first! $activitySounds = DB::Aowow()->selectRow('SELECT * FROM ?_creature_sounds WHERE `id` = ?d', $this->subject->getField('displayId1')); array_shift($activitySounds); // remove id-column $this->soundIds = array_merge($this->soundIds, array_values($activitySounds)); if ($this->soundIds) { $sounds = new SoundList(array(['id', $this->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 :( $this->extendGlobalData($sounds->getJSGlobals(GLOBALINFO_SELF)); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $data, 'visibleCols' => $activitySounds ? 'activity' : null ), SoundList::$brickFile)); } } // tab: conditions $cnd = new Conditions(); $cnd->getBySourceEntry($this->typeId, Conditions::SRC_CREATURE_TEMPLATE_VEHICLE) ->getBySourceGroup($this->typeId, Conditions::SRC_SPELL_CLICK_EVENT) ->getByCondition(Type::NPC, $this->typeId) ->prepare(); if ($tab = $cnd->toListviewTab()) { $this->extendGlobalData($cnd->getJsGlobals()); $this->lvTabs->addDataTab(...$tab); } parent::generate(); } private function getRepForId(array $entries, array &$spillover) : array { $rows = DB::World()->select( 'SELECT `creature_id` AS "npc", `RewOnKillRepFaction1` AS "faction", `RewOnKillRepValue1` AS "qty", `MaxStanding1` AS "maxRank", `isTeamAward1` AS "spillover" FROM creature_onkill_reputation WHERE `creature_id` IN (?a) AND `RewOnKillRepFaction1` > 0 UNION SELECT `creature_id` AS "npc", `RewOnKillRepFaction2` AS "faction", `RewOnKillRepValue2` AS "qty", `MaxStanding2` AS "maxRank", `isTeamAward2` AS "spillover" FROM creature_onkill_reputation WHERE `creature_id` IN (?a) AND `RewOnKillRepFaction2` > 0', $entries, $entries ); $factions = new FactionList(array(['id', array_column($rows, 'faction')])); $result = []; foreach ($rows as $row) { if (!$factions->getEntry($row['faction'])) continue; $set = array( $row['faction'], // factionId [$row['qty'], 0], // qty $factions->getField('name', true), // name $row['maxRank'] && $row['maxRank'] < REP_EXALTED ? Lang::game('rep', $row['maxRank']) : null, // cap $row['npc'], // npcId 0 // spilloverCat ); $cuRate = DB::World()->selectCell('SELECT `creature_rate` FROM reputation_reward_rate WHERE `creature_rate` <> 1 AND `faction` = ?d', $row['faction']); if ($cuRate && User::isInGroup(U_GROUP_EMPLOYEE)) $set[1][1] = $set[1][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($set[1][0] > 0 ? '+' : '').($set[1][0] * ($cuRate - 1))); else if ($cuRate) $set[1][1] = $set[1][0] * $cuRate; if ($row['spillover']) { $spill = [[$set[1][0] / 2, 0], $row['maxRank']]; if ($cuRate && User::isInGroup(U_GROUP_EMPLOYEE)) $spill[0][1] = $spill[0][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($set[1][0] > 0 ? '+' : '').($spill[0][0] * ($cuRate - 1) * 0.5)); else if ($cuRate) $spill[0][1] = $set[1][1] / 2; $spillover[$factions->getField('cat')] = $spill; $set[6] = $factions->getField('cat'); // set spillover } $result[] = $set; } return $result; } private function getOnKillRep(array $dummyIds, int $mapType) : array { $spilledParents = []; $reputation = []; // base NPC if ($base = $this->getRepForId([$this->typeId], $spilledParents)) $reputation[] = [Lang::npc('modes', 1, 0), $base]; // difficulty dummys if ($dummyIds && ($mapType == 1 || $mapType == 2)) { $alt = []; $rep = $this->getRepForId(array_keys($dummyIds), $spilledParents); // order by difficulty foreach ($rep as $i => [, , , , $npcId]) $alt[$dummyIds[$npcId]][] = $rep[$i]; // apply by difficulty foreach ($alt as $mode => $dat) $reputation[] = [Lang::npc('modes', $mapType, $mode), $dat]; } // get spillover factions and apply if ($spilledParents) { $spilled = new FactionList(array(['parentFactionId', array_keys($spilledParents)])); foreach ($reputation as $i => [, $data]) { foreach ($data as [$factionId, , , , , , $spillover]) { if (!$spillover) continue; foreach ($spilled->iterate() as $spId => $__) { // find parent if ($spilled->getField('parentFactionId') != $spillover) continue; // don't readd parent if ($factionId == $spId) continue; $spMax = $spilledParents[$spillover][1]; $reputation[$i][1][] = array( $spId, $spilledParents[$spillover][0], $spilled->getField('name', true), $spMax && $spMax < REP_EXALTED ? Lang::game('rep', $spMax) : null ); } } } } return $reputation; } private function getQuotes() : ?array { [$quotes, $nQuotes, $soundIds] = Game::getQuotesForCreature($this->typeId, true, $this->subject->getField('name', true)); if ($soundIds) $this->soundIds = array_merge($this->soundIds, $soundIds); return $quotes ? [$quotes, $nQuotes] : null; } private function getCreatureStats(int $mapType, array $altIds) : array { $stats = []; $modes = []; // get difficulty versions if set $hint = '[tooltip name=%3$s][table cellspacing=10][tr]%1s[/tr][/table][/tooltip][span class=tip tooltip=%3$s]%2s[/span]'; $modeRow = '[tr][td]%s  [/td][td]%s[/td][/tr]'; // Health $health = $this->subject->getBaseStats('health'); $stats['health'] = Util::ucFirst(Lang::spell('powerTypes', -2)).Lang::main('colon').($health[0] < $health[1] ? Lang::nf($health[0]).' - '.Lang::nf($health[1]) : Lang::nf($health[0])); // Mana (may be 0) $mana = $this->subject->getBaseStats('power'); $stats['mana'] = $mana[0] ? Lang::spell('powerTypes', 0).Lang::main('colon').($mana[0] < $mana[1] ? Lang::nf($mana[0]).' - '.Lang::nf($mana[1]) : Lang::nf($mana[0])) : ''; // Armor $armor = $this->subject->getBaseStats('armor'); $stats['armor'] = Lang::npc('armor').($armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); // Resistances $resNames = [null, 'hol', 'fir', 'nat', 'fro', 'sha', 'arc']; $tmpRes = []; $stats['resistance'] = ''; foreach ($this->subject->getBaseStats('resistance') as $sc => $amt) if ($amt) $tmpRes[] = '[span class="moneyschool'.$resNames[$sc].'"]'.$amt.'[/span]'; if ($tmpRes) { $stats['resistance'] = Lang::npc('resistances').'[br]'; if (count($tmpRes) > 3) $stats['resistance'] .= implode(' ', array_slice($tmpRes, 0, 3)).'[br]'.implode(' ', array_slice($tmpRes, 3)); else $stats['resistance'] .= implode(' ', $tmpRes); } // Melee Damage $melee = $this->subject->getBaseStats('melee'); if ($_ = $this->subject->getField('dmgSchool')) // magic damage $stats['melee'] = Lang::npc('melee').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'; else // phys. damage $stats['melee'] = Lang::npc('melee').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]); // Ranged Damage $ranged = $this->subject->getBaseStats('ranged'); $stats['ranged'] = Lang::npc('ranged').Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1]); foreach ($altIds as $id => $mode) { if (!$this->altNPCs->getEntry($id)) continue; $m = Lang::npc('modes', $mapType, $mode); // Health $health = $this->altNPCs->getBaseStats('health'); $modes['health'][] = sprintf($modeRow, $m, $health[0] < $health[1] ? Lang::nf($health[0]).' - '.Lang::nf($health[1]) : Lang::nf($health[0])); // Mana (may be 0) $mana = $this->altNPCs->getBaseStats('power'); $modes['mana'][] = $mana[0] ? sprintf($modeRow, $m, $mana[0] < $mana[1] ? Lang::nf($mana[0]).' - '.Lang::nf($mana[1]) : Lang::nf($mana[0])) : null; // Armor $armor = $this->altNPCs->getBaseStats('armor'); $modes['armor'][] = sprintf($modeRow, $m, $armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); // Resistances if (array_filter($this->altNPCs->getBaseStats('resistance'))) { if (!isset($modes['resistance'])) // init table head $modes['resistance'][] = '[td][/td][td][span class="moneyschoolhol" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolfir" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolnat" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolfro" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolsha" style="margin: 0px 5px"][/span][/td][td][span class="moneyschoolarc" style="margin: 0px 5px"][/span][/td]'; if (!$stats['resistance']) // base creature has no resistance. -> display list item. $stats['resistance'] = Lang::npc('resistances').'…'; $tmpRes = ''; foreach ($this->altNPCs->getBaseStats('resistance') as $sc => $amt) $tmpRes .= '[td][span style="margin: 0px 5px"]'.$amt.'[/span][/td]'; $modes['resistance'][] = '[td]'.$m.'    [/td]'.$tmpRes; } // Melee Damage $melee = $this->altNPCs->getBaseStats('melee'); if ($_ = $this->altNPCs->getField('dmgSchool')) // magic damage $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'); else // phys. damage $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1])); // Ranged Damage $ranged = $this->altNPCs->getBaseStats('ranged'); $modes['ranged'][] = sprintf($modeRow, $m, Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1])); } if ($modes) foreach ($stats as $k => $v) if ($v) $stats[$k] = sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k); return $stats; } } ?>