From e7d49befdf0a0b9f62f77b7f628c92f8395f20a1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 27 Jun 2015 20:53:44 +0200 Subject: [PATCH 0001/1249] Updated README with installation instructions thats it folks .. lets call it v1.0 --- .htaccess | 2 +- README | Bin 4186 -> 8640 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.htaccess b/.htaccess index 1628a258..e0ea4395 100644 --- a/.htaccess +++ b/.htaccess @@ -1,5 +1,5 @@ Order Deny,Allow - + Deny from all diff --git a/README b/README index c078fdcabdbba2d4d7363a536b11ff911de47aa8..99f0bc72dc7916761964f21a67a01f3192af4c83 100644 GIT binary patch literal 8640 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 xD)O1S& Date: Sat, 27 Jun 2015 20:53:44 +0200 Subject: [PATCH 0002/1249] Updated README with installation instructions thats it folks .. lets call it v1.0 --- .htaccess | 2 +- README | Bin 4186 -> 0 bytes README.md | Bin 0 -> 8606 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 README create mode 100644 README.md diff --git a/.htaccess b/.htaccess index 1628a258..e0ea4395 100644 --- a/.htaccess +++ b/.htaccess @@ -1,5 +1,5 @@ Order Deny,Allow - + Deny from all diff --git a/README b/README deleted file mode 100644 index c078fdcabdbba2d4d7363a536b11ff911de47aa8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4186 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 Date: Sun, 28 Jun 2015 15:28:11 +0200 Subject: [PATCH 0003/1249] Setup: testSelf should reference README.md instead of README, otherwise setup will fail --- setup/tools/clisetup/firstrun.func.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/tools/clisetup/firstrun.func.php b/setup/tools/clisetup/firstrun.func.php index 4da48f9f..41c74e4c 100644 --- a/setup/tools/clisetup/firstrun.func.php +++ b/setup/tools/clisetup/firstrun.func.php @@ -150,8 +150,8 @@ function firstrun($resume) $prot = $res['force_ssl'] ? 'https://' : 'http://'; if ($res['site_host']) { - if (!$test($prot.$res['site_host'].'/README', $resp)) - $error[] = ' * could not access '.$prot.$res['site_host'].'/README ['.$resp.']'; + if (!$test($prot.$res['site_host'].'/README.md', $resp)) + $error[] = ' * could not access '.$prot.$res['site_host'].'/README.md ['.$resp.']'; } else $error[] = ' * SITE_HOST is empty'; From 9897a6cf233b52c9a63e2d850f534947eeeec529 Mon Sep 17 00:00:00 2001 From: Carbenium Date: Sun, 28 Jun 2015 15:30:15 +0200 Subject: [PATCH 0004/1249] Setup: folder 'static/download/searchplugins/' has to be created during setup --- setup/tools/fileGen.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/tools/fileGen.class.php b/setup/tools/fileGen.class.php index 143282dc..d04c47b1 100644 --- a/setup/tools/fileGen.class.php +++ b/setup/tools/fileGen.class.php @@ -54,7 +54,8 @@ class FileGen 'static/uploads/screenshots/resized', 'static/uploads/screenshots/temp', 'static/uploads/screenshots/thumb', - 'static/uploads/temp/' + 'static/uploads/temp/', + 'static/download/searchplugins/' ); public static $txtConstants = array( From 4a809343469ed2c20379727004dcc71b6b21e57d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 28 Jun 2015 16:56:36 +0200 Subject: [PATCH 0005/1249] added premium-related placeholder images (not for acutal use, just a layout reference) --- static/images/logos/special/premium/header.gif | Bin 0 -> 5861 bytes static/images/logos/special/premium/home.jpg | Bin 0 -> 6896 bytes .../logos/special/subscribe/patron-icon.png | Bin 0 -> 5698 bytes static/images/premium/premium-small.png | Bin 0 -> 4143 bytes static/images/premium/user-badge.png | Bin 0 -> 30928 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/logos/special/premium/header.gif create mode 100644 static/images/logos/special/premium/home.jpg create mode 100644 static/images/logos/special/subscribe/patron-icon.png create mode 100644 static/images/premium/premium-small.png create mode 100644 static/images/premium/user-badge.png diff --git a/static/images/logos/special/premium/header.gif b/static/images/logos/special/premium/header.gif new file mode 100644 index 0000000000000000000000000000000000000000..c6c1ba1e903530988ca33b9b5e6ebb64a87f995d GIT binary patch literal 5861 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 T&mmUKp*GH8UUonb1l;}y?Uyn^ literal 0 HcmV?d00001 diff --git a/static/images/logos/special/premium/home.jpg b/static/images/logos/special/premium/home.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bb6a3c8bc2bb5eb9e2f8115a55cd521eb2334caf GIT binary patch literal 6896 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 T*l+*b;s3tJ|KCw6(#(GVYI%~P literal 0 HcmV?d00001 diff --git a/static/images/logos/special/subscribe/patron-icon.png b/static/images/logos/special/subscribe/patron-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7076eefbaded96c10d0e952f6d50c821f8f212 GIT binary patch literal 5698 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 H4}$*%GOA>5 literal 0 HcmV?d00001 diff --git a/static/images/premium/premium-small.png b/static/images/premium/premium-small.png new file mode 100644 index 0000000000000000000000000000000000000000..82440ec833f7962ef02269ad07369c5f8684e235 GIT binary patch literal 4143 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 Date: Sun, 28 Jun 2015 17:09:07 +0200 Subject: [PATCH 0006/1249] Setup: unpacked db_structure.sql README: added required db-version --- README.md | Bin 8606 -> 8750 bytes setup/db_structure.7z | Bin 81261 -> 0 bytes setup/db_structure.sql | 2328 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2328 insertions(+) delete mode 100644 setup/db_structure.7z create mode 100644 setup/db_structure.sql diff --git a/README.md b/README.md index 5181763bd4919410c34142d5a1eb87b2e69e39a2..71730e827b3bf2a8fc8749a4e2b9a2c2c3874f8e 100644 GIT binary patch delta 178 zcmbQ|yv}8V9lNA9g91Y|LkNQlgA3?z$zDocRubg&MP4M^rQXl}M;S7hbRWXNSG02^E~Ig>}5 ey_lg8Xhg!~%{;M_^LW%YoA73du<$Z)F#rG}t|Jcs delta 89 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 P&;b<&0RSL?q=5hcse?g( diff --git a/setup/db_structure.sql b/setup/db_structure.sql new file mode 100644 index 00000000..f47f9b01 --- /dev/null +++ b/setup/db_structure.sql @@ -0,0 +1,2328 @@ +-- MySQL dump 10.13 Distrib 5.5.30, for Linux (x86_64) +-- +-- Host: localhost Database: sarjuuk_aowow +-- ------------------------------------------------------ +-- Server version 5.5.30-30.1 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `aowow_account` +-- + +DROP TABLE IF EXISTS `aowow_account`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `extId` int(10) unsigned NOT NULL COMMENT 'external user id', + `user` varchar(64) NOT NULL COMMENT 'login', + `passHash` varchar(128) NOT NULL, + `displayName` varchar(64) NOT NULL COMMENT 'nickname', + `email` varchar(64) NOT NULL, + `joinDate` int(10) unsigned NOT NULL COMMENT 'unixtime', + `allowExpire` tinyint(1) unsigned NOT NULL, + `dailyVotes` smallint(5) unsigned NOT NULL DEFAULT '0', + `consecutiveVisits` smallint(5) unsigned NOT NULL DEFAULT '0', + `curIP` varchar(45) NOT NULL, + `prevIP` varchar(45) NOT NULL, + `curLogin` int(15) unsigned NOT NULL COMMENT 'unixtime', + `prevLogin` int(15) unsigned NOT NULL, + `locale` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '0,2,3,6,8', + `userGroups` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'bitmask', + `avatar` varchar(50) NOT NULL DEFAULT '' COMMENT 'icon-string for internal or id for upload', + `title` varchar(50) NOT NULL DEFAULT '' COMMENT 'user can obtain custom titles', + `description` text NOT NULL COMMENT 'markdown formated', + `userPerms` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT 'bool isAdmin', + `status` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT 'flag, see defines', + `statusTimer` int(10) unsigned NOT NULL DEFAULT '0', + `token` varchar(40) NOT NULL COMMENT 'creation & recovery', + PRIMARY KEY (`id`), + UNIQUE KEY `user` (`user`) +) ENGINE=MyISAM AUTO_INCREMENT=30 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_banned` +-- + +DROP TABLE IF EXISTS `aowow_account_banned`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_banned` ( + `id` int(16) unsigned NOT NULL, + `userId` int(11) unsigned NOT NULL COMMENT 'affected accountId', + `staffId` int(11) unsigned NOT NULL COMMENT 'executive accountId', + `typeMask` tinyint(4) unsigned NOT NULL COMMENT 'ACC_BAN_*', + `start` int(10) unsigned NOT NULL COMMENT 'unixtime', + `end` int(10) unsigned NOT NULL COMMENT 'automatic unban @ unixtime', + `reason` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_bannedips` +-- + +DROP TABLE IF EXISTS `aowow_account_bannedips`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_bannedips` ( + `ip` varchar(45) NOT NULL, + `type` tinyint(4) NOT NULL COMMENT '0: onSignin; 1:onSignup', + `count` smallint(6) NOT NULL COMMENT 'nFails', + `unbanDate` int(11) NOT NULL COMMENT 'automatic remove @ unixtime', + PRIMARY KEY (`ip`,`type`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_cookies` +-- + +DROP TABLE IF EXISTS `aowow_account_cookies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_cookies` ( + `userId` int(10) unsigned NOT NULL, + `name` varchar(127) NOT NULL, + `data` text NOT NULL, + PRIMARY KEY (`userId`), + UNIQUE KEY `userId_name` (`userId`,`name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_reputation` +-- + +DROP TABLE IF EXISTS `aowow_account_reputation`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_reputation` ( + `userId` int(10) unsigned NOT NULL, + `action` tinyint(3) unsigned NOT NULL COMMENT 'e.g. upvote a comment', + `amount` tinyint(3) unsigned NOT NULL, + `sourceA` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'e.g. upvoting user', + `sourceB` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'e.g. upvoted commentId', + `date` int(10) unsigned NOT NULL DEFAULT '0', + UNIQUE KEY `userId_action_source` (`userId`,`action`,`sourceA`,`sourceB`), + KEY `userId` (`userId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='reputation log'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_account_weightscales` +-- + +DROP TABLE IF EXISTS `aowow_account_weightscales`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_account_weightscales` ( + `id` int(32) NOT NULL AUTO_INCREMENT, + `account` int(32) NOT NULL, + `name` varchar(32) NOT NULL, + `weights` text NOT NULL, + PRIMARY KEY (`id`,`account`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_achievement` +-- + +DROP TABLE IF EXISTS `aowow_achievement`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_achievement` ( + `id` smallint(5) unsigned NOT NULL, + `faction` tinyint(3) unsigned NOT NULL, + `map` smallint(6) NOT NULL, + `chainId` tinyint(3) unsigned NOT NULL, + `chainPos` tinyint(3) unsigned NOT NULL, + `category` smallint(6) unsigned NOT NULL, + `parentCat` smallint(6) NOT NULL, + `points` tinyint(3) unsigned NOT NULL, + `orderInGroup` tinyint(3) unsigned NOT NULL, + `iconId` mediumint(8) unsigned NOT NULL, + `flags` tinyint(3) unsigned NOT NULL, + `reqCriteriaCount` tinyint(3) unsigned NOT NULL, + `refAchievement` smallint(5) unsigned NOT NULL, + `itemExtra` mediumint(8) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL COMMENT 'see defines.php for flags', + `name_loc0` varchar(78) NOT NULL, + `name_loc2` varchar(79) NOT NULL, + `name_loc3` varchar(86) NOT NULL, + `name_loc6` varchar(78) NOT NULL, + `name_loc8` varchar(76) NOT NULL, + `description_loc0` text NOT NULL, + `description_loc2` text NOT NULL, + `description_loc3` text NOT NULL, + `description_loc6` text NOT NULL, + `description_loc8` text NOT NULL, + `reward_loc0` varchar(74) NOT NULL, + `reward_loc2` varchar(88) NOT NULL, + `reward_loc3` varchar(92) NOT NULL, + `reward_loc6` varchar(83) NOT NULL, + `reward_loc8` varchar(95) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_achievementcategory` +-- + +DROP TABLE IF EXISTS `aowow_achievementcategory`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_achievementcategory` ( + `id` int(16) NOT NULL, + `parentCategory` mediumint(11) NOT NULL, + `name_loc0` varchar(255) NOT NULL, + `name_loc2` varchar(255) NOT NULL, + `name_loc3` varchar(255) NOT NULL, + `name_loc6` varchar(255) NOT NULL, + `name_loc8` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_achievement` (`parentCategory`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_achievementcriteria` +-- + +DROP TABLE IF EXISTS `aowow_achievementcriteria`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_achievementcriteria` ( + `id` smallint(5) unsigned NOT NULL, + `refAchievementId` smallint(5) unsigned NOT NULL, + `type` tinyint(3) unsigned NOT NULL, + `value1` int(10) unsigned NOT NULL, + `value2` int(11) unsigned NOT NULL, + `value3` int(10) unsigned NOT NULL, + `value4` int(11) unsigned NOT NULL, + `value5` int(11) unsigned NOT NULL, + `value6` int(11) unsigned NOT NULL, + `name_loc0` varchar(92) NOT NULL, + `name_loc2` varchar(104) NOT NULL, + `name_loc3` varchar(128) NOT NULL, + `name_loc6` varchar(119) NOT NULL, + `name_loc8` varchar(118) NOT NULL, + `completionFlags` tinyint(3) unsigned NOT NULL, + `groupFlags` tinyint(3) unsigned NOT NULL, + `timeLimit` smallint(5) unsigned NOT NULL, + `order` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_achievement` (`refAchievementId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_announcements` +-- + +DROP TABLE IF EXISTS `aowow_announcements`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_announcements` ( + `id` int(16) NOT NULL AUTO_INCREMENT COMMENT 'iirc negative Ids cant be deleted', + `page` varchar(256) NOT NULL, + `name` varchar(256) NOT NULL, + `groupMask` smallint(5) unsigned NOT NULL, + `style` varchar(256) NOT NULL, + `mode` tinyint(4) unsigned NOT NULL COMMENT '0:pageTop; 1:contentTop', + `status` tinyint(4) unsigned NOT NULL COMMENT '0:disabled; 1:enabled; 2:deleted', + `text_loc0` text NOT NULL, + `text_loc2` text NOT NULL, + `text_loc3` text NOT NULL, + `text_loc6` text NOT NULL, + `text_loc8` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_articles` +-- + +DROP TABLE IF EXISTS `aowow_articles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_articles` ( + `type` tinyint(4) NOT NULL, + `typeId` int(11) NOT NULL, + `locale` tinyint(4) NOT NULL, + `article` text COMMENT 'Markdown formated', + `quickInfo` text COMMENT 'Markdown formated', + UNIQUE KEY `type` (`type`,`typeId`,`locale`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_characters` +-- + +DROP TABLE IF EXISTS `aowow_characters`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_characters` ( + `id` int(11) NOT NULL, + `name` varchar(50) NOT NULL, + `race` tinyint(3) unsigned NOT NULL, + `class` tinyint(3) unsigned NOT NULL, + `gender` tinyint(3) unsigned NOT NULL, + `level` tinyint(3) unsigned NOT NULL, + `description` varchar(150) NOT NULL, + `iconString` varchar(50) NOT NULL, + `titleId` tinyint(3) unsigned NOT NULL, + `guildId` mediumint(8) unsigned NOT NULL, + `guildRank` tinyint(3) unsigned NOT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_classes` +-- + +DROP TABLE IF EXISTS `aowow_classes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_classes` ( + `id` int(16) NOT NULL, + `fileString` varchar(128) NOT NULL, + `name_loc0` varchar(128) NOT NULL, + `name_loc2` varchar(128) NOT NULL, + `name_loc3` varchar(128) NOT NULL, + `name_loc6` varchar(128) NOT NULL, + `name_loc8` varchar(128) NOT NULL, + `powerType` tinyint(4) NOT NULL, + `raceMask` int(16) NOT NULL, + `roles` int(16) NOT NULL, + `skills` varchar(32) NOT NULL, + `flags` mediumint(16) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `weaponTypeMask` int(32) NOT NULL, + `armorTypeMask` int(32) NOT NULL, + `expansion` tinyint(2) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_comments` +-- + +DROP TABLE IF EXISTS `aowow_comments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_comments` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Comment ID', + `type` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Type of Page', + `typeId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'ID Of Page', + `userId` int(10) unsigned NOT NULL COMMENT 'User ID', + `roles` smallint(5) unsigned NOT NULL, + `body` text NOT NULL COMMENT 'Comment text', + `date` int(11) NOT NULL COMMENT 'Comment timestap', + `flags` smallint(6) NOT NULL DEFAULT '0' COMMENT 'deleted, outofdate, sticky', + `replyTo` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'Reply To, comment ID', + `editUserId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Last Edit User ID', + `editDate` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Last Edit Time', + `editCount` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT 'Count Of Edits', + `deleteUserId` int(10) unsigned NOT NULL DEFAULT '0', + `deleteDate` int(10) unsigned NOT NULL DEFAULT '0', + `responseUserId` int(10) unsigned NOT NULL DEFAULT '0', + `responseBody` text, + `responseRoles` smallint(5) unsigned NOT NULL DEFAULT '0', + UNIQUE KEY `id` (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_comments_rates` +-- + +DROP TABLE IF EXISTS `aowow_comments_rates`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_comments_rates` ( + `commentId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Comment ID', + `userId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'User ID', + `value` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'Rating Set', + PRIMARY KEY (`commentId`,`userId`), + UNIQUE KEY `commentId_userId` (`commentId`,`userId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_config` +-- + +DROP TABLE IF EXISTS `aowow_config`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_config` ( + `key` varchar(25) NOT NULL, + `value` varchar(255) NOT NULL, + `flags` tinyint(3) unsigned NOT NULL DEFAULT '0', + `comment` varchar(255) NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_creature` +-- + +DROP TABLE IF EXISTS `aowow_creature`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT '0', + `cuFlags` int(10) unsigned NOT NULL DEFAULT '0', + `difficultyEntry1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `difficultyEntry2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `difficultyEntry3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `KillCredit1` int(10) unsigned NOT NULL DEFAULT '0', + `KillCredit2` int(10) unsigned NOT NULL DEFAULT '0', + `displayId1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `displayId2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `displayId3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `displayId4` mediumint(8) unsigned NOT NULL DEFAULT '0', + `textureString` varchar(50) DEFAULT NULL, + `modelId` mediumint(8) NOT NULL, + `iconString` varchar(50) DEFAULT NULL COMMENT 'first texture of first model for search (up to 11 other skins omitted..)', + `name_loc0` varchar(100) NOT NULL DEFAULT '0', + `name_loc2` varchar(100) DEFAULT NULL, + `name_loc3` varchar(100) DEFAULT NULL, + `name_loc6` varchar(100) DEFAULT NULL, + `name_loc8` varchar(100) DEFAULT NULL, + `subname_loc0` varchar(100) DEFAULT NULL, + `subname_loc2` varchar(100) DEFAULT NULL, + `subname_loc3` varchar(100) DEFAULT NULL, + `subname_loc6` varchar(100) DEFAULT NULL, + `subname_loc8` varchar(100) DEFAULT NULL, + `minLevel` tinyint(3) unsigned NOT NULL DEFAULT '1', + `maxLevel` tinyint(3) unsigned NOT NULL DEFAULT '1', + `exp` smallint(6) NOT NULL DEFAULT '0', + `faction` smallint(5) unsigned NOT NULL DEFAULT '0', + `npcflag` int(10) unsigned NOT NULL DEFAULT '0', + `rank` tinyint(3) unsigned NOT NULL DEFAULT '0', + `dmgSchool` tinyint(4) NOT NULL DEFAULT '0', + `dmgMultiplier` float NOT NULL DEFAULT '1', + `atkSpeed` int(10) unsigned NOT NULL DEFAULT '0', + `rngAtkSpeed` int(10) unsigned NOT NULL DEFAULT '0', + `mleVariance` float NOT NULL DEFAULT '1', + `rngVariance` float NOT NULL DEFAULT '1', + `unitClass` tinyint(3) unsigned NOT NULL DEFAULT '0', + `unitFlags` int(10) unsigned NOT NULL DEFAULT '0', + `unitFlags2` int(10) unsigned NOT NULL DEFAULT '0', + `dynamicFlags` int(10) unsigned NOT NULL DEFAULT '0', + `family` tinyint(4) NOT NULL DEFAULT '0', + `trainerType` tinyint(4) NOT NULL DEFAULT '0', + `trainerSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `trainerClass` tinyint(3) unsigned NOT NULL DEFAULT '0', + `trainerRace` tinyint(3) unsigned NOT NULL DEFAULT '0', + `dmgMin` float unsigned NOT NULL DEFAULT '0', + `dmgMax` float unsigned NOT NULL DEFAULT '0', + `mleAtkPwrMin` smallint(5) unsigned NOT NULL DEFAULT '0', + `mleAtkPwrMax` smallint(5) unsigned NOT NULL DEFAULT '0', + `rngAtkPwrMin` smallint(5) unsigned NOT NULL DEFAULT '0', + `rngAtkPwrMax` smallint(5) unsigned NOT NULL DEFAULT '0', + `type` tinyint(3) unsigned NOT NULL DEFAULT '0', + `typeFlags` int(10) unsigned NOT NULL DEFAULT '0', + `lootId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `pickpocketLootId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `skinLootId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell4` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell5` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell6` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell7` mediumint(8) unsigned NOT NULL DEFAULT '0', + `spell8` mediumint(8) unsigned NOT NULL DEFAULT '0', + `petSpellDataId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `vehicleId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `minGold` mediumint(8) unsigned NOT NULL DEFAULT '0', + `maxGold` mediumint(8) unsigned NOT NULL DEFAULT '0', + `aiName` varchar(50) NOT NULL DEFAULT '', + `healthMin` int(10) unsigned NOT NULL DEFAULT '1', + `healthMax` int(10) unsigned NOT NULL DEFAULT '1', + `manaMin` int(10) unsigned NOT NULL DEFAULT '1', + `manaMax` int(10) unsigned NOT NULL DEFAULT '1', + `armorMin` mediumint(8) unsigned NOT NULL DEFAULT '1', + `armorMax` mediumint(8) unsigned NOT NULL DEFAULT '1', + `racialLeader` tinyint(3) unsigned NOT NULL DEFAULT '0', + `questItem1` int(10) unsigned NOT NULL DEFAULT '0', + `questItem2` int(10) unsigned NOT NULL DEFAULT '0', + `questItem3` int(10) unsigned NOT NULL DEFAULT '0', + `questItem4` int(10) unsigned NOT NULL DEFAULT '0', + `questItem5` int(10) unsigned NOT NULL DEFAULT '0', + `questItem6` int(10) unsigned NOT NULL DEFAULT '0', + `mechanicImmuneMask` int(10) unsigned NOT NULL DEFAULT '0', + `flagsExtra` int(10) unsigned NOT NULL DEFAULT '0', + `scriptName` varchar(50) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `idx_name` (`name_loc0`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_creature_waypoints` +-- + +DROP TABLE IF EXISTS `aowow_creature_waypoints`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature_waypoints` ( + `creatureOrPath` int(11) NOT NULL, + `point` tinyint(3) unsigned NOT NULL, + `areaId` smallint(5) unsigned NOT NULL, + `floor` tinyint(3) unsigned NOT NULL, + `posX` float unsigned NOT NULL, + `posY` float unsigned NOT NULL, + `wait` mediumint(8) unsigned NOT NULL, + PRIMARY KEY (`creatureOrPath`,`point`,`areaId`,`floor`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_currencies` +-- + +DROP TABLE IF EXISTS `aowow_currencies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_currencies` ( + `id` int(16) NOT NULL, + `category` mediumint(8) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `iconId` mediumint(9) NOT NULL, + `itemId` int(16) NOT NULL, + `name_loc0` varchar(64) NOT NULL, + `name_loc2` varchar(64) NOT NULL, + `name_loc3` varchar(64) NOT NULL, + `name_loc6` varchar(64) NOT NULL, + `name_loc8` varchar(64) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_errors` +-- + +DROP TABLE IF EXISTS `aowow_errors`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_errors` ( + `date` int(10) unsigned DEFAULT NULL, + `version` smallint(5) unsigned NOT NULL, + `phpError` smallint(5) unsigned NOT NULL, + `file` varchar(250) NOT NULL, + `line` smallint(5) unsigned NOT NULL, + `query` varchar(250) NOT NULL, + `userGroups` smallint(5) unsigned NOT NULL, + `message` text, + PRIMARY KEY (`file`,`line`,`phpError`,`version`,`userGroups`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_events` +-- + +DROP TABLE IF EXISTS `aowow_events`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_events` ( + `id` tinyint(3) unsigned NOT NULL, + `holidayId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `cuFlags` int(10) unsigned NOT NULL DEFAULT '0', + `startTime` bigint(20) NOT NULL, + `endTime` bigint(20) NOT NULL, + `occurence` bigint(20) unsigned NOT NULL, + `length` bigint(20) unsigned NOT NULL, + `requires` varchar(255) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `holidayId` (`holidayId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_factions` +-- + +DROP TABLE IF EXISTS `aowow_factions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_factions` ( + `id` smallint(5) unsigned NOT NULL, + `repIdx` smallint(5) unsigned NOT NULL, + `side` tinyint(1) unsigned NOT NULL, + `expansion` tinyint(1) unsigned NOT NULL, + `qmNpcIds` varchar(12) NOT NULL COMMENT 'space separated', + `templateIds` tinytext NOT NULL COMMENT 'space separated', + `cuFlags` int(10) unsigned NOT NULL, + `parentFactionId` smallint(5) unsigned NOT NULL, + `spilloverRateIn` float(8,2) NOT NULL, + `spilloverRateOut` float(8,2) NOT NULL, + `spilloverMaxRank` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(35) NOT NULL, + `name_loc2` varchar(49) NOT NULL, + `name_loc3` varchar(40) NOT NULL, + `name_loc6` varchar(50) NOT NULL, + `name_loc8` varchar(47) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_factiontemplate` +-- + +DROP TABLE IF EXISTS `aowow_factiontemplate`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_factiontemplate` ( + `id` smallint(5) unsigned NOT NULL, + `factionId` smallint(5) unsigned NOT NULL, + `A` tinyint(4) NOT NULL COMMENT 'Aliance: -1 - hostile, 1 - friendly, 0 - neutral', + `H` tinyint(4) NOT NULL COMMENT 'Horde: -1 - hostile, 1 - friendly, 0 - neutral', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_glyphproperties` +-- + +DROP TABLE IF EXISTS `aowow_glyphproperties`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_glyphproperties` ( + `id` smallint(5) unsigned NOT NULL, + `spellId` mediumint(11) unsigned NOT NULL, + `typeFlags` tinyint(3) unsigned NOT NULL, + `iconId` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_holidays` +-- + +DROP TABLE IF EXISTS `aowow_holidays`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_holidays` ( + `id` smallint(6) unsigned NOT NULL, + `bossCreature` mediumint(8) unsigned NOT NULL, + `achievementCatOrId` mediumint(9) NOT NULL, + `name_loc0` varchar(36) NOT NULL, + `name_loc2` varchar(42) NOT NULL, + `name_loc3` varchar(36) NOT NULL, + `name_loc6` varchar(49) NOT NULL, + `name_loc8` varchar(29) NOT NULL, + `description_loc0` text, + `description_loc2` text, + `description_loc3` text, + `description_loc6` text, + `description_loc8` text, + `looping` tinyint(2) NOT NULL, + `scheduleType` tinyint(2) NOT NULL, + `textureString` varchar(30) NOT NULL, + `iconString` varchar(51) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_icons` +-- + +DROP TABLE IF EXISTS `aowow_icons`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_icons` ( + `id` mediumint(9) NOT NULL, + `iconString` varchar(55) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_item_stats` +-- + +DROP TABLE IF EXISTS `aowow_item_stats`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_item_stats` ( + `id` mediumint(8) unsigned NOT NULL, + `nsockets` tinyint(3) unsigned NOT NULL, + `dmgmin1` smallint(5) unsigned NOT NULL, + `dmgmax1` smallint(5) unsigned NOT NULL, + `speed` float(8,2) NOT NULL, + `dps` float(8,2) NOT NULL, + `mledmgmin` smallint(5) unsigned NOT NULL, + `mledmgmax` smallint(5) unsigned NOT NULL, + `mlespeed` float(8,2) NOT NULL, + `mledps` float(8,2) NOT NULL, + `rgddmgmin` smallint(5) unsigned NOT NULL, + `rgddmgmax` smallint(5) unsigned NOT NULL, + `rgdspeed` float(8,2) NOT NULL, + `rgddps` float(8,2) NOT NULL, + `dmg` float(8,2) NOT NULL, + `damagetype` tinyint(4) NOT NULL, + `mana` smallint(6) NOT NULL, + `health` smallint(6) NOT NULL, + `agi` smallint(6) NOT NULL, + `str` smallint(6) NOT NULL, + `int` smallint(6) NOT NULL, + `spi` smallint(6) NOT NULL, + `sta` smallint(6) NOT NULL, + `energy` smallint(6) NOT NULL, + `rage` smallint(6) NOT NULL, + `focus` smallint(6) NOT NULL, + `runicpwr` smallint(6) NOT NULL, + `defrtng` smallint(6) NOT NULL, + `dodgertng` smallint(6) NOT NULL, + `parryrtng` smallint(6) NOT NULL, + `blockrtng` smallint(6) NOT NULL, + `mlehitrtng` smallint(6) NOT NULL, + `rgdhitrtng` smallint(6) NOT NULL, + `splhitrtng` smallint(6) NOT NULL, + `mlecritstrkrtng` smallint(6) NOT NULL, + `rgdcritstrkrtng` smallint(6) NOT NULL, + `splcritstrkrtng` smallint(6) NOT NULL, + `_mlehitrtng` smallint(6) NOT NULL, + `_rgdhitrtng` smallint(6) NOT NULL, + `_splhitrtng` smallint(6) NOT NULL, + `_mlecritstrkrtng` smallint(6) NOT NULL, + `_rgdcritstrkrtng` smallint(6) NOT NULL, + `_splcritstrkrtng` smallint(6) NOT NULL, + `mlehastertng` smallint(6) NOT NULL, + `rgdhastertng` smallint(6) NOT NULL, + `splhastertng` smallint(6) NOT NULL, + `hitrtng` smallint(6) NOT NULL, + `critstrkrtng` smallint(6) NOT NULL, + `_hitrtng` smallint(6) NOT NULL, + `_critstrkrtng` smallint(6) NOT NULL, + `resirtng` smallint(6) NOT NULL, + `hastertng` smallint(6) NOT NULL, + `exprtng` smallint(6) NOT NULL, + `atkpwr` smallint(6) NOT NULL, + `mleatkpwr` smallint(6) NOT NULL, + `rgdatkpwr` smallint(6) NOT NULL, + `feratkpwr` smallint(6) NOT NULL, + `splheal` smallint(6) NOT NULL, + `spldmg` smallint(6) NOT NULL, + `manargn` smallint(6) NOT NULL, + `armorpenrtng` smallint(6) NOT NULL, + `splpwr` smallint(6) NOT NULL, + `healthrgn` smallint(6) NOT NULL, + `splpen` smallint(6) NOT NULL, + `block` smallint(6) NOT NULL, + `mastrtng` smallint(6) NOT NULL, + `armor` smallint(6) NOT NULL, + `armorbonus` smallint(6) NOT NULL, + `firres` smallint(6) NOT NULL, + `frores` smallint(6) NOT NULL, + `holres` smallint(6) NOT NULL, + `shares` smallint(6) NOT NULL, + `natres` smallint(6) NOT NULL, + `arcres` smallint(6) NOT NULL, + `firsplpwr` smallint(6) NOT NULL, + `frosplpwr` smallint(6) NOT NULL, + `holsplpwr` smallint(6) NOT NULL, + `shasplpwr` smallint(6) NOT NULL, + `natsplpwr` smallint(6) NOT NULL, + `arcsplpwr` smallint(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `item` (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemenchantment` +-- + +DROP TABLE IF EXISTS `aowow_itemenchantment`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemenchantment` ( + `id` smallint(5) unsigned NOT NULL, + `type1` tinyint(4) unsigned NOT NULL, + `type2` tinyint(4) unsigned NOT NULL, + `type3` tinyint(4) unsigned NOT NULL, + `amount1` smallint(6) NOT NULL, + `amount2` smallint(6) NOT NULL, + `amount3` smallint(6) NOT NULL, + `object1` mediumint(9) unsigned NOT NULL, + `object2` mediumint(9) unsigned NOT NULL, + `object3` smallint(6) unsigned NOT NULL, + `text_loc0` varchar(65) NOT NULL, + `text_loc2` varchar(91) NOT NULL, + `text_loc3` varchar(84) NOT NULL, + `text_loc6` varchar(89) NOT NULL, + `text_loc8` varchar(96) NOT NULL, + `gemReference` mediumint(8) unsigned NOT NULL, + `conditionId` tinyint(3) unsigned NOT NULL, + `skillLine` smallint(5) unsigned NOT NULL, + `skillLevel` smallint(5) unsigned NOT NULL, + `requiredLevel` tinyint(3) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemenchantmentcondition` +-- + +DROP TABLE IF EXISTS `aowow_itemenchantmentcondition`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemenchantmentcondition` ( + `id` smallint(5) unsigned NOT NULL, + `color1` tinyint(3) unsigned NOT NULL DEFAULT '0', + `color2` tinyint(3) unsigned NOT NULL DEFAULT '0', + `color3` tinyint(3) unsigned NOT NULL DEFAULT '0', + `color4` tinyint(3) unsigned NOT NULL DEFAULT '0', + `color5` tinyint(3) unsigned NOT NULL DEFAULT '0', + `comparator1` tinyint(4) unsigned NOT NULL DEFAULT '0', + `comparator2` tinyint(4) unsigned NOT NULL DEFAULT '0', + `comparator3` tinyint(4) unsigned NOT NULL DEFAULT '0', + `comparator4` tinyint(4) unsigned NOT NULL DEFAULT '0', + `comparator5` tinyint(4) unsigned NOT NULL DEFAULT '0', + `cmpColor1` tinyint(3) unsigned NOT NULL DEFAULT '0', + `cmpColor2` tinyint(3) unsigned NOT NULL DEFAULT '0', + `cmpColor3` tinyint(3) unsigned NOT NULL DEFAULT '0', + `cmpColor4` tinyint(3) unsigned NOT NULL DEFAULT '0', + `cmpColor5` tinyint(3) unsigned NOT NULL DEFAULT '0', + `value1` tinyint(3) unsigned NOT NULL DEFAULT '0', + `value2` tinyint(3) unsigned NOT NULL DEFAULT '0', + `value3` tinyint(3) unsigned NOT NULL DEFAULT '0', + `value4` tinyint(3) unsigned NOT NULL DEFAULT '0', + `value5` tinyint(3) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemextendedcost` +-- + +DROP TABLE IF EXISTS `aowow_itemextendedcost`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemextendedcost` ( + `id` smallint(5) unsigned NOT NULL, + `reqHonorPoints` mediumint(8) unsigned NOT NULL, + `reqArenaPoints` smallint(5) unsigned NOT NULL, + `reqArenaSlot` tinyint(3) unsigned NOT NULL, + `reqItemId1` mediumint(8) unsigned NOT NULL, + `reqItemId2` mediumint(8) unsigned NOT NULL, + `reqItemId3` mediumint(8) unsigned NOT NULL, + `reqItemId4` mediumint(8) unsigned NOT NULL, + `reqItemId5` mediumint(8) unsigned NOT NULL, + `itemCount1` smallint(5) unsigned NOT NULL, + `itemCount2` smallint(5) unsigned NOT NULL, + `itemCount3` smallint(5) unsigned NOT NULL, + `itemCount4` smallint(5) unsigned NOT NULL, + `itemCount5` smallint(5) unsigned NOT NULL, + `reqPersonalRating` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `id` (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemlimitcategory` +-- + +DROP TABLE IF EXISTS `aowow_itemlimitcategory`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemlimitcategory` ( + `id` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(31) NOT NULL, + `name_loc2` varchar(36) NOT NULL, + `name_loc3` varchar(34) NOT NULL, + `name_loc6` varchar(40) NOT NULL, + `name_loc8` varchar(35) NOT NULL, + `count` tinyint(3) unsigned NOT NULL, + `isGem` tinyint(3) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemrandomenchant` +-- + +DROP TABLE IF EXISTS `aowow_itemrandomenchant`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemrandomenchant` ( + `id` smallint(6) NOT NULL, + `name_loc0` varchar(250) NOT NULL, + `name_loc2` varchar(250) NOT NULL, + `name_loc3` varchar(250) NOT NULL, + `name_loc6` varchar(250) NOT NULL, + `name_loc8` varchar(250) NOT NULL, + `nameINT` char(250) NOT NULL, + `enchantId1` smallint(5) unsigned NOT NULL, + `enchantId2` smallint(5) unsigned NOT NULL, + `enchantId3` smallint(5) unsigned NOT NULL, + `enchantId4` smallint(5) unsigned NOT NULL, + `enchantId5` smallint(5) unsigned NOT NULL, + `allocationPct1` smallint(5) unsigned NOT NULL, + `allocationPct2` smallint(5) unsigned NOT NULL, + `allocationPct3` smallint(5) unsigned NOT NULL, + `allocationPct4` smallint(5) unsigned NOT NULL, + `allocationPct5` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemrandomproppoints` +-- + +DROP TABLE IF EXISTS `aowow_itemrandomproppoints`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemrandomproppoints` ( + `Id` smallint(5) unsigned NOT NULL, + `epic1` smallint(5) unsigned NOT NULL, + `epic2` smallint(5) unsigned NOT NULL, + `epic3` smallint(5) unsigned NOT NULL, + `epic4` smallint(5) unsigned NOT NULL, + `epic5` smallint(5) unsigned NOT NULL, + `rare1` smallint(5) unsigned NOT NULL, + `rare2` smallint(5) unsigned NOT NULL, + `rare3` smallint(5) unsigned NOT NULL, + `rare4` smallint(5) unsigned NOT NULL, + `rare5` smallint(5) unsigned NOT NULL, + `uncommon1` smallint(5) unsigned NOT NULL, + `uncommon2` smallint(5) unsigned NOT NULL, + `uncommon3` smallint(5) unsigned NOT NULL, + `uncommon4` smallint(5) unsigned NOT NULL, + `uncommon5` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`Id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_items` +-- + +DROP TABLE IF EXISTS `aowow_items`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_items` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT '0', + `class` tinyint(3) unsigned NOT NULL DEFAULT '0', + `classBak` tinyint(3) NOT NULL, + `subClass` tinyint(3) NOT NULL DEFAULT '0', + `subClassBak` tinyint(3) NOT NULL, + `subSubClass` tinyint(3) NOT NULL, + `name_loc0` varchar(127) NOT NULL DEFAULT '', + `name_loc2` varchar(127) DEFAULT NULL, + `name_loc3` varchar(127) DEFAULT NULL, + `name_loc6` varchar(127) DEFAULT NULL, + `name_loc8` varchar(127) DEFAULT NULL, + `displayId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `quality` tinyint(3) unsigned NOT NULL DEFAULT '0', + `flags` bigint(20) NOT NULL DEFAULT '0', + `flagsExtra` int(10) unsigned NOT NULL DEFAULT '0', + `buyCount` tinyint(3) unsigned NOT NULL DEFAULT '1', + `buyPrice` bigint(20) NOT NULL DEFAULT '0', + `sellPrice` int(10) unsigned NOT NULL DEFAULT '0', + `repairPrice` int(10) unsigned NOT NULL, + `slot` tinyint(3) NOT NULL, + `slotBak` tinyint(3) unsigned NOT NULL DEFAULT '0', + `requiredClass` int(11) NOT NULL DEFAULT '-1', + `requiredRace` int(11) NOT NULL DEFAULT '-1', + `itemLevel` smallint(5) unsigned NOT NULL DEFAULT '0', + `requiredLevel` tinyint(3) unsigned NOT NULL DEFAULT '0', + `requiredSkill` smallint(5) unsigned NOT NULL DEFAULT '0', + `requiredSkillRank` smallint(5) unsigned NOT NULL DEFAULT '0', + `requiredSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `requiredHonorRank` mediumint(8) unsigned NOT NULL DEFAULT '0', + `requiredCityRank` mediumint(8) unsigned NOT NULL DEFAULT '0', + `requiredFaction` smallint(5) unsigned NOT NULL DEFAULT '0', + `requiredFactionRank` smallint(5) unsigned NOT NULL DEFAULT '0', + `maxCount` int(11) NOT NULL DEFAULT '0', + `cuFlags` int(10) unsigned NOT NULL, + `model` varchar(50) NOT NULL, + `stackable` int(11) DEFAULT '1', + `slots` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statType1` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue1` smallint(6) NOT NULL DEFAULT '0', + `statType2` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue2` smallint(6) NOT NULL DEFAULT '0', + `statType3` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue3` smallint(6) NOT NULL DEFAULT '0', + `statType4` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue4` smallint(6) NOT NULL DEFAULT '0', + `statType5` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue5` smallint(6) NOT NULL DEFAULT '0', + `statType6` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue6` smallint(6) NOT NULL DEFAULT '0', + `statType7` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue7` smallint(6) NOT NULL DEFAULT '0', + `statType8` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue8` smallint(6) NOT NULL DEFAULT '0', + `statType9` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue9` smallint(6) NOT NULL DEFAULT '0', + `statType10` tinyint(3) unsigned NOT NULL DEFAULT '0', + `statValue10` smallint(6) NOT NULL DEFAULT '0', + `scalingStatDistribution` smallint(6) NOT NULL DEFAULT '0', + `scalingStatValue` int(10) unsigned NOT NULL DEFAULT '0', + `dmgMin1` float NOT NULL DEFAULT '0', + `dmgMax1` float NOT NULL DEFAULT '0', + `dmgType1` tinyint(3) unsigned NOT NULL DEFAULT '0', + `dmgMin2` float NOT NULL DEFAULT '0', + `dmgMax2` float NOT NULL DEFAULT '0', + `dmgType2` tinyint(3) unsigned NOT NULL DEFAULT '0', + `delay` smallint(5) unsigned NOT NULL DEFAULT '1000', + `armor` smallint(5) unsigned NOT NULL DEFAULT '0', + `armorDamageModifier` float NOT NULL DEFAULT '0', + `block` mediumint(8) unsigned NOT NULL DEFAULT '0', + `resHoly` tinyint(3) unsigned NOT NULL DEFAULT '0', + `resFire` tinyint(3) unsigned NOT NULL DEFAULT '0', + `resNature` tinyint(3) unsigned NOT NULL DEFAULT '0', + `resFrost` tinyint(3) unsigned NOT NULL DEFAULT '0', + `resShadow` tinyint(3) unsigned NOT NULL DEFAULT '0', + `resArcane` tinyint(3) unsigned NOT NULL DEFAULT '0', + `ammoType` tinyint(3) unsigned NOT NULL DEFAULT '0', + `rangedModRange` float NOT NULL DEFAULT '0', + `spellId1` mediumint(8) NOT NULL DEFAULT '0', + `spellTrigger1` tinyint(3) unsigned NOT NULL DEFAULT '0', + `spellCharges1` smallint(6) DEFAULT NULL, + `spellppmRate1` float NOT NULL DEFAULT '0', + `spellCooldown1` int(11) NOT NULL DEFAULT '-1', + `spellCategory1` smallint(5) unsigned NOT NULL DEFAULT '0', + `spellCategoryCooldown1` int(11) NOT NULL DEFAULT '-1', + `spellId2` mediumint(8) NOT NULL DEFAULT '0', + `spellTrigger2` tinyint(3) unsigned NOT NULL DEFAULT '0', + `spellCharges2` smallint(6) DEFAULT NULL, + `spellppmRate2` float NOT NULL DEFAULT '0', + `spellCooldown2` int(11) NOT NULL DEFAULT '-1', + `spellCategory2` smallint(5) unsigned NOT NULL DEFAULT '0', + `spellCategoryCooldown2` int(11) NOT NULL DEFAULT '-1', + `spellId3` mediumint(8) NOT NULL DEFAULT '0', + `spellTrigger3` tinyint(3) unsigned NOT NULL DEFAULT '0', + `spellCharges3` smallint(6) DEFAULT NULL, + `spellppmRate3` float NOT NULL DEFAULT '0', + `spellCooldown3` int(11) NOT NULL DEFAULT '-1', + `spellCategory3` smallint(5) unsigned NOT NULL DEFAULT '0', + `spellCategoryCooldown3` int(11) NOT NULL DEFAULT '-1', + `spellId4` mediumint(8) NOT NULL DEFAULT '0', + `spellTrigger4` tinyint(3) unsigned NOT NULL DEFAULT '0', + `spellCharges4` smallint(6) DEFAULT NULL, + `spellppmRate4` float NOT NULL DEFAULT '0', + `spellCooldown4` int(11) NOT NULL DEFAULT '-1', + `spellCategory4` smallint(5) unsigned NOT NULL DEFAULT '0', + `spellCategoryCooldown4` int(11) NOT NULL DEFAULT '-1', + `spellId5` mediumint(8) NOT NULL DEFAULT '0', + `spellTrigger5` tinyint(3) unsigned NOT NULL DEFAULT '0', + `spellCharges5` smallint(6) DEFAULT NULL, + `spellppmRate5` float NOT NULL DEFAULT '0', + `spellCooldown5` int(11) NOT NULL DEFAULT '-1', + `spellCategory5` smallint(5) unsigned NOT NULL DEFAULT '0', + `spellCategoryCooldown5` int(11) NOT NULL DEFAULT '-1', + `bonding` tinyint(3) unsigned NOT NULL DEFAULT '0', + `description_loc0` varchar(255) NOT NULL DEFAULT '', + `description_loc2` varchar(255) DEFAULT NULL, + `description_loc3` varchar(255) DEFAULT NULL, + `description_loc6` varchar(255) DEFAULT NULL, + `description_loc8` varchar(255) DEFAULT NULL, + `pageTextId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `languageId` tinyint(3) unsigned NOT NULL DEFAULT '0', + `startQuest` mediumint(8) unsigned NOT NULL DEFAULT '0', + `lockId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `randomEnchant` mediumint(8) NOT NULL DEFAULT '0', + `itemset` mediumint(8) unsigned NOT NULL DEFAULT '0', + `durability` smallint(5) unsigned NOT NULL DEFAULT '0', + `area` mediumint(8) unsigned NOT NULL DEFAULT '0', + `map` smallint(6) NOT NULL DEFAULT '0', + `bagFamily` mediumint(8) NOT NULL DEFAULT '0', + `totemCategory` mediumint(8) NOT NULL DEFAULT '0', + `socketColor1` tinyint(4) NOT NULL DEFAULT '0', + `socketContent1` mediumint(8) NOT NULL DEFAULT '0', + `socketColor2` tinyint(4) NOT NULL DEFAULT '0', + `socketContent2` mediumint(8) NOT NULL DEFAULT '0', + `socketColor3` tinyint(4) NOT NULL DEFAULT '0', + `socketContent3` mediumint(8) NOT NULL DEFAULT '0', + `socketBonus` mediumint(8) NOT NULL DEFAULT '0', + `gemColorMask` mediumint(8) NOT NULL DEFAULT '0', + `requiredDisenchantSkill` smallint(6) NOT NULL DEFAULT '-1', + `disenchantId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `duration` int(10) unsigned NOT NULL DEFAULT '0', + `itemLimitCategory` smallint(6) NOT NULL DEFAULT '0', + `holidayId` int(11) unsigned NOT NULL DEFAULT '0', + `scriptName` varchar(64) NOT NULL DEFAULT '', + `foodType` tinyint(3) unsigned NOT NULL DEFAULT '0', + `gemEnchantmentId` mediumint(8) NOT NULL, + `minMoneyLoot` int(10) unsigned NOT NULL DEFAULT '0', + `maxMoneyLoot` int(10) unsigned NOT NULL DEFAULT '0', + `flagsCustom` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `idx_name` (`name_loc0`), + KEY `items_index` (`class`), + KEY `idx_model` (`displayId`), + KEY `idx_faction` (`requiredFaction`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_itemset` +-- + +DROP TABLE IF EXISTS `aowow_itemset`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_itemset` ( + `id` int(16) NOT NULL, + `refSetId` int(11) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `name_loc0` varchar(255) NOT NULL, + `name_loc2` varchar(255) NOT NULL, + `name_loc3` varchar(255) NOT NULL, + `name_loc6` varchar(255) NOT NULL, + `name_loc8` varchar(255) NOT NULL, + `item1` mediumint(11) unsigned NOT NULL, + `item2` mediumint(11) unsigned NOT NULL, + `item3` mediumint(11) unsigned NOT NULL, + `item4` mediumint(11) unsigned NOT NULL, + `item5` mediumint(11) unsigned NOT NULL, + `item6` mediumint(11) unsigned NOT NULL, + `item7` mediumint(11) unsigned NOT NULL, + `item8` mediumint(11) unsigned NOT NULL, + `item9` mediumint(11) unsigned NOT NULL, + `item10` mediumint(11) unsigned NOT NULL, + `spell1` mediumint(11) unsigned NOT NULL, + `spell2` mediumint(11) unsigned NOT NULL, + `spell3` mediumint(11) unsigned NOT NULL, + `spell4` mediumint(11) unsigned NOT NULL, + `spell5` mediumint(11) unsigned NOT NULL, + `spell6` mediumint(11) unsigned NOT NULL, + `spell7` mediumint(11) unsigned NOT NULL, + `spell8` mediumint(11) unsigned NOT NULL, + `bonus1` tinyint(1) unsigned NOT NULL, + `bonus2` tinyint(1) unsigned NOT NULL, + `bonus3` tinyint(1) unsigned NOT NULL, + `bonus4` tinyint(1) unsigned NOT NULL, + `bonus5` tinyint(1) unsigned NOT NULL, + `bonus6` tinyint(1) unsigned NOT NULL, + `bonus7` tinyint(1) unsigned NOT NULL, + `bonus8` tinyint(1) unsigned NOT NULL, + `bonusText_loc0` varchar(256) NOT NULL, + `bonusText_loc2` varchar(256) NOT NULL, + `bonusText_loc3` varchar(256) NOT NULL, + `bonusText_loc6` varchar(256) NOT NULL, + `bonusText_loc8` varchar(256) NOT NULL, + `bonusParsed` varchar(256) NOT NULL COMMENT 'serialized itemMods', + `npieces` tinyint(3) NOT NULL, + `minLevel` smallint(6) NOT NULL, + `maxLevel` smallint(6) NOT NULL, + `reqLevel` smallint(6) NOT NULL, + `classMask` mediumint(9) NOT NULL, + `heroic` tinyint(1) NOT NULL COMMENT 'bool', + `quality` tinyint(4) NOT NULL, + `type` smallint(6) NOT NULL COMMENT 'g_itemset_types', + `contentGroup` smallint(6) NOT NULL COMMENT 'g_itemset_notes', + `holidayId` smallint(3) NOT NULL, + `skillId` smallint(3) unsigned NOT NULL, + `skillLevel` smallint(3) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_lock` +-- + +DROP TABLE IF EXISTS `aowow_lock`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_lock` ( + `id` mediumint(11) unsigned NOT NULL, + `type1` tinyint(1) unsigned NOT NULL, + `type2` tinyint(1) unsigned NOT NULL, + `type3` tinyint(1) unsigned NOT NULL, + `type4` tinyint(1) unsigned NOT NULL, + `type5` tinyint(1) unsigned NOT NULL, + `properties1` mediumint(11) unsigned NOT NULL, + `properties2` mediumint(11) unsigned NOT NULL, + `properties3` mediumint(11) unsigned NOT NULL, + `properties4` mediumint(11) unsigned NOT NULL, + `properties5` mediumint(11) unsigned NOT NULL, + `reqSkill1` mediumint(11) unsigned NOT NULL, + `reqSkill2` mediumint(11) unsigned NOT NULL, + `reqSkill3` mediumint(11) unsigned NOT NULL, + `reqSkill4` mediumint(11) unsigned NOT NULL, + `reqSkill5` mediumint(11) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_mailtemplate` +-- + +DROP TABLE IF EXISTS `aowow_mailtemplate`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_mailtemplate` ( + `id` smallint(5) unsigned NOT NULL, + `subject_loc0` varchar(128) NOT NULL, + `subject_loc2` varchar(128) NOT NULL, + `subject_loc3` varchar(128) NOT NULL, + `subject_loc6` varchar(128) NOT NULL, + `subject_loc8` varchar(128) NOT NULL, + `text_loc0` text NOT NULL, + `text_loc2` text NOT NULL, + `text_loc3` text NOT NULL, + `text_loc6` text NOT NULL, + `text_loc8` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_news` +-- + +DROP TABLE IF EXISTS `aowow_news`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_news` ( + `id` int(16) unsigned NOT NULL, + `active` tinyint(3) unsigned NOT NULL DEFAULT '1', + `extraWide` tinyint(3) unsigned NOT NULL DEFAULT '0', + `bgImgUrl` varchar(150) NOT NULL DEFAULT '', + `text_loc0` text NOT NULL, + `text_loc2` text NOT NULL, + `text_loc3` text NOT NULL, + `text_loc6` text NOT NULL, + `text_loc8` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_news_overlay` +-- + +DROP TABLE IF EXISTS `aowow_news_overlay`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_news_overlay` ( + `newsId` int(11) unsigned NOT NULL, + `left` smallint(5) unsigned NOT NULL, + `width` smallint(5) unsigned NOT NULL, + `url` varchar(150) NOT NULL, + `title_loc0` varchar(100) NOT NULL DEFAULT '', + `title_loc2` varchar(100) NOT NULL DEFAULT '', + `title_loc3` varchar(100) NOT NULL DEFAULT '', + `title_loc6` varchar(100) NOT NULL DEFAULT '', + `title_loc8` varchar(100) NOT NULL DEFAULT '' +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_objects` +-- + +DROP TABLE IF EXISTS `aowow_objects`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_objects` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT '0', + `type` tinyint(3) unsigned NOT NULL DEFAULT '0', + `typeCat` tinyint(3) NOT NULL DEFAULT '0', + `event` smallint(5) unsigned NOT NULL DEFAULT '0', + `displayId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `name_loc0` varchar(100) DEFAULT NULL, + `name_loc2` varchar(100) DEFAULT NULL, + `name_loc3` varchar(100) DEFAULT NULL, + `name_loc6` varchar(100) DEFAULT NULL, + `name_loc8` varchar(100) DEFAULT NULL, + `faction` smallint(5) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `cuFlags` int(10) unsigned NOT NULL DEFAULT '0', + `questItem1` int(11) unsigned NOT NULL DEFAULT '0', + `questItem2` int(11) unsigned NOT NULL DEFAULT '0', + `questItem3` int(11) unsigned NOT NULL DEFAULT '0', + `questItem4` int(11) unsigned NOT NULL DEFAULT '0', + `questItem5` int(11) unsigned NOT NULL DEFAULT '0', + `questItem6` int(11) unsigned NOT NULL DEFAULT '0', + `lootId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `lockId` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSkill` smallint(5) unsigned NOT NULL DEFAULT '0', + `pageTextId` smallint(5) unsigned NOT NULL DEFAULT '0', + `linkedTrap` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqQuest` smallint(5) unsigned NOT NULL DEFAULT '0', + `spellFocusId` smallint(5) unsigned NOT NULL DEFAULT '0', + `onUseSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `onSuccessSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `auraSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `triggeredSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `miscInfo` varchar(128) NOT NULL, + `ScriptOrAI` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_name` (`name_loc0`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_pet` +-- + +DROP TABLE IF EXISTS `aowow_pet`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_pet` ( + `id` int(11) NOT NULL, + `category` mediumint(8) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `minLevel` smallint(6) NOT NULL, + `maxLevel` smallint(6) NOT NULL, + `foodMask` int(11) NOT NULL, + `type` tinyint(4) NOT NULL, + `exotic` tinyint(4) NOT NULL, + `expansion` tinyint(4) NOT NULL, + `name_loc0` varchar(64) NOT NULL, + `name_loc2` varchar(64) NOT NULL, + `name_loc3` varchar(64) NOT NULL, + `name_loc6` varchar(64) NOT NULL, + `name_loc8` varchar(64) NOT NULL, + `iconString` varchar(128) NOT NULL, + `skillLineId` mediumint(9) NOT NULL, + `spellId1` mediumint(9) NOT NULL, + `spellId2` mediumint(9) NOT NULL, + `spellId3` mediumint(9) NOT NULL, + `spellId4` mediumint(9) NOT NULL, + `armor` mediumint(9) NOT NULL, + `damage` mediumint(9) NOT NULL, + `health` mediumint(9) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_quests` +-- + +DROP TABLE IF EXISTS `aowow_quests`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quests` ( + `id` mediumint(8) unsigned NOT NULL DEFAULT '0', + `method` tinyint(3) unsigned NOT NULL DEFAULT '2', + `level` smallint(3) NOT NULL DEFAULT '1', + `minLevel` tinyint(3) unsigned NOT NULL DEFAULT '0', + `maxLevel` tinyint(3) unsigned NOT NULL DEFAULT '0', + `zoneOrSort` smallint(6) NOT NULL DEFAULT '0', + `zoneOrSortBak` smallint(6) NOT NULL DEFAULT '0', + `type` smallint(5) unsigned NOT NULL DEFAULT '0', + `suggestedPlayers` tinyint(3) unsigned NOT NULL DEFAULT '0', + `timeLimit` int(10) unsigned NOT NULL DEFAULT '0', + `holidayId` smallint(6) NOT NULL DEFAULT '0', + `prevQuestId` mediumint(8) NOT NULL DEFAULT '0', + `nextQuestId` mediumint(8) NOT NULL DEFAULT '0', + `exclusiveGroup` mediumint(8) NOT NULL DEFAULT '0', + `nextQuestIdChain` mediumint(8) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `specialFlags` tinyint(3) unsigned NOT NULL DEFAULT '0', + `cuFlags` int(10) unsigned NOT NULL DEFAULT '0', + `reqClassMask` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqRaceMask` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSkillId` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSkillPoints` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqFactionId1` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqFactionId2` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqFactionValue1` mediumint(8) NOT NULL DEFAULT '0', + `reqFactionValue2` mediumint(8) NOT NULL DEFAULT '0', + `reqMinRepFaction` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqMaxRepFaction` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqMinRepValue` mediumint(8) NOT NULL DEFAULT '0', + `reqMaxRepValue` mediumint(8) NOT NULL DEFAULT '0', + `reqPlayerKills` tinyint(3) unsigned NOT NULL DEFAULT '0', + `sourceItemId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `sourceItemCount` tinyint(3) unsigned NOT NULL DEFAULT '0', + `sourceSpellId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardXP` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardOrReqMoney` int(11) NOT NULL DEFAULT '0', + `rewardMoneyMaxLevel` int(10) unsigned NOT NULL DEFAULT '0', + `rewardSpell` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardSpellCast` int(11) NOT NULL DEFAULT '0', + `rewardHonorPoints` int(11) NOT NULL DEFAULT '0', + `rewardMailTemplateId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardMailDelay` int(11) unsigned NOT NULL DEFAULT '0', + `rewardTitleId` tinyint(3) unsigned NOT NULL DEFAULT '0', + `rewardTalents` tinyint(3) unsigned NOT NULL DEFAULT '0', + `rewardArenaPoints` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardItemId1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardItemId2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardItemId3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardItemId4` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardItemCount1` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardItemCount2` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardItemCount3` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardItemCount4` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemId1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemId2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemId3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemId4` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemId5` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemId6` mediumint(8) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemCount1` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemCount2` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemCount3` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemCount4` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemCount5` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardChoiceItemCount6` smallint(5) unsigned NOT NULL DEFAULT '0', + `rewardFactionId1` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId2` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId3` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId4` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionId5` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'faction id from Faction.dbc in this case', + `rewardFactionValue1` mediumint(8) NOT NULL DEFAULT '0', + `rewardFactionValue2` mediumint(8) NOT NULL DEFAULT '0', + `rewardFactionValue3` mediumint(8) NOT NULL DEFAULT '0', + `rewardFactionValue4` mediumint(8) NOT NULL DEFAULT '0', + `rewardFactionValue5` mediumint(8) NOT NULL DEFAULT '0', + `name_loc0` text, + `name_loc2` text, + `name_loc3` text, + `name_loc6` text, + `name_loc8` text, + `objectives_loc0` text, + `objectives_loc2` text, + `objectives_loc3` text, + `objectives_loc6` text, + `objectives_loc8` text, + `details_loc0` text, + `details_loc2` text, + `details_loc3` text, + `details_loc6` text, + `details_loc8` text, + `end_loc0` text, + `end_loc2` text, + `end_loc3` text, + `end_loc6` text, + `end_loc8` text, + `offerReward_loc0` text, + `offerReward_loc2` text, + `offerReward_loc3` text, + `offerReward_loc6` text, + `offerReward_loc8` text, + `requestItems_loc0` text, + `requestItems_loc2` text, + `requestItems_loc3` text, + `requestItems_loc6` text, + `requestItems_loc8` text, + `completed_loc0` text, + `completed_loc2` text, + `completed_loc3` text, + `completed_loc6` text, + `completed_loc8` text, + `reqNpcOrGo1` mediumint(8) NOT NULL DEFAULT '0', + `reqNpcOrGo2` mediumint(8) NOT NULL DEFAULT '0', + `reqNpcOrGo3` mediumint(8) NOT NULL DEFAULT '0', + `reqNpcOrGo4` mediumint(8) NOT NULL DEFAULT '0', + `reqNpcOrGoCount1` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqNpcOrGoCount2` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqNpcOrGoCount3` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqNpcOrGoCount4` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSourceItemId1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqSourceItemId2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqSourceItemId3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqSourceItemId4` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqSourceItemCount1` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSourceItemCount2` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSourceItemCount3` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSourceItemCount4` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqItemId1` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqItemId2` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqItemId3` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqItemId4` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqItemId5` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqItemId6` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqItemCount1` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqItemCount2` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqItemCount3` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqItemCount4` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqItemCount5` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqItemCount6` smallint(5) unsigned NOT NULL DEFAULT '0', + `objectiveText1_loc0` text, + `objectiveText1_loc2` text, + `objectiveText1_loc3` text, + `objectiveText1_loc6` text, + `objectiveText1_loc8` text, + `objectiveText2_loc0` text, + `objectiveText2_loc2` text, + `objectiveText2_loc3` text, + `objectiveText2_loc6` text, + `objectiveText2_loc8` text, + `objectiveText3_loc0` text, + `objectiveText3_loc2` text, + `objectiveText3_loc3` text, + `objectiveText3_loc6` text, + `objectiveText3_loc8` text, + `objectiveText4_loc0` text, + `objectiveText4_loc2` text, + `objectiveText4_loc3` text, + `objectiveText4_loc6` text, + `objectiveText4_loc8` text, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_quests_startend` +-- + +DROP TABLE IF EXISTS `aowow_quests_startend`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quests_startend` ( + `type` tinyint(4) unsigned NOT NULL, + `typeId` mediumint(9) unsigned NOT NULL, + `questId` mediumint(9) unsigned NOT NULL, + `method` tinyint(4) unsigned NOT NULL COMMENT '&0x1: starts; &0x2:ends', + `eventId` smallint(6) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`type`,`typeId`,`questId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_races` +-- + +DROP TABLE IF EXISTS `aowow_races`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_races` ( + `id` int(16) NOT NULL, + `classMask` bigint(20) NOT NULL, + `flags` bigint(20) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `factionId` bigint(20) NOT NULL, + `startAreaId` bigint(20) NOT NULL, + `leader` bigint(20) NOT NULL, + `baseLanguage` bigint(20) NOT NULL, + `side` int(3) NOT NULL, + `fileString` varchar(64) NOT NULL, + `name_loc0` varchar(64) NOT NULL, + `name_loc2` varchar(64) NOT NULL, + `name_loc3` varchar(64) NOT NULL, + `name_loc6` varchar(64) NOT NULL, + `name_loc8` varchar(64) NOT NULL, + `expansion` int(1) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_reports` +-- + +DROP TABLE IF EXISTS `aowow_reports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_reports` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `userId` mediumint(8) unsigned NOT NULL, + `assigned` mediumint(8) unsigned NOT NULL DEFAULT '0', + `status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '0:new; 1:solved; 2:rejected', + `mode` tinyint(3) unsigned NOT NULL, + `reason` tinyint(3) unsigned NOT NULL, + `subject` mediumint(9) NOT NULL DEFAULT '0', + `ip` varchar(50) NOT NULL, + `description` text NOT NULL, + `userAgent` varchar(255) NOT NULL, + `appName` varchar(32) NOT NULL, + `url` varchar(255) NOT NULL, + `relatedUrl` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `userId` (`userId`) +) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_scalingstatdistribution` +-- + +DROP TABLE IF EXISTS `aowow_scalingstatdistribution`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_scalingstatdistribution` ( + `id` smallint(5) unsigned NOT NULL, + `statMod1` tinyint(4) NOT NULL, + `statMod2` tinyint(4) NOT NULL, + `statMod3` tinyint(4) NOT NULL, + `statMod4` tinyint(4) NOT NULL, + `statMod5` tinyint(4) NOT NULL, + `statMod6` tinyint(4) NOT NULL, + `statMod7` tinyint(4) NOT NULL, + `statMod8` tinyint(4) NOT NULL, + `statMod9` tinyint(4) NOT NULL, + `statMod10` tinyint(4) NOT NULL, + `modifier1` smallint(5) unsigned NOT NULL, + `modifier2` smallint(5) unsigned NOT NULL, + `modifier3` smallint(5) unsigned NOT NULL, + `modifier4` smallint(5) unsigned NOT NULL, + `modifier5` smallint(5) unsigned NOT NULL, + `modifier6` smallint(5) unsigned NOT NULL, + `modifier7` smallint(5) unsigned NOT NULL, + `modifier8` smallint(5) unsigned NOT NULL, + `modifier9` smallint(5) unsigned NOT NULL, + `modifier10` smallint(5) unsigned NOT NULL, + `maxLevel` tinyint(3) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_scalingstatvalues` +-- + +DROP TABLE IF EXISTS `aowow_scalingstatvalues`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_scalingstatvalues` ( + `id` tinyint(3) unsigned NOT NULL, + `charLevel` tinyint(3) unsigned NOT NULL, + `shoulderMultiplier` tinyint(3) unsigned NOT NULL, + `trinketMultiplier` tinyint(3) unsigned NOT NULL, + `weaponMultiplier` tinyint(3) unsigned NOT NULL, + `rangedMultiplier` tinyint(3) unsigned NOT NULL, + `clothShoulderArmor` tinyint(3) unsigned NOT NULL, + `leatherShoulderArmor` smallint(5) unsigned NOT NULL, + `mailShoulderArmor` smallint(5) unsigned NOT NULL, + `plateShoulderArmor` smallint(5) unsigned NOT NULL, + `weaponDPS1H` tinyint(3) unsigned NOT NULL, + `weaponDPS2H` tinyint(3) unsigned NOT NULL, + `casterDPS1H` tinyint(3) unsigned NOT NULL, + `casterDPS2H` tinyint(3) unsigned NOT NULL, + `rangedDPS` tinyint(3) unsigned NOT NULL, + `wandDPS` tinyint(3) unsigned NOT NULL, + `spellPower` smallint(5) unsigned NOT NULL, + `primBudged` tinyint(3) unsigned NOT NULL, + `tertBudged` tinyint(3) unsigned NOT NULL, + `clothCloakArmor` tinyint(3) unsigned NOT NULL, + `clothChestArmor` smallint(5) unsigned NOT NULL, + `leatherChestArmor` smallint(5) unsigned NOT NULL, + `mailChestArmor` smallint(5) unsigned NOT NULL, + `plateChestArmor` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_screenshots` +-- + +DROP TABLE IF EXISTS `aowow_screenshots`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_screenshots` ( + `id` int(16) unsigned NOT NULL AUTO_INCREMENT, + `type` tinyint(4) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, + `uploader` int(16) unsigned NOT NULL, + `date` int(32) unsigned NOT NULL, + `width` smallint(5) unsigned NOT NULL, + `height` smallint(5) unsigned NOT NULL, + `caption` varchar(250) DEFAULT NULL, + `status` tinyint(3) unsigned NOT NULL COMMENT 'see defines.php - CC_FLAG_*', + `approvedBy` int(16) unsigned DEFAULT NULL, + `deletedBy` int(16) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `type` (`type`,`typeId`) +) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_shapeshiftforms` +-- + +DROP TABLE IF EXISTS `aowow_shapeshiftforms`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_shapeshiftforms` ( + `Id` bigint(20) NOT NULL, + `name_loc0` text NOT NULL, + `name_loc2` text NOT NULL, + `name_loc3` text NOT NULL, + `name_loc6` text NOT NULL, + `name_loc8` text NOT NULL, + `flags` bigint(20) NOT NULL, + `creatureType` bigint(20) NOT NULL, + `displayIdA` bigint(20) NOT NULL, + `displayIdH` bigint(20) NOT NULL, + `spellId1` bigint(20) NOT NULL, + `spellId2` bigint(20) NOT NULL, + `spellId3` bigint(20) NOT NULL, + `spellId4` bigint(20) NOT NULL, + `spellId5` bigint(20) NOT NULL, + `spellId6` bigint(20) NOT NULL, + `spellId7` bigint(20) NOT NULL, + `spellId8` bigint(20) NOT NULL, + PRIMARY KEY (`Id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_skillline` +-- + +DROP TABLE IF EXISTS `aowow_skillline`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_skillline` ( + `Id` smallint(5) unsigned NOT NULL, + `typeCat` tinyint(4) NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `categoryId` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(64) NOT NULL, + `name_loc2` varchar(64) NOT NULL, + `name_loc3` varchar(64) NOT NULL, + `name_loc6` varchar(64) NOT NULL, + `name_loc8` varchar(64) NOT NULL, + `description_loc0` text NOT NULL, + `description_loc2` text NOT NULL, + `description_loc3` text NOT NULL, + `description_loc6` text NOT NULL, + `description_loc8` text NOT NULL, + `iconId` smallint(5) unsigned NOT NULL, + `professionMask` smallint(5) unsigned NOT NULL, + `recipeSubClass` tinyint(3) unsigned NOT NULL, + `specializations` varchar(30) NOT NULL COMMENT 'space-separated spellIds', + PRIMARY KEY (`Id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_source` +-- + +DROP TABLE IF EXISTS `aowow_source`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_source` ( + `type` tinyint(4) unsigned NOT NULL, + `typeId` mediumint(9) unsigned NOT NULL, + `moreType` tinyint(4) unsigned DEFAULT NULL, + `moreTypeId` mediumint(9) unsigned DEFAULT NULL, + `moreZoneId` mediumint(9) unsigned DEFAULT NULL, + `src1` tinyint(1) unsigned DEFAULT NULL COMMENT 'Crafted', + `src2` tinyint(3) unsigned DEFAULT NULL COMMENT 'Drop (npc / object / item) (modeMask)', + `src3` tinyint(3) unsigned DEFAULT NULL COMMENT 'PvP (g_sources_pvp)', + `src4` tinyint(3) unsigned DEFAULT NULL COMMENT 'Quest (side)', + `src5` tinyint(1) unsigned DEFAULT NULL COMMENT 'Vendor', + `src6` tinyint(1) unsigned DEFAULT NULL COMMENT 'Trainer', + `src7` tinyint(1) unsigned DEFAULT NULL COMMENT 'Discovery', + `src8` tinyint(1) unsigned DEFAULT NULL COMMENT 'Redemption', + `src9` tinyint(1) unsigned DEFAULT NULL COMMENT 'Talent', + `src10` tinyint(1) unsigned DEFAULT NULL COMMENT 'Starter', + `src11` tinyint(1) unsigned DEFAULT NULL COMMENT 'Event (special; not holidays) [not used]', + `src12` tinyint(1) unsigned DEFAULT NULL COMMENT 'Achievemement', + `src13` tinyint(3) unsigned DEFAULT NULL COMMENT 'Misc Source (sourceStringId)', + `src14` tinyint(1) unsigned DEFAULT NULL COMMENT 'Black Market [not used]', + `src15` tinyint(1) unsigned DEFAULT NULL COMMENT 'Disenchanted', + `src16` tinyint(1) unsigned DEFAULT NULL COMMENT 'Fished', + `src17` tinyint(1) unsigned DEFAULT NULL COMMENT 'Gathered', + `src18` tinyint(1) unsigned DEFAULT NULL COMMENT 'Milled', + `src19` tinyint(1) unsigned DEFAULT NULL COMMENT 'Mined', + `src20` tinyint(1) unsigned DEFAULT NULL COMMENT 'Prospected', + `src21` tinyint(1) unsigned DEFAULT NULL COMMENT 'Pickpocketed', + `src22` tinyint(1) unsigned DEFAULT NULL COMMENT 'Salvaged', + `src23` tinyint(1) unsigned DEFAULT NULL COMMENT 'Skinned', + `src24` tinyint(1) unsigned DEFAULT NULL COMMENT 'In-Game Store [not used]', + PRIMARY KEY (`type`,`typeId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_sourcestrings` +-- + +DROP TABLE IF EXISTS `aowow_sourcestrings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_sourcestrings` ( + `id` int(16) NOT NULL, + `source_loc0` varchar(128) NOT NULL, + `source_loc2` varchar(128) NOT NULL, + `source_loc3` varchar(128) NOT NULL, + `source_loc6` varchar(128) NOT NULL, + `source_loc8` varchar(128) NOT NULL, + PRIMARY KEY (`id`), + KEY `Id` (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spawns` +-- + +DROP TABLE IF EXISTS `aowow_spawns`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spawns` ( + `guid` int(11) NOT NULL COMMENT '< 0: vehicle accessory', + `type` tinyint(3) unsigned NOT NULL, + `typeId` int(10) unsigned NOT NULL, + `respawn` int(10) unsigned NOT NULL COMMENT 'in seconds', + `spawnMask` tinyint(3) unsigned NOT NULL, + `phaseMask` smallint(5) unsigned NOT NULL, + `areaId` smallint(5) unsigned NOT NULL, + `floor` tinyint(3) unsigned NOT NULL, + `posX` float unsigned NOT NULL, + `posY` float unsigned NOT NULL, + `pathId` int(10) unsigned NOT NULL, + PRIMARY KEY (`guid`,`type`,`floor`), + KEY `type_idx` (`typeId`,`type`), + KEY `zone_idx` (`areaId`), + KEY `guid` (`guid`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spell` +-- + +DROP TABLE IF EXISTS `aowow_spell`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spell` ( + `id` mediumint(8) unsigned NOT NULL, + `category` smallint(5) unsigned NOT NULL, + `dispelType` tinyint(3) unsigned NOT NULL, + `mechanic` tinyint(3) unsigned NOT NULL, + `attributes0` int(10) unsigned NOT NULL, + `attributes1` int(10) unsigned NOT NULL, + `attributes2` int(10) unsigned NOT NULL, + `attributes3` int(10) unsigned NOT NULL, + `attributes4` int(10) unsigned NOT NULL, + `attributes5` int(10) unsigned NOT NULL, + `attributes6` int(10) unsigned NOT NULL, + `attributes7` int(10) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `typeCat` smallint(6) NOT NULL, + `stanceMask` int(10) unsigned NOT NULL, + `stanceMaskNot` int(10) unsigned NOT NULL, + `spellFocusObject` smallint(5) unsigned NOT NULL, + `castTime` mediumint(8) unsigned NOT NULL, + `recoveryTime` int(10) unsigned NOT NULL, + `recoveryCategory` int(10) unsigned NOT NULL, + `startRecoveryTime` mediumint(8) unsigned NOT NULL, + `startRecoveryCategory` smallint(5) unsigned NOT NULL, + `procChance` tinyint(3) unsigned NOT NULL, + `procCharges` tinyint(3) unsigned NOT NULL, + `procCustom` float NOT NULL, + `procCooldown` smallint(6) unsigned NOT NULL, + `maxLevel` tinyint(3) unsigned NOT NULL, + `baseLevel` tinyint(3) unsigned NOT NULL, + `spellLevel` tinyint(3) unsigned NOT NULL, + `talentLevel` tinyint(3) unsigned NOT NULL, + `duration` int(16) NOT NULL DEFAULT '0', + `powerType` tinyint(4) NOT NULL, + `powerCost` smallint(5) unsigned NOT NULL, + `powerCostPerLevel` tinyint(3) unsigned NOT NULL, + `powerCostPercent` tinyint(3) unsigned NOT NULL, + `powerPerSecond` smallint(5) unsigned NOT NULL, + `powerPerSecondPerLevel` tinyint(3) unsigned NOT NULL, + `powerGainRunicPower` smallint(5) unsigned NOT NULL, + `powerCostRunes` smallint(5) unsigned NOT NULL, + `rangeId` smallint(5) unsigned NOT NULL, + `stackAmount` smallint(5) unsigned NOT NULL, + `tool1` mediumint(8) unsigned NOT NULL, + `tool2` mediumint(8) unsigned NOT NULL, + `toolCategory1` tinyint(3) unsigned NOT NULL, + `toolCategory2` tinyint(3) unsigned NOT NULL, + `reagent1` mediumint(8) unsigned NOT NULL, + `reagent2` mediumint(8) unsigned NOT NULL, + `reagent3` mediumint(8) unsigned NOT NULL, + `reagent4` mediumint(8) unsigned NOT NULL, + `reagent5` mediumint(8) unsigned NOT NULL, + `reagent6` mediumint(8) unsigned NOT NULL, + `reagent7` mediumint(8) unsigned NOT NULL, + `reagent8` mediumint(8) unsigned NOT NULL, + `reagentCount1` tinyint(3) unsigned NOT NULL, + `reagentCount2` tinyint(3) unsigned NOT NULL, + `reagentCount3` tinyint(3) unsigned NOT NULL, + `reagentCount4` tinyint(3) unsigned NOT NULL, + `reagentCount5` tinyint(3) unsigned NOT NULL, + `reagentCount6` tinyint(3) unsigned NOT NULL, + `reagentCount7` tinyint(3) unsigned NOT NULL, + `reagentCount8` tinyint(3) unsigned NOT NULL, + `equippedItemClass` tinyint(4) NOT NULL, + `equippedItemSubClassMask` int(11) NOT NULL, + `equippedItemInventoryTypeMask` int(10) unsigned NOT NULL, + `effect1Id` smallint(5) unsigned NOT NULL, + `effect2Id` smallint(5) unsigned NOT NULL, + `effect3Id` smallint(5) unsigned NOT NULL, + `effect1DieSides` mediumint(9) NOT NULL, + `effect2DieSides` mediumint(9) NOT NULL, + `effect3DieSides` mediumint(9) NOT NULL, + `effect1RealPointsPerLevel` float NOT NULL, + `effect2RealPointsPerLevel` float NOT NULL, + `effect3RealPointsPerLevel` float NOT NULL, + `effect1BasePoints` int(11) NOT NULL, + `effect2BasePoints` int(11) NOT NULL, + `effect3BasePoints` int(11) NOT NULL, + `effect1Mechanic` tinyint(3) unsigned NOT NULL, + `effect2Mechanic` tinyint(3) unsigned NOT NULL, + `effect3Mechanic` tinyint(3) unsigned NOT NULL, + `effect1ImplicitTargetA` smallint(6) NOT NULL, + `effect2ImplicitTargetA` smallint(6) NOT NULL, + `effect3ImplicitTargetA` smallint(6) NOT NULL, + `effect1ImplicitTargetB` smallint(6) NOT NULL, + `effect2ImplicitTargetB` smallint(6) NOT NULL, + `effect3ImplicitTargetB` smallint(6) NOT NULL, + `effect1RadiusMin` smallint(5) unsigned NOT NULL, + `effect1RadiusMax` smallint(5) unsigned NOT NULL DEFAULT '0', + `effect2RadiusMin` smallint(5) unsigned NOT NULL, + `effect2RadiusMax` smallint(5) unsigned NOT NULL DEFAULT '0', + `effect3RadiusMin` smallint(5) unsigned NOT NULL, + `effect3RadiusMax` smallint(5) unsigned NOT NULL DEFAULT '0', + `effect1AuraId` smallint(5) unsigned NOT NULL, + `effect2AuraId` smallint(5) unsigned NOT NULL, + `effect3AuraId` smallint(5) unsigned NOT NULL, + `effect1Periode` mediumint(8) unsigned NOT NULL, + `effect2Periode` mediumint(8) unsigned NOT NULL, + `effect3Periode` mediumint(8) unsigned NOT NULL, + `effect1ValueMultiplier` float NOT NULL, + `effect2ValueMultiplier` float NOT NULL, + `effect3ValueMultiplier` float NOT NULL, + `effect1ChainTarget` smallint(5) unsigned NOT NULL, + `effect2ChainTarget` smallint(5) unsigned NOT NULL, + `effect3ChainTarget` smallint(5) unsigned NOT NULL, + `effect1CreateItemId` mediumint(8) unsigned NOT NULL, + `effect2CreateItemId` mediumint(8) unsigned NOT NULL, + `effect3CreateItemId` mediumint(8) unsigned NOT NULL, + `effect1MiscValue` int(11) NOT NULL, + `effect2MiscValue` int(11) NOT NULL, + `effect3MiscValue` int(11) NOT NULL, + `effect1MiscValueB` mediumint(9) NOT NULL, + `effect2MiscValueB` mediumint(9) NOT NULL, + `effect3MiscValueB` mediumint(9) NOT NULL, + `effect1TriggerSpell` mediumint(9) NOT NULL, + `effect2TriggerSpell` mediumint(9) NOT NULL, + `effect3TriggerSpell` mediumint(9) NOT NULL, + `effect1PointsPerComboPoint` mediumint(9) NOT NULL, + `effect2PointsPerComboPoint` mediumint(9) NOT NULL, + `effect3PointsPerComboPoint` mediumint(9) NOT NULL, + `effect1SpellClassMaskA` int(10) unsigned NOT NULL, + `effect2SpellClassMaskA` int(10) unsigned NOT NULL, + `effect3SpellClassMaskA` int(10) unsigned NOT NULL, + `effect1SpellClassMaskB` int(10) unsigned NOT NULL, + `effect2SpellClassMaskB` int(10) unsigned NOT NULL, + `effect3SpellClassMaskB` int(10) unsigned NOT NULL, + `effect1SpellClassMaskC` int(10) unsigned NOT NULL, + `effect2SpellClassMaskC` int(10) unsigned NOT NULL, + `effect3SpellClassMaskC` int(10) unsigned NOT NULL, + `effect1DamageMultiplier` float NOT NULL, + `effect2DamageMultiplier` float NOT NULL, + `effect3DamageMultiplier` float NOT NULL, + `effect1BonusMultiplier` float NOT NULL, + `effect2BonusMultiplier` float NOT NULL, + `effect3BonusMultiplier` float NOT NULL, + `iconId` smallint(5) unsigned NOT NULL, + `iconIdAlt` mediumint(9) NOT NULL, + `rankNo` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(85) NOT NULL, + `name_loc2` varchar(85) NOT NULL, + `name_loc3` varchar(85) NOT NULL, + `name_loc6` varchar(91) NOT NULL, + `name_loc8` varchar(50) NOT NULL, + `rank_loc0` varchar(21) NOT NULL, + `rank_loc2` varchar(24) NOT NULL, + `rank_loc3` varchar(22) NOT NULL, + `rank_loc6` varchar(27) NOT NULL, + `rank_loc8` varchar(29) NOT NULL, + `description_loc0` text NOT NULL, + `description_loc2` text NOT NULL, + `description_loc3` text NOT NULL, + `description_loc6` text NOT NULL, + `description_loc8` text NOT NULL, + `buff_loc0` text NOT NULL, + `buff_loc2` text NOT NULL, + `buff_loc3` text NOT NULL, + `buff_loc6` text NOT NULL, + `buff_loc8` text NOT NULL, + `maxTargetLevel` tinyint(3) unsigned NOT NULL, + `spellFamilyId` tinyint(3) unsigned NOT NULL, + `spellFamilyFlags1` int(10) unsigned NOT NULL, + `spellFamilyFlags2` int(10) unsigned NOT NULL, + `spellFamilyFlags3` int(10) unsigned NOT NULL, + `maxAffectedTargets` tinyint(3) unsigned NOT NULL, + `damageClass` tinyint(3) unsigned NOT NULL, + `skillLine1` smallint(6) NOT NULL DEFAULT '0', + `skillLine2OrMask` bigint(32) NOT NULL DEFAULT '0', + `reqRaceMask` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqClassMask` smallint(5) unsigned NOT NULL DEFAULT '0', + `reqSpellId` mediumint(8) unsigned NOT NULL DEFAULT '0', + `reqSkillLevel` smallint(5) unsigned NOT NULL DEFAULT '0', + `learnedAt` smallint(6) unsigned NOT NULL DEFAULT '0', + `skillLevelGrey` smallint(6) unsigned NOT NULL DEFAULT '0', + `skillLevelYellow` smallint(6) unsigned NOT NULL DEFAULT '0', + `schoolMask` tinyint(3) unsigned NOT NULL, + `spellDescriptionVariableId` tinyint(3) unsigned NOT NULL, + `trainingCost` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `category` (`typeCat`), + KEY `spell` (`id`) USING BTREE, + KEY `effects` (`effect1Id`,`effect2Id`,`effect3Id`), + KEY `items` (`effect1CreateItemId`,`effect2CreateItemId`,`effect3CreateItemId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spelldifficulty` +-- + +DROP TABLE IF EXISTS `aowow_spelldifficulty`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spelldifficulty` ( + `normal10` mediumint(8) unsigned NOT NULL, + `normal25` mediumint(8) unsigned NOT NULL, + `heroic10` mediumint(8) unsigned NOT NULL, + `heroic25` mediumint(8) unsigned NOT NULL, + KEY `normal10` (`normal10`), + KEY `normal25` (`normal25`), + KEY `heroic10` (`heroic10`), + KEY `heroic25` (`heroic25`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spellfocusobject` +-- + +DROP TABLE IF EXISTS `aowow_spellfocusobject`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spellfocusobject` ( + `Id` smallint(5) unsigned NOT NULL, + `name_loc0` varchar(83) NOT NULL, + `name_loc2` varchar(89) NOT NULL, + `name_loc3` varchar(95) NOT NULL, + `name_loc6` varchar(90) NOT NULL, + `name_loc8` varchar(91) NOT NULL, + PRIMARY KEY (`Id`), + KEY `Id` (`Id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='merge into gameobject..?'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spellrange` +-- + +DROP TABLE IF EXISTS `aowow_spellrange`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spellrange` ( + `id` tinyint(3) unsigned NOT NULL, + `rangeMinHostile` tinyint(3) unsigned NOT NULL, + `rangeMinFriend` tinyint(3) unsigned NOT NULL, + `rangeMaxHostile` smallint(5) unsigned NOT NULL, + `rangeMaxFriend` smallint(5) unsigned NOT NULL, + `rangeType` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(27) NOT NULL, + `name_loc2` varchar(27) NOT NULL, + `name_loc3` varchar(27) NOT NULL, + `name_loc6` varchar(27) NOT NULL, + `name_loc8` varchar(27) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spellvariables` +-- + +DROP TABLE IF EXISTS `aowow_spellvariables`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spellvariables` ( + `id` tinyint(3) unsigned NOT NULL, + `vars` varchar(368) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_taxinodes` +-- + +DROP TABLE IF EXISTS `aowow_taxinodes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_taxinodes` ( + `id` smallint(5) unsigned NOT NULL, + `mapId` smallint(6) unsigned NOT NULL, + `posX` float unsigned NOT NULL, + `posY` float unsigned NOT NULL, + `type` tinyint(4) unsigned NOT NULL COMMENT 'usually NPC (1) but could support GOs (2)', + `typeId` mediumint(9) unsigned NOT NULL, + `reactA` tinyint(4) NOT NULL, + `reactH` tinyint(4) NOT NULL, + `name_loc0` varchar(46) NOT NULL, + `name_loc2` varchar(62) NOT NULL, + `name_loc3` varchar(55) NOT NULL, + `name_loc6` varchar(63) NOT NULL, + `name_loc8` varchar(50) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_taxipath` +-- + +DROP TABLE IF EXISTS `aowow_taxipath`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_taxipath` ( + `id` smallint(5) unsigned NOT NULL, + `startNodeId` smallint(6) unsigned NOT NULL, + `endNodeId` smallint(6) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_titles` +-- + +DROP TABLE IF EXISTS `aowow_titles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_titles` ( + `id` tinyint(3) unsigned NOT NULL, + `category` tinyint(3) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `gender` tinyint(3) unsigned NOT NULL, + `side` tinyint(3) unsigned NOT NULL, + `expansion` tinyint(3) unsigned NOT NULL, + `src12Ext` mediumint(9) unsigned NOT NULL, + `holidayId` smallint(5) unsigned NOT NULL, + `male_loc0` varchar(33) NOT NULL, + `male_loc2` varchar(35) NOT NULL, + `male_loc3` varchar(37) NOT NULL, + `male_loc6` varchar(34) NOT NULL, + `male_loc8` varchar(37) NOT NULL, + `female_loc0` varchar(33) NOT NULL, + `female_loc2` varchar(35) NOT NULL, + `female_loc3` varchar(39) NOT NULL, + `female_loc6` varchar(35) NOT NULL, + `female_loc8` varchar(41) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_totemcategory` +-- + +DROP TABLE IF EXISTS `aowow_totemcategory`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_totemcategory` ( + `id` tinyint(3) unsigned NOT NULL, + `name_loc0` varchar(29) NOT NULL, + `name_loc2` varchar(45) NOT NULL, + `name_loc3` varchar(31) NOT NULL, + `name_loc6` varchar(36) NOT NULL, + `name_loc8` varchar(69) NOT NULL, + `category` tinyint(3) unsigned NOT NULL, + `categoryMask` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_videos` +-- + +DROP TABLE IF EXISTS `aowow_videos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_videos` ( + `id` int(16) NOT NULL, + `type` int(8) NOT NULL, + `typeId` int(16) NOT NULL, + `uploader` int(16) NOT NULL, + `date` int(32) NOT NULL, + `videoId` varchar(12) NOT NULL, + `caption` text, + `status` int(8) NOT NULL, + `approvedBy` int(16) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `type` (`type`,`typeId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_zones` +-- + +DROP TABLE IF EXISTS `aowow_zones`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_zones` ( + `id` smallint(5) unsigned NOT NULL COMMENT 'Zone Id', + `mapId` smallint(5) unsigned NOT NULL COMMENT 'Map Identifier', + `mapIdBak` smallint(5) unsigned NOT NULL, + `parentArea` smallint(6) unsigned NOT NULL, + `category` tinyint(4) unsigned NOT NULL, + `flags` int(11) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `faction` tinyint(2) unsigned NOT NULL, + `expansion` tinyint(2) unsigned NOT NULL, + `type` tinyint(2) unsigned NOT NULL, + `maxPlayer` tinyint(4) NOT NULL, + `itemLevelReqN` smallint(5) unsigned NOT NULL, + `itemLevelReqH` smallint(5) unsigned NOT NULL, + `levelReq` tinyint(3) unsigned NOT NULL, + `levelReqLFG` tinyint(4) unsigned NOT NULL, + `levelHeroic` tinyint(3) unsigned NOT NULL, + `levelMin` tinyint(4) unsigned NOT NULL, + `levelMax` tinyint(4) unsigned NOT NULL, + `attunementsN` text NOT NULL COMMENT 'space separated; type:typeId', + `attunementsH` text NOT NULL COMMENT 'space separated; type:typeId', + `parentAreaId` smallint(5) unsigned NOT NULL, + `parentX` float NOT NULL, + `parentY` float NOT NULL, + `name_loc0` varchar(120) NOT NULL COMMENT 'Map Name', + `name_loc2` varchar(120) NOT NULL, + `name_loc3` varchar(120) NOT NULL, + `name_loc6` varchar(120) NOT NULL, + `name_loc8` varchar(120) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2015-05-10 14:03:11 +-- MySQL dump 10.13 Distrib 5.5.30, for Linux (x86_64) +-- +-- Host: localhost Database: sarjuuk_aowow +-- ------------------------------------------------------ +-- Server version 5.5.30-30.1 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Dumping data for table `aowow_announcements` +-- + +LOCK TABLES `aowow_announcements` WRITE; +/*!40000 ALTER TABLE `aowow_announcements` DISABLE KEYS */; +INSERT INTO `aowow_announcements` VALUES (4,'compare','Help: Item Comparison Tool',0,'padding-left: 55px; background-image: url(STATIC_URL/images/announcements/help-small.png); background-position: 10px center',1,1,'First time? - Don\'t be shy! Just check out our [url=?help=item-comparison]Help page[/url]!','Première visite? - Ne soyez pas intimidé! Vous n\'avez qu\'à lire notre [url=?help=item-comparison]page d\'aide[/url] !','Euer erstes Mal? Nur keine falsche Scheu! Schaut einfach auf unsere [url=?help=item-comparison]Hilfeseite[/url]!','¿Tu primera vez? ¡No seas vergonzoso! !Mira nuestra [url=?help=item-comparison]página de ayuda[/url]!','Впервые? Ðе ÑтеÑнÑйтеÑÑŒ поÑетить нашу [url=?help=item-comparison]Ñправочную Ñтраницу[/url]!'),(3,'profile','Quick Help: Profiler',0,'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center',1,1,'[h3]First Time?[/h3]\n\nThe [b]Profiler[/b] lets you [span class=tip title=\"e.g. See how\'d you look as a different race!\"]edit your character[/span], find gear upgrades, check your gear score, and more!\n\n[ul]\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=?user]user page[/url].[/li]\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\n[/ul]\n\nFor more information, check out our extensive [b][url=?help=profiler]help page[/url][/b]!','','','',''),(2,'profiler','Help: Profiler',0,'padding-left: 80px; background-image: url(STATIC_URL/images/announcements/help-large.gif); background-position: 10px center',1,1,'[h3]First Time?[/h3]\r\n\r\nThe [b]Profiler[/b] tool lets you [span class=tip title=\"e.g. See how\'d you look as a different race, try different gear or talents, and more!\"]edit your character[/span], find gear upgrades, check your gear score, and more!\r\n\r\n[ul]\r\n[li][b]Right-click[/b] slots to change items, add gems/enchants, or find upgrades.[/li]\r\n[li]Use the [b]Claim character[/b] button to add your own characters to your [url=/?user]user page[/url].[/li]\r\n[li]Save a modified character to your Aowow account by using the [b]Save as[/b] button.[/li]\r\n[li][b]Statistics[/b] will update in real time as you make tweaks.[/li]\r\n[/ul]\r\n\r\nFor more information, check out our extensive [url=?help=profiler]help page[/url]!','','','',''); +/*!40000 ALTER TABLE `aowow_announcements` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_articles` +-- + +LOCK TABLES `aowow_articles` WRITE; +/*!40000 ALTER TABLE `aowow_articles` DISABLE KEYS */; +INSERT INTO `aowow_articles` VALUES (13,4,0,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]',NULL),(14,1,0,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.',NULL),(13,1,0,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]',NULL),(13,2,0,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]',NULL),(14,2,0,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.',NULL),(13,3,0,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]',NULL),(13,5,0,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]',NULL),(13,6,0,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]',NULL),(13,7,0,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]',NULL),(13,8,0,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]',NULL),(13,9,0,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]',NULL),(13,11,0,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]',NULL),(14,3,0,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.',NULL),(14,4,0,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.',NULL),(14,5,0,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.',NULL),(14,6,0,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.',NULL),(14,7,0,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.',NULL),(14,8,0,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.',NULL),(14,10,0,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.',NULL),(14,11,0,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.',NULL),(8,21,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=301], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.',NULL),(8,47,0,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.',NULL),(8,54,0,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.',NULL),(8,59,0,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]',NULL),(8,68,0,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.',NULL),(8,69,0,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.',NULL),(8,70,0,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.',NULL),(8,72,0,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].',NULL),(8,76,0,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.',NULL),(8,81,0,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.',NULL),(8,87,0,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].',NULL),(8,92,0,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.',NULL),(8,93,0,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.',NULL),(8,270,0,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.',NULL),(8,349,0,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].',NULL),(8,369,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.',NULL),(8,470,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.',NULL),(8,471,0,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.',NULL),(8,509,0,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].',NULL),(8,510,0,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].',NULL),(8,529,0,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.',NULL),(8,530,0,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].',NULL),(8,576,0,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]',NULL),(-13,0,0,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,5,0,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,3,0,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,2,0,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(-13,6,0,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(-13,4,0,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(-13,1,0,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(-10,0,0,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-16,0,0,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(-8,0,0,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(-99,0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(-3,0,0,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(-7,0,0,'[small]this page for example[/small]',NULL),(-1,0,0,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(-99,0,3,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(-99,0,6,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(-99,0,0,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(-13,7,0,'Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: www (default), en, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=341] -> [event=341]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL); +/*!40000 ALTER TABLE `aowow_articles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_config` +-- + +LOCK TABLES `aowow_config` WRITE; +/*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; +INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',129,'default: 500 - max results for search'),('sql_limit_default','300',129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',129,'default: 10 - max results for suggestions'),('sql_limit_none','0',129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',129,'default: 60 - time to live for RSS (in seconds)'),('cache_decay','25200',129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('session_timeout_delay','3600',129,'default: 60 * 60 - non-permanent session times out in time() + X'),('failed_auth_exclusion','900',129,'default: 15 * 60 - how long an account is closed after exceeding failed_auth_count (in seconds)'),('failed_auth_count','5',129,'default: 5 - how often invalid passwords are tolerated'),('name','Aowow Database Viewer (ADV)',136,' - website title'),('name_short','Aowow',136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('allow_register','1',132,'default: 1 - allow/disallow account creation (requires auth_mode 0)'),('debug','0',132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',132,'default: 0 - display brb gnomes and block access for non-staff'),('auth_mode','0',145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('rep_req_upvote','125',129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',129,'default: 100 - activated an account'),('rep_reward_upvoted','5',129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',129,'default: -200 - moderator revoked rights'),('user_max_votes','50',129,'default: 50 - vote limit per day'),('rep_req_votemore_add','250',129,'default: 250 - required reputation per additional vote past threshold'),('force_ssl','0',132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('cache_mode','1',161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('locales','333',161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('account_create_save_decay','604800',129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('account_recovery_decay','300',129,'default: 300 - time to recover your account and new recovery requests are blocked'),('serialize_precision','4',65,' - some derelict code, probably unused'),('screenshot_min_size','200',129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',136,' - points js to executable files (automaticly set on first run)'),('static_host','',136,' - points js to images & scripts (automaticly set on first run)'); +/*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_news` +-- + +LOCK TABLES `aowow_news` WRITE; +/*!40000 ALTER TABLE `aowow_news` DISABLE KEYS */; +INSERT INTO `aowow_news` VALUES (1,1,0,'','[pad]Welcome to [b][span class=q5]AoWoW[/span][/b]!','[pad]Bienvenue à [b][span class=q5]AoWoW[/span][/b]!','[pad]Willkommen bei [b][span class=q5]AoWoW[/span][/b]!','','Добро[pad] пожаловать на [b][span class=q5]AoWoW[/span][/b]!'),(2,0,1,'STATIC_URL/images/logos/newsbox-explained.png','[ul]\n[li][i]just demoing the newsbox here..[/i][/li]\n[li][b][url=http://www.example.com]..with urls[/url][/b][/li]\n[li][b]..typeLinks [item=45533][/b][/li]\n[li][b]..also, over there to the right is an overlay-trigger =>[/b][/li]\n[/ul]\n\n[ul]\n[li][tooltip name=demotip]hey, it hints you stuff![/tooltip][b][span class=tip tooltip=demotip]..hover me[/span][/b][/li]\n[/ul]','','','',''); +/*!40000 ALTER TABLE `aowow_news` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_news_overlay` +-- + +LOCK TABLES `aowow_news_overlay` WRITE; +/*!40000 ALTER TABLE `aowow_news_overlay` DISABLE KEYS */; +INSERT INTO `aowow_news_overlay` VALUES (2,405,100,'http://example.com','example overlay','','','',''); +/*!40000 ALTER TABLE `aowow_news_overlay` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_sourcestrings` +-- + +LOCK TABLES `aowow_sourcestrings` WRITE; +/*!40000 ALTER TABLE `aowow_sourcestrings` DISABLE KEYS */; +INSERT INTO `aowow_sourcestrings` VALUES (1,'Arena Season 1','Saison 1 des combats d\'arène','Arenasaison 1','Temporada de arena 1','Сезон арены 1'),(2,'Arena Season 2','Saison 2 des combats d\'arène','Arenasaison 2','Temporada de arena 2','Сезон арены 2'),(3,'Arena Season 3','Saison 3 des combats d\'arène','Arenasaison 3','Temporada de arena 3','Сезон арены 3'),(4,'Arena Season 4','Saison 4 des combats d\'arène','Arenasaison 4','Temporada de arena 4','Сезон арены 4'),(5,'Arena Season 5','Saison 5 des combats d\'arène','Arenasaison 5','Temporada de arena 5','Сезон арены 5'),(6,'Arena Season 6','Saison 6 des combats d\'arène','Arenasaison 6','Temporada de arena 6','Сезон арены 6'),(7,'Arena Season 7','Saison 7 des combats d\'arène','Arenasaison 7','Temporada de arena 7','Сезон арены 7'),(8,'Arena Season 8','Saison 8 des combats d\'arène','Arenasaison 8','Temporada de arena 8','Сезон арены 8'),(9,'2009 Arena Tournament','Tournoi 2009 des combats d\'arène','2009 Arena-Turnier','Torneo de arena 2009','Турнир арены 2009'); +/*!40000 ALTER TABLE `aowow_sourcestrings` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2015-05-10 14:11:18 From 2f767ba835744463dfad9e80acc0c0e67a46d8a9 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 28 Jun 2015 19:23:26 +0200 Subject: [PATCH 0007/1249] misc fixes --- localization/locale_enus.php | 2 +- pages/quests.php | 2 +- template/pages/quest.tpl.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 9eb1ff48..69975291 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -484,7 +484,7 @@ $lang = array( 'raidFaction' => "Raid faction", 'boss' => "Final boss", 'reqLevels' => "Required levels: [tooltip=instancereqlevel_tip]%d[/tooltip], [tooltip=lfgreqlevel_tip]%d[/tooltip]", - 'zonePartOf' => "This zone is part of [zone=%].", + 'zonePartOf' => "This zone is part of [zone=%s].", 'autoRez' => "Automatic resurrection", 'city' => "City", 'territory' => "Territory", diff --git a/pages/quests.php b/pages/quests.php index 19c1dfb4..d982d5a1 100644 --- a/pages/quests.php +++ b/pages/quests.php @@ -22,7 +22,7 @@ class QuestsPage extends GenericPage $this->validCats = Util::$questClasses; // needs reviewing (not allowed to set this as default) $this->filterObj = new QuestListFilter(); - $this->getCategoryFromUrl($pageParam);; + $this->getCategoryFromUrl($pageParam); parent::__construct($pageCall, $pageParam); diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index 4ae5ae37..50ca83ae 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -40,12 +40,12 @@ if ($e = $this->end):

 

-
objectiveList): if ($e = $this->end): + echo "
\n"; echo ' '.Lang::quest('providedItem').Lang::main('colon')."\n"; endif; ?> From 7673e256c8bb8dd4f22db6351b3ac7cbd554fe73 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 29 Jun 2015 00:04:13 +0200 Subject: [PATCH 0008/1249] Setup: added some checks when useing WIN lets just say, the CLI works somewhat differently.. --- setup/tools/CLISetup.class.php | 26 +++++++++++++++++------- setup/tools/clisetup/siteconfig.func.php | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/setup/tools/CLISetup.class.php b/setup/tools/CLISetup.class.php index ef3b5e2c..63e2167c 100644 --- a/setup/tools/CLISetup.class.php +++ b/setup/tools/CLISetup.class.php @@ -12,7 +12,8 @@ class CLISetup const CHR_BELL = 7; const CHR_BACK = 8; const CHR_TAB = 9; - const CHR_RETURN = 10; + const CHR_LF = 10; + const CHR_CR = 13; const CHR_ESC = 27; const CHR_BACKSPACE = 127; @@ -325,10 +326,18 @@ class CLISetup /* read input */ /**************/ + /* + since the CLI on WIN ist not interactive, the following things have to be considered + you do not receive keystrokes but whole strings upon pressing (wich also appends a \r) + as such and probably other control chars can not be registered + this also means, you can't hide input at all, least process it + */ + public static function readInput(&$fields, $singleChar = false) { - // prevent default output - readline_callback_handler_install('', function() { }); + // prevent default output on *nix (readline doen't exist for WIN) + if (!self::$win) + readline_callback_handler_install('', function() { }); foreach ($fields as $name => $data) { @@ -353,7 +362,10 @@ class CLISetup if ($keyId == self::CHR_TAB) // ignore this one continue; - if ($keyId == self::CHR_ESC) + if ($keyId == self::CHR_CR) // also ignore this bastard! + continue; + + if ($keyId == self::CHR_ESC) // will not be send on WIN .. other ways of returning from setup? (besides ctrl + c) { echo chr(self::CHR_BELL); return false; @@ -366,7 +378,7 @@ class CLISetup $charBuff = substr($charBuff, 0, -1); echo chr(self::CHR_BACK)." ".chr(self::CHR_BACK); } - else if ($keyId == self::CHR_RETURN) + else if ($keyId == self::CHR_LF) { $fields[$name] = $charBuff; break; @@ -374,10 +386,10 @@ class CLISetup else if (!$validPattern || preg_match($validPattern, $char)) { $charBuff .= $char; - if (!$isHidden) + if (!$isHidden && !self::$win) // see note above echo $char; - if ($singleChar) + if ($singleChar && !self::$win) // see note above { $fields[$name] = $charBuff; break; diff --git a/setup/tools/clisetup/siteconfig.func.php b/setup/tools/clisetup/siteconfig.func.php index da93c325..bda95f6f 100644 --- a/setup/tools/clisetup/siteconfig.func.php +++ b/setup/tools/clisetup/siteconfig.func.php @@ -140,7 +140,7 @@ function siteconfig() $buff .= $conf['flags'] & CON_FLAG_PHP ? " PHP: " : "AOWOW: "; $buff .= $conf['flags'] & CON_FLAG_PHP ? strtolower($conf['key']) : strtoupper('cfg_'.$conf['key']); - if ($info[1]) + if (!empty($info[1])) $buff .= " - ".$info[1]; CLISetup::log($buff); From a3c9b52073b7a092d6e6c41d4fb7af3b52b3e257 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 1 Jul 2015 14:06:58 +0200 Subject: [PATCH 0009/1249] Screenshots: fixed sql-error in manager Comments: no longer appear as upvoted for anonymous user can be voted on, again Class detail page: removed default limit on class ability query (300). now shows all spells Filter/Items: added missing table-prefix; fixed search for iconString Spells/Search: display triggered player abilities as misc. spells. (previously hidden) Misc: removed some obscure piece of old config --- includes/ajaxHandler.class.php | 2 +- includes/community.class.php | 6 +++--- includes/kernel.php | 20 +++----------------- includes/types/item.class.php | 2 +- pages/class.php | 3 ++- pages/search.php | 9 ++++++--- pages/spells.php | 14 +++++++++++--- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index 656d00a5..d3d1d6c8 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -408,7 +408,7 @@ class AjaxHandler $result = ['success' => 1, 'up' => 0, 'down' => 0]; break; case 'vote': // up, down and remove - if (!User::$id || !$this->get('id') || $this->get('rating')) + if (!User::$id || !$this->get('id') || !$this->get('rating')) { $result = ['error' => 1, 'message' => Lang::main('genericError')]; break; diff --git a/includes/community.class.php b/includes/community.class.php index 33457b14..3381d4a2 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -29,8 +29,8 @@ class CommunityContent a3.displayName AS deleteUser, a4.displayName AS responseUser, IFNULL(SUM(cr.value), 0) AS rating, - SUM(IF (cr.userId = ?d, value, 0)) AS userRating, - SUM(IF (r.userId = ?d, 1, 0)) AS userReported + SUM(IF(cr.userId > 0 AND cr.userId = ?d, cr.value, 0)) AS userRating, + SUM(IF( r.userId > 0 AND r.userId = ?d, 1, 0)) AS userReported FROM ?_comments c JOIN @@ -238,7 +238,7 @@ class CommunityContent { $screenshots = DB::Aowow()->select(' SELECT s.id, a.displayName AS user, s.date, s.width, s.height, s.type, s.typeId, s.caption, s.status, s.status AS "flags" - FROM ?_screenshots s, + FROM ?_screenshots s LEFT JOIN ?_account a ON s.userIdOwner = a.id WHERE { s.type = ?d} diff --git a/includes/kernel.php b/includes/kernel.php index e7d8f73d..021a05b7 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -145,6 +145,9 @@ if (defined('CFG_SITE_HOST')) // points js to exec if (!CLI) { + if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST')) + die('error: SITE_HOST or STATIC_HOST not configured'); + // Setup Session session_set_cookie_params(15 * YEAR, '/', '', $secure, true); session_cache_limiter('private'); @@ -152,23 +155,6 @@ if (!CLI) if (!empty($AoWoWconf['aowow']) && User::init()) User::save(); // save user-variables in session - // todo: (low) - move to setup web-interface (when it begins its existance) - if (!defined('CFG_SITE_HOST') || !defined('CFG_STATIC_HOST')) - { - $host = substr($_SERVER['SERVER_NAME'].strtr($_SERVER['SCRIPT_NAME'], ['index.php' => '']), 0, -1); - - define('HOST_URL', ($secure ? 'https://' : 'http://').$host); - define('STATIC_URL', ($secure ? 'https://' : 'http://').$host.'/static'); - - if (User::isInGroup(U_GROUP_ADMIN)) // initial set - { - DB::Aowow()->query('REPLACE INTO ?_config VALUES (?a)', - [['site_host', $host, CON_FLAG_TYPE_STRING | CON_FLAG_PERSISTENT, 'default: '.$host.' - points js to executable files (automaticly set on first run)'], - ['static_host', $host.'/static', CON_FLAG_TYPE_STRING | CON_FLAG_PERSISTENT, 'default: '.$host.'/static - points js to images & scripts (automaticly set on first run)']] - ); - } - } - // hard-override locale for this call (should this be here..?) // all strings attached.. if (!empty($AoWoWconf['aowow'])) diff --git a/includes/types/item.class.php b/includes/types/item.class.php index e15559d3..768fe6a5 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -1725,7 +1725,7 @@ class ItemListFilter extends Filter 59 => [FILTER_CR_NUMERIC, 'durability', null, true], // dura 104 => [FILTER_CR_STRING, 'description', true ], // flavortext 7 => [FILTER_CR_BOOLEAN, 'description_loc0', true ], // hasflavortext - 142 => [FILTER_CR_STRING, 'iconString', ], // icon + 142 => [FILTER_CR_STRING, 'ic.iconString', ], // icon 12 => [FILTER_CR_BOOLEAN, 'itemset', ], // partofset 13 => [FILTER_CR_BOOLEAN, 'randomEnchant', ], // randomlyenchanted 14 => [FILTER_CR_BOOLEAN, 'pageTextId', ], // readable diff --git a/pages/class.php b/pages/class.php index df8139e5..d52d1925 100644 --- a/pages/class.php +++ b/pages/class.php @@ -126,7 +126,8 @@ class ClassPage extends GenericPage 'OR', ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], ['s.rankNo', 0] - ] + ], + CFG_SQL_LIMIT_NONE ); $genSpells = new SpellList($conditions); diff --git a/pages/search.php b/pages/search.php index 832296e1..92a28d32 100644 --- a/pages/search.php +++ b/pages/search.php @@ -680,7 +680,6 @@ class SearchPage extends GenericPage { $data[$abilities->id]['param1'] = strToLower($abilities->getField('iconString')); $data[$abilities->id]['param2'] = $abilities->ranks[$abilities->id]; - } } @@ -1360,11 +1359,15 @@ class SearchPage extends GenericPage return $result; } - private function _searchSpell($cndBase) // 24 Spells (Misc + GM) $searchMask & 0x1000000 + private function _searchSpell($cndBase) // 24 Spells (Misc + GM + triggered abilities) $searchMask & 0x1000000 { $result = []; $cnd = array_merge($cndBase, array( - ['s.typeCat', [0, -9]], + [ + 'OR', + ['s.typeCat', [0, -9]], + ['AND', ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], ['s.typeCat', [7, -2]]] + ], $this->createLookup() )); $misc = new SpellList($cnd); diff --git a/pages/spells.php b/pages/spells.php index fbbb0f1e..bf7370d3 100644 --- a/pages/spells.php +++ b/pages/spells.php @@ -345,10 +345,15 @@ class SpellsPage extends GenericPage } break; - case 0: // misc. Spells + case 0: // misc. Spells & triggered player abilities array_push($visibleCols, 'level'); - $conditions[] = ['s.typeCat', 0]; + $conditions[] = [ + 'OR', + ['s.typeCat', 0], + ['AND', ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], ['s.typeCat', [7, -2]]] + ]; + break; } } @@ -387,13 +392,16 @@ class SpellsPage extends GenericPage if (in_array(9, $_['cr']) && !in_array('source', $visibleCols)) $visibleCols[] = 'source'; - $mask = $spells->hasSetFields(['reagent1', 'skillLines', 'trainingCost']); + $mask = $spells->hasSetFields(['reagent1', 'skillLines', 'trainingCost', 'reqClassMask']); if ($mask & 0x1) $visibleCols[] = 'reagents'; if (!($mask & 0x2) && $this->category && !in_array($this->category[0], [9, 11])) $hiddenCols[] = 'skill'; if (($mask & 0x4)) $visibleCols[] = 'trainingcost'; + if (($mask & 0x8) && !in_array('singleclass', $visibleCols)) + $visibleCols[] = 'singleclass'; + if ($visibleCols) $tab['params']['visibleCols'] = '$'.Util::toJSON($visibleCols); From cc886b4db96362e0da1366d4c494f7c506ac7f12 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 1 Jul 2015 21:13:22 +0200 Subject: [PATCH 0010/1249] Quest Detail Page: properly distinguish between provided, required and provided-required items --- includes/shared.php | 2 +- pages/quest.php | 37 ++++++++++---- template/pages/quest.tpl.php | 94 +++++++++++++++++++----------------- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 9b5f5248..09fe6d84 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ objectiveList = []; + $this->providedItem = []; // gather ids for lookup $olItems = $olNPCs = $olGOs = $olFactions = []; @@ -351,17 +352,18 @@ class QuestPage extends GenericPage // items $olItems[0] = array( // srcItem on idx:0 $this->subject->getField('sourceItemId'), - $this->subject->getField('sourceItemCount') + $this->subject->getField('sourceItemCount'), + false ); for ($i = 1; $i < 7; $i++) // reqItem in idx:1-6 { $id = $this->subject->getField('reqItemId'.$i); $qty = $this->subject->getField('reqItemCount'.$i); - if (!$id || !$qty || $id == $olItems[0][0]) + if (!$id || !$qty) continue; - $olItems[$i] = [$id, $qty]; + $olItems[$i] = [$id, $qty, $id == $olItems[0][0]]; } if ($ids = array_column($olItems, 0)) @@ -369,18 +371,33 @@ class QuestPage extends GenericPage $olItemData = new ItemList(array(['id', $ids])); $this->extendGlobalData($olItemData->getJSGlobals(GLOBALINFO_SELF)); - foreach ($olItems as $i => $pair) + $providedRequired = false; + foreach ($olItems as $i => list($itemId, $qty, $provided)) { - if (!$pair[0] || !in_array($pair[0], $olItemData->getFoundIDs())) + if (!$i || !$itemId || !in_array($itemId, $olItemData->getFoundIDs())) continue; + if ($provided) + $providedRequired = true; + $this->objectiveList[] = array( 'typeStr' => Util::$typeStrings[TYPE_ITEM], - 'id' => $pair[0], - 'name' => $olItemData->json[$pair[0]]['name'], - 'qty' => $pair[1] > 1 ? $pair[1] : 0, - 'quality' => 7 - $olItemData->json[$pair[0]]['quality'], - 'extraText' => $i ? '' : ' ('.Lang::quest('provided').')' + 'id' => $itemId, + 'name' => $olItemData->json[$itemId]['name'], + 'qty' => $qty > 1 ? $qty : 0, + 'quality' => 7 - $olItemData->json[$itemId]['quality'], + 'extraText' => $provided ? ' ('.Lang::quest('provided').')' : '' + ); + } + + // if providd item is not required by quest, list it below other requirements + if (!$providedRequired && $olItems[0][0] && in_array($olItems[0][0], $olItemData->getFoundIDs())) + { + $this->providedItem = array( + 'id' => $olItems[0][0], + 'name' => $olItemData->json[$olItems[0][0]]['name'], + 'qty' => $olItems[0][1] > 1 ? $olItems[0][1] : 0, + 'quality' => 7 - $olItemData->json[$olItems[0][0]]['quality'] ); } } diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index 50ca83ae..db43e72a 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -32,62 +32,52 @@ elseif ($this->offerReward): echo $this->offerReward."\n"; endif; -if ($e = $this->end): +if ($this->end || $this->objectiveList): ?> - -suggestedPl): ?> - - -

 

 

objectiveList): - if ($e = $this->end): - echo "
\n"; - echo ' '.Lang::quest('providedItem').Lang::main('colon')."\n"; + if ($this->end): + echo "

 

".$this->end."\n"; endif; -?> - - $ol): - if (isset($ol['text'])): - echo ' \n"; - elseif (!empty($ol['proxy'])): // this implies creatures - echo ' \n"; + elseif (!empty($ol['proxy'])): // this implies creatures + echo ' \n"; + elseif (isset($ol['typeStr'])): + if (in_array($ol['typeStr'], ['item', 'spell'])): + echo ' '; + else /* if (in_array($ol['typeStr'], ['npc', 'object', 'faction'])) */: + echo ' '; + endif; + + echo '\n"; endif; + endforeach; + endif; - echo " \n"; - elseif (isset($ol['typeStr'])): - if (in_array($ol['typeStr'], ['item', 'spell'])): - echo ' '; - else /* if (in_array($ol['typeStr'], ['npc', 'object', 'faction'])) */: - echo ' '; - endif; - - echo '\n"; - endif; - endforeach; - - if ($this->suggestedPl && !$this->end): + if ($this->suggestedPl): echo ' \n"; endif; ?> @@ -95,7 +85,7 @@ if ($o = $this->objectiveList): providedItem): + echo "
\n"; + echo ' '.Lang::quest('providedItem').Lang::main('colon')."\n"; + echo "

 

'.$ol['text']."

 

'.$ol['name'].$ol['extraText'].''.($ol['qty'] > 1 ? ' ('.$ol['qty'].')' : null).'
\n"; + if ($o = $this->objectiveList): + foreach ($o as $i => $ol): + if (isset($ol['text'])): + echo '

 

'.$ol['text']."

 

'.$ol['name'].$ol['extraText'].''.($ol['qty'] > 1 ? ' ('.$ol['qty'].')' : null).'
\n"; - $block1 = array_slice($ol['proxy'], 0, ceil(count($ol['proxy']) / 2), true); - $block2 = array_slice($ol['proxy'], ceil(count($ol['proxy']) / 2), null, true); + $block1 = array_slice($ol['proxy'], 0, ceil(count($ol['proxy']) / 2), true); + $block2 = array_slice($ol['proxy'], ceil(count($ol['proxy']) / 2), null, true); - echo "
\n"; - foreach ($block1 as $pId => $name): - echo ' \n"; - endforeach; - echo "
  •  
'.$name."
\n"; - - if ($block2): // may be empty echo "
\n"; - foreach ($block2 as $pId => $name): + foreach ($block1 as $pId => $name): echo ' \n"; endforeach; echo "
  •  
'.$name."
\n"; + + if ($block2): // may be empty + echo "
\n"; + foreach ($block2 as $pId => $name): + echo ' \n"; + endforeach; + echo "
  •  
'.$name."
\n"; + endif; + + echo "
  •  
'.$ol['name'].''.($ol['extraText']).(!empty($ol['qty']) ? ' ('.$ol['qty'].')' : null)."
  •  
'.$ol['name'].''.(!$this->end ? $ol['extraText'] : null).(!empty($ol['qty']) ? ' ('.$ol['qty'].')' : null)."

 

'.Lang::quest('suggestedPl').Lang::main('colon').$this->suggestedPl."
\n"; + echo ' '; + echo '\n"; +?> +
'.$p['name'].''.($p['qty'] ? ' ('.$ol['qty'].')' : null)."
+ + +brick('mapper'); From b76fc3b2a75ffa276b037042287cfeb0ce815699 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 1 Jul 2015 21:51:13 +0200 Subject: [PATCH 0011/1249] added memory_limit to initial config --- README.md | Bin 8750 -> 9230 bytes setup/db_structure.sql | 2 +- setup/updates/1435777200_01.sql | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 setup/updates/1435777200_01.sql diff --git a/README.md b/README.md index 71730e827b3bf2a8fc8749a4e2b9a2c2c3874f8e..8b15c2973550876be5a2d8a11f59217b0765328c 100644 GIT binary patch delta 464 zcmah_yQ;!a5S&&)Yda5E3W^^fA_`iIm6ajJ7cr5<2Uht58?mtQ6GZR_o!MNujUY?T zIXkmEJDW%UJbc~@of>Lz7-EG5mI!fTHbMX&F*b|<5oSDRD4~ct*_7Fiu_Cj$G|SaY zwGrl~u}1>WSYZwaW+C?MNtxx?iMQ$;#yq97a?oc6%gz8={w%ifK^NTM8V_+Ll;8JhkN=GQXmZ*l|H>Ho%9^Ym%`BluhI-zd$#ZiQ>*4XeadQ~&?~ delta 12 TcmeD4Sm&}~9_Qvd?g1 diff --git a/setup/db_structure.sql b/setup/db_structure.sql index f47f9b01..939d8054 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2282,7 +2282,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_config` WRITE; /*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',129,'default: 500 - max results for search'),('sql_limit_default','300',129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',129,'default: 10 - max results for suggestions'),('sql_limit_none','0',129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',129,'default: 60 - time to live for RSS (in seconds)'),('cache_decay','25200',129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('session_timeout_delay','3600',129,'default: 60 * 60 - non-permanent session times out in time() + X'),('failed_auth_exclusion','900',129,'default: 15 * 60 - how long an account is closed after exceeding failed_auth_count (in seconds)'),('failed_auth_count','5',129,'default: 5 - how often invalid passwords are tolerated'),('name','Aowow Database Viewer (ADV)',136,' - website title'),('name_short','Aowow',136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('allow_register','1',132,'default: 1 - allow/disallow account creation (requires auth_mode 0)'),('debug','0',132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',132,'default: 0 - display brb gnomes and block access for non-staff'),('auth_mode','0',145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('rep_req_upvote','125',129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',129,'default: 100 - activated an account'),('rep_reward_upvoted','5',129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',129,'default: -200 - moderator revoked rights'),('user_max_votes','50',129,'default: 50 - vote limit per day'),('rep_req_votemore_add','250',129,'default: 250 - required reputation per additional vote past threshold'),('force_ssl','0',132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('cache_mode','1',161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('locales','333',161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('account_create_save_decay','604800',129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('account_recovery_decay','300',129,'default: 300 - time to recover your account and new recovery requests are blocked'),('serialize_precision','4',65,' - some derelict code, probably unused'),('screenshot_min_size','200',129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',136,' - points js to executable files (automaticly set on first run)'),('static_host','',136,' - points js to images & scripts (automaticly set on first run)'); +INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',129,'default: 500 - max results for search'),('sql_limit_default','300',129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',129,'default: 10 - max results for suggestions'),('sql_limit_none','0',129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',129,'default: 60 - time to live for RSS (in seconds)'),('cache_decay','25200',129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('session_timeout_delay','3600',129,'default: 60 * 60 - non-permanent session times out in time() + X'),('failed_auth_exclusion','900',129,'default: 15 * 60 - how long an account is closed after exceeding failed_auth_count (in seconds)'),('failed_auth_count','5',129,'default: 5 - how often invalid passwords are tolerated'),('name','Aowow Database Viewer (ADV)',136,' - website title'),('name_short','Aowow',136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('allow_register','1',132,'default: 1 - allow/disallow account creation (requires auth_mode 0)'),('debug','0',132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',132,'default: 0 - display brb gnomes and block access for non-staff'),('auth_mode','0',145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('rep_req_upvote','125',129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',129,'default: 100 - activated an account'),('rep_reward_upvoted','5',129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',129,'default: -200 - moderator revoked rights'),('user_max_votes','50',129,'default: 50 - vote limit per day'),('rep_req_votemore_add','250',129,'default: 250 - required reputation per additional vote past threshold'),('force_ssl','0',132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('cache_mode','1',161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('locales','333',161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('account_create_save_decay','604800',129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('account_recovery_decay','300',129,'default: 300 - time to recover your account and new recovery requests are blocked'),('serialize_precision','4',65,' - some derelict code, probably unused'),('screenshot_min_size','200',129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',136,' - points js to executable files'),('static_host','',136,' - points js to images & scripts'),('memory_limit','2048M',200,'default: 2048M - parsing spell.dbc is quite intense'); /*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/updates/1435777200_01.sql b/setup/updates/1435777200_01.sql new file mode 100644 index 00000000..d83e66cd --- /dev/null +++ b/setup/updates/1435777200_01.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO aowow_config (`key`, `value`, `flags`, `comment`) VALUES + ('memory_limit', '2048M', 0xC8, 'default: 2048M - parsing spell.dbc is quite intense'); From 2619c88db46968a15b8de9ba3e31df96e36d4ec0 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Jul 2015 16:01:37 +0200 Subject: [PATCH 0012/1249] Admins/Screenshots * allow empty captions on edit * set date of reputation reward to time of upload not approve --- includes/ajaxHandler.class.php | 8 ++++---- includes/utilities.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index d3d1d6c8..684ebec0 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -1141,11 +1141,11 @@ class AjaxHandler // resp: '' private function admin_handleSSEditalt() { - if (!$this->get('id') || !$this->post('alt')) + if (!$this->get('id') || $this->post('alt') === null) return ''; // doesn't need to be htmlEscaped, ths javascript does that - DB::Aowow()->query('UPDATE ?_screenshots SET caption = ? WHERE id = ?d', $this->post('alt'), $this->get('id')); + DB::Aowow()->query('UPDATE ?_screenshots SET caption = ? WHERE id = ?d', trim($this->post('alt')), $this->get('id')); return ''; } @@ -1167,7 +1167,7 @@ class AjaxHandler foreach ($ids as $id) { // must not be already approved - if ($_ = DB::Aowow()->selectCell('SELECT userIdOwner FROM ?_screenshots WHERE (status & ?d) = 0 AND id = ?d', CC_FLAG_APPROVED, $id)) + if ($_ = DB::Aowow()->selectRow('SELECT userIdOwner, date FROM ?_screenshots WHERE (status & ?d) = 0 AND id = ?d', CC_FLAG_APPROVED, $id)) { // should also error-log if (!file_exists(sprintf($path, 'pending', $id))) @@ -1209,7 +1209,7 @@ class AjaxHandler // set as approved in DB and gain rep (once!) DB::Aowow()->query('UPDATE ?_screenshots SET status = ?d, userIdApprove = ?d WHERE id = ?d', CC_FLAG_APPROVED, User::$id, $id); - Util::gainSiteReputation($_, SITEREP_ACTION_UPLOAD, ['id' => $id, 'what' => 1]); + Util::gainSiteReputation($_['userIdOwner'], SITEREP_ACTION_UPLOAD, ['id' => $id, 'what' => 1, 'date' => $_['date']]); } } diff --git a/includes/utilities.php b/includes/utilities.php index a4819978..a8827cad 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1477,7 +1477,7 @@ class Util return false; $x['sourceA'] = $miscData['id']; // screenshotId or videoId - $x['sourceB'] = $miscData['what']; // screenshot or video + $x['sourceB'] = $miscData['what']; // screenshot:1 or video:NYD $x['amount'] = CFG_REP_REWARD_UPLOAD; break; case SITEREP_ACTION_GOOD_REPORT: // NYI @@ -1508,7 +1508,7 @@ class Util $x = array_merge($x, array( 'userId' => $user, 'action' => $action, - 'date' => time() + 'date' => !empty($miscData['date']) ? $miscData['date'] : time() )); return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x)); From af39933cc8890ec66b4af8e1b7b4c595ca7c0d4b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Jul 2015 16:40:48 +0200 Subject: [PATCH 0013/1249] Utility implemented subpage: latest-screenshots --- includes/community.class.php | 16 ++++++++++------ pages/utility.php | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/includes/community.class.php b/includes/community.class.php index 3381d4a2..81457ee2 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -464,19 +464,23 @@ class CommunityContent return $videos; } - public static function getScreenshots($typeOrUser, $typeId = 0, &$nFound = 0) + public static function getScreenshots($typeOrUser = 0, $typeId = 0, &$nFound = 0) { $screenshots = DB::Aowow()->selectPage($nFound, " SELECT s.id, a.displayName AS user, s.date, s.width, s.height, s.caption, IF(s.status & ?d, 1, 0) AS 'sticky', s.type, s.typeId FROM ?_screenshots s LEFT JOIN ?_account a ON s.userIdOwner = a.id - WHERE {s.userIdOwner = ?d }{s.type = ? }{AND s.typeId = ? }AND s.status & ?d AND (s.status & ?d) = 0", + WHERE {s.userIdOwner = ?d AND }{s.type = ? AND }{s.typeId = ? AND }s.status & ?d AND (s.status & ?d) = 0 + {ORDER BY ?# DESC} + {LIMIT ?d}", CC_FLAG_STICKY, - $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, + $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, CC_FLAG_APPROVED, - CC_FLAG_DELETED + CC_FLAG_DELETED, + !$typeOrUser ? 'date' : DBSIMPLE_SKIP, + !$typeOrUser ? CFG_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP ); if ($typeOrUser < 0) // only for user page diff --git a/pages/utility.php b/pages/utility.php index e4310a3c..4677ae61 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -76,7 +76,7 @@ class UtilityPage extends GenericPage case 'latest-screenshots': $this->lvTabs[] = array( 'file' => 'screenshot', - 'data' => [], + 'data' => CommunityContent::getScreenshots(), 'params' => [] ); break; From 444e372a6633d3de8288a726439a3263ee2c3b2c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Jul 2015 17:35:05 +0200 Subject: [PATCH 0014/1249] Utility/Random no longer tries to lookup a random profile Utility/Comments implemented sub-page: most-comments --- pages/utility.php | 94 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 15 deletions(-) diff --git a/pages/utility.php b/pages/utility.php index 4677ae61..1666edc4 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -36,6 +36,8 @@ class UtilityPage extends GenericPage else $this->name .= Lang::main('colon') . Lang::main('mostComments', 0); } + + $this->lvTabs = []; } public function display($override = '') @@ -61,7 +63,7 @@ class UtilityPage extends GenericPage switch ($this->page) { case 'random': - $type = array_rand(array_filter(Util::$typeStrings)); + $type = array_rand(array_keys(array_filter(Util::$typeClasses))); $typeId = (new Util::$typeClasses[$type](null))->getRandomId(); header('Location: ?'.Util::$typeStrings[$type].'='.$typeId, true, 302); @@ -114,7 +116,7 @@ class UtilityPage extends GenericPage $typeObj = new $classStr($cnd); if (!$typeObj->error) { - $this->extendGlobalData($typeObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED | GLOBALINFO_REWARDS)); + $this->extendGlobalData($typeObj->getJSGlobals(GLOBALINFO_ANY)); $this->lvTabs[] = array( 'file' => $typeObj::$brickFile, 'data' => $typeObj->getListviewData(), @@ -127,13 +129,56 @@ class UtilityPage extends GenericPage if ($this->category && !in_array($this->category[0], [1, 7, 30])) header('Location: ?most-comments=1'.($this->rss ? '&rss' : null), true, 302); - $this->lvTabs[] = array( - 'file' => 'commentpreview', - 'data' => [], - 'params' => [] + $params = array( + 'extraCols' => '$[Listview.funcBox.createSimpleCol(\'ncomments\', \'tab_comments\', \'10%\', \'ncomments\')]', + 'sort' => '$[\'-ncomments\']' ); + + foreach (Util::$typeClasses as $type => $classStr) + { + if (!$classStr) + continue; + + $comments = DB::Aowow()->selectCol(' + SELECT `typeId` AS ARRAY_KEY, count(1) AS nComments FROM ?_comments + WHERE `replyTo` = 0 AND (`flags` & ?d) = 0 AND `type`= ?d AND `date` > (UNIX_TIMESTAMP() - ?d) + GROUP BY `type`, `typeId` + LIMIT 100', + CC_FLAG_DELETED, + $type, + (isset($this->category[0]) ? $this->category[0] : 1) * DAY + ); + if (!$comments) + continue; + + $typeClass = new $classStr(array(['id', array_keys($comments)])); + if (!$typeClass->error) + { + $data = $typeClass->getListviewData(); + foreach ($data as $typeId => &$d) + $d['ncomments'] = $comments[$typeId]; + + $this->extendGlobalData($typeClass->getJSGlobals(GLOBALINFO_ANY)); + $this->lvTabs[] = array( + 'file' => $typeClass::$brickFile, + 'data' => $data, + 'params' => $params, + '_type' => Util::$typeStrings[$type] + ); + } + } break; } + + // found nothing => set empty content + if (!$this->lvTabs) + { + $this->lvTabs[] = array( + 'file' => 'commentpreview', // anything, doesn't matter what + 'data' => [], + 'params' => [] + ); + } } protected function generateRSS() @@ -149,16 +194,35 @@ class UtilityPage extends GenericPage "".CFG_TTL_RSS."\n". "".date(DATE_RSS)."\n"; - foreach ($this->lvTabs[0]['data'] as $row) + + if ($this->page == 'most-comments') { - $xml .= "\n". - "<![CDATA[".htmlentities($row['subject'])."]]>\n". - "".HOST_URL.'?go-to-comment&id='.$row['id']."\n". - "\n". // todo (low): preview should be html-formated - "".date(DATE_RSS, time() - $row['elapsed'])."\n". - "".HOST_URL.'?go-to-comment&id='.$row['id']."\n". - "\n". - "\n"; + foreach ($this->lvTabs as $tab) + { + foreach ($tab['data'] as $row) + { + $xml .= "\n". + "<![CDATA[".htmlentities($tab['_type'] == 'item' ? substr($row['name'], 1) : $row['name'])."]]>\n". + "".$tab['_type']."\n". + "".HOST_URL.'/?'.$tab['_type'].'='.$row['id']."\n". + "".$row['ncomments']."\n". + "\n"; + } + } + } + else + { + foreach ($this->lvTabs[0]['data'] as $row) + { + $xml .= "\n". + "<![CDATA[".htmlentities($row['subject'])."]]>\n". + "".HOST_URL.'?go-to-comment&id='.$row['id']."\n". + "\n". // todo (low): preview should be html-formated + "".date(DATE_RSS, time() - $row['elapsed'])."\n". + "".HOST_URL.'?go-to-comment&id='.$row['id']."\n". + "\n". + "\n"; + } } $xml .= "\n"; From 5db946590e1e453daf98562f69825e8fd6322cd0 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 4 Jul 2015 19:37:10 +0200 Subject: [PATCH 0015/1249] display user reputation in top-menu --- localization/locale_dede.php | 1 + localization/locale_enus.php | 1 + localization/locale_eses.php | 1 + localization/locale_frfr.php | 1 + localization/locale_ruru.php | 1 + static/css/aowow.css | 10 ++++++++++ template/bricks/headerMenu.tpl.php | 1 + 7 files changed, 16 insertions(+) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 68df160f..bffd2daf 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -51,6 +51,7 @@ $lang = array( 'and' => " und ", 'or' => " oder ", 'back' => "Zurück", + 'reputationTip' => "Rufpunkte", // filter 'extSearch' => "Erweiterte Suche", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 69975291..3408ce57 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -46,6 +46,7 @@ $lang = array( 'and' => " and ", 'or' => " or ", 'back' => "Back", + 'reputationTip' => "Reputation points", // filter 'extSearch' => "Extended search", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 256518e8..8885c034 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -51,6 +51,7 @@ $lang = array( 'and' => " y ", 'or' => " o ", 'back' => "Arrière", + 'reputationTip' => "Puntos de reputación", // filter 'extSearch' => "Extender búsqueda", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 11411611..bdc9ede7 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -51,6 +51,7 @@ $lang = array( 'and' => " et ", 'or' => " ou ", 'back' => "Redro", + 'reputationTip' => "Points de réputation", // filter 'extSearch' => "Recherche avancée", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 58c119ff..b5b03719 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -51,6 +51,7 @@ $lang = array( 'and' => " и ", 'or' => " или ", 'back' => "Ðазад", + 'reputationTip' => "Очки репутации", // filter 'extSearch' => "РаÑширенный поиÑк", diff --git a/static/css/aowow.css b/static/css/aowow.css index a17058ad..accbaf6d 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -3775,3 +3775,13 @@ a#toplinks-language { font-size: 11px; padding: 3px 0 3px 20px; } + +/* site-rep custom */ +span#toplinks-rep { + margin-right: 0.8em; +} + +span#toplinks-rep a { + margin: 0px; + color: #0C9722; +} diff --git a/template/bricks/headerMenu.tpl.php b/template/bricks/headerMenu.tpl.php index f96143b9..36daec81 100644 --- a/template/bricks/headerMenu.tpl.php +++ b/template/bricks/headerMenu.tpl.php @@ -1,6 +1,7 @@ '.User::$displayName.''; + echo '('.User::getReputation().')'; else: echo ''.Lang::main('signIn').''; endif; From 5ecb46633ac1151cf4739f4fe0233122b957835d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Jul 2015 00:06:48 +0200 Subject: [PATCH 0016/1249] UserPage * display amount of pending screenshots / videos * corrected amount of comment ratings on display --- pages/user.php | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/pages/user.php b/pages/user.php index f60bf9e8..a0ec9730 100644 --- a/pages/user.php +++ b/pages/user.php @@ -55,29 +55,59 @@ class UserPage extends GenericPage // contrib -> [url=http://www.wowhead.com/client]Data uploads: n [small]([tooltip=tooltip_totaldatauploads]xx.y MB[/tooltip])[/small][/url] $co = DB::Aowow()->selectRow( - 'SELECT COUNT(DISTINCT c.id) AS sum, COUNT(cr.commentId) AS nRates FROM ?_comments c LEFT JOIN ?_comments_rates cr ON cr.commentId = c.id WHERE c.replyTo = 0 AND c.userId = ?d', + 'SELECT COUNT(DISTINCT c.id) AS sum, SUM(IFNULL(cr.value, 0)) AS nRates FROM ?_comments c LEFT JOIN ?_comments_rates cr ON cr.commentId = c.id AND cr.userId <> 0 WHERE c.replyTo = 0 AND c.userId = ?d', $this->user['id'] ); if ($co['sum']) - $contrib[] = Lang::user('comments').Lang::main('colon').$co['sum'].' [small]([tooltip=tooltip_totalratings]'.$co['nRates'].'[/tooltip])[/small]'; + $contrib[] = Lang::user('comments').Lang::main('colon').$co['sum'].($co['nRates'] ? ' [small]([tooltip=tooltip_totalratings]'.$co['nRates'].'[/tooltip])[/small]' : null); - $ss = DB::Aowow()->selectRow('SELECT COUNT(id) AS sum, SUM(IF(status & ?d, 1, 0)) as nSticky FROM ?_screenshots WHERE userIdOwner = ?d AND status & ?d AND (status & ?d) = 0', + $ss = DB::Aowow()->selectRow('SELECT COUNT(*) AS sum, SUM(IF(status & ?d, 1, 0)) AS nSticky, SUM(IF(status & ?d, 0, 1)) AS nPending FROM ?_screenshots WHERE userIdOwner = ?d AND (status & ?d) = 0', CC_FLAG_STICKY, - $this->user['id'], CC_FLAG_APPROVED, + $this->user['id'], CC_FLAG_DELETED ); if ($ss['sum']) - $contrib[] = Lang::user('screenshots').Lang::main('colon').$ss['sum'].' [small]([tooltip=tooltip_normal]'.($ss['sum'] - $ss['nSticky']).'[/tooltip] + [tooltip=tooltip_sticky]'.$ss['nSticky'].'[/tooltip])[/small]'; + { + $buff = []; + if ($ss['nSticky'] || $ss['nPending']) + { + if ($normal = ($ss['sum'] - $ss['nSticky'] - $ss['nPending'])) + $buff[] = '[tooltip=tooltip_normal]'.$normal.'[/tooltip]'; - $vi = DB::Aowow()->selectRow('SELECT COUNT(id) AS sum, SUM(IF(status & ?d, 1, 0)) as nSticky FROM ?_videos WHERE userIdOwner = ?d AND status & ?d AND (status & ?d) = 0', + if ($ss['nSticky']) + $buff[] = '[tooltip=tooltip_sticky]'.$ss['nSticky'].'[/tooltip]'; + + if ($ss['nPending']) + $buff[] = '[tooltip=tooltip_pending]'.$ss['nPending'].'[/tooltip]'; + } + + $contrib[] = Lang::user('screenshots').Lang::main('colon').$ss['sum'].($buff ? ' [small]('.implode($buff, ' + ').')[/small]' : null); + } + + $vi = DB::Aowow()->selectRow('SELECT COUNT(id) AS sum, SUM(IF(status & ?d, 1, 0)) AS nSticky, SUM(IF(status & ?d, 0, 1)) AS nPending FROM ?_videos WHERE userIdOwner = ?d AND (status & ?d) = 0', CC_FLAG_STICKY, - $this->user['id'], CC_FLAG_APPROVED, + $this->user['id'], CC_FLAG_DELETED ); if ($vi['sum']) - $contrib[] = Lang::user('videos').Lang::main('colon').$vi['sum'].' [small]([tooltip=tooltip_normal]'.($vi['sum'] - $vi['nSticky']).'[/tooltip] + [tooltip=tooltip_sticky]'.$vi['nSticky'].'[/tooltip])[/small]'; + { + $buff = []; + if ($vi['nSticky'] || $vi['nPending']) + { + if ($normal = ($vi['sum'] - $vi['nSticky'] - $vi['nPending'])) + $buff[] = '[tooltip=tooltip_normal]'.$normal.'[/tooltip]'; + + if ($vi['nSticky']) + $buff[] = '[tooltip=tooltip_sticky]'.$vi['nSticky'].'[/tooltip]'; + + if ($vi['nPending']) + $buff[] = '[tooltip=tooltip_pending]'.$vi['nPending'].'[/tooltip]'; + } + + $contrib[] = Lang::user('videos').Lang::main('colon').$vi['sum'].($buff ? ' [small]('.implode($buff, ' + ').')[/small]' : null); + } // contrib -> Forum posts: 5769 [small]([tooltip=topics]579[/tooltip] + [tooltip=replies]5190[/tooltip])[/small] From 09e0886cdd763662ea259ac66b53e898d75d1288 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Jul 2015 00:37:54 +0200 Subject: [PATCH 0017/1249] Utility * implemented sub-page: latest-videos (can't be suggested yet, i know!) * also show subject on latest-screenshots --- includes/community.class.php | 26 +++++++++++++++----------- pages/utility.php | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/includes/community.class.php b/includes/community.class.php index 81457ee2..0d459725 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -417,22 +417,26 @@ class CommunityContent return $comments; } - public static function getVideos($typeOrUser, $typeId = 0, &$nFound = 0) + public static function getVideos($typeOrUser = 0, $typeId = 0, &$nFound = 0) { $videos = DB::Aowow()->selectPage($nFound, " SELECT v.id, a.displayName AS user, v.date, v.videoId, v.caption, IF(v.status & ?d, 1, 0) AS 'sticky', v.type, v.typeId FROM ?_videos v LEFT JOIN ?_account a ON v.userIdOwner = a.id - WHERE {v.userIdOwner = ?d }{v.type = ? }{AND v.typeId = ? }AND v.status & ?d AND (v.status & ?d) = 0", + WHERE {v.userIdOwner = ?d AND }{v.type = ? AND }{v.typeId = ? AND }v.status & ?d AND (v.status & ?d) = 0 + {ORDER BY ?# DESC} + {LIMIT ?d}", CC_FLAG_STICKY, - $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, + $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, + $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, CC_FLAG_APPROVED, - CC_FLAG_DELETED + CC_FLAG_DELETED, + !$typeOrUser ? 'date' : DBSIMPLE_SKIP, + !$typeOrUser ? CFG_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP ); - if ($typeOrUser < 0) // only for user page + if ($typeOrUser <= 0) // not for search by type/typeId { foreach ($videos as $v) self::addSubject($v['type'], $v['typeId']); @@ -443,7 +447,7 @@ class CommunityContent // format data to meet requirements of the js foreach ($videos as &$v) { - if ($typeOrUser < 0) // only for user page + if ($typeOrUser <= 0) // not for search by type/typeId { if (!empty(self::$subjCache[$v['type']][$v['typeId']]) && !is_numeric(self::$subjCache[$v['type']][$v['typeId']])) $v['subject'] = self::$subjCache[$v['type']][$v['typeId']]; @@ -452,7 +456,7 @@ class CommunityContent } $v['date'] = date(Util::$dateFormatInternal, $v['date']); - $v['videoType'] = 1; // always youtube + $v['videoType'] = 1; // always youtube if (!$v['sticky']) unset($v['sticky']); @@ -483,7 +487,7 @@ class CommunityContent !$typeOrUser ? CFG_SQL_LIMIT_SEARCH : DBSIMPLE_SKIP ); - if ($typeOrUser < 0) // only for user page + if ($typeOrUser <= 0) // not for search by type/typeId { foreach ($screenshots as $s) self::addSubject($s['type'], $s['typeId']); @@ -494,7 +498,7 @@ class CommunityContent // format data to meet requirements of the js foreach ($screenshots as &$s) { - if ($typeOrUser < 0) // only for user page + if ($typeOrUser <= 0) // not for search by type/typeId { if (!empty(self::$subjCache[$s['type']][$s['typeId']]) && !is_numeric(self::$subjCache[$s['type']][$s['typeId']])) $s['subject'] = self::$subjCache[$s['type']][$s['typeId']]; diff --git a/pages/utility.php b/pages/utility.php index 1666edc4..dbf6caca 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -85,7 +85,7 @@ class UtilityPage extends GenericPage case 'latest-videos': $this->lvTabs[] = array( 'file' => 'video', - 'data' => [], + 'data' => CommunityContent::getVideos(), 'params' => [] ); break; From ed67493bb8a73793f2dbc140518fb8bc52999a08 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Jul 2015 19:13:11 +0200 Subject: [PATCH 0018/1249] Items/Filter fixed error, when appending empty upg= to query NPC/Text handled rare case of uppercase string-placeholders --- pages/items.php | 4 ++-- pages/npc.php | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pages/items.php b/pages/items.php index 3317bece..00340df4 100644 --- a/pages/items.php +++ b/pages/items.php @@ -232,7 +232,7 @@ class ItemsPage extends GenericPage if (isset($this->filter['sl'])) // skip lookups for unselected slots $groups = array_intersect($groups, (array)$this->filter['sl']); - if (isset($this->filter['upg'])) // skip lookups for slots we dont have items to upgrade for + if (!empty($this->filter['upg'])) // skip lookups for slots we dont have items to upgrade for $groups = array_intersect($groups, (array)$this->filter['upg']); if ($groups) @@ -416,7 +416,7 @@ class ItemsPage extends GenericPage } // reformat for use in template - if (isset($this->filter['upg'])) + if (!empty($this->filter['upg'])) $this->filter['upg'] = implode(':', array_keys($this->filter['upg'])); // whoops, we have no data? create emergency content diff --git a/pages/npc.php b/pages/npc.php index f6692b59..6489b5de 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -957,6 +957,9 @@ class NpcPage extends GenericPage if (in_array($t['type'], [2, 16]) && strpos($msg, '%s') === false) $msg = '%s '.$msg; + // fixup: bad case-insensivity + $msg = str_replace('%S', '%s', $msg); + $line = array( 'range' => $t['range'], 'type' => 2, // [type: 0, 12] say: yellow-ish From f2ec843f7e8c87400f315a407bb0b41d2c28b36a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Jul 2015 21:17:55 +0200 Subject: [PATCH 0019/1249] Lang rename /gameObjects?/ => /objects?/ so it can be localized via typeString --- localization/locale_dede.php | 4 ++-- localization/locale_enus.php | 4 ++-- localization/locale_eses.php | 4 ++-- localization/locale_frfr.php | 4 ++-- localization/locale_ruru.php | 4 ++-- pages/object.php | 4 ++-- pages/objects.php | 2 +- pages/spell.php | 2 +- static/js/locale_dede.js | 2 +- static/js/locale_enus.js | 2 +- static/js/locale_eses.js | 2 +- static/js/locale_frfr.js | 2 +- static/js/locale_ruru.js | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index bffd2daf..bcf88373 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -179,8 +179,8 @@ $lang = array( 'difficulty' => "Modus", 'dispelType' => "Bannart", 'duration' => "Dauer", - 'gameObject' => "Objekt", - 'gameObjects' => "Objekte", + 'object' => "Objekt", + 'objects' => "Objekte", 'glyphType' => "Glyphenart", 'race' => "Volk", 'races' => "Völker", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 3408ce57..46388f03 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -174,8 +174,8 @@ $lang = array( 'difficulty' => "Difficulty", 'dispelType' => "Dispel type", 'duration' => "Duration", - 'gameObject' => "object", - 'gameObjects' => "Objects", + 'object' => "object", + 'objects' => "Objects", 'glyphType' => "Glyph type", 'race' => "race", 'races' => "Races", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 8885c034..653f58d3 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -179,8 +179,8 @@ $lang = array( 'difficulty' => "Dificultad", 'dispelType' => "Tipo de disipación", 'duration' => "Duración", - 'gameObject' => "entidad", - 'gameObjects' => "Entidades", + 'object' => "entidad", + 'objects' => "Entidades", 'glyphType' => "Tipo de glifo", 'race' => "raza", 'races' => "Razas", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index bdc9ede7..975b917f 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -179,8 +179,8 @@ $lang = array( 'difficulty' => "Difficulté", 'dispelType' => "Type de dissipation", 'duration' => "Durée", - 'gameObject' => "entité", - 'gameObjects' => "Entités", + 'object' => "entité", + 'objects' => "Entités", 'glyphType' => "Type de glyphe", 'race' => "race", 'races' => "Races", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index b5b03719..4e0e2d8a 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -179,8 +179,8 @@ $lang = array( 'difficulty' => "СложноÑть", 'dispelType' => "Тип раÑÑеиваниÑ", 'duration' => "ДлительноÑть", - 'gameObject' => "объект", - 'gameObjects' => "Объекты", + 'object' => "объект", + 'objects' => "Объекты", 'glyphType' => "Тип Ñимвола", 'race' => "раÑа", 'races' => "РаÑÑ‹", diff --git a/pages/object.php b/pages/object.php index 0a1f324d..96d5ce2e 100644 --- a/pages/object.php +++ b/pages/object.php @@ -49,7 +49,7 @@ class ObjectPage extends GenericPage protected function generateTitle() { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('gameObject'))); + array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('object'))); } protected function generateContent() @@ -489,7 +489,7 @@ class ObjectPage extends GenericPage public function notFound() { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('gameObject'), Lang::gameObject('notFound')); + return parent::notFound(Lang::game('object'), Lang::gameObject('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/objects.php b/pages/objects.php index 4a90c44e..10d9a7bc 100644 --- a/pages/objects.php +++ b/pages/objects.php @@ -25,7 +25,7 @@ class ObjectsPage extends GenericPage parent::__construct($pageCall, $pageParam); - $this->name = Util::ucFirst(Lang::game('gameObjects')); + $this->name = Util::ucFirst(Lang::game('objects')); $this->subCat = $pageParam ? '='.$pageParam : ''; } diff --git a/pages/spell.php b/pages/spell.php index 6f8ce548..07b2854b 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -1690,7 +1690,7 @@ class SpellPage extends GenericPage case 105: // Summon Object (slot 2) case 106: // Summon Object (slot 3) case 107: // Summon Object (slot 4) - $_ = Util::ucFirst(Lang::game('gameObject')).' #'.$effMV; + $_ = Util::ucFirst(Lang::game('object')).' #'.$effMV; if ($summon = $this->subject->getModelInfo($this->typeId, $i)) { $_ = $summon['typeId'] ? ' ('.$summon['displayName'].')' : ' (#0)'; diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index aa4e10ee..0b4065ad 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -2235,7 +2235,7 @@ var g_operators = { var g_world_object_types = { 3: 'Kreatur', 4: 'Spieler', - 5: 'Gameobject', + 5: 'Objekt', 7: 'Spielerleiche' }; diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 14be7ad5..b2e5f697 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -2282,7 +2282,7 @@ var g_operators = { var g_world_object_types = { 3: 'Creature', 4: 'Player', - 5: 'Gameobject', + 5: 'Object', 7: 'Player Corpse' }; diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 5ddd19b2..338c63e2 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -2238,7 +2238,7 @@ var g_operators = { var g_world_object_types = { 3: 'Creature', 4: 'Player', - 5: 'Gameobject', + 5: 'Entidad', 7: 'Player Corpse' }; diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 738f351b..43533018 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -2225,7 +2225,7 @@ var g_operators = { var g_world_object_types = { 3: 'Creature', 4: 'Player', - 5: 'Gameobject', + 5: 'Entité', 7: 'Player Corpse' }; diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index c174fec5..83a5613a 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -2225,7 +2225,7 @@ var g_operators = { var g_world_object_types = { 3: 'Creature', 4: 'Player', - 5: 'Gameobject', + 5: 'Объект', 7: 'Player Corpse' }; From 4a47900860d58893ad3cbf316c0ad154e74c1d3a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 5 Jul 2015 22:51:47 +0200 Subject: [PATCH 0020/1249] Utility/RSS use SimpleXML to generate rss-feed send rss as utf-8 fixed some misc errors when generating a feed --- localization/locale_dede.php | 4 +- localization/locale_enus.php | 4 +- localization/locale_eses.php | 4 +- localization/locale_frfr.php | 4 +- localization/locale_ruru.php | 4 +- pages/utility.php | 212 ++++++++++++++++++++++++----------- 6 files changed, 154 insertions(+), 78 deletions(-) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index bcf88373..51b01ba7 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -14,8 +14,7 @@ $lang = array( 'timeUnits' => array( 'sg' => ["Jahr", "Monat", "Woche", "Tag", "Stunde", "Minute", "Sekunde", "Millisekunde"], 'pl' => ["Jahre", "Monate", "Wochen", "Tage", "Stunden", "Minuten", "Sekunden", "Millisekunden"], - 'ab' => ["J.", "M.", "W.", "Tag", "Std.", "Min.", "Sek.", "Ms."], - 'ago' => 'vor %s' + 'ab' => ["J.", "M.", "W.", "Tag", "Std.", "Min.", "Sek.", "Ms."] ), 'main' => array( 'name' => "Name", @@ -52,6 +51,7 @@ $lang = array( 'or' => " oder ", 'back' => "Zurück", 'reputationTip' => "Rufpunkte", + 'byUserTimeAgo' => "Von %1s vor %s", // filter 'extSearch' => "Erweiterte Suche", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 46388f03..876d88ca 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -9,8 +9,7 @@ $lang = array( 'timeUnits' => array( 'sg' => ["year", "month", "week", "day", "hour", "minute", "second", "millisecond"], 'pl' => ["years", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds"], - 'ab' => ["yr", "mo", "wk", "day", "hr", "min", "sec", "ms"], - 'ago' => '%s ago' + 'ab' => ["yr", "mo", "wk", "day", "hr", "min", "sec", "ms"] ), 'main' => array( 'name' => "name", @@ -47,6 +46,7 @@ $lang = array( 'or' => " or ", 'back' => "Back", 'reputationTip' => "Reputation points", + 'byUserTimeAgo' => "By %1s %s ago", // filter 'extSearch' => "Extended search", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 653f58d3..7b56d5aa 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -14,8 +14,7 @@ $lang = array( 'timeUnits' => array( 'sg' => ["año", "mes", "semana", "día", "hora", "minuto", "segundo", "milisegundo"], 'pl' => ["años", "meses", "semanas", "dias", "horas", "minutos", "segundos", "milisegundos"], - 'ab' => ["año", "mes", "sem", "", "h", "min", "seg", "ms"], - 'ago' => 'hace %s' + 'ab' => ["año", "mes", "sem", "", "h", "min", "seg", "ms"] ), 'main' => array( 'name' => "nombre", @@ -52,6 +51,7 @@ $lang = array( 'or' => " o ", 'back' => "Arrière", 'reputationTip' => "Puntos de reputación", + 'byUserTimeAgo' => "Por %1s hace %s", // filter 'extSearch' => "Extender búsqueda", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 975b917f..67941ed4 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -14,8 +14,7 @@ $lang = array( 'timeUnits' => array( 'sg' => ["année", "mois", "semaine", "jour", "heure", "minute", "seconde", "milliseconde"], 'pl' => ["années", "mois", "semaines", "jours", "heures", "minutes", "secondes", "millisecondes"], - 'ab' => ["an", "mo", "sem", "jour", "h", "min", "s", "ms"], - 'ago' => 'il y a %s' + 'ab' => ["an", "mo", "sem", "jour", "h", "min", "s", "ms"] ), 'main' => array( 'name' => "nom", @@ -52,6 +51,7 @@ $lang = array( 'or' => " ou ", 'back' => "Redro", 'reputationTip' => "Points de réputation", + 'byUserTimeAgo' => "Par %1s il y a %s", // filter 'extSearch' => "Recherche avancée", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 4e0e2d8a..59867a62 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -14,8 +14,7 @@ $lang = array( 'timeUnits' => array( 'sg' => ["год", "меÑÑц", "неделÑ", "день", "чаÑ", "минута", "Ñекунда", "миллиÑекунда"], 'pl' => ["годы", "меÑÑцы", "недели", "дн.", "чаÑÑ‹", "мин", "Ñекунды", "миллиÑекундах"], - 'ab' => ["г.", "меÑ.", "нед.", "дн", "ч.", "мин", "Ñек.", "мÑ"], - 'ago' => '%s назад' + 'ab' => ["г.", "меÑ.", "нед.", "дн", "ч.", "мин", "Ñек.", "мÑ"] ), 'main' => array( 'name' => "название", @@ -52,6 +51,7 @@ $lang = array( 'or' => " или ", 'back' => "Ðазад", 'reputationTip' => "Очки репутации", + 'byUserTimeAgo' => "От %1s %s назад", // filter 'extSearch' => "РаÑширенный поиÑк", diff --git a/pages/utility.php b/pages/utility.php index dbf6caca..2ca1d815 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -16,8 +16,10 @@ class UtilityPage extends GenericPage 'latest-additions', 'latest-articles', 'latest-comments', 'latest-screenshots', 'random', 'unrated-comments', 11 => 'latest-videos', 12 => 'most-comments', 13 => 'missing-screenshots' ); + private $page = ''; private $rss = false; + private $feedData = []; public function __construct($pageCall, $pageParam) { @@ -44,7 +46,7 @@ class UtilityPage extends GenericPage { if ($this->rss) // this should not be cached { - header('Content-Type: application/rss+xml; charset=ISO-8859-1'); + header('Content-Type: application/rss+xml; charset=UTF-8'); die($this->generateRSS()); } else @@ -68,31 +70,103 @@ class UtilityPage extends GenericPage header('Location: ?'.Util::$typeStrings[$type].'='.$typeId, true, 302); die(); - case 'latest-comments': - $this->lvTabs[] = array( - 'file' => 'commentpreview', - 'data' => CommunityContent::getCommentPreviews(), - 'params' => [] - ); + case 'latest-comments': // rss + $data = CommunityContent::getCommentPreviews(); + + if ($this->rss) + { + foreach ($data as $d) + { + // todo (low): preview should be html-formated + $this->feedData[] = array( + 'title' => [true, [], Util::ucFirst(Lang::game(Util::$typeStrings[$d['type']])).Lang::main('colon').htmlentities($d['subject'])], + 'link' => [false, [], HOST_URL.'/?go-to-comment&id='.$d['id']], + 'description' => [true, [], htmlentities($d['preview'])."

".sprintf(Lang::main('byUserTimeAgo'), $d['user'], Util::formatTime($d['elapsed'] * 1000, true))], + 'pubDate' => [false, [], date(DATE_RSS, time() - $d['elapsed'])], + 'guid' => [false, [], HOST_URL.'/?go-to-comment&id='.$d['id']] + // 'domain' => [false, [], null] + ); + } + } + else + { + $this->lvTabs[] = array( + 'file' => 'commentpreview', + 'data' => $data, + 'params' => [] + ); + } break; - case 'latest-screenshots': - $this->lvTabs[] = array( - 'file' => 'screenshot', - 'data' => CommunityContent::getScreenshots(), - 'params' => [] - ); + case 'latest-screenshots': // rss + $data = CommunityContent::getScreenshots(); + + if ($this->rss) + { + foreach ($data as $d) + { + $desc = ''; + if ($d['caption']) + $desc .= '
'.$d['caption']; + $desc .= "

".sprintf(Lang::main('byUserTimeAgo'), $d['user'], Util::formatTime($d['elapsed'] * 1000, true)); + + // enclosure/length => filesize('static/uploads/screenshots/thumb/'.$d['id'].'.jpg') .. always set to this placeholder value though + $this->feedData[] = array( + 'title' => [true, [], Util::ucFirst(Lang::game(Util::$typeStrings[$d['type']])).Lang::main('colon').htmlentities($d['subject'])], + 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#screenshots:id='.$d['id']], + 'description' => [true, [], $desc], + 'pubDate' => [false, [], date(DATE_RSS, time() - $d['elapsed'])], + 'enclosure' => [false, ['url' => STATIC_URL.'/uploads/screenshots/thumb/'.$d['id'].'.jpg', 'length' => 12345, 'type' => 'image/jpeg'], null], + 'guid' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#screenshots:id='.$d['id']], + // 'domain' => [false, [], live|ptr] + ); + } + } + else + { + $this->lvTabs[] = array( + 'file' => 'screenshot', + 'data' => $data, + 'params' => [] + ); + } break; - case 'latest-videos': - $this->lvTabs[] = array( - 'file' => 'video', - 'data' => CommunityContent::getVideos(), - 'params' => [] - ); + case 'latest-videos': // rss + $data = CommunityContent::getVideos(); + + if ($this->rss) + { + foreach ($data as $d) + { + $desc = ''; + if ($d['caption']) + $desc .= '
'.$d['caption']; + $desc .= "

".sprintf(Lang::main('byUserTimeAgo'), $d['user'], Util::formatTime($d['elapsed'] * 1000, true)); + + // is enclosure/length .. is this even relevant..? + $this->feedData[] = array( + 'title' => [true, [], Util::ucFirst(Lang::game(Util::$typeStrings[$d['type']])).Lang::main('colon').htmlentities($row['subject'])], + 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#videos:id='.$d['id']], + 'description' => [true, [], $desc], + 'pubDate' => [false, [], date(DATE_RSS, time() - $row['elapsed'])], + 'enclosure' => [false, ['url' => '//i3.ytimg.com/vi/'.$d['videoId'].'/default.jpg', 'length' => 12345, 'type' => 'image/jpeg'], null], + 'guid' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$d['type']].'='.$d['typeId'].'#videos:id='.$d['id']], + // 'domain' => [false, [], live|ptr] + ); + } + } + else + { + $this->lvTabs[] = array( + 'file' => 'video', + 'data' => $data, + 'params' => [] + ); + } break; - case 'latest-articles': + case 'latest-articles': // rss $this->lvTabs = []; break; - case 'latest-additions': + case 'latest-additions': // rss $extraText = ''; break; case 'unrated-comments': @@ -125,7 +199,7 @@ class UtilityPage extends GenericPage } } break; - case 'most-comments': + case 'most-comments': // rss if ($this->category && !in_array($this->category[0], [1, 7, 30])) header('Location: ?most-comments=1'.($this->rss ? '&rss' : null), true, 302); @@ -155,23 +229,39 @@ class UtilityPage extends GenericPage if (!$typeClass->error) { $data = $typeClass->getListviewData(); - foreach ($data as $typeId => &$d) - $d['ncomments'] = $comments[$typeId]; - $this->extendGlobalData($typeClass->getJSGlobals(GLOBALINFO_ANY)); - $this->lvTabs[] = array( - 'file' => $typeClass::$brickFile, - 'data' => $data, - 'params' => $params, - '_type' => Util::$typeStrings[$type] - ); + if ($this->rss) + { + foreach ($data as $typeId => &$d) + { + $this->feedData[] = array( + 'title' => [true, [], htmlentities(Util::$typeStrings[$type] == 'item' ? substr($d['name'], 1) : $d['name'])], + 'type' => [false, [], Util::$typeStrings[$type]], + 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$type].'='.$d['id']], + 'ncomments' => [false, [], $comments[$typeId]['ncomments']] + ); + } + } + else + { + foreach ($data as $typeId => &$d) + $d['ncomments'] = $comments[$typeId]['ncomments']; + + $this->extendGlobalData($typeClass->getJSGlobals(GLOBALINFO_ANY)); + $this->lvTabs[] = array( + 'file' => $typeClass::$brickFile, + 'data' => $data, + 'params' => $params + ); + } } } + break; } // found nothing => set empty content - if (!$this->lvTabs) + if (!$this->lvTabs && !$this->rss) { $this->lvTabs[] = array( 'file' => 'commentpreview', // anything, doesn't matter what @@ -185,49 +275,35 @@ class UtilityPage extends GenericPage { $this->generateContent(); - $xml = "\n". - "\n\n". - "".CFG_NAME_SHORT.' - '.$this->name."\n". - "".HOST_URL.'?'.$this->page . ($this->category ? '='.$this->category[0] : null)."\n". - "".CFG_NAME."\n". - "".implode('-', str_split(User::$localeString, 2))."\n". - "".CFG_TTL_RSS."\n". - "".date(DATE_RSS)."\n"; + $root = new SimpleXML(''); + $root->addAttribute('version', '2.0'); + $channel = $root->addChild('channel'); - if ($this->page == 'most-comments') + $channel->addChild('title', CFG_NAME_SHORT.' - '.$this->name); + $channel->addChild('link', HOST_URL.'/?'.$this->page . ($this->category ? '='.$this->category[0] : null)); + $channel->addChild('description', CFG_NAME); + $channel->addChild('language', implode('-', str_split(User::$localeString, 2))); + $channel->addChild('ttl', CFG_TTL_RSS); + $channel->addChild('lastBuildDate', date(DATE_RSS)); + + foreach ($this->feedData as $row) { - foreach ($this->lvTabs as $tab) + $item = $channel->addChild('item'); + + foreach ($row as $key => list($isCData, $attrib, $text)) { - foreach ($tab['data'] as $row) - { - $xml .= "\n". - "<![CDATA[".htmlentities($tab['_type'] == 'item' ? substr($row['name'], 1) : $row['name'])."]]>\n". - "".$tab['_type']."\n". - "".HOST_URL.'/?'.$tab['_type'].'='.$row['id']."\n". - "".$row['ncomments']."\n". - "\n"; - } - } - } - else - { - foreach ($this->lvTabs[0]['data'] as $row) - { - $xml .= "\n". - "<![CDATA[".htmlentities($row['subject'])."]]>\n". - "".HOST_URL.'?go-to-comment&id='.$row['id']."\n". - "\n". // todo (low): preview should be html-formated - "".date(DATE_RSS, time() - $row['elapsed'])."\n". - "".HOST_URL.'?go-to-comment&id='.$row['id']."\n". - "\n". - "\n"; + if ($isCData && $text) + $child = $item->addChild($key)->addCData($text); + else + $child = $item->addChild($key, $text); + + foreach ($attrib as $k => $v) + $child->addAttribute($k, $v); } } - $xml .= "\n"; - - return $xml; + return $root->asXML(); } protected function generateTitle() From 0cb5d6b8968520fa45974cbe94b6679e65d3ce23 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 8 Jul 2015 23:19:23 +0200 Subject: [PATCH 0021/1249] CLISetup * added msg-level INFO * changed some WARN-level messages to INFO Util * added function to handle directories aowow wants to write to / read from SiteConfig * group options to be less cluttered * allow empty strings (numerical values must still at least be 0) * renamed account related config values to be make more sense * make cache path configurable * make session save path configurable - use this to avoid the garbage collect cron job on Debian or Ubuntu, that cleans sessions and only depends on your php.ini (NOTE: putting this inside a web-enabled directory is a risk!) --- includes/ajaxHandler.class.php | 14 +--- includes/kernel.php | 23 ++++-- includes/user.class.php | 10 +-- includes/utilities.php | 21 +++++ index.php | 1 + pages/account.php | 32 ++++---- pages/admin.php | 46 ++++------- pages/genericPage.class.php | 3 + setup/db_structure.sql | 3 +- setup/setup.php | 2 +- setup/tools/CLISetup.class.php | 23 ++++-- setup/tools/clisetup/account.func.php | 2 +- setup/tools/clisetup/dbconfig.func.php | 4 +- setup/tools/clisetup/siteconfig.func.php | 98 +++++++++++++----------- setup/tools/filegen/complexImg.func.php | 6 +- setup/tools/filegen/simpleImg.func.php | 4 +- setup/updates/1436392800_01.sql | 15 ++++ template/pages/acc-signIn.tpl.php | 2 +- 18 files changed, 179 insertions(+), 130 deletions(-) create mode 100644 setup/updates/1436392800_01.sql diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index 684ebec0..b725a738 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -698,8 +698,6 @@ class AjaxHandler if (!strlen($key)) return 'empty option name given'; - if (!strlen($val)) - return 'empty value given'; if (preg_match('/[^a-z0-9_\.\-]/i', $key, $m)) return 'invalid chars in option name: "'.$m[0].'"'; @@ -718,20 +716,14 @@ class AjaxHandler if (!strlen($key)) return 'empty option name given'; - if (!strlen($val)) - return 'empty value given'; - - if (substr($key, 0, 4) == 'CFG_') - $key = substr($key, 4); $flags = DB::Aowow()->selectCell('SELECT `flags` FROM ?_config WHERE `key` = ?', $key); if (!$flags) return 'configuration option not found'; - if (preg_match('/[^a-z0-9_\-]/i', $key, $m)) - return 'invalid chars in option name: "'.$m[0].'"'; - - if ($flags & CON_FLAG_TYPE_INT && !preg_match('/^-?\d+$/i', $val)) + if (!($flags & CON_FLAG_TYPE_STRING) && !strlen($val)) + return 'empty value given'; + else if ($flags & CON_FLAG_TYPE_INT && !preg_match('/^-?\d+$/i', $val)) return "value must be integer"; else if ($flags & CON_FLAG_TYPE_FLOAT && !preg_match('/^-?\d*(,|.)?\d+$/i', $val)) return "value must be float"; diff --git a/includes/kernel.php b/includes/kernel.php index 021a05b7..a6b61e62 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -68,12 +68,15 @@ if (!empty($AoWoWconf['characters'])) $sets = DB::isConnectable(DB_AOWOW) ? DB::Aowow()->select('SELECT `key` AS ARRAY_KEY, `value`, `flags` FROM ?_config') : []; foreach ($sets as $k => $v) { - // this should not have been possible - if (!strlen($v['value'])) - continue; - $php = $v['flags'] & CON_FLAG_PHP; + // this should not have been possible + if (!strlen($v['value']) && !($v['flags'] & CON_FLAG_TYPE_STRING) && !$php) + { + Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!'); + continue; + } + if ($v['flags'] & CON_FLAG_TYPE_INT) $val = intVal($v['value']); else if ($v['flags'] & CON_FLAG_TYPE_FLOAT) @@ -82,9 +85,14 @@ foreach ($sets as $k => $v) $val = (bool)$v['value']; else if ($v['flags'] & CON_FLAG_TYPE_STRING) $val = preg_replace('/[^\p{L}0-9~\s_\-\'\/\.:,]/ui', '', $v['value']); - else + else if ($php) { - Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: '.($php ? 'PHP' : 'Aowow').' config value '.($php ? strtolower($k) : 'CFG_'.strtoupper($k)).' has no type set. Value forced to 0!'); + Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: PHP config value '.strtolower($k).' has no type set - config will not be used!'); + continue; + } + else // if (!$php) + { + Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!'); $val = 0; } @@ -149,6 +157,9 @@ if (!CLI) die('error: SITE_HOST or STATIC_HOST not configured'); // Setup Session + if (CFG_SESSION_CACHE_DIR && Util::checkOrCreateDirectory(CFG_SESSION_CACHE_DIR)) + session_save_path(CFG_SESSION_CACHE_DIR); + session_set_cookie_params(15 * YEAR, '/', '', $secure, true); session_cache_limiter('private'); session_start(); diff --git a/includes/user.class.php b/includes/user.class.php index e4e67df8..b0dc04a0 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -39,7 +39,7 @@ class User // check IP bans if ($ipBan = DB::Aowow()->selectRow('SELECT count, unbanDate FROM ?_account_bannedips WHERE ip = ? AND type = 0', self::$ip)) { - if ($ipBan['count'] > CFG_FAILED_AUTH_COUNT && $ipBan['unbanDate'] > time()) + if ($ipBan['count'] > CFG_ACC_FAILED_AUTH_COUNT && $ipBan['unbanDate'] > time()) return false; else if ($ipBan['unbanDate'] <= time()) DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE ip = ?', self::$ip); @@ -213,7 +213,7 @@ class User $user = 0; $hash = ''; - switch (CFG_AUTH_MODE) + switch (CFG_ACC_AUTH_MODE) { case AUTH_MODE_SELF: { @@ -223,11 +223,11 @@ class User // handle login try limitation $ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 0 AND ip = ?', self::$ip); if (!$ip || $ip['unbanDate'] < time()) // no entry exists or time expired; set count to 1 - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, CFG_FAILED_AUTH_EXCLUSION); + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, CFG_ACC_FAILED_AUTH_BLOCK); else // entry already exists; increment count - DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ?', CFG_FAILED_AUTH_EXCLUSION, self::$ip); + DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ?', CFG_ACC_FAILED_AUTH_BLOCK, self::$ip); - if ($ip && $ip['count'] >= CFG_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time()) + if ($ip && $ip['count'] >= CFG_ACC_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time()) return AUTH_IPBANNED; $query = DB::Aowow()->SelectRow(' diff --git a/includes/utilities.php b/includes/utilities.php index a8827cad..a362bb0d 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -18,6 +18,8 @@ class SimpleXML extends SimpleXMLElement class Util { + const FILE_ACCESS = 0755; + public static $resistanceFields = array( null, 'resHoly', 'resFire', 'resNature', 'resFrost', 'resShadow', 'resArcane' ); @@ -688,6 +690,10 @@ class Util 'large' => 'style="background-image: url(%s/images/wow/icons/large/%s.jpg)"', ); + public static $configCats = array( + 'Site', 'Caching', 'Account', 'Session', 'Site Reputation', 'Other' + ); + public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; public static $wowheadLink = ''; private static $notes = []; @@ -1705,6 +1711,21 @@ class Util return json_encode($data, $flags); } + + public static function checkOrCreateDirectory($path) + { + // remove multiple slashes + $path = preg_replace('|/+|', '/', $path); + + if (!is_dir($path) && !@mkdir($path, self::FILE_ACCESS, true)) + self::addNote(U_GROUP_EMPLOYEE, 'could not create directory: '.$path); + else if (!is_writable($path) && !@chmod($path, self::FILE_ACCESS)) + self::addNote(U_GROUP_EMPLOYEE, 'cannot write into directory: '.$path); + else + return true; + + return false; + } } ?> diff --git a/index.php b/index.php index 20ae26ee..9cc47359 100644 --- a/index.php +++ b/index.php @@ -109,6 +109,7 @@ switch ($pageCall) case 'cookie': // lossless cookies and user settings case 'contactus': case 'comment': + // case 'filter': // just a note: this would be accessed from filtrable pages as ?filter=typeStr (with POST-data) and forwards back to page with GET-data .. why? Hell if i know.. case 'go-to-comment': // find page the comment is on and forward case 'locale': // subdomain-workaround, change the language if (($_ = (new AjaxHandler($pageParam))->handle($pageCall)) !== null) diff --git a/pages/account.php b/pages/account.php index 7261f46b..5f87f98b 100644 --- a/pages/account.php +++ b/pages/account.php @@ -73,7 +73,7 @@ class AccountPage extends GenericPage switch ($this->category[0]) { case 'forgotpassword': - if (CFG_AUTH_MODE != AUTH_MODE_SELF) // only recover own accounts + if (CFG_ACC_AUTH_MODE != AUTH_MODE_SELF) // only recover own accounts $this->error(); $this->tpl = 'acc-recover'; @@ -85,7 +85,7 @@ class AccountPage extends GenericPage $this->head = sprintf(Lang::account('recoverPass'), $nStep); break; case 'forgotusername': - if (CFG_AUTH_MODE != AUTH_MODE_SELF) // only recover own accounts + if (CFG_ACC_AUTH_MODE != AUTH_MODE_SELF) // only recover own accounts $this->error(); $this->tpl = 'acc-recover'; @@ -123,7 +123,7 @@ class AccountPage extends GenericPage break; case 'signup': - if (!CFG_ALLOW_REGISTER || CFG_AUTH_MODE != AUTH_MODE_SELF) + if (!CFG_ACC_ALLOW_REGISTER || CFG_ACC_AUTH_MODE != AUTH_MODE_SELF) $this->error(); $this->tpl = 'acc-signUp'; @@ -142,7 +142,7 @@ class AccountPage extends GenericPage { $nStep = 2; DB::Aowow()->query('UPDATE ?_account SET status = ?d WHERE token = ?', ACC_STATUS_OK, $_GET['token']); - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, ?d + 1, UNIX_TIMESTAMP() + ?d)', User::$ip, CFG_FAILED_AUTH_COUNT, CFG_FAILED_AUTH_EXCLUSION); + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, ?d + 1, UNIX_TIMESTAMP() + ?d)', User::$ip, CFG_ACC_FAILED_AUTH_COUNT, CFG_ACC_FAILED_AUTH_BLOCK); Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); @@ -371,7 +371,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup return Lang::account('accInactive'); case AUTH_IPBANNED: User::destroy(); - return sprintf(Lang::account('loginExceeded'), Util::formatTime(CFG_FAILED_AUTH_EXCLUSION * 1000)); + return sprintf(Lang::account('loginExceeded'), Util::formatTime(CFG_ACC_FAILED_AUTH_BLOCK * 1000)); case AUTH_INTERNAL_ERR: User::destroy(); return Lang::main('intError'); @@ -403,10 +403,10 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup // limit account creation $ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 1 AND ip = ?', User::$ip); - if ($ip && $ip['count'] >= CFG_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time()) + if ($ip && $ip['count'] >= CFG_ACC_FAILED_AUTH_COUNT && $ip['unbanDate'] >= time()) { - DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', CFG_FAILED_AUTH_EXCLUSION, User::$ip); - return sprintf(Lang::account('signupExceeded'), Util::formatTime(CFG_FAILED_AUTH_EXCLUSION * 1000)); + DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', CFG_ACC_FAILED_AUTH_BLOCK, User::$ip); + return sprintf(Lang::account('signupExceeded'), Util::formatTime(CFG_ACC_FAILED_AUTH_BLOCK * 1000)); } // username taken @@ -424,18 +424,18 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup $this->_post['remember_me'] != 'yes', User::$localeId, ACC_STATUS_NEW, - CFG_ACCOUNT_CREATE_SAVE_DECAY, + CFG_ACC_CREATE_SAVE_DECAY, $token ); if (!$id) // something went wrong return Lang::main('intError'); - else if ($_ = $this->sendMail(Lang::mail('accConfirm', 0), sprintf(Lang::mail('accConfirm', 1), $token), CFG_ACCOUNT_CREATE_SAVE_DECAY)) + else if ($_ = $this->sendMail(Lang::mail('accConfirm', 0), sprintf(Lang::mail('accConfirm', 1), $token), CFG_ACC_CREATE_SAVE_DECAY)) { // success:: update ip-bans if (!$ip || $ip['unbanDate'] < time()) - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, 1, UNIX_TIMESTAMP() + ?d)', User::$ip, CFG_FAILED_AUTH_EXCLUSION); + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, 1, UNIX_TIMESTAMP() + ?d)', User::$ip, CFG_ACC_FAILED_AUTH_BLOCK); else - DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', CFG_FAILED_AUTH_EXCLUSION, User::$ip); + DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', CFG_ACC_FAILED_AUTH_BLOCK, User::$ip); return $_; } @@ -443,11 +443,11 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup private function doRecoverPass() { - if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_PASS, CFG_ACCOUNT_RECOVERY_DECAY, $token)) + if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_PASS, CFG_ACC_RECOVERY_DECAY, $token)) return $_; // send recovery mail - return $this->sendMail(Lang::mail('resetPass', 0), sprintf(Lang::mail('resetPass', 1), $token), CFG_ACCOUNT_RECOVERY_DECAY); + return $this->sendMail(Lang::mail('resetPass', 0), sprintf(Lang::mail('resetPass', 1), $token), CFG_ACC_RECOVERY_DECAY); } private function doResetPass() @@ -475,11 +475,11 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup private function doRecoverUser() { - if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_USER, CFG_ACCOUNT_RECOVERY_DECAY, $token)) + if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_USER, CFG_ACC_RECOVERY_DECAY, $token)) return $_; // send recovery mail - return $this->sendMail(Lang::mail('recoverUser', 0), sprintf(Lang::mail('recoverUser', 1), $token), CFG_ACCOUNT_RECOVERY_DECAY); + return $this->sendMail(Lang::mail('recoverUser', 0), sprintf(Lang::mail('recoverUser', 1), $token), CFG_ACC_RECOVERY_DECAY); } private function initRecovery($type, $delay, &$token) diff --git a/pages/admin.php b/pages/admin.php index f8a3726c..245a1537 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -60,12 +60,13 @@ class AdminPage extends GenericPage private function handleConfig() { $this->addCSS(array( - ['string' => '.grid input[type=\'text\'] { width:250px; }'], + ['string' => '.grid input[type=\'text\'], .grid input[type=\'number\'] { width:250px; text-align:left; }'], ['string' => '.grid input[type=\'button\'] { width:65px; padding:2px; }'], - ['string' => '.disabled { opacity:0.4 !important; }'], ['string' => '.grid a.tip { margin:0px 5px; opacity:0.8; }'], ['string' => '.grid a.tip:hover { opacity:1; }'], - ['string' => '.status { position:absolute; right:5px; }'], + ['string' => '.grid tr { height:30px; }'], + ['string' => '.grid .disabled { opacity:0.4 !important; }'], + ['string' => '.grid .status { position:absolute; right:5px; }'], )); // well .. fuck! @@ -256,7 +257,7 @@ class AdminPage extends GenericPage } else if (node.tagName == 'INPUT') // string or numeric { - if (node.value.search(/[^\d\s\/\*\-\+\.]/i) == -1) + if (node.value && node.value.search(/[^\d\s\/\*\-\+\.]/i) == -1) node.value = eval(node.value); value = node.value; @@ -264,7 +265,7 @@ class AdminPage extends GenericPage value = value.toString().trim(); - if (!value.length) + if (!value.length && (node.tagName != 'INPUT' || node.type != 'text')) { $WH.ae(_status, createStatusIcon('value is empty')); return; @@ -298,7 +299,7 @@ class AdminPage extends GenericPage else if (node.tagName == 'SELECT') // opt-list $(node).find('option').each(function(idx, opt) { opt.selected = opt.value == val; }); else if (node.tagName == 'INPUT') // string or numeric - node.value = val; + node.value = node.type == 'text' ? val : eval(val); } function cfg_remove(id) @@ -339,42 +340,27 @@ class AdminPage extends GenericPage $head = ''; - // for aowow - if ($rows = DB::Aowow()->select('SELECT * FROM ?_config WHERE (flags & ?d) = 0 ORDER BY `key` ASC', CON_FLAG_PHP)) + foreach (Util::$configCats as $id => $catName) + if ($rows = DB::Aowow()->select('SELECT * FROM ?_config WHERE cat = ?d ORDER BY `flags`DESC, `key` ASC', $id)) { $buff = $head; foreach ($rows as $r) $buff .= $this->configAddRow($r); + if ($id == 5) //cat: misc + $buff .= ''; + $buff .= '
KeyValueOptions
new configuration
'; $this->lvTabs[] = array( 'file' => null, 'data' => $buff, 'params' => array( - 'name' => 'Aowow', - 'id' => 'aowow' + 'name' => $catName, + 'id' => Util::urlize($catName) ) ); } - - // for php - $rows = DB::Aowow()->select('SELECT * FROM ?_config WHERE flags & ?d ORDER BY `key` ASC', CON_FLAG_PHP); - $buff = $head; - foreach ($rows as $r) - $buff .= $this->configAddRow($r); - - $buff .= 'new configuration'; - $buff .= ''; - - $this->lvTabs[] = array( - 'file' => null, - 'data' => $buff, - 'params' => array( - 'name' => 'PHP', - 'id' => 'php' - ) - ); } private function handlePhpInfo() @@ -490,7 +476,7 @@ class AdminPage extends GenericPage { $buff = ''; $info = explode(' - ', $r['comment']); - $key = $r['flags'] & CON_FLAG_PHP ? strtolower($r['key']) : 'CFG_'.strtoupper($r['key']); + $key = $r['flags'] & CON_FLAG_PHP ? strtolower($r['key']) : strtoupper($r['key']); // name if (!empty($info[1])) @@ -522,7 +508,7 @@ class AdminPage extends GenericPage $buff .= ''; } else - $buff .= ''; + $buff .= ''; // actions $buff .= ''; diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index f1e2be26..4b9be014 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -88,6 +88,9 @@ class GenericPage { $this->time = microtime(true); + if (CFG_CACHE_DIR && Util::checkOrCreateDirectory(CFG_CACHE_DIR)) + $this->cacheDir = substr(CFG_CACHE_DIR, -1) != '/' ? CFG_CACHE_DIR.'/' : CFG_CACHE_DIR; + // force page refresh if (isset($_GET['refresh']) && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV)) { diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 939d8054..1ecf4c50 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -376,6 +376,7 @@ DROP TABLE IF EXISTS `aowow_config`; CREATE TABLE `aowow_config` ( `key` varchar(25) NOT NULL, `value` varchar(255) NOT NULL, + `cat` tinyint(3) unsigned NOT NULL DEFAULT '5', `flags` tinyint(3) unsigned NOT NULL DEFAULT '0', `comment` varchar(255) NOT NULL, PRIMARY KEY (`key`) @@ -2282,7 +2283,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_config` WRITE; /*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',129,'default: 500 - max results for search'),('sql_limit_default','300',129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',129,'default: 10 - max results for suggestions'),('sql_limit_none','0',129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',129,'default: 60 - time to live for RSS (in seconds)'),('cache_decay','25200',129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('session_timeout_delay','3600',129,'default: 60 * 60 - non-permanent session times out in time() + X'),('failed_auth_exclusion','900',129,'default: 15 * 60 - how long an account is closed after exceeding failed_auth_count (in seconds)'),('failed_auth_count','5',129,'default: 5 - how often invalid passwords are tolerated'),('name','Aowow Database Viewer (ADV)',136,' - website title'),('name_short','Aowow',136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('allow_register','1',132,'default: 1 - allow/disallow account creation (requires auth_mode 0)'),('debug','0',132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',132,'default: 0 - display brb gnomes and block access for non-staff'),('auth_mode','0',145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('rep_req_upvote','125',129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',129,'default: 100 - activated an account'),('rep_reward_upvoted','5',129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',129,'default: -200 - moderator revoked rights'),('user_max_votes','50',129,'default: 50 - vote limit per day'),('rep_req_votemore_add','250',129,'default: 250 - required reputation per additional vote past threshold'),('force_ssl','0',132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('cache_mode','1',161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('locales','333',161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('account_create_save_decay','604800',129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('account_recovery_decay','300',129,'default: 300 - time to recover your account and new recovery requests are blocked'),('serialize_precision','4',65,' - some derelict code, probably unused'),('screenshot_min_size','200',129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',136,' - points js to executable files'),('static_host','',136,' - points js to images & scripts'),('memory_limit','2048M',200,'default: 2048M - parsing spell.dbc is quite intense'); +INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',0,129,'default: 500 - max results for search'),('sql_limit_default','300',0,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',0,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',0,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',0,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',0,136,' - website title'),('name_short','Aowow',0,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',0,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',0,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',0,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',0,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',0,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',0,129,'default: 50 - vote limit per day'),('force_ssl','0',0,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',0,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',0,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',0,136,' - points js to executable files'),('static_host','',0,136,' - points js to images & scripts'),('cache_decay','25200',1,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',1,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',1,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',2,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',2,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',2,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',2,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',2,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',2,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',3,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',3,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','0',3,200,'default: 0 - probability to remove session data on garbage collection'),('session_cache_dir','',3,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',4,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',4,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',4,129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',4,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',4,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',4,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',4,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',4,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',4,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',4,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',4,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',4,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',4,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',4,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',4,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',4,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',4,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',4,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','4',5,65,' - some derelict code, probably unused'),('memory_limit','2048M',5,200,'default: 2048M - parsing spell.dbc is quite intense'); /*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/setup.php b/setup/setup.php index ca423bc4..973235f1 100644 --- a/setup/setup.php +++ b/setup/setup.php @@ -23,7 +23,7 @@ require_once 'setup/tools/imagecreatefromblp.func.php'; function finish() { if (!getopt('d', ['delete'])) // generated with TEMPORARY keyword. Manual deletion is not needed - CLISetup::log('generated dbc_* - tables kept available'); + CLISetup::log('generated dbc_* - tables kept available', CLISetup::LOG_INFO); // send "i'm in use @" - ping $u = !empty($_SERVER['USER']) ? $_SERVER['USER'] : 'NULL'; diff --git a/setup/tools/CLISetup.class.php b/setup/tools/CLISetup.class.php index 63e2167c..618b0bed 100644 --- a/setup/tools/CLISetup.class.php +++ b/setup/tools/CLISetup.class.php @@ -17,11 +17,10 @@ class CLISetup const CHR_ESC = 27; const CHR_BACKSPACE = 127; - const FILE_ACCESS = 0755; - const LOG_OK = 0; const LOG_WARN = 1; const LOG_ERROR = 2; + const LOG_INFO = 3; private static $win = true; private static $logFile = ''; @@ -200,6 +199,11 @@ class CLISetup return "\e[33m".$str."\e[0m"; } + public static function blue($str) + { + return "\e[36m".$str."\e[0m"; + } + public static function bold($str) { return "\e[1m".$str."\e[0m"; @@ -230,15 +234,18 @@ class CLISetup $msg = str_pad(date('H:i:s'), 10); switch ($lvl) { - case self::LOG_ERROR: // red error + case self::LOG_ERROR: // red critical error $msg .= '['.self::red('ERR').'] '; break; - case self::LOG_WARN: // yellow warn - $msg .= '['.self::yellow('INFO').'] '; + case self::LOG_WARN: // yellow notice + $msg .= '['.self::yellow('WARN').'] '; break; case self::LOG_OK: // green success $msg .= '['.self::green('OK').'] '; break; + case self::LOG_INFO: // blue info + $msg .= '['.self::blue('INFO').'] '; + break; default: $msg .= ' '; } @@ -281,7 +288,7 @@ class CLISetup self::log(sprintf(ERR_CREATE_FILE, self::bold($file)), self::LOG_ERROR); if ($success) - @chmod($file, self::FILE_ACCESS); + @chmod($file, Util::FILE_ACCESS); return $success; } @@ -290,13 +297,13 @@ class CLISetup { if (is_dir($dir)) { - if (!is_writable($dir) && !@chmod($dir, self::FILE_ACCESS)) + if (!is_writable($dir) && !@chmod($dir, Util::FILE_ACCESS)) self::log('cannot write into output directory '.$dir, self::LOG_ERROR); return is_writable($dir); } - if (@mkdir($dir, self::FILE_ACCESS, true)) + if (@mkdir($dir, Util::FILE_ACCESS, true)) return true; self::log('could not create output directory '.$dir, self::LOG_ERROR); diff --git a/setup/tools/clisetup/account.func.php b/setup/tools/clisetup/account.func.php index 2ca70a85..f3260ab1 100644 --- a/setup/tools/clisetup/account.func.php +++ b/setup/tools/clisetup/account.func.php @@ -55,7 +55,7 @@ function account() else { CLISetup::log(); - CLISetup::log("account creation aborted", CLISetup::LOG_WARN); + CLISetup::log("account creation aborted", CLISetup::LOG_INFO); } } diff --git a/setup/tools/clisetup/dbconfig.func.php b/setup/tools/clisetup/dbconfig.func.php index b0acc649..b5b0dda7 100644 --- a/setup/tools/clisetup/dbconfig.func.php +++ b/setup/tools/clisetup/dbconfig.func.php @@ -132,7 +132,7 @@ function dbconfig() else { CLISetup::log(); - CLISetup::log("edit canceled! returning to list...", CLISetup::LOG_WARN); + CLISetup::log("edit canceled! returning to list...", CLISetup::LOG_INFO); sleep(1); continue 2; } @@ -141,7 +141,7 @@ function dbconfig() else { CLISetup::log(); - CLISetup::log("db setup aborted", CLISetup::LOG_WARN); + CLISetup::log("db setup aborted", CLISetup::LOG_INFO); break 2; } } diff --git a/setup/tools/clisetup/siteconfig.func.php b/setup/tools/clisetup/siteconfig.func.php index bda95f6f..0ff1e5ff 100644 --- a/setup/tools/clisetup/siteconfig.func.php +++ b/setup/tools/clisetup/siteconfig.func.php @@ -13,6 +13,8 @@ if (!CLI) function siteconfig() { + $reqKeys = ['SITE_HOST', 'STATIC_HOST']; + if (!DB::isConnected(DB_AOWOW)) { CLISetup::log(); @@ -25,55 +27,65 @@ function siteconfig() CLISetup::log(); CLISetup::log('select a numerical index to use the corresponding entry'); - $results = DB::Aowow()->select('SELECT *, (flags & ?d) AS php FROM ?_config ORDER BY php ASC', CON_FLAG_PHP); + $sumNum = 0; + $cfgList = []; $hasEmpty = false; - - foreach ($results as $idx => $data) + foreach (Util::$configCats as $idx => $cat) { - if (!($data['flags'] & CON_FLAG_PHP) && $data['value'] === '') - $hasEmpty = true; + CLISetup::log('===== '.$cat.' ====='); + $results = DB::Aowow()->select('SELECT *, (flags & ?d) AS php FROM ?_config WHERE `cat` = ?d ORDER BY `key` ASC', CON_FLAG_PHP, $idx); - $php = $data['flags'] & CON_FLAG_PHP; - $buff = "[".CLISetup::bold($idx)."] ".($idx > 9 ? '' : ' ').($php ? ' PHP ' : ' AOWOW '); - $buff .= str_pad($php ? strtolower($data['key']) : strtoupper('cfg_'.$data['key']), 35); - if ($data['value'] === '') - $buff .= CLISetup::red(''); - else + foreach ($results as $num => $data) { - $info = explode(' - ', $data['comment']); + if (!($data['flags'] & CON_FLAG_PHP) && $data['value'] === '' && in_array($data['key'], $reqKeys)) + $hasEmpty = true; - if ($data['flags'] & CON_FLAG_TYPE_BOOL) - $buff .= '[bool] '.($data['value'] ? '' : ''); - else if ($data['flags'] & CON_FLAG_OPT_LIST && !empty($info[2])) + $cfgList[$sumNum + $num] = $data; + + $php = $data['flags'] & CON_FLAG_PHP; + $buff = "[".CLISetup::bold($sumNum + $num)."] ".(($sumNum + $num) > 9 ? '' : ' ').($php ? ' PHP ' : ' AOWOW '); + $buff .= str_pad($php ? strtolower($data['key']) : strtoupper($data['key']), 35); + if ($data['value'] === '') + $buff .= in_array($data['key'], $reqKeys) ? CLISetup::red('') : ''; + else { - $buff .= "[opt] "; - foreach (explode(', ', $info[2]) as $option) + $info = explode(' - ', $data['comment']); + + if ($data['flags'] & CON_FLAG_TYPE_BOOL) + $buff .= '[bool] '.($data['value'] ? '' : ''); + else if ($data['flags'] & CON_FLAG_OPT_LIST && !empty($info[2])) { - $opt = explode(':', $option); - $buff .= '['.($data['value'] == $opt[0] ? 'x' : ' ').']'.$opt[1].' '; + $buff .= "[opt] "; + foreach (explode(', ', $info[2]) as $option) + { + $opt = explode(':', $option); + $buff .= '['.($data['value'] == $opt[0] ? 'x' : ' ').']'.$opt[1].' '; + } } - } - else if ($data['flags'] & CON_FLAG_BITMASK && !empty($info[2])) - { - $buff .= "[mask] "; - foreach (explode(', ', $info[2]) as $option) + else if ($data['flags'] & CON_FLAG_BITMASK && !empty($info[2])) { - $opt = explode(':', $option); - $buff .= '['.($data['value'] & (1 << $opt[0]) ? 'x' : ' ').']'.$opt[1].' '; + $buff .= "[mask] "; + foreach (explode(', ', $info[2]) as $option) + { + $opt = explode(':', $option); + $buff .= '['.($data['value'] & (1 << $opt[0]) ? 'x' : ' ').']'.$opt[1].' '; + } } + else if ($data['flags'] & CON_FLAG_TYPE_STRING) + $buff .= "[str] ".$data['value']; + else if ($data['flags'] & CON_FLAG_TYPE_FLOAT) + $buff .= "[float] ".floatVal($data['value']); + else /* if ($data['flags'] & CON_FLAG_TYPE_INT) */ + $buff .= "[int] ".intVal($data['value']); } - else if ($data['flags'] & CON_FLAG_TYPE_STRING) - $buff .= "[str] ".$data['value']; - else if ($data['flags'] & CON_FLAG_TYPE_FLOAT) - $buff .= "[float] ".floatVal($data['value']); - else /* if ($data['flags'] & CON_FLAG_TYPE_INT) */ - $buff .= "[int] ".intVal($data['value']); + + CLISetup::log($buff); } - CLISetup::log($buff); + $sumNum += count($results); } - CLISetup::log(str_pad("[".CLISetup::bold(count($results))."]", 21)."add another php configuration"); + CLISetup::log(str_pad("[".CLISetup::bold($sumNum)."]", 21)."add another php configuration"); if ($hasEmpty) { @@ -85,7 +97,7 @@ function siteconfig() if (CLISetup::readInput($inp) && $inp && $inp['idx'] !== '') { // add new php setting - if ($inp['idx'] == count($results)) + if ($inp['idx'] == $sumNum) { CLISetup::log(); CLISetup::log("Adding additional php configuration."); @@ -123,16 +135,16 @@ function siteconfig() else { CLISetup::log(); - CLISetup::log("edit canceled! returning to list...", CLISetup::LOG_WARN); + CLISetup::log("edit canceled! returning to list...", CLISetup::LOG_INFO); sleep(1); break; } } } // edit existing setting - else if ($inp['idx'] >= 0 && $inp['idx'] < count($results)) + else if ($inp['idx'] >= 0 && $inp['idx'] < $sumNum) { - $conf = $results[$inp['idx']]; + $conf = $cfgList[$inp['idx']]; $info = explode(' - ', $conf['comment']); $buff = ''; @@ -240,11 +252,11 @@ function siteconfig() while (true) { $use = $value; - if (CLISetup::readInput($use, $single) && $use) + if (CLISetup::readInput($use, $single)) { CLISetup::log(); - if (!$validate($use['idx'])) + if (!$validate($use ? $use['idx'] : '')) { CLISetup::log("value not in range", CLISetup::LOG_ERROR); sleep(1); @@ -260,7 +272,7 @@ function siteconfig() } else { - CLISetup::log("edit canceled! returning to selection...", CLISetup::LOG_WARN); + CLISetup::log("edit canceled! returning to selection...", CLISetup::LOG_INFO); sleep(1); break; } @@ -293,7 +305,7 @@ function siteconfig() else { CLISetup::log(); - CLISetup::log("edit canceled! returning to list...", CLISetup::LOG_WARN); + CLISetup::log("edit canceled! returning to list...", CLISetup::LOG_INFO); sleep(1); break; } @@ -309,7 +321,7 @@ function siteconfig() else { CLISetup::log(); - CLISetup::log("site configuration aborted", CLISetup::LOG_WARN); + CLISetup::log("site configuration aborted", CLISetup::LOG_INFO); break; } } diff --git a/setup/tools/filegen/complexImg.func.php b/setup/tools/filegen/complexImg.func.php index 510920e8..f09f80ec 100644 --- a/setup/tools/filegen/complexImg.func.php +++ b/setup/tools/filegen/complexImg.func.php @@ -83,7 +83,7 @@ if (!CLI) $file = $path.'.png'; if (CLISetup::fileExists($file)) { - CLISetup::log('manually converted png file present for '.$path.'.', CLISetup::LOG_WARN); + CLISetup::log('manually converted png file present for '.$path.'.', CLISetup::LOG_INFO); $result = imagecreatefrompng($file); } @@ -152,7 +152,7 @@ if (!CLI) if ($ok) { - chmod($name.'.'.$ext, CLISetup::FILE_ACCESS); + chmod($name.'.'.$ext, Util::FILE_ACCESS); CLISetup::log($done.' - image '.$name.'.'.$ext.' written', CLISetup::LOG_OK); } else @@ -396,7 +396,7 @@ if (!CLI) $p = sprintf($imgPath, $mapLoc).$paths[0]; if (CLISetup::fileExists($p)) { - CLISetup::log(' - using files from '.($mapLoc ?: '/').' for locale '.Util::$localeStrings[$l], CLISetup::LOG_WARN); + CLISetup::log(' - using files from '.($mapLoc ?: '/').' for locale '.Util::$localeStrings[$l], CLISetup::LOG_INFO); $mapSrcDir = $p.'/'; break; } diff --git a/setup/tools/filegen/simpleImg.func.php b/setup/tools/filegen/simpleImg.func.php index 6564467f..9e9ee830 100644 --- a/setup/tools/filegen/simpleImg.func.php +++ b/setup/tools/filegen/simpleImg.func.php @@ -27,7 +27,7 @@ if (!CLI) $file = $path.'.png'; if (CLISetup::fileExists($file)) { - CLISetup::log('manually converted png file present for '.$path.'.', CLISetup::LOG_WARN); + CLISetup::log('manually converted png file present for '.$path.'.', CLISetup::LOG_INFO); $result = imagecreatefrompng($file); } @@ -183,7 +183,7 @@ if (!CLI) if ($ok) { - chmod($name.'.'.$ext, CLISetup::FILE_ACCESS); + chmod($name.'.'.$ext, Util::FILE_ACCESS); CLISetup::log($done.' - image '.$name.'.'.$ext.' written', CLISetup::LOG_OK); } else diff --git a/setup/updates/1436392800_01.sql b/setup/updates/1436392800_01.sql new file mode 100644 index 00000000..9e912077 --- /dev/null +++ b/setup/updates/1436392800_01.sql @@ -0,0 +1,15 @@ +ALTER TABLE `aowow_config` + ADD COLUMN `cat` TINYINT(3) UNSIGNED NOT NULL DEFAULT '5' AFTER `value`; + +INSERT IGNORE INTO `aowow_config` (`key`, `value`, `cat`, `flags`, `comment`) VALUES + ('cache_dir', '', 1, 136, 'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'), + ('session.gc_maxlifetime', '604800', 3, 200, 'default: 7*24*60*60 - lifetime of session data'), + ('session.gc_probability', '0', 3, 200, 'default: 0 - probability to remove session data on garbage collection'), + ('session_cache_dir', '', 3, 136, 'default: - php sessions are saved here. Leave empty to use php default directory.'); + +UPDATE `aowow_config` SET `key` = 'acc_failed_auth_block' WHERE `key` = 'failed_auth_exclusion'; +UPDATE `aowow_config` SET `key` = 'acc_failed_auth_count' WHERE `key` = 'failed_auth_count'; +UPDATE `aowow_config` SET `key` = 'acc_allow_register' WHERE `key` = 'allow_register'; +UPDATE `aowow_config` SET `key` = 'acc_auth_mode' WHERE `key` = 'auth_mode'; +UPDATE `aowow_config` SET `key` = 'acc_create_save_decay' WHERE `key` = 'account_create_save_decay'; +UPDATE `aowow_config` SET `key` = 'acc_recovery_decay' WHERE `key` = 'account_recovery_decay'; diff --git a/template/pages/acc-signIn.tpl.php b/template/pages/acc-signIn.tpl.php index 5228695d..5503d52f 100644 --- a/template/pages/acc-signIn.tpl.php +++ b/template/pages/acc-signIn.tpl.php @@ -61,7 +61,7 @@
'.Lang::account('accCreate')."\n"; endif; ?> From f6d80cf4738581a136437027ee425bd6549d3a45 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 9 Jul 2015 23:29:20 +0200 Subject: [PATCH 0022/1249] Spawns * display phaseMask as hexadecimal * hide phasemasks, containing every single phase (0xFFFF) --- includes/types/basetype.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/types/basetype.class.php b/includes/types/basetype.class.php index fb17643b..fd60ae81 100644 --- a/includes/types/basetype.class.php +++ b/includes/types/basetype.class.php @@ -580,8 +580,8 @@ trait spawnHelper if (User::isInGroup(U_GROUP_STAFF)) { - if ($s['phaseMask'] > 1) - $label[] = Lang::game('phases').Lang::main('colon').$s['phaseMask']; + if ($s['phaseMask'] > 1 && ($s['phaseMask'] & 0xFFFF) != 0xFFFF) + $label[] = Lang::game('phases').Lang::main('colon').Util::asHex($s['phaseMask']); if ($s['spawnMask'] == 15) $label[] = Lang::game('mode').Lang::main('colon').Lang::game('modes', -1); From 08270ae09ed86497b56b75756e6a367e42cdfc91 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 9 Jul 2015 23:31:57 +0200 Subject: [PATCH 0023/1249] Screenshots * reworked upload process to be more user-friendly * handle bricks that consist mostly of localized text separately * added handle to display errors, when adding CommunityContent on the page they originated from --- includes/utilities.php | 4 +- localization/lang.class.php | 4 +- localization/locale_dede.php | 38 +- localization/locale_enus.php | 38 +- localization/locale_eses.php | 38 +- localization/locale_frfr.php | 38 +- localization/locale_ruru.php | 38 +- pages/genericPage.class.php | 50 +- pages/screenshot.php | 330 ++--- static/js/screenshot.js | 1125 ++++++++++++++++- template/bricks/contribute.tpl.php | 2 +- .../{bricks => localized}/contrib_0.tpl.php | 11 +- .../{bricks => localized}/contrib_2.tpl.php | 17 +- .../{bricks => localized}/contrib_3.tpl.php | 17 +- .../{bricks => localized}/contrib_6.tpl.php | 17 +- .../{bricks => localized}/contrib_8.tpl.php | 17 +- template/localized/ssReminder_0.tpl.php | 10 + template/localized/ssReminder_3.tpl.php | 10 + template/pages/screenshot.tpl.php | 84 +- 19 files changed, 1518 insertions(+), 370 deletions(-) rename template/{bricks => localized}/contrib_0.tpl.php (90%) rename template/{bricks => localized}/contrib_2.tpl.php (87%) rename template/{bricks => localized}/contrib_3.tpl.php (89%) rename template/{bricks => localized}/contrib_6.tpl.php (87%) rename template/{bricks => localized}/contrib_8.tpl.php (90%) create mode 100644 template/localized/ssReminder_0.tpl.php create mode 100644 template/localized/ssReminder_3.tpl.php diff --git a/includes/utilities.php b/includes/utilities.php index a362bb0d..8b162347 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1395,11 +1395,11 @@ class Util public static function createHash($length = 40) // just some random numbers for unsafe identifictaion purpose { - static $seed = ".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + static $seed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; $hash = ''; for ($i = 0; $i < $length; $i++) - $hash .= substr($seed, mt_rand(0, 62), 1); + $hash .= substr($seed, mt_rand(0, 61), 1); return $hash; } diff --git a/localization/lang.class.php b/localization/lang.class.php index f87ffca8..5f775d37 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -8,7 +8,10 @@ class Lang private static $user; private static $mail; private static $game; + private static $maps; + private static $screenshot; + // types private static $achievement; private static $chrClass; private static $currency; @@ -17,7 +20,6 @@ class Lang private static $gameObject; private static $item; private static $itemset; - private static $maps; private static $npc; private static $pet; private static $quest; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 51b01ba7..0e89a619 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -143,30 +143,24 @@ $lang = array( 'genericError' => "Ein Fehler trat auf; aktualisiert die Seite und versucht es nochmal. Wenn der Fehler bestehen bleibt, bitte meldet es bei feedback", # LANG.genericerror 'bannedRating' => "Ihr wurdet davon gesperrt, Kommentare zu bewerten.", # LANG.tooltip_banned_rating 'tooManyVotes' => "Ihr habt die tägliche Grenze für erlaubte Bewertungen erreicht. Kommt morgen mal wieder!", # LANG.tooltip_too_many_votes - - // screenshots - 'prepError' => "Bei der Aufbereitung eures Screenshots ist ein Fehler aufgetreten", - 'cropHint' => "Schneidet das Bild zu, indem ihr die Auswahl verschiebt.
Bitte beachtet Screenshots: Tipps & Tricks für eine optimale Darstellung.", + ), + 'screenshot' => array( + 'submission' => "Screenshot-Einsendung", + 'selectAll' => "Alles auswählen", + 'cropHint' => "Ihr könnt Euren Screenshot zuschneiden und beschriften.", + 'displayOn' => "Hochgeladen für:[br]%s - [%s=%d]", 'caption' => "Kurzbeschreibung", - 'originalSize' => "Originalgröße", - 'targetSize' => "Zielgröße", - 'minSize' => "Mindestgröße", - 'displayOn' => "Hochgeladen für: %s[br][%s=%d]", - 'ssEdit' => "Screenshot bearbeiten", - 'ssUpload' => "Screenshot hochladen", - 'ssSubmit' => "Screenshot einsenden", - 'ssErrors' => array( - 'noUpload' => "Die Datei wurde nicht hochgeladen!", - 'maxSize' => "Die Datei überschreitet die max. Größe von %s!", - 'interrupted' => "Der Vorgang wurde unterbrochen!", - 'noFile' => "Es wurde keine Datei empfangen!", - 'noDest' => "Die Seite auf welcher der Screenshot angezeigt werden sollte existiert nicht!", + 'charLimit' => "Optional, bis zu 200 Zeichen", + 'thanks' => array( + 'contrib' => "Vielen Dank für Euren Beitrag!", + 'goBack' => 'Klickt hier, um zu der vorherigen Seite zurückzukehren.', + 'note' => "Hinweis: Euer Screenshot muss zunächst zugelassen werden, bevor es auf der Seite erscheint. Dies kann bis zu 72 Stunden dauern." + ), + 'error' => array( + 'unkFormat' => "Unbekanntes Bildformat.", + 'tooSmall' => "Euer Screenshot ist viel zu klein. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'selectSS' => "Wählt bitte den Screenshot aus, den Ihr hochladen möchtet.", 'notAllowed' => "Es ist euch nicht erlaubt einen Screenshot hochzuladen!", - 'noImage' => "Die hochgeladene Datei ist kein Bild!", - 'wrongFormat' => "Das Bild muss im PNG oder JPG-Format sein!", - 'load' => "Das Bild konnte nicht geladen werden!", - 'tooSmall' => "Die Abmessungen sind zu klein! (kleiner als %d x %d)", - 'tooLarge' => "Die Abmessungen sind zu groß! (größer als %d x %d)" ) ), 'game' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 876d88ca..3e115810 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -138,30 +138,24 @@ $lang = array( 'genericError' => "An error has occurred; refresh the page and try again. If the error persists email feedback", # LANG.genericerror 'bannedRating' => "You have been banned from rating comments.", # LANG.tooltip_banned_rating 'tooManyVotes' => "You have reached the daily voting cap. Come back tomorrow!", # LANG.tooltip_too_many_votes - - // screenshots - 'prepError' => "An error occured preparing your screenshot", - 'cropHint' => "Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.", + ), + 'screenshot' => array( + 'submission' => "Screenshot Submission", + 'selectAll' => "Select all", + 'cropHint' => "You may crop your screenshot and enter a caption.", + 'displayOn' => "Displayed on:[br]%s - [%s=%d]", 'caption' => "Caption", - 'originalSize' => "Original size", - 'targetSize' => "Target size", - 'minSize' => "Minimum size", - 'displayOn' => "Displayed on: %s[br][%s=%d]", - 'ssEdit' => "Edit uploaded screenshot", - 'ssUpload' => "Screenshot Upload", - 'ssSubmit' => "Submit Screenshot", - 'ssErrors' => array( - 'noUpload' => "The file was not uploaded!", - 'maxSize' => "The file exceeds the maximum size of %s!", - 'interrupted' => "The upload process was interrupted!", - 'noFile' => "The file was not received!", - 'noDest' => "The page this screenshot should be displayed on, does not exist!", + 'charLimit' => "Optional, up to 200 characters", + 'thanks' => array( + 'contrib' => "Thanks a lot for your contribution!", + 'goBack' => 'Click here to go back to the page you came from.', + 'note' => "Note: Your screenshot will need to be approved before appearing on the site. This can take up to 72 hours." + ), + 'error' => array( + 'unkFormat' => "Unknown image format.", + 'tooSmall' => "Your screenshot is way too small. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'selectSS' => "Please select the screenshot to upload.", 'notAllowed' => "You are not allowed to upload screenshots!", - 'noImage' => "The uploaded file is not an image file!", - 'wrongFormat' => "The image file must be a png or jpg!", - 'load' => "The image file could not be loaded!", - 'tooSmall' => "The image size is too small! (lower than %d x %d)", - 'tooLarge' => "The image size is too large! (greater than %d x %d)" ) ), 'game' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 7b56d5aa..e158657a 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -143,30 +143,24 @@ $lang = array( 'genericError' => "Ha ocurrido un error; refresca la página e inténtalo de nuevo. Si el error persiste manda un correo a feedback", # LANG.genericerror 'bannedRating' => "Has sido baneado y no podrás valorar comentarios.", # LANG.tooltip_banned_rating 'tooManyVotes' => "Has alcanzado el límite diario de votos. Vuelve mañana.", # LANG.tooltip_too_many_votes - - // screenshots - 'prepError' => "[An error occured preparing your screenshot]", - 'cropHint' => "[Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.]", + ), + 'screenshot' => array( + 'submission' => "Enviar una captura de pantalla", + 'selectAll' => "Seleccionar todos", + 'cropHint' => "Puede reducir su imagen e introducir una etiqueta.", + 'displayOn' => "[Displayed on:[br]%s - [%s=%d]]", 'caption' => "[Caption]", - 'originalSize' => "[Original size]", - 'targetSize' => "[Target size]", - 'minSize' => "[Minimum size]", - 'displayOn' => "[Displayed on: %s[br][%s=%d]]", - 'ssEdit' => "[Edit uploaded screenshot]", - 'ssUpload' => "[Screenshot Upload]", - 'ssSubmit' => "[Submit Screenshot]", - 'ssErrors' => array( - 'noUpload' => "[The file was not uploaded!]", - 'maxSize' => "[The file exceeds the maximum size of %s!]", - 'interrupted' => "[The upload process was interrupted!]", - 'noFile' => "[The file was not received!]", - 'noDest' => "[The page this screenshot should be displayed on, does not exist!]", + 'charLimit' => "Opcional, hasta 200 caracteres", + 'thanks' => array( + 'contrib' => "¡Muchísimas gracias por tu aportación!", + 'goBack' => 'aquí vuelve a la página de la que viniste.', + 'note' => "Nota: Su captura de imagen tiene que ser aprobada antes de que pueda aparecer en el sitio. Esto puede tomar hasta 72 horas." + ), + 'error' => array( + 'unkFormat' => "Formato de imagen desconocido.", + 'tooSmall' => "Su captura de pantalla es muy pequeña. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'selectSS' => "Por favor seleccione la captura de pantalla para subir.", 'notAllowed' => "[You are not allowed to upload screenshots!]", - 'noImage' => "[The uploaded file is not an image file!]", - 'wrongFormat' => "[The image file must be a png or jpg!]", - 'load' => "[The image file could not be loaded!]", - 'tooSmall' => "[The image size is too small! (lower than %d x %d)]", - 'tooLarge' => "[The image size is too large! (greater than %d x %d)]" ) ), 'game' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 67941ed4..3ef26007 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -143,30 +143,24 @@ $lang = array( 'genericError' => "Une erreur est survenue; Actualisez la page et essayez à nouveau. Si l'erreur persiste, envoyez un email à feedback", # LANG.genericerror 'bannedRating' => "Vous avez été banni du score des commentaires.", # LANG.tooltip_banned_rating 'tooManyVotes' => "Vous avez voté trop souvent aujourd'hui! Revenez demain.", # LANG.tooltip_too_many_votes - - // screenshots - 'prepError' => "[An error occured preparing your screenshot]", - 'cropHint' => "[Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.]", + ), + 'screenshot' => array( + 'submission' => "Envoi d'une capture d'écran", + 'selectAll' => "Sélectionner tout", + 'cropHint' => "Vous pouvez recadrer votre capture d'écran.", + 'displayOn' => "[Displayed on:[br]%s - [%s=%d]]", 'caption' => "[Caption]", - 'originalSize' => "[Original size]", - 'targetSize' => "[Target size]", - 'minSize' => "[Minimum size]", - 'displayOn' => "[Displayed on: %s[br][%s=%d]]", - 'ssEdit' => "[Edit uploaded screenshot]", - 'ssUpload' => "[Screenshot Upload]", - 'ssSubmit' => "[Submit Screenshot]", - 'ssErrors' => array( - 'noUpload' => "[The file was not uploaded!]", - 'maxSize' => "[The file exceeds the maximum size of %s!]", - 'interrupted' => "[The upload process was interrupted!]", - 'noFile' => "[The file was not received!]", - 'noDest' => "[The page this screenshot should be displayed on, does not exist!]", + 'charLimit' => "Optionnel, jusqu'à 200 caractères", + 'thanks' => array( + 'contrib' => "Merci beaucoup de votre contribution!", + 'goBack' => 'ici pour retourner à la page d\'où vous venez.', + 'note' => "Note : Votre capture d'écran devra être approuvée avant d'apparaître sur le site. Cela peut prendre jusqu'à 72 heures." + ), + 'error' => array( + 'unkFormat' => "Format d'image inconnu.", + 'tooSmall' => "Votre capture est bien trop petite. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'selectSS' => "Veuillez sélectionner la capture d'écran à envoyer.", 'notAllowed' => "[You are not allowed to upload screenshots!]", - 'noImage' => "[The uploaded file is not an image file!]", - 'wrongFormat' => "[The image file must be a png or jpg!]", - 'load' => "[The image file could not be loaded!]", - 'tooSmall' => "[The image size is too small! (lower than %d x %d)]", - 'tooLarge' => "[The image size is too large! (greater than %d x %d)]" ) ), 'game' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 59867a62..ced46cfc 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -143,30 +143,24 @@ $lang = array( 'genericError' => "Произошла ошибка; обновите Ñтраницу и попробуйте Ñнова. ЕÑли ÑÐ¸Ñ‚ÑƒÐ°Ñ†Ð¸Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€ÑетÑÑ, отправьте Ñообщение на feedback", # LANG.genericerror 'bannedRating' => "Вам была заблокирована возможноÑть оценивать комментарии.", # LANG.tooltip_banned_rating 'tooManyVotes' => "Ð’Ñ‹ ÑÐµÐ³Ð¾Ð´Ð½Ñ Ð¿Ñ€Ð¾Ð³Ð¾Ð»Ð¾Ñовали Ñлишком много раз! Ð’Ñ‹ Ñможете продолжить завтра.", # LANG.tooltip_too_many_votes - - // screenshots - 'prepError' => "[An error occured preparing your screenshot]", - 'cropHint' => "[Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.]", + ), + 'screenshot' => array( + 'submission' => "Добавление изображениÑ", + 'selectAll' => "Выбрать вÑÑ‘", + 'cropHint' => "Ð’Ñ‹ можете произвеÑти кадрирование Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¸ указать заголовок.", + 'displayOn' => "[Displayed on:[br]%s - [%s=%d]]", 'caption' => "[Caption]", - 'originalSize' => "[Original size]", - 'targetSize' => "[Target size]", - 'minSize' => "[Minimum size]", - 'displayOn' => "[Displayed on: %s[br][%s=%d]]", - 'ssEdit' => "[Edit uploaded screenshot]", - 'ssUpload' => "[Screenshot Upload]", - 'ssSubmit' => "[Submit Screenshot]", - 'ssErrors' => array( - 'noUpload' => "[The file was not uploaded!]", - 'maxSize' => "[The file exceeds the maximum size of %s!]", - 'interrupted' => "[The upload process was interrupted!]", - 'noFile' => "[The file was not received!]", - 'noDest' => "[The page this screenshot should be displayed on, does not exist!]", + 'charLimit' => "Ðе обÑзательно, вплоть до 200 знаков", + 'thanks' => array( + 'contrib' => "СпаÑибо за ваш вклад!", + 'goBack' => 'здеÑÑŒ чтобы перейти к предыдущей Ñтранице.', + 'note' => "Примечание: Перед поÑвлением на Ñайте, ваше изображение должно быть одобрено. Это может занÑть до 72 чаÑов." + ), + 'error' => array( + 'unkFormat' => "неизвеÑтный формат изображениÑ.", + 'tooSmall' => "Изображение Ñлишком маленькое. (< ".CFG_SCREENSHOT_MIN_SIZE."x".CFG_SCREENSHOT_MIN_SIZE.").", + 'selectSS' => "Выберите изображение Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸.", 'notAllowed' => "[You are not allowed to upload screenshots!]", - 'noImage' => "[The uploaded file is not an image file!]", - 'wrongFormat' => "[The image file must be a png or jpg!]", - 'load' => "[The image file could not be loaded!]", - 'tooSmall' => "[The image size is too small! (lower than %d x %d)]", - 'tooLarge' => "[The image size is too large! (greater than %d x %d)]" ) ), 'game' => array( diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index 4b9be014..5c55ffce 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -10,7 +10,11 @@ trait DetailPage protected $category = null; // not used on detail pages protected $lvTabs = []; // most pages have this - private $subject = null; // so it will not get cached + protected $ssError = null; + protected $coError = null; + protected $viError = null; + + protected $subject = null; // so it will not get cached protected function generateCacheKey($withStaff = true) { @@ -25,6 +29,21 @@ trait DetailPage return implode('_', $key); } + + + protected function applyCCErrors() + { + if (!empty($_SESSION['error']['co'])) + $this->coError = $_SESSION['error']['co']; + + if (!empty($_SESSION['error']['ss'])) + $this->ssError = $_SESSION['error']['ss']; + + if (!empty($_SESSION['error']['vi'])) + $this->viError = $_SESSION['error']['vi']; + + unset($_SESSION['error']); + } } @@ -133,6 +152,10 @@ class GenericPage $this->maintenance(); else if (CFG_MAINTENANCE && User::isInGroup(U_GROUP_EMPLOYEE)) Util::addNote(U_GROUP_EMPLOYEE, 'Maintenance mode enabled!'); + + // get errors from previous page from session and apply to template + if (method_exists($this, 'applyCCErrors')) + $this->applyCCErrors(); } /**********/ @@ -369,8 +392,8 @@ class GenericPage array_unshift($this->title, Lang::main('nfPageTitle')); $this->notFound = array( - 'title' => $this->typeId ? Util::ucFirst($title).' #'.$this->typeId : $title, - 'msg' => !$msg && $this->typeId ? sprintf(Lang::main('pageNotFound'), $title) : $msg + 'title' => isset($this->typeId) ? Util::ucFirst($title).' #'.$this->typeId : $title, + 'msg' => !$msg && isset($this->typeId) ? sprintf(Lang::main('pageNotFound'), $title) : $msg ); $this->hasComContent = false; Util::arraySumByKey($this->mysql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); @@ -509,6 +532,19 @@ class GenericPage include('template/listviews/'.$file.'.tpl.php'); } + public function localizedBrick($file, $loc = LOCALE_EN) // load brick with more text then vars + { + if (!$this->isSaneInclude('template/localized/', $file.'_'.$loc)) + { + if ($loc == LOCALE_EN || !$this->isSaneInclude('template/localized/', $file.'_'.LOCALE_EN)) + echo User::isInGroup(U_GROUP_EMPLOYEE) ? "\n\nError: nonexistant template requested: template/localized/".$file.'_'.$loc.".tpl.php\n\n" : null; + else + include('template/localized/'.$file.'_'.LOCALE_EN.'.tpl.php'); + } + else + include('template/localized/'.$file.'_'.$loc.'.tpl.php'); + } + /**********************/ /* Prepare js-Globals */ /**********************/ @@ -662,8 +698,9 @@ class GenericPage if (!CFG_CACHE_MODE || CFG_DEBUG) return; - $cKey = $this->generateCacheKey(); - $cache = []; + $noCache = ['coError', 'ssError', 'viError']; + $cKey = $this->generateCacheKey(); + $cache = []; if (!$saveString) { foreach ($this as $key => $val) @@ -672,7 +709,8 @@ class GenericPage { // public, protected and an undocumented flag added to properties created on the fly..? if ((new ReflectionProperty($this, $key))->getModifiers() & 0x1300) - $cache[$key] = $val; + if (!in_array($key, $noCache)) + $cache[$key] = $val; } catch (ReflectionException $e) { } // shut up! } diff --git a/pages/screenshot.php b/pages/screenshot.php index fda12f7b..b98fb30e 100644 --- a/pages/screenshot.php +++ b/pages/screenshot.php @@ -7,84 +7,194 @@ if (!defined('AOWOW_REVISION')) class ScreenshotPage extends GenericPage { + const MAX_W = 488; + const MAX_H = 325; + protected $tpl = 'screenshot'; protected $js = ['Cropper.js']; protected $css = [['path' => 'Cropper.css']]; protected $reqAuth = true; + protected $tabId = 0; private $tmpPath = 'static/uploads/temp/'; private $pendingPath = 'static/uploads/screenshots/pending/'; private $destination = null; + private $minSize = CFG_SCREENSHOT_MIN_SIZE; + + protected $validCats = ['add', 'crop', 'complete', 'thankyou']; protected $destType = 0; protected $destTypeId = 0; + protected $imgHash = ''; public function __construct($pageCall, $pageParam) { parent::__construct($pageCall, $pageParam); - $this->name = Lang::main('ssEdit'); - // do not htmlEscape caption. It's applied as textnode - $this->caption = !empty($_POST['screenshotcaption']) ? $_POST['screenshotcaption'] : ''; + $this->name = Lang::screenshot('submission'); + $this->command = $pageParam; - // what are its other uses..? (finalize is custom) - if ($pageParam == 'finalize') + if ($this->minSize <= 0) { - if (!$this->handleFinalize()) - $this->error(); + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::__construct() - config error: dimensions for uploaded screenshots egual or less than zero. Value forced to 200'); + $this->minSize = 200; } - else if ($pageParam != 'add') - $this->error(); // get screenshot destination - foreach ($_GET as $k => $v) + // target delivered as screenshot=&.. (hash is optional) + if (preg_match('/^screenshot=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'], $m)) { - if ($v) // target delivered as empty type.typeId key - continue; - - $x = explode('_', $k); // . => _ as array key - if (count($x) != 2) - continue; - // no such type - if (empty(Util::$typeClasses[$x[0]])) - continue; + if (empty(Util::$typeClasses[$m[1]])) + $this->error(); - $t = Util::$typeClasses[$x[0]]; - $c = [['id', intVal($x[1])]]; - if ($x[0] == TYPE_WORLDEVENT) // ohforfsake.. - $c = array_merge($c, ['OR', ['holidayId', intVal($x[1])]]); + $t = Util::$typeClasses[$m[1]]; + $c = [['id', intVal($m[2])]]; + if ($m[1] == TYPE_WORLDEVENT && $m[2] < 0) // ohforfsake.. + $c = [['id', -intVal($m[2])]]; $this->destination = new $t($c); // no such typeId - if ($this->destination->error) - continue; + if ($this->destination->error) + $this->error(); - $this->destType = intVal($x[0]); - $this->destTypeId = intVal($x[1]); + // only accept/expect hash for crop & complete + if (empty($m[4]) && ($this->command == 'crop' || $this->command == 'complete')) + $this->error(); + else if (!empty($m[4]) && ($this->command == 'add' || $this->command == 'thankyou')) + $this->error(); + else if (!empty($m[4])) + $this->imgHash = $m[4]; + + $this->destType = intVal($m[1]); + $this->destTypeId = intVal($m[2]); + } + else + $this->error(); + } + + protected function generateContent() + { + switch ($this->command) + { + case 'add': + if ($this->handleAdd()) + header('Location: ?screenshot=crop&'.$this->destType.'.'.$this->destTypeId.'.'.$this->imgHash, true, 302); + else + header('Location: ?'.Util::$typeStrings[$this->destType].'='.$this->destTypeId.'#submit-a-screenshot', true, 302); + die(); + case 'crop': + $this->handleCrop(); + break; + case 'complete': + if ($_ = $this->handleComplete()) + $this->notFound(Lang::main('nfPageTitle'), sprintf(Lang::main('intError2'), '#'.$_)); + else + header('Location: ?screenshot=thankyou&'.$this->destType.'.'.$this->destTypeId, true, 302); + die(); + case 'thankyou': + $this->tpl = 'text-page-generic'; + $this->handleThankyou(); + break; } } - private function handleFinalize() + + /*******************/ + /* command handler */ + /*******************/ + + + private function handleAdd() { - if (empty($_SESSION['ssUpload'])) + $this->imgHash = Util::createHash(16); + + if (User::$banStatus & ACC_BAN_SCREENSHOT) + { + $_SESSION['error']['ss'] = Lang::screenshot('error', 'notAllowed'); return false; + } - // as its saved in session it should be valid - $file = $_SESSION['ssUpload']['file']; + if ($_ = $this->validateScreenshot($isPNG)) + { + $_SESSION['error']['ss'] = $_; + return false; + } + $im = $isPNG ? $this->loadFromPNG() : $this->loadFromJPG(); + if (!$im) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + $oSize = $rSize = [imagesx($im), imagesy($im)]; + $rel = $oSize[0] / $oSize[1]; + + // check for oversize and refit to crop-screen + if ($rel >= 1.5 && $oSize[0] > self::MAX_W) + $rSize = [self::MAX_W, self::MAX_W / $rel]; + else if ($rel < 1.5 && $oSize[1] > self::MAX_H) + $rSize = [self::MAX_H * $rel, self::MAX_H]; + + $name = User::$displayName.'-'.$this->destType.'-'.$this->destTypeId.'-'.$this->imgHash; + + $this->writeImage($im, $oSize, $name.'_original'); // use this image for work + $this->writeImage($im, $rSize, $name); // use this image to display + + return true; + } + + private function handleCrop() + { + $im = imagecreatefromjpeg($this->tmpPath.$this->ssName().'_original.jpg'); + + $oSize = $rSize = [imagesx($im), imagesy($im)]; + $rel = $oSize[0] / $oSize[1]; + + // check for oversize and refit to crop-screen + if ($rel >= 1.5 && $oSize[0] > self::MAX_W) + $rSize = [self::MAX_W, self::MAX_W / $rel]; + else if ($rel < 1.5 && $oSize[1] > self::MAX_H) + $rSize = [self::MAX_H * $rel, self::MAX_H]; + + // r: resized; o: original + // r: x <= 488 && y <= 325 while x proportional to y + // mincrop is optional and specifies the minimum resulting image size + $this->cropper = [ + 'url' => STATIC_URL.'/uploads/temp/'.$this->ssName().'.jpg', + 'parent' => 'ss-container', + 'oWidth' => $oSize[0], + 'rWidth' => $rSize[0], + 'oHeight' => $oSize[1], + 'rHeight' => $rSize[1], + 'type' => $this->destType, // only used to check against NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] + 'typeId' => $this->destTypeId // i guess this was used to upload arbitrary imagery + ]; + + // minimum dimensions + if (!User::isInGroup(U_GROUP_STAFF)) + $this->cropper['minCrop'] = $this->minSize; + + // target + $this->infobox = sprintf(Lang::screenshot('displayOn'), Util::ucFirst(Lang::game(Util::$typeStrings[$this->destType])), Util::$typeStrings[$this->destType], $this->destTypeId); + $this->extendGlobalIds($this->destType, $this->destTypeId); + } + + private function handleComplete() + { // check tmp file - $fullPath = $this->tmpPath.$file.'_original.jpg'; + $fullPath = $this->tmpPath.$this->ssName().'_original.jpg'; if (!file_exists($fullPath)) - return false; + return 1; // check post data - if (empty($_POST) || empty($_POST['selection'])) - return false; + if (empty($_POST) || empty($_POST['coords'])) + return 2; - $dims = explode(',', $_POST['selection']); + $dims = explode(',', $_POST['coords']); if (count($dims) != 4) - return false; + return 3; Util::checkNumeric($dims); @@ -105,109 +215,32 @@ class ScreenshotPage extends GenericPage // write to db $newId = DB::Aowow()->query( 'INSERT INTO ?_screenshots (type, typeId, userIdOwner, date, width, height, caption) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, ?d, ?)', - $_SESSION['ssUpload']['type'], $_SESSION['ssUpload']['typeId'], + $this->destType, $this->destTypeId, User::$id, $w, $h, - $this->caption + !empty($_POST['screenshotalt']) ? $_POST['screenshotalt'] : '' ); // write to file if (is_int($newId)) // 0 is valid, NULL or FALSE is not imagejpeg($destImg, $this->pendingPath.$newId.'.jpg', 100); - - unset($_SESSION['ssUpload']); - header('Location: ?user='.User::$displayName.'#screenshots'); + else + return 6; } - protected function generateContent() + private function handleThankyou() { - $maxW = 488; - $maxH = 325; - $minCrop = CFG_SCREENSHOT_MIN_SIZE; - - if ($minCrop <= 0) - { - Util::addNote(U_GROUP_DEV | U_GROUP_ADMIN, 'ScreenshotPage::generateContent() - config error: dimensions for uploaded screenshots egual or less than zero. Value forced to 200'); - $minCrop = 200; - } - - if (!$this->destType) - { - $this->error = Lang::main('ssErrors', 'noDest'); - return; - } - - if (User::$banStatus & ACC_BAN_SCREENSHOT) - { - $this->error = Lang::main('ssErrors', 'notAllowed'); - return; - } - - if ($_ = $this->validateScreenshot($isPNG)) - { - $this->error = $_; - return; - } - - $im = $isPNG ? $this->loadFromPNG() : $this->loadFromJPG(); - if (!$im) - { - $this->error = Lang::main('ssErrors', 'load'); - return; - } - - $name = User::$displayName.'-'.$this->destType.'-'.$this->destTypeId.'-'.Util::createHash(16); - $oSize = $rSize = [imagesx($im), imagesy($im)]; - $rel = $oSize[0] / $oSize[1]; - - // not sure if this is the best way - $_SESSION['ssUpload'] = array( - 'file' => $name, - 'type' => $this->destType, - 'typeId' => $this->destTypeId - ); - - // check for oversize and refit to crop-screen - if ($rel >= 1.5 && $oSize[0] > $maxW) - $rSize = [$maxW, $maxW / $rel]; - else if ($rel < 1.5 && $oSize[1] > $maxH) - $rSize = [$maxH * $rel, $maxH]; - - $this->writeImage($im, $oSize, $name.'_original'); // use this image for work - $this->writeImage($im, $rSize, $name); // use this image to display - - // r: resized; o: original - // r: x <= 488 && y <= 325 while x proportional to y - // mincrop is optional and specifies the minimum resulting image size - $this->cropper = [ - 'url' => $this->tmpPath.$name.'.jpg', - 'parent' => 'ss-container', - 'oWidth' => $oSize[0], - 'rWidth' => $rSize[0], - 'oHeight' => $oSize[1], - 'rHeight' => $rSize[1], - ]; - - $infobox = []; - - // target - $infobox[] = sprintf(Lang::main('displayOn'), Util::ucFirst(Lang::game(Util::$typeStrings[$this->destType])), Util::$typeStrings[$this->destType], $this->destTypeId); - $this->extendGlobalIds($this->destType, $this->destTypeId); - - // dimensions - $infobox[] = Lang::main('originalSize').Lang::main('colon').$oSize[0].' x '.$oSize[1]; - $infobox[] = Lang::main('targetSize').Lang::main('colon').'[span id=qf-newSize][/span]'; - - // minimum dimensions - if (!User::isInGroup(U_GROUP_STAFF)) - { - $infobox[] = Lang::main('minSize').Lang::main('colon').$minCrop.' x '.$minCrop; - $this->cropper['minCrop'] = $minCrop; - } - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; + $this->extraHTML = Lang::screenshot('thanks', 'contrib').'

'; + $this->extraHTML .= sprintf(Lang::screenshot('thanks', 'goBack'), Util::$typeStrings[$this->destType], $this->destTypeId)."

\n"; + $this->extraHTML .= ''.Lang::screenshot('thanks', 'note').''; } + + /**********/ + /* helper */ + /**********/ + + private function loadFromPNG() { $image = imagecreatefrompng($_FILES['screenshotfile']['tmp_name']); @@ -240,35 +273,38 @@ class ScreenshotPage extends GenericPage { // no upload happened or some error occured if (!$_FILES || empty($_FILES['screenshotfile'])) - return Lang::main('ssErrors', 'noUpload'); + return Lang::screenshot('error', 'selectSS'); switch ($_FILES['screenshotfile']['error']) { case 1: - return sprintf(Lang::main('ssErrors', 'maxSize'), ini_get('upload_max_filesize'));; + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - the file exceeds the maximum size of '.ini_get('upload_max_filesize')); + return Lang::screenshot('error', 'selectSS'); case 3: - return Lang::main('ssErrors', 'interrupted'); + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - upload was interrupted'); + return Lang::screenshot('error', 'selectSS'); case 4: - return Lang::main('ssErrors', 'noFile'); + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - no file was received'); + return Lang::screenshot('error', 'selectSS'); case 6: - Util::addNote(U_GROUP_ADMIN, 'ScreenshotPage::validateScreenshot() - temporary upload directory is not set'); + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - temporary upload directory is not set'); return Lang::main('intError'); case 7: - Util::addNote(U_GROUP_ADMIN, 'ScreenshotPage::validateScreenshot() - could not write temporary file to disk'); - return Lang::main('genericError'); + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - could not write temporary file to disk'); + return Lang::main('intError'); } // points to invalid file (hack attempt) if (!is_uploaded_file($_FILES['screenshotfile']['tmp_name'])) { - Util::addNote(U_GROUP_ADMIN, 'ScreenshotPage::validateScreenshot() - uploaded file not in upload directory'); - return Lang::main('genericError'); + Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - uploaded file not in upload directory'); + return Lang::main('intError'); } // invalid file $is = getimagesize($_FILES['screenshotfile']['tmp_name']); if (!$is || empty($is['mime'])) - return Lang::main('ssErrors', 'notImage'); + return Lang::screenshot('error', 'selectSS'); // allow jpeg, png switch ($is['mime']) @@ -279,23 +315,27 @@ class ScreenshotPage extends GenericPage case 'image/jpeg': break; default: - return Lang::main('ssErrors', 'wrongFormat'); + return Lang::screenshot('error', 'unkFormat'); } // size-missmatch: 4k UHD upper limit; 150px lower limit - if ($is[0] < 150 || $is[1] < 150) - return sprintf(Lang::main('ssErrors', 'tooSmall'), 150, 150); - - if ($is[0] > 3840 || $is[1] > 2160) - return sprintf(Lang::main('ssErrors', 'tooLarge'), 150, 150); + if ($is[0] < $this->minSize || $is[1] < $this->minSize) + return Lang::screenshot('error', 'tooSmall'); + else if ($is[0] > 3840 || $is[1] > 2160) + return Lang::screenshot('error', 'selectSS'); return null; } + private function ssName() + { + return $this->imgHash ? User::$displayName.'-'.$this->destType.'-'.$this->destTypeId.'-'.$this->imgHash : ''; + } + protected function generatePath() { } protected function generateTitle() { - array_unshift($this->title, Lang::main('ssUpload')); + array_unshift($this->title, Lang::screenshot('submission')); } } diff --git a/static/js/screenshot.js b/static/js/screenshot.js index 8515855a..d13821bc 100644 --- a/static/js/screenshot.js +++ b/static/js/screenshot.js @@ -1 +1,1124 @@ -var ss_managedRow = null; var ss_getAll = false; // never changed (maybe with ?admin=screenshots&all) var ssm_ViewedRow = null; var ssm_screenshotData = []; var ssm_screenshotPages = []; var ssm_numPagesFound = 0; var ssm_numPages = 0; // never accessed var ssm_numPending = 0; var ssm_statuses = { 0 : 'Pending', 999: 'Deleted', 100: 'Approved', 105: 'Sticky' }; function makePipe() { var sp = $WH.ce('span'); $WH.ae(sp, $WH.ct(' ')); var sm = $WH.ce('small'); sm.className = 'q0'; $WH.ae(sm, $WH.ct('|')); $WH.ae(sp, sm); $WH.ae(sp, $WH.ct(' ')); return sp; } function ss_OnResize() { var _ = Math.max(100, Math.min($WH.g_getWindowSize().h - 50, 700)); $WH.ge('menu-container').style.height = $WH.ge('pages-container').style.height = _ + 'px'; $WH.ge('data-container').style.height = _ + 'px'; } $WH.aE(window, 'resize', ss_OnResize); function ss_Refresh(openNext, type, typeId) { new Ajax('?admin=screenshots&action=list' + (ss_getAll ? '&all' : ''), { method: 'get', onSuccess: function (xhr) { eval(xhr.responseText); if (ssm_screenshotPages.length > 0) { $WH.ge('show-all-pages').innerHTML = ' – Show All (' + ssm_numPagesFound + ')'; ssm_UpdatePages(); if (openNext) { ss_Manage($WH.ge('pages-container').firstChild.firstChild, ssm_screenshotPages[0].type, ssm_screenshotPages[0].typeId, true); } else if (type && typeId) { ss_Manage(null, type, typeId, true); } } else { $WH.ee($WH.ge('show-all-pages')); $WH.ge('pages-container').innerHTML = 'NO SCREENZSHOT NEEDS 2 BE APPRVED NOW KTHX. :)'; if (type && typeId) { ss_Manage(null, type, typeId, true); } } } }); } function ss_Manage(_this, type, typeId, openNext) { new Ajax('?admin=screenshots&action=manage&type=' + type + '&typeid=' + typeId, { method: 'get', onSuccess: function (xhr) { eval(xhr.responseText); ssm_numPending = 0; for (var i in ssm_screenshotData) { if (ssm_screenshotData[i].pending) { ssm_numPending++; } } var nRows = ssm_screenshotData.length; $WH.ge('screenshotTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); ssm_UpdateList(openNext); ssm_UpdateMassLinks(); if (ss_managedRow != null) { ss_ColorizeRow('transparent'); } ss_managedRow = _this; if (ss_managedRow != null) { ss_ColorizeRow('#282828'); } } }); } function ss_ManageUser() { var username = $WH.ge('usermanage'); username.value = $WH.trim(username.value); if (username.value.length < 4) { alert('Username must be at least 4 characters long.'); username.focus(); return false; } if (username.value.match(/[^a-z0-9]/i) != null) { alert('Username can only contain letters and numbers.'); username.focus(); return false; } new Ajax('?admin=screenshots&action=manage&user=' + username.value, { method: 'get', onSuccess: function (xhr) { eval(xhr.responseText); var nRows = ssm_screenshotData.length; $WH.ge('screenshotTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); ssm_UpdateList(); ssm_UpdateMassLinks(); if (ss_managedRow != null) { ss_ColorizeRow('transparent'); } } }); return true; } function ss_ColorizeRow(color) { for (var i = 0; i < ss_managedRow.childNodes.length; ++i) { ss_managedRow.childNodes[i].style.backgroundColor = color; } } function ssm_GetScreenshot(id) { for (var i in ssm_screenshotData) { if (ssm_screenshotData[i].id == id) { return ssm_screenshotData[i]; } } return null; } function ssm_View(row, id) { if (ssm_ViewedRow != null) { ssm_ColorizeRow('transparent'); } ssm_ViewedRow = row; ssm_ColorizeRow('#282828'); var screenshot = ssm_GetScreenshot(id); if (screenshot != null) { ScreenshotManager.show(screenshot); } } function ssm_ColorizeRow(color) { for (var i = 0; i < ssm_ViewedRow.childNodes.length; ++i) { ssm_ViewedRow.childNodes[i].style.backgroundColor = color; } } function ssm_ConfirmMassApprove() { ajaxAnchor(this); // sarjuuk custom - there has to be something in place or we are manually using a script for ajax return false; // return true; } function ssm_ConfirmMassDelete() { if (confirm('Delete selected screenshot(s)?')) // sarjuuk custom - see above ajaxAnchor(this); return false; // return confirm('Delete selected screenshot(s)?'); } function ssm_ConfirmMassSticky() { if (confirm('Sticky selected screenshot(s)?')) // sarjuuk custom - see above ajaxAnchor(this); return false; // return confirm('Sticky selected screenshot(s)?'); } function ssm_UpdatePages(UNUSED) { var pc = $WH.ge('pages-container'); $WH.ee(pc); var tbl = $WH.ce('table'); tbl.className = 'grid'; tbl.style.width = '400px'; var tr = $WH.ce('tr'); var th = $WH.ce('th'); $WH.ae(th, $WH.ct('Page')); $WH.ae(tr, th); th = $WH.ce('th'); $WH.ae(th, $WH.ct('Submitted')); $WH.ae(tr, th); th = $WH.ce('th'); th.align = 'right'; $WH.ae(th, $WH.ct('#')); $WH.ae(tr, th); $WH.ae(tbl, tr); var now = new Date(); for (var i in ssm_screenshotPages) { var ssPage = ssm_screenshotPages[i]; tr = $WH.ce('tr'); tr.onclick = ss_Manage.bind(tr, tr, ssPage.type, ssPage.typeId, true, i); var td = $WH.ce('td'); var a = $WH.ce('a'); a.href = '?' + g_types[ssPage.type] + '=' + ssPage.typeId; a.target = '_blank'; $WH.ae(a, $WH.ct(ssPage.name)); $WH.ae(td, a); $WH.ae(tr, td); td = $WH.ce('td'); var elapsed = new Date(ssPage.date); $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); $WH.ae(tr, td); td = $WH.ce('td'); td.align = 'right'; $WH.ae(td, $WH.ct(ssPage.count)); $WH.ae(tr, td); $WH.ae(tbl, tr); } $WH.ae(pc, tbl); } function ssm_UpdateList(openNext) { var tsl = $WH.ge('theScreenshotsList'); var tBody = false; var i = 1; while (tsl.childNodes.length > i) { if (tsl.childNodes[i].nodeName == 'TR' && tBody) { $WH.de(tsl.childNodes[i]); } else if (tsl.childNodes[i].nodeName == 'TR') { tBody = true; } else { i++; } } var now = new Date(); var ssId = 0; for (var i in ssm_screenshotData) { var screenshot = ssm_screenshotData[i]; var tr = $WH.ce('tr'); if (ssId == 0 && screenshot.pending) { ssId = screenshot.id; tr.id = 'highlightedRow'; } var td = $WH.ce('td'); td.align = 'center'; var a = $WH.ce('a'); a.href = g_staticUrl + '/uploads/screenshots/' + (screenshot.status != 999 && !screenshot.pending ? 'normal' : 'pending') + '/' + screenshot.id + '.jpg'; a.target = '_blank'; a.onclick = function (id, e) { $WH.sp(e); (ssm_View.bind(this, id))(); return false; }.bind(tr, screenshot.id); var img = $WH.ce('img'); img.src = g_staticUrl + '/uploads/screenshots/' + (screenshot.status != 999 && !screenshot.pending ? 'thumb' : 'pending') + '/' + screenshot.id + '.jpg'; img.height = 50; $WH.ae(a, img); $WH.ae(td, a); $WH.ae(tr, td); td = $WH.ce('td'); if (screenshot.status != 999 && !screenshot.pending) { var a = $WH.ce('a'); a.href = '?' + g_types[screenshot.type] + '=' + screenshot.typeId + '#screenshots:id=' + screenshot.id; a.target = '_blank'; a.onclick = function (e) { $WH.sp(e); }; $WH.ae(a, $WH.ct(screenshot.id)); $WH.ae(td, a); } else { $WH.ae(td, $WH.ct(screenshot.id)); } $WH.ae(tr, td); td = $WH.ce('td'); td.id = 'alt-' + screenshot.id; var sp = $WH.ce('span'); sp.style.paddingRight = '8px'; if (screenshot.caption) { var sp2 = $WH.ce('span'); sp2.className = 'q2'; var b = $WH.ce('b'); $WH.ae(b, $WH.ct(screenshot.caption)); $WH.ae(sp2, b); $WH.ae(sp, sp2); } else { var it = $WH.ce('i'); it.className = 'q0'; $WH.ae(it, $WH.ct('NULL')); $WH.ae(sp, it); } $WH.ae(td, sp); sp = $WH.ce('span'); sp.style.whiteSpace = 'nowrap'; var a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (id, e) { $WH.sp(e); (ssm_ShowEdit.bind(this, id))() }.bind(a, screenshot); $WH.ae(a, $WH.ct('Edit')); $WH.ae(sp, a); $WH.ae(sp, makePipe()); a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (id, e) { $WH.sp(e); (ssm_Clear.bind(this, id))() }.bind(a, screenshot); $WH.ae(a, $WH.ct('Clear')); $WH.ae(sp, a); $WH.ae(td, sp); $WH.ae(tr, td); td = $WH.ce('td'); var elapsed = new Date(screenshot.date); $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); $WH.ae(tr, td); td = $WH.ce('td'); a = $WH.ce('a'); a.href = '?user=' + screenshot.user; a.target = '_blank'; a.onclick = function (e) { $WH.sp(e); }; $WH.ae(a, $WH.ct(screenshot.user)); $WH.ae(td, a); $WH.ae(tr, td); td = $WH.ce('td'); $WH.ae(td, $WH.ct(ssm_statuses[screenshot.status])); $WH.ae(tr, td); td = $WH.ce('td'); var cb = $WH.ce('input'); cb.type = 'checkbox'; cb.value = screenshot.id; cb.onclick = function (e) { $WH.sp(e); (ssm_UpdateMassLinks.bind(this))(); }.bind(cb); $WH.ae(td, cb); $WH.ae(td, $WH.ct(' ')); if (screenshot.status != 999) { tr.onclick = function (id) { ssm_View(this, id); return false; }.bind(tr, screenshot.id); if (screenshot.id == ssId && openNext) { ssm_View(tr, screenshot.id); } if (screenshot.pending) { a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); (ssm_Approve.bind(this, false))() }.bind(screenshot); $WH.ae(a, $WH.ct('Approve')); $WH.ae(td, a); } else { $WH.ae(td, $WH.ct('Approve')); } $WH.ae(td, makePipe()); if (screenshot.status != 105) { a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); (ssm_Sticky.bind(this, false))(); }.bind(screenshot); $WH.ae(a, $WH.ct('Make sticky')); $WH.ae(td, a); } else { $WH.ae(td, $WH.ct('Make sticky')); } $WH.ae(td, makePipe()); a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); (ssm_Delete.bind(this, false))(); }.bind(screenshot); $WH.ae(a, $WH.ct('Delete')); $WH.ae(td, a); $WH.ae(td, makePipe()); a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); var id = prompt('Enter the ID to move this screenshot to:'); (ssm_Relocate.bind(this, id))(); }.bind(screenshot); $WH.ae(a, $WH.ct('Relocate')); $WH.ae(td, a); } $WH.ae(tr, td); $WH.ae(tsl, tr); } } function ssm_UpdateMassLinks() { var buff = ''; var i = 0; var tSL = $WH.ge('theScreenshotsList'); var inp = $WH.gE(tSL, 'input'); $WH.array_walk(inp, function (x) { if (x.checked) { buff += x.value + ','; ++i; } }); buff = $WH.rtrim(buff, ','); var selCnt = $WH.ge('withselected'); if (i > 0) { selCnt.style.display = ''; $WH.gE(selCnt, 'b')[0].firstChild.nodeValue = '(' + i + ')'; var c = $WH.ge('massapprove'); var b = $WH.ge('massdelete'); var a = $WH.ge('masssticky'); c.href = '?admin=screenshots&action=approve&id=' + buff; c.onclick = ssm_ConfirmMassApprove; b.href = '?admin=screenshots&action=delete&id=' + buff; b.onclick = ssm_ConfirmMassDelete; a.href = '?admin=screenshots&action=sticky&id=' + buff; a.onclick = ssm_ConfirmMassSticky; } else { selCnt.style.display = 'none'; } } function ssm_MassSelect(action) { var tSL = $WH.ge('theScreenshotsList'); var inp = $WH.gE(tSL, 'input'); switch (parseInt(action)) { case 1: $WH.array_walk(inp, function (x) { x.checked = true }); break; case 0: $WH.array_walk(inp, function (x) { x.checked = false }); break; case -1: $WH.array_walk(inp, function (x) { x.checked = !x.checked }); break; case 2: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 0 }); break; case 5: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).unique == 1 && ssm_GetScreenshot(x.value).status == 0 }); break; case 3: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 100 }); break; case 4: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 105 }); break; default: return; } ssm_UpdateMassLinks(); } function ssm_ShowEdit(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id) } else { node = $WH.ge('alt-' + screenshot.id) } var sp = $WH.gE(node, 'span')[0]; var div = $WH.ce('div'); div.style.whiteSpace = 'nowrap'; var iCaption = $WH.ce('input'); iCaption.type = 'text'; iCaption.value = screenshot.caption; iCaption.maxLength = 200; iCaption.size = 35; iCaption.onclick = function (e) { $WH.sp(e); } // sarjuuk - custom to inhibit screenshot popup, when clicking into input element div.appendChild(iCaption); var btn = $WH.ce('input'); btn.type = 'button'; btn.value = 'Update'; btn.onclick = function (i, j, k) { if (!j) { $WH.sp(k); } (ssm_Edit.bind(this, i, j))(); }.bind(btn, screenshot, isAlt); div.appendChild(btn); var c = $WH.ce('span'); c.appendChild($WH.ct(' ')); div.appendChild(c); btn = $WH.ce('input'); btn.type = 'button'; btn.value = 'Cancel'; btn.onclick = function (i, j, k) { if (!j) { $WH.sp(k); } (ssm_CancelEdit.bind(this, i, j))(); }.bind(btn, screenshot, isAlt); div.appendChild(btn); sp.style.display = 'none'; sp.nextSibling.style.display = 'none'; node.insertBefore(div, sp); iCaption.focus() } function ssm_CancelEdit(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id); } else { node = $WH.ge('alt-' + screenshot.id); } var sp = $WH.gE(node, 'span')[1]; sp.style.display = ''; sp.nextSibling.style.display = ''; node.removeChild(node.firstChild); } function ssm_Edit(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id); } else { node = $WH.ge('alt-' + screenshot.id); } var desc = node.firstChild.childNodes; if (desc[0].value == screenshot.caption) { ssm_CancelEdit(screenshot, isAlt); return } screenshot.caption = desc[0].value; ssm_CancelEdit(screenshot, isAlt); node = node.firstChild; while (node.childNodes.length > 0) { node.removeChild(node.firstChild); } $WH.ae(node, $WH.ct(screenshot.caption)); new Ajax('?admin=screenshots&action=editalt&id=' + screenshot.id, { method: 'POST', params: 'alt=' + $WH.urlencode(screenshot.caption) }) } function ssm_Clear(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id); } else { node = $WH.ge('alt-' + screenshot.id); } var sp = $WH.gE(node, 'span'); var a = $WH.gE(sp[1], 'a'); sp = sp[0]; if (screenshot.caption == '') { return; } screenshot.caption = ''; sp.innerHTML = 'NULL'; new Ajax('?admin=screenshots&action=editalt&id=' + screenshot.id, { method: 'POST', params: 'alt=' + $WH.urlencode('') }) } function ssm_Approve(openNext) { var _self = this; new Ajax('?admin=screenshots&action=approve&id=' + _self.id, { method: 'get', onSuccess: function (x) { Lightbox.hide(); if (ssm_numPending == 1 && _self.pending) { ss_Refresh(true); } else { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); } } }) } function ssm_Sticky(openNext) { var _self = this; new Ajax('?admin=screenshots&action=sticky&id=' + _self.id, { method: 'get', onSuccess: function (x) { Lightbox.hide(); if (ssm_numPending == 1 && _self.pending) { ss_Refresh(true); } else { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); } } }) } function ssm_Delete(openNext) { var _self = this; new Ajax('?admin=screenshots&action=delete&id=' + _self.id, { method: 'get', onSuccess: function (x) { Lightbox.hide(); if (ssm_numPending == 1 && _self.pending) { ss_Refresh(true); } else { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); } } }); } function ssm_Relocate(typeId) { var _self = this; new Ajax('?admin=screenshots&action=relocate&id=' + _self.id + '&typeid=' + typeId, { method: 'get', onSuccess: function (x) { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, typeId); } }); } var ScreenshotManager = new function () { var screenshot, pos, imgWidth, imgHeight, scale, desiredScale, container, screen, imgDiv, aPrev, aNext, aCover, aOriginal, divFrom, divCaption, __div, h2Name, u, aEdit, aClear, spApprove, aApprove, aMakeSticky, aDelete, loadingImage, lightboxComponents; function computeDimensions(captionExtraHeight) { var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 122 - captionExtraHeight)); if (screenshot.id) { desiredScale = Math.min(772 / screenshot.width, 618 / screenshot.height); scale = Math.min(772 / screenshot.width, availHeight / screenshot.height); } else { desiredScale = scale = 1; } if (desiredScale > 1) { desiredScale = 1; } if (scale > 1) { scale = 1; } imgWidth = Math.round(scale * screenshot.width); imgHeight = Math.round(scale * screenshot.height); var lbWidth = Math.max(480, imgWidth); Lightbox.setSize(lbWidth + 20, imgHeight + 116 + captionExtraHeight); if (captionExtraHeight) { imgDiv.firstChild.width = imgWidth; imgDiv.firstChild.height = imgHeight; } } function render(resizing) { if (resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) { return; } container.style.visibility = 'hidden'; var resized = (screenshot.width > 772 || screenshot.height > 618); computeDimensions(0); var url = g_staticUrl + '/uploads/screenshots/' + (screenshot.pending ? 'pending' : 'normal') + '/' + screenshot.id + '.jpg'; var html = ''; } divCaption.innerHTML = html; } else { divCaption.innerHTML = 'NULL'; } __div.id = 'alt2-' + screenshot.id; aEdit.onclick = ssm_ShowEdit.bind(aEdit, screenshot, true); aClear.onclick = ssm_Clear.bind(aClear, screenshot, true); if (screenshot.next !== undefined) { aPrev.style.display = aNext.style.display = ''; aCover.style.display = 'none'; } else { aPrev.style.display = aNext.style.display = 'none'; aCover.style.display = ''; } } Lightbox.reveal(); if (divCaption.offsetHeight > 18) { computeDimensions(divCaption.offsetHeight - 18); } container.style.visibility = 'visible'; } function nextScreenshot() { if (screenshot.next !== undefined) { screenshot = ssm_screenshotData[screenshot.next]; } onRender(); } function prevScreenshot() { if (screenshot.prev !== undefined) { screenshot = ssm_screenshotData[screenshot.prev]; } onRender(); } function onResize() { render(1); } function onHide() { aApprove.onclick = aMakeSticky.onclick = aDelete.onclick = null; cancelImageLoading(); } function onShow(dest, first, opt) { screenshot = opt; container = dest; if (first) { dest.className = 'screenshotviewer'; screen = $WH.ce('div'); screen.className = 'screenshotviewer-screen'; aPrev = $WH.ce('a'); aNext = $WH.ce('a'); aPrev.className = 'screenshotviewer-prev'; aNext.className = 'screenshotviewer-next'; aPrev.href = 'javascript:;'; aNext.href = 'javascript:;'; var foo = $WH.ce('span'); $WH.ae(foo, $WH.ce('b')); $WH.ae(aPrev, foo); var foo = $WH.ce('span'); $WH.ae(foo, $WH.ce('b')); $WH.ae(aNext, foo); aPrev.onclick = prevScreenshot; aNext.onclick = nextScreenshot; aCover = $WH.ce('a'); aCover.className = 'screenshotviewer-cover'; aCover.href = 'javascript:;'; aCover.onclick = Lightbox.hide; var foo = $WH.ce('span'); $WH.ae(foo, $WH.ce('b')); $WH.ae(aCover, foo); $WH.ae(screen, aPrev); $WH.ae(screen, aNext); $WH.ae(screen, aCover); var _div = $WH.ce('div'); _div.className = 'text'; h2Name = $WH.ce('h2'); h2Name.className = 'first'; $WH.ae(h2Name, $WH.ct(screenshot.name)); $WH.ae(_div, h2Name); $WH.ae(dest, _div); imgDiv = $WH.ce('div'); $WH.ae(screen, imgDiv); $WH.ae(dest, screen); var _div = $WH.ce('div'); _div.style.paddingTop = '6px'; _div.style.cssFloat = _div.style.styleFloat = 'right'; _div.className = 'bigger-links'; aApprove = $WH.ce('a'); aApprove.href = 'javascript:;'; $WH.ae(aApprove, $WH.ct('Approve')); $WH.ae(_div, aApprove); spApprove = $WH.ce('span'); spApprove.style.display = 'none'; $WH.ae(spApprove, $WH.ct('Approve')); $WH.ae(_div, spApprove); $WH.ae(_div, makePipe()); aMakeSticky = $WH.ce('a'); aMakeSticky.href = 'javascript:;'; $WH.ae(aMakeSticky, $WH.ct('Make sticky')); $WH.ae(_div, aMakeSticky); $WH.ae(_div, makePipe()); aDelete = $WH.ce('a'); aDelete.href = 'javascript:;'; $WH.ae(aDelete, $WH.ct('Delete')); $WH.ae(_div, aDelete); u = _div; $WH.ae(dest, _div); divFrom = $WH.ce('div'); divFrom.className = 'screenshotviewer-from'; var sp = $WH.ce('span'); $WH.ae(sp, $WH.ct(LANG.lvscreenshot_from)); $WH.ae(sp, $WH.ce('a')); $WH.ae(sp, $WH.ct(' ')); $WH.ae(sp, $WH.ce('span')); $WH.ae(divFrom, sp); $WH.ae(dest, divFrom); _div = $WH.ce('div'); _div.className = 'clear'; $WH.ae(dest, _div); var aClose = $WH.ce('a'); aClose.className = 'screenshotviewer-close'; aClose.href = 'javascript:;'; aClose.onclick = Lightbox.hide; $WH.ae(aClose, $WH.ce('span')); $WH.ae(dest, aClose); aOriginal = $WH.ce('a'); aOriginal.className = 'screenshotviewer-original'; aOriginal.href = 'javascript:;'; aOriginal.target = '_blank'; $WH.ae(aOriginal, $WH.ce('span')); $WH.ae(dest, aOriginal); __div = $WH.ce('div'); divCaption = $WH.ce('span'); divCaption.style.paddingRight = '8px'; $WH.ae(__div, divCaption); var sp = $WH.ce('span'); sp.style.whiteSpace = 'nowrap'; aEdit = $WH.ce('a'); aEdit.href = 'javascript:;'; $WH.ae(aEdit, $WH.ct('Edit')); $WH.ae(sp, aEdit); $WH.ae(sp, makePipe()); aClear = $WH.ce('a'); aClear.href = 'javascript:;'; $WH.ae(aClear, $WH.ct('Clear')); $WH.ae(sp, aClear); $WH.ae(__div, sp); $WH.ae(dest, __div); _div = $WH.ce('div'); _div.className = 'clear'; $WH.ae(dest, _div); } else { $WH.ee(h2Name); $WH.ae(h2Name, $WH.ct(screenshot.name)); } onRender(); } function onRender() { if (screenshot.pending) { aApprove.onclick = ssm_Approve.bind(screenshot, true); aMakeSticky.onclick = ssm_Sticky.bind(screenshot, true); aDelete.onclick = ssm_Delete.bind(screenshot, true); } else { aMakeSticky.onclick = ssm_Sticky.bind(screenshot, true); aDelete.onclick = ssm_Delete.bind(screenshot, true); } aApprove.style.display = screenshot.pending ? '' : 'none'; spApprove.style.display = screenshot.pending ? 'none' : ''; if (!screenshot.width || !screenshot.height) { if (loadingImage) { loadingImage.onload = null; loadingImage.onerror = null; } else { container.className = ''; lightboxComponents = []; while (container.firstChild) { lightboxComponents.push(container.firstChild); $WH.de(container.firstChild); } } var lightboxTimer = setTimeout(function () { screenshot.width = 126; screenshot.height = 22; computeDimensions(0); screenshot.width = null; screenshot.height = null; var div = $WH.ce('div'); div.style.margin = '0 auto'; div.style.width = '126px'; var img = $WH.ce('img'); img.src = g_staticUrl + '/images/ui/misc/progress-anim.gif'; img.width = 126; img.height = 22; $WH.ae(div, img); $WH.ae(container, div); Lightbox.reveal(); container.style.visiblity = 'visible'; }, 150); loadingImage = new Image(); loadingImage.onload = (function (screen, timer) { clearTimeout(timer); screen.width = this.width; screen.height = this.height; loadingImage = null; restoreLightbox(); render(); }).bind(loadingImage, screenshot, lightboxTimer); loadingImage.onerror = (function (timer) { clearTimeout(timer); loadingImage = null; Lightbox.hide(); restoreLightbox(); }).bind(loadingImage, lightboxTimer); loadingImage.src = (screenshot.url ? screenshot.url : g_staticUrl + '/uploads/screenshots/' + (screenshot.pending ? 'pending' : 'normal') + '/' + screenshot.id + '.jpg'); } else { render(); } } function cancelImageLoading() { if (!loadingImage) { return; } loadingImage.onload = null; loadingImage.onerror = null; loadingImage = null; restoreLightbox(); } function restoreLightbox() { if (!lightboxComponents) { return; } $WH.ee(container); container.className = 'screenshotviewer'; for (var i = 0; i < lightboxComponents.length; ++i) { $WH.ae(container, lightboxComponents[i]); } lightboxComponents = null; } this.show = function (opt) { Lightbox.show('screenshotmanager', { onShow: onShow, onHide: onHide, onResize: onResize }, opt); } }; \ No newline at end of file +var ss_managedRow = null; +var ss_getAll = false; // never changed (maybe with ?admin=screenshots&all) +var ssm_ViewedRow = null; +var ssm_screenshotData = []; +var ssm_screenshotPages = []; +var ssm_numPagesFound = 0; +var ssm_numPages = 0; // never accessed +var ssm_numPending = 0; +var ssm_statuses = { + 0 : 'Pending', + 999: 'Deleted', + 100: 'Approved', + 105: 'Sticky' +}; + +function makePipe() { + var sp = $WH.ce('span'); + $WH.ae(sp, $WH.ct(' ')); + + var sm = $WH.ce('small'); + sm.className = 'q0'; + $WH.ae(sm, $WH.ct('|')); + + $WH.ae(sp, sm); + $WH.ae(sp, $WH.ct(' ')); + + return sp; +} + +function ss_OnResize() { + var _ = Math.max(100, Math.min($WH.g_getWindowSize().h - 50, 700)); + + $WH.ge('menu-container').style.height = $WH.ge('pages-container').style.height = _ + 'px'; + $WH.ge('data-container').style.height = _ + 'px'; +} + +$WH.aE(window, 'resize', ss_OnResize); + +function ss_Refresh(openNext, type, typeId) { + new Ajax('?admin=screenshots&action=list' + (ss_getAll ? '&all' : ''), { + method: 'get', + onSuccess: function (xhr) { + eval(xhr.responseText); + + if (ssm_screenshotPages.length > 0) { + $WH.ge('show-all-pages').innerHTML = ' – Show All (' + ssm_numPagesFound + ')'; + + ssm_UpdatePages(); + + if (openNext) { + ss_Manage($WH.ge('pages-container').firstChild.firstChild, ssm_screenshotPages[0].type, ssm_screenshotPages[0].typeId, true); + } + else if (type && typeId) { + ss_Manage(null, type, typeId, true); + } + } + else { + $WH.ee($WH.ge('show-all-pages')); + $WH.ge('pages-container').innerHTML = 'NO SCREENZSHOT NEEDS 2 BE APPRVED NOW KTHX. :)'; + if (type && typeId) { + ss_Manage(null, type, typeId, true); + } + } + } + }); +} + +function ss_Manage(_this, type, typeId, openNext) { + new Ajax('?admin=screenshots&action=manage&type=' + type + '&typeid=' + typeId, { + method: 'get', + onSuccess: function (xhr) { + + eval(xhr.responseText); + ssm_numPending = 0; + + for (var i in ssm_screenshotData) { + if (ssm_screenshotData[i].pending) { + ssm_numPending++; + } + } + + var nRows = ssm_screenshotData.length; + $WH.ge('screenshotTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); + + ssm_UpdateList(openNext); + ssm_UpdateMassLinks(); + + if (ss_managedRow != null) { + ss_ColorizeRow('transparent'); + } + + ss_managedRow = _this; + + if (ss_managedRow != null) { + ss_ColorizeRow('#282828'); + } + } + }); +} + +function ss_ManageUser() { + var username = $WH.ge('usermanage'); + username.value = $WH.trim(username.value); + + if (username.value.length < 4) { + alert('Username must be at least 4 characters long.'); + username.focus(); + + return false; + } + + if (username.value.match(/[^a-z0-9]/i) != null) { + alert('Username can only contain letters and numbers.'); + username.focus(); + + return false; + } + + new Ajax('?admin=screenshots&action=manage&user=' + username.value, { + method: 'get', + onSuccess: function (xhr) { + eval(xhr.responseText); + var nRows = ssm_screenshotData.length; + $WH.ge('screenshotTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); + ssm_UpdateList(); + ssm_UpdateMassLinks(); + if (ss_managedRow != null) { + ss_ColorizeRow('transparent'); + } + } + }); + + return true; +} + +function ss_ColorizeRow(color) { + for (var i = 0; i < ss_managedRow.childNodes.length; ++i) { + ss_managedRow.childNodes[i].style.backgroundColor = color; + } +} + +function ssm_GetScreenshot(id) { + for (var i in ssm_screenshotData) { + if (ssm_screenshotData[i].id == id) { + return ssm_screenshotData[i]; + } + } + + return null; +} + +function ssm_View(row, id) { + if (ssm_ViewedRow != null) { + ssm_ColorizeRow('transparent'); + } + + ssm_ViewedRow = row; + ssm_ColorizeRow('#282828'); + + var screenshot = ssm_GetScreenshot(id); + if (screenshot != null) { + ScreenshotManager.show(screenshot); + } +} + +function ssm_ColorizeRow(color) { + for (var i = 0; i < ssm_ViewedRow.childNodes.length; ++i) { + ssm_ViewedRow.childNodes[i].style.backgroundColor = color; + } +} + +function ssm_ConfirmMassApprove() { + ajaxAnchor(this); // sarjuuk custom - there has to be something in place or we are manually using a script for ajax + + return false; + // return true; +} + +function ssm_ConfirmMassDelete() { + if (confirm('Delete selected screenshot(s)?')) // sarjuuk custom - see above + ajaxAnchor(this); + + return false; + // return confirm('Delete selected screenshot(s)?'); +} + +function ssm_ConfirmMassSticky() { + if (confirm('Sticky selected screenshot(s)?')) // sarjuuk custom - see above + ajaxAnchor(this); + + return false; + // return confirm('Sticky selected screenshot(s)?'); +} + +function ssm_UpdatePages(UNUSED) { + var pc = $WH.ge('pages-container'); + $WH.ee(pc); + + var tbl = $WH.ce('table'); + tbl.className = 'grid'; + tbl.style.width = '400px'; + + var tr = $WH.ce('tr'); + + var th = $WH.ce('th'); + $WH.ae(th, $WH.ct('Page')); + $WH.ae(tr, th); + + th = $WH.ce('th'); + $WH.ae(th, $WH.ct('Submitted')); + $WH.ae(tr, th); + + th = $WH.ce('th'); + th.align = 'right'; + $WH.ae(th, $WH.ct('#')); + $WH.ae(tr, th); + + $WH.ae(tbl, tr); + + var now = new Date(); + for (var i in ssm_screenshotPages) { + var ssPage = ssm_screenshotPages[i]; + tr = $WH.ce('tr'); + tr.onclick = ss_Manage.bind(tr, tr, ssPage.type, ssPage.typeId, true, i); + + var td = $WH.ce('td'); + var a = $WH.ce('a'); + a.href = '?' + g_types[ssPage.type] + '=' + ssPage.typeId; + a.target = '_blank'; + $WH.ae(a, $WH.ct(ssPage.name)); + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var elapsed = new Date(ssPage.date); + $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); + $WH.ae(tr, td); + + td = $WH.ce('td'); + td.align = 'right'; + $WH.ae(td, $WH.ct(ssPage.count)); + $WH.ae(tr, td); + + $WH.ae(tbl, tr); + } + + $WH.ae(pc, tbl); +} + +function ssm_UpdateList(openNext) { + var tsl = $WH.ge('theScreenshotsList'); + var tBody = false; + var i = 1; + + while (tsl.childNodes.length > i) { + if (tsl.childNodes[i].nodeName == 'TR' && tBody) { + $WH.de(tsl.childNodes[i]); + } + else if (tsl.childNodes[i].nodeName == 'TR') { + tBody = true; + } + else { + i++; + } + } + + var now = new Date(); + var ssId = 0; + for (var i in ssm_screenshotData) { + var screenshot = ssm_screenshotData[i]; + var tr = $WH.ce('tr'); + if (ssId == 0 && screenshot.pending) { + ssId = screenshot.id; + tr.id = 'highlightedRow'; + } + + var td = $WH.ce('td'); + td.align = 'center'; + + var a = $WH.ce('a'); + a.href = g_staticUrl + '/uploads/screenshots/' + (screenshot.status != 999 && !screenshot.pending ? 'normal' : 'pending') + '/' + screenshot.id + '.jpg'; + a.target = '_blank'; + a.onclick = function (id, e) { + $WH.sp(e); + (ssm_View.bind(null, this, id))(); + return false; + }.bind(tr, screenshot.id); + + var img = $WH.ce('img'); + img.src = g_staticUrl + '/uploads/screenshots/' + (screenshot.status != 999 && !screenshot.pending ? 'thumb' : 'pending') + '/' + screenshot.id + '.jpg'; + img.height = 50; + $WH.ae(a, img); + + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + if (screenshot.status != 999 && !screenshot.pending) { + var a = $WH.ce('a'); + a.href = '?' + g_types[screenshot.type] + '=' + screenshot.typeId + '#screenshots:id=' + screenshot.id; + a.target = '_blank'; + a.onclick = function (e) { + $WH.sp(e); + }; + $WH.ae(a, $WH.ct(screenshot.id)); + $WH.ae(td, a); + } + else { + $WH.ae(td, $WH.ct(screenshot.id)); + } + $WH.ae(tr, td); + + td = $WH.ce('td'); + td.id = 'alt-' + screenshot.id; + + var sp = $WH.ce('span'); + sp.style.paddingRight = '8px'; + if (screenshot.caption) { + var sp2 = $WH.ce('span'); + sp2.className = 'q2'; + var b = $WH.ce('b'); + $WH.ae(b, $WH.ct(screenshot.caption)); + $WH.ae(sp2, b); + $WH.ae(sp, sp2); + } + else { + var it = $WH.ce('i'); + it.className = 'q0'; + $WH.ae(it, $WH.ct('NULL')); + $WH.ae(sp, it); + } + $WH.ae(td, sp); + + sp = $WH.ce('span'); + sp.style.whiteSpace = 'nowrap'; + + var a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (id, e) { + $WH.sp(e); + (ssm_ShowEdit.bind(this, id))() + }.bind(a, screenshot); + $WH.ae(a, $WH.ct('Edit')); + $WH.ae(sp, a); + $WH.ae(sp, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (id, e) { + $WH.sp(e); + (ssm_Clear.bind(this, id))() + }.bind(a, screenshot); + $WH.ae(a, $WH.ct('Clear')); + $WH.ae(sp, a); + $WH.ae(td, sp); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var elapsed = new Date(screenshot.date); + $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); + $WH.ae(tr, td); + + td = $WH.ce('td'); + a = $WH.ce('a'); + a.href = '?user=' + screenshot.user; + a.target = '_blank'; + a.onclick = function (e) { + $WH.sp(e); + }; + $WH.ae(a, $WH.ct(screenshot.user)); + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + $WH.ae(td, $WH.ct(ssm_statuses[screenshot.status])); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var cb = $WH.ce('input'); + cb.type = 'checkbox'; + cb.value = screenshot.id; + cb.onclick = function (e) { + $WH.sp(e); + (ssm_UpdateMassLinks.bind(this))(); + }.bind(cb); + $WH.ae(td, cb); + $WH.ae(td, $WH.ct(' ')); + + if (screenshot.status != 999) { + tr.onclick = function (id) { + ssm_View(this, id); + return false; + }.bind(tr, screenshot.id); + + if (screenshot.id == ssId && openNext) { + ssm_View(tr, screenshot.id); + } + + if (screenshot.pending) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (ssm_Approve.bind(this, false))() + }.bind(screenshot); + $WH.ae(a, $WH.ct('Approve')); + $WH.ae(td, a); + } + else { + $WH.ae(td, $WH.ct('Approve')); + } + + $WH.ae(td, makePipe()); + + if (screenshot.status != 105) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (ssm_Sticky.bind(this, false))(); + }.bind(screenshot); + $WH.ae(a, $WH.ct('Make sticky')); + $WH.ae(td, a); + } + else { + $WH.ae(td, $WH.ct('Make sticky')); + } + $WH.ae(td, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (ssm_Delete.bind(this, false))(); + }.bind(screenshot); + $WH.ae(a, $WH.ct('Delete')); + $WH.ae(td, a); + $WH.ae(td, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + var id = prompt('Enter the ID to move this screenshot to:'); + (ssm_Relocate.bind(this, id))(); + }.bind(screenshot); + $WH.ae(a, $WH.ct('Relocate')); + $WH.ae(td, a); + } + + $WH.ae(tr, td); + $WH.ae(tsl, tr); + } +} + +function ssm_UpdateMassLinks() { + var buff = ''; + var i = 0; + var tSL = $WH.ge('theScreenshotsList'); + var inp = $WH.gE(tSL, 'input'); + + $WH.array_walk(inp, function (x) { + if (x.checked) { + buff += x.value + ','; + ++i; + } + }); + + buff = $WH.rtrim(buff, ','); + + var selCnt = $WH.ge('withselected'); + if (i > 0) { + selCnt.style.display = ''; + $WH.gE(selCnt, 'b')[0].firstChild.nodeValue = '(' + i + ')'; + + var c = $WH.ge('massapprove'); + var b = $WH.ge('massdelete'); + var a = $WH.ge('masssticky'); + + c.href = '?admin=screenshots&action=approve&id=' + buff; + c.onclick = ssm_ConfirmMassApprove; + + b.href = '?admin=screenshots&action=delete&id=' + buff; + b.onclick = ssm_ConfirmMassDelete; + + a.href = '?admin=screenshots&action=sticky&id=' + buff; + a.onclick = ssm_ConfirmMassSticky; + } + else { + selCnt.style.display = 'none'; + } +} + +function ssm_MassSelect(action) { + var tSL = $WH.ge('theScreenshotsList'); + var inp = $WH.gE(tSL, 'input'); + + switch (parseInt(action)) { + case 1: + $WH.array_walk(inp, function (x) { x.checked = true }); + break; + case 0: + $WH.array_walk(inp, function (x) { x.checked = false }); + break; + case -1: + $WH.array_walk(inp, function (x) { x.checked = !x.checked }); + break; + case 2: + $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 0 }); + break; + case 5: + $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).unique == 1 && ssm_GetScreenshot(x.value).status == 0 }); + break; + case 3: + $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 100 }); + break; + case 4: + $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 105 }); + break; + default: + return; + } + + ssm_UpdateMassLinks(); +} + +function ssm_ShowEdit(screenshot, isAlt) { + var node; + + if (isAlt) { + node = $WH.ge('alt2-' + screenshot.id) + } + else { + node = $WH.ge('alt-' + screenshot.id) + } + + var sp = $WH.gE(node, 'span')[0]; + var div = $WH.ce('div'); + div.style.whiteSpace = 'nowrap'; + var iCaption = $WH.ce('input'); + iCaption.type = 'text'; + iCaption.value = screenshot.caption; + iCaption.maxLength = 200; + iCaption.size = 35; + iCaption.onclick = function (e) { $WH.sp(e); } // sarjuuk - custom to inhibit screenshot popup, when clicking into input element + div.appendChild(iCaption); + + var btn = $WH.ce('input'); + btn.type = 'button'; + btn.value = 'Update'; + btn.onclick = function (i, j, k) { + if (!j) { + $WH.sp(k); + } + + (ssm_Edit.bind(this, i, j))(); + }.bind(btn, screenshot, isAlt); + div.appendChild(btn); + + var c = $WH.ce('span'); + c.appendChild($WH.ct(' ')); + div.appendChild(c); + + btn = $WH.ce('input'); + btn.type = 'button'; + btn.value = 'Cancel'; + btn.onclick = function (i, j, k) { + if (!j) { + $WH.sp(k); + } + + (ssm_CancelEdit.bind(this, i, j))(); + }.bind(btn, screenshot, isAlt); + div.appendChild(btn); + + sp.style.display = 'none'; + sp.nextSibling.style.display = 'none'; + node.insertBefore(div, sp); + + iCaption.focus() +} + +function ssm_CancelEdit(screenshot, isAlt) { + var node; + + if (isAlt) { + node = $WH.ge('alt2-' + screenshot.id); + } + else { + node = $WH.ge('alt-' + screenshot.id); + } + + var sp = $WH.gE(node, 'span')[1]; + sp.style.display = ''; + sp.nextSibling.style.display = ''; + + node.removeChild(node.firstChild); +} + +function ssm_Edit(screenshot, isAlt) { + var node; + + if (isAlt) { + node = $WH.ge('alt2-' + screenshot.id); + } + else { + node = $WH.ge('alt-' + screenshot.id); + } + + var desc = node.firstChild.childNodes; + if (desc[0].value == screenshot.caption) { + ssm_CancelEdit(screenshot, isAlt); + return + } + screenshot.caption = desc[0].value; + + ssm_CancelEdit(screenshot, isAlt); + + node = node.firstChild; + while (node.childNodes.length > 0) { + node.removeChild(node.firstChild); + } + $WH.ae(node, $WH.ct(screenshot.caption)); + + new Ajax('?admin=screenshots&action=editalt&id=' + screenshot.id, { + method: 'POST', + params: 'alt=' + $WH.urlencode(screenshot.caption) + }) +} + +function ssm_Clear(screenshot, isAlt) { + var node; + + if (isAlt) { + node = $WH.ge('alt2-' + screenshot.id); + } + else { + node = $WH.ge('alt-' + screenshot.id); + } + + var sp = $WH.gE(node, 'span'); + var a = $WH.gE(sp[1], 'a'); + sp = sp[0]; + + if (screenshot.caption == '') { + return; + } + + screenshot.caption = ''; + sp.innerHTML = 'NULL'; + + new Ajax('?admin=screenshots&action=editalt&id=' + screenshot.id, { + method: 'POST', + params: 'alt=' + $WH.urlencode('') + }) +} + +function ssm_Approve(openNext) { + var _self = this; + new Ajax('?admin=screenshots&action=approve&id=' + _self.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (ssm_numPending == 1 && _self.pending) { + ss_Refresh(true); + } + else { + ss_Refresh(); + ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); + } + } + }) +} + +function ssm_Sticky(openNext) { + var _self = this; + new Ajax('?admin=screenshots&action=sticky&id=' + _self.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (ssm_numPending == 1 && _self.pending) { + ss_Refresh(true); + } + else { + ss_Refresh(); + ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); + } + } + }) +} + +function ssm_Delete(openNext) { + var _self = this; + new Ajax('?admin=screenshots&action=delete&id=' + _self.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (ssm_numPending == 1 && _self.pending) { + ss_Refresh(true); + } + else { + ss_Refresh(); + ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); + } + } + }); +} + +function ssm_Relocate(typeId) { + var _self = this; + new Ajax('?admin=screenshots&action=relocate&id=' + _self.id + '&typeid=' + typeId, { + method: 'get', + onSuccess: function (x) { + ss_Refresh(); + ss_Manage(ss_managedRow, _self.type, typeId); + } + }); +} + +var ScreenshotManager = new function () { + var + screenshot, + pos, + imgWidth, + imgHeight, + scale, + desiredScale, + container, + screen, + imgDiv, + aPrev, + aNext, + aCover, + aOriginal, + divFrom, + divCaption, + __div, + h2Name, + u, + aEdit, + aClear, + spApprove, + aApprove, + aMakeSticky, + aDelete, + loadingImage, + lightboxComponents; + + function computeDimensions(captionExtraHeight) { + var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 122 - captionExtraHeight)); + + if (screenshot.id) { + desiredScale = Math.min(772 / screenshot.width, 618 / screenshot.height); + scale = Math.min(772 / screenshot.width, availHeight / screenshot.height); + } + else { + desiredScale = scale = 1; + } + + if (desiredScale > 1) { + desiredScale = 1; + } + + if (scale > 1) { + scale = 1; + } + + imgWidth = Math.round(scale * screenshot.width); + imgHeight = Math.round(scale * screenshot.height); + var lbWidth = Math.max(480, imgWidth); + + Lightbox.setSize(lbWidth + 20, imgHeight + 116 + captionExtraHeight); + + if (captionExtraHeight) { + imgDiv.firstChild.width = imgWidth; + imgDiv.firstChild.height = imgHeight; + } + } + + function render(resizing) { + if (resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) { + return; + } + + container.style.visibility = 'hidden'; + + var + resized = (screenshot.width > 772 || screenshot.height > 618); + + computeDimensions(0); + + var url = g_staticUrl + '/uploads/screenshots/' + (screenshot.pending ? 'pending' : 'normal') + '/' + screenshot.id + '.jpg'; + + var html = ''; + } + + divCaption.innerHTML = html; + } + else { + divCaption.innerHTML = 'NULL'; + } + + __div.id = 'alt2-' + screenshot.id; + + aEdit.onclick = ssm_ShowEdit.bind(aEdit, screenshot, true); + aClear.onclick = ssm_Clear.bind(aClear, screenshot, true); + + if (screenshot.next !== undefined) { + aPrev.style.display = aNext.style.display = ''; + aCover.style.display = 'none'; + } + else { + aPrev.style.display = aNext.style.display = 'none'; + aCover.style.display = ''; + } + } + + Lightbox.reveal(); + + if (divCaption.offsetHeight > 18) { + computeDimensions(divCaption.offsetHeight - 18); + } + container.style.visibility = 'visible'; + } + + function nextScreenshot() { + if (screenshot.next !== undefined) { + screenshot = ssm_screenshotData[screenshot.next]; + } + + onRender(); + } + + function prevScreenshot() { + if (screenshot.prev !== undefined) { + screenshot = ssm_screenshotData[screenshot.prev]; + } + + onRender(); + } + + function onResize() { + render(1); + } + + function onHide() { + aApprove.onclick = aMakeSticky.onclick = aDelete.onclick = null; + cancelImageLoading(); + } + + function onShow(dest, first, opt) { + screenshot = opt; + container = dest; + + if (first) { + dest.className = 'screenshotviewer'; + + screen = $WH.ce('div'); + + screen.className = 'screenshotviewer-screen'; + + aPrev = $WH.ce('a'); + aNext = $WH.ce('a'); + aPrev.className = 'screenshotviewer-prev'; + aNext.className = 'screenshotviewer-next'; + aPrev.href = 'javascript:;'; + aNext.href = 'javascript:;'; + + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aPrev, foo); + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aNext, foo); + + aPrev.onclick = prevScreenshot; + aNext.onclick = nextScreenshot; + + aCover = $WH.ce('a'); + aCover.className = 'screenshotviewer-cover'; + aCover.href = 'javascript:;'; + aCover.onclick = Lightbox.hide; + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aCover, foo); + $WH.ae(screen, aPrev); + $WH.ae(screen, aNext); + $WH.ae(screen, aCover); + var _div = $WH.ce('div'); + _div.className = 'text'; + h2Name = $WH.ce('h2'); + h2Name.className = 'first'; + $WH.ae(h2Name, $WH.ct(screenshot.name)); + $WH.ae(_div, h2Name); + $WH.ae(dest, _div); + + imgDiv = $WH.ce('div'); + $WH.ae(screen, imgDiv); + + $WH.ae(dest, screen); + + var _div = $WH.ce('div'); + _div.style.paddingTop = '6px'; + _div.style.cssFloat = _div.style.styleFloat = 'right'; + _div.className = 'bigger-links'; + aApprove = $WH.ce('a'); + aApprove.href = 'javascript:;'; + $WH.ae(aApprove, $WH.ct('Approve')); + $WH.ae(_div, aApprove); + spApprove = $WH.ce('span'); + spApprove.style.display = 'none'; + $WH.ae(spApprove, $WH.ct('Approve')); + $WH.ae(_div, spApprove); + $WH.ae(_div, makePipe()); + aMakeSticky = $WH.ce('a'); + aMakeSticky.href = 'javascript:;'; + $WH.ae(aMakeSticky, $WH.ct('Make sticky')); + $WH.ae(_div, aMakeSticky); + $WH.ae(_div, makePipe()); + aDelete = $WH.ce('a'); + aDelete.href = 'javascript:;'; + $WH.ae(aDelete, $WH.ct('Delete')); + $WH.ae(_div, aDelete); + u = _div; + $WH.ae(dest, _div); + divFrom = $WH.ce('div'); + divFrom.className = 'screenshotviewer-from'; + var sp = $WH.ce('span'); + $WH.ae(sp, $WH.ct(LANG.lvscreenshot_from)); + $WH.ae(sp, $WH.ce('a')); + $WH.ae(sp, $WH.ct(' ')); + $WH.ae(sp, $WH.ce('span')); + $WH.ae(divFrom, sp); + $WH.ae(dest, divFrom); + _div = $WH.ce('div'); + _div.className = 'clear'; + $WH.ae(dest, _div); + var aClose = $WH.ce('a'); + aClose.className = 'screenshotviewer-close'; + aClose.href = 'javascript:;'; + aClose.onclick = Lightbox.hide; + $WH.ae(aClose, $WH.ce('span')); + $WH.ae(dest, aClose); + + aOriginal = $WH.ce('a'); + aOriginal.className = 'screenshotviewer-original'; + aOriginal.href = 'javascript:;'; + aOriginal.target = '_blank'; + $WH.ae(aOriginal, $WH.ce('span')); + $WH.ae(dest, aOriginal); + + __div = $WH.ce('div'); + divCaption = $WH.ce('span'); + divCaption.style.paddingRight = '8px'; + $WH.ae(__div, divCaption); + var sp = $WH.ce('span'); + sp.style.whiteSpace = 'nowrap'; + aEdit = $WH.ce('a'); + aEdit.href = 'javascript:;'; + $WH.ae(aEdit, $WH.ct('Edit')); + $WH.ae(sp, aEdit); + $WH.ae(sp, makePipe()); + aClear = $WH.ce('a'); + aClear.href = 'javascript:;'; + $WH.ae(aClear, $WH.ct('Clear')); + $WH.ae(sp, aClear); + $WH.ae(__div, sp); + $WH.ae(dest, __div); + _div = $WH.ce('div'); + _div.className = 'clear'; + $WH.ae(dest, _div); + } + else { + $WH.ee(h2Name); + $WH.ae(h2Name, $WH.ct(screenshot.name)); + } + + onRender(); + } + + function onRender() { + if (screenshot.pending) { + aApprove.onclick = ssm_Approve.bind(screenshot, true); + aMakeSticky.onclick = ssm_Sticky.bind(screenshot, true); + aDelete.onclick = ssm_Delete.bind(screenshot, true); + } + else { + aMakeSticky.onclick = ssm_Sticky.bind(screenshot, true); + aDelete.onclick = ssm_Delete.bind(screenshot, true); + } + + aApprove.style.display = screenshot.pending ? '' : 'none'; + spApprove.style.display = screenshot.pending ? 'none' : ''; + + if (!screenshot.width || !screenshot.height) { + if (loadingImage) { + loadingImage.onload = null; + loadingImage.onerror = null; + } + else { + container.className = ''; + lightboxComponents = []; + + while (container.firstChild) { + lightboxComponents.push(container.firstChild); + $WH.de(container.firstChild); + } + } + + var lightboxTimer = setTimeout(function () { + screenshot.width = 126; + screenshot.height = 22; + + computeDimensions(0); + + screenshot.width = null; + screenshot.height = null; + + var div = $WH.ce('div'); + div.style.margin = '0 auto'; + div.style.width = '126px'; + + var img = $WH.ce('img'); + img.src = g_staticUrl + '/images/ui/misc/progress-anim.gif'; + img.width = 126; + img.height = 22; + + $WH.ae(div, img); + $WH.ae(container, div); + + Lightbox.reveal(); + container.style.visiblity = 'visible'; + }, 150); + + loadingImage = new Image(); + loadingImage.onload = (function (screen, timer) { + clearTimeout(timer); + screen.width = this.width; + screen.height = this.height; + loadingImage = null; + restoreLightbox(); + render(); + }).bind(loadingImage, screenshot, lightboxTimer); + loadingImage.onerror = (function (timer) { + clearTimeout(timer); + loadingImage = null; + Lightbox.hide(); + restoreLightbox(); + }).bind(loadingImage, lightboxTimer); + loadingImage.src = (screenshot.url ? screenshot.url : g_staticUrl + '/uploads/screenshots/' + (screenshot.pending ? 'pending' : 'normal') + '/' + screenshot.id + '.jpg'); + } + else { + render(); + } + } + + function cancelImageLoading() { + if (!loadingImage) { + return; + } + + loadingImage.onload = null; + loadingImage.onerror = null; + loadingImage = null; + + restoreLightbox(); + } + + function restoreLightbox() { + if (!lightboxComponents) { + return; + } + + $WH.ee(container); + container.className = 'screenshotviewer'; + for (var i = 0; i < lightboxComponents.length; ++i) { + $WH.ae(container, lightboxComponents[i]); + } + + lightboxComponents = null; + } + + this.show = function (opt) { + Lightbox.show('screenshotmanager', { + onShow: onShow, + onHide: onHide, + onResize: onResize + }, opt); + } +}; diff --git a/template/bricks/contribute.tpl.php b/template/bricks/contribute.tpl.php index 90289714..4e4ddbca 100644 --- a/template/bricks/contribute.tpl.php +++ b/template/bricks/contribute.tpl.php @@ -6,7 +6,7 @@
brick('contrib_'.User::$localeId); + $this->localizedBrick('contrib', User::$localeId); ?>
diff --git a/template/bricks/contrib_0.tpl.php b/template/localized/contrib_0.tpl.php similarity index 90% rename from template/bricks/contrib_0.tpl.php rename to template/localized/contrib_0.tpl.php index e6f2531f..3216d5df 100644 --- a/template/bricks/contrib_0.tpl.php +++ b/template/localized/contrib_0.tpl.php @@ -30,11 +30,12 @@ if (User::$id > 0):
  • Be sure to read the tips & tricks if you haven't before.
  • +ssError ? '
    '.$this->ssError."
    \n
    \n" : ''; +?>
    - File:
    -
    - Caption: Optional, up to 200 characters
    +
    @@ -51,10 +52,8 @@ if (User::$id > 0):
    - URL: Supported: YouTube only + Supported: YouTube only
    - Title: Optional, up to 200 characters
    -
    diff --git a/template/bricks/contrib_2.tpl.php b/template/localized/contrib_2.tpl.php similarity index 87% rename from template/bricks/contrib_2.tpl.php rename to template/localized/contrib_2.tpl.php index 57dae98e..0bb8593a 100644 --- a/template/bricks/contrib_2.tpl.php +++ b/template/localized/contrib_2.tpl.php @@ -16,7 +16,7 @@ if (User::$id > 0):
    - +
    @@ -30,13 +30,14 @@ if (User::$id > 0):
  • Assurez-vous de lire les trucs et astuces si ce n'est pas déjà fait.
  • +ssError ? '
    '.$this->ssError."
    \n
    \n" : ''; +?>
    - File :
    +
    - Caption : Optional, up to 200 characters
    -
    - +
    Note: Votre capture d'écran devra être approuvé avant d'apparaitre sur le site. @@ -51,11 +52,9 @@ if (User::$id > 0):
    - URL : Supporté: Youtube seulement + Supporté: Youtube seulement
    - Title : Optional, up to 200 characters
    -
    - +
    Note: Votre vidéo devra être approuvé avant d'apparaitre sur le site. diff --git a/template/bricks/contrib_3.tpl.php b/template/localized/contrib_3.tpl.php similarity index 89% rename from template/bricks/contrib_3.tpl.php rename to template/localized/contrib_3.tpl.php index 4f6cf8bb..93ac0d93 100644 --- a/template/bricks/contrib_3.tpl.php +++ b/template/localized/contrib_3.tpl.php @@ -20,7 +20,6 @@ if (User::$id > 0):
    - - - - @@ -30,13 +30,14 @@ if (User::$id > 0):
  • Asegurate de leer las sugerencias y trucos si no lo has hecho antes.
  • +ssError ? '
    '.$this->ssError."
    \n
    \n" : ''; +?>
    - File:
    +
    - Caption: Optional, up to 200 characters
    -
    - +
    Nota: Su captura de imagen deberá ser aprobado antes de aparecer en el sitio. @@ -51,11 +52,9 @@ if (User::$id > 0):
    - URL: Soportado: Sólo YouTube + Soportado: Sólo YouTube
    - Title: Optional, up to 200 characters
    -
    - +
    Nota: Tu vídeo deberá ser aprobado antes de aparecer en el sitio. diff --git a/template/bricks/contrib_8.tpl.php b/template/localized/contrib_8.tpl.php similarity index 90% rename from template/bricks/contrib_8.tpl.php rename to template/localized/contrib_8.tpl.php index 2ba5e8ec..ebf79bc7 100644 --- a/template/bricks/contrib_8.tpl.php +++ b/template/localized/contrib_8.tpl.php @@ -16,7 +16,7 @@ if (User::$id > 0):
    - +
    @@ -30,13 +30,14 @@ if (User::$id > 0):
  • ЕÑли вы ещё не читали, то наÑтоÑтельно рекомендуем вам прочеÑть Ñоветы и оÑобенноÑти Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ð¹ при помощи Ñнимков Ñкрана.
  • +ssError ? '
    '.$this->ssError."
    \n
    \n" : ''; +?>
    - File:
    +
    - Caption: Optional, up to 200 characters
    -
    - +
    Примечание: перед тем как поÑвитьÑÑ Ð½Ð° Ñайте, ваше Скриншот должны быть утверждены. @@ -51,11 +52,9 @@ if (User::$id > 0):
    - URL: ПоддерживаетÑÑ: только YouTube + ПоддерживаетÑÑ: только YouTube
    - Title: Optional, up to 200 characters
    -
    - +
    Примечание: перед тем как поÑвитьÑÑ Ð½Ð° Ñайте, ваше видео должно быть одобрено. diff --git a/template/localized/ssReminder_0.tpl.php b/template/localized/ssReminder_0.tpl.php new file mode 100644 index 00000000..5adbd61b --- /dev/null +++ b/template/localized/ssReminder_0.tpl.php @@ -0,0 +1,10 @@ +

    Reminder

    + Your screenshot will not be approved if it doesn't correspond to the following guidelines. + +
      +
    • Be sure to turn up your graphics settings to make sure the shot looks good!
    • +
    • Model viewer shots are deleted on sight (this also includes character select, typically).
    • +
    • Don't include the onscreen text and the selection circle of a NPC.
    • +
    • Don't include any UI in the shot if you can help it.
    • +
    • Use the screenshot cropping tool to focus on the item as much as possible and reduce any unnecessary surrounding, as to better show off the item in question when reduced to the thumbnail that will be present on the item's page.
    • +
    diff --git a/template/localized/ssReminder_3.tpl.php b/template/localized/ssReminder_3.tpl.php new file mode 100644 index 00000000..038fd885 --- /dev/null +++ b/template/localized/ssReminder_3.tpl.php @@ -0,0 +1,10 @@ +

    Hinweis

    + Euer Screenshot wird nicht zugelassen werden, wenn er nicht unseren Richtlinien entspricht. + +
      +
    • Achtet darauf die Qualität Eurer Video Einstellungen zu erhöhen, damit Euer Bild gut aussieht!
    • +
    • Bilder des Modelviewers werden sofort gelöscht (das beinhaltet in der Regel auch Bilder der Charakterauswahl).
    • +
    • Vermeidet Bildschirmtext und die Zielmarkierung an NPCs.
    • +
    • Bezieht das Interface nicht ins Bild mit ein, wenn Ihr es vermeiden könnt.
    • +
    • Benutzt das Zuschneidewerkzeug um den Fokus so weit wie möglich auf das Objekt zu legen und unnötigen Rand zu reduzieren. Dies hilft das Objekt besser in der Vorschau zu erkennen, welche auf der entsprechenden Seite angezeigt wird.
    • +
    diff --git a/template/pages/screenshot.tpl.php b/template/pages/screenshot.tpl.php index e6d1083f..9cb51ab8 100644 --- a/template/pages/screenshot.tpl.php +++ b/template/pages/screenshot.tpl.php @@ -11,60 +11,46 @@ $this->brick('pageTemplate'); $this->brick('infobox'); -if (isset($this->error)): -?> -
    - -
    -

    -
    error; ?>
    -

    name; ?>

    -mode == 'add'): -?> -
    -
    - + +
    +
    + +
    + +
    + +
    + + +
    -
    - - -
    text counter ph
    -
    - - - -
    + +localizedBrick('ssReminder', User::$localeId); ?> + + + + + - -
    -
    From 54695a9490f41bd565f0f59c098af0786b6c0a41 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Jul 2015 15:17:34 +0200 Subject: [PATCH 0024/1249] Currency * expanded tooltips * moved cap from hardcoded to DB * can now set description (manually) Itemset * expanded tooltips Lang * number formating is now locale-aware --- includes/types/achievement.class.php | 2 +- includes/types/currency.class.php | 20 +++++- includes/types/item.class.php | 14 ++-- includes/types/itemset.class.php | 72 ++++++++++++++++++++- includes/types/spell.class.php | 2 +- includes/utilities.php | 2 +- localization/lang.class.php | 14 ++++ pages/currency.php | 53 +++++++++++++-- pages/itemset.php | 79 +++++++++++------------ pages/npc.php | 25 ++++--- pages/user.php | 2 +- setup/db_structure.sql | 1 + setup/tools/filegen/templates/power.js.in | 47 ++++++++++---- setup/tools/sqlgen/currencies.func.php | 4 +- setup/updates/1436619600_01.sql | 5 ++ static/css/basic.css | 25 +++---- 16 files changed, 271 insertions(+), 96 deletions(-) create mode 100644 setup/updates/1436619600_01.sql diff --git a/includes/types/achievement.class.php b/includes/types/achievement.class.php index 3a2c1224..24e49d37 100644 --- a/includes/types/achievement.class.php +++ b/includes/types/achievement.class.php @@ -207,7 +207,7 @@ class AchievementList extends BaseType } if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER) - $criteria .= '- '.Util::jsEscape($crtName).' '.number_format($crt['value2' ] / 10000).'
    '; + $criteria .= '- '.Util::jsEscape($crtName).' '.Lang::nf($crt['value2' ] / 10000).'
    '; else $criteria .= '- '.Util::jsEscape($crtName).'
    '; diff --git a/includes/types/currency.class.php b/includes/types/currency.class.php index 8b219883..39700278 100644 --- a/includes/types/currency.class.php +++ b/includes/types/currency.class.php @@ -52,7 +52,25 @@ class CurrencyList extends BaseType return $data; } - public function renderTooltip() { } + public function renderTooltip() + { + if (!$this->curTpl) + return array(); + + $x = '
    '; + $x .= ''.Util::jsEscape($this->getField('name', true)).'
    '; + + // cata+ (or go fill it by hand) + if ($_ = $this->getField('description', true)) + $x .= '
    '.Util::jsEscape($_).'
    '; + + if ($_ = $this->getField('cap')) + $x .= '
    '.Lang::currency('cap').Lang::main('colon').''.Lang::nf($_).'
    '; + + $x .= '
    '; + + return $x; + } } ?> diff --git a/includes/types/item.class.php b/includes/types/item.class.php index 768fe6a5..94bc4fdb 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -594,14 +594,14 @@ class ItemList extends BaseType $dps = $speed ? ($dmgmin1 + $dmgmax1) / (2 * $speed) : 0; if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin1 && $dmgmax1) - $x .= Lang::item('addsDps').' '.number_format(($dmgmin1 + $dmgmax1) / 2, 1).' '.Lang::item('dps2').'
    '; + $x .= Lang::item('addsDps').' '.Lang::nf(($dmgmin1 + $dmgmax1) / 2, 1).' '.Lang::item('dps2').'
    '; else if ($dps) { if ($_class == ITEM_CLASS_WEAPON) { $x .= ''; $x .= ''; - $x .= ''; + $x .= ''; $x .= '
    '.sprintf($this->curTpl['dmgType1'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin1'].' - '.$this->curTpl['dmgMax1'], Lang::game('sc', $this->curTpl['dmgType1'])).''.Lang::item('speed').' '.number_format($speed, 2).''.Lang::item('speed').' '.Lang::nf($speed, 2).'
    '; } else @@ -612,7 +612,7 @@ class ItemList extends BaseType $x .= '+'.sprintf($this->curTpl['dmgType2'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin2'].' - '.$this->curTpl['dmgMax2'], Lang::game('sc', $this->curTpl['dmgType2'])).'
    '; if ($_class == ITEM_CLASS_WEAPON) - $x .= '('.number_format($dps, 1).' '.Lang::item('dps').')
    '; + $x .= '('.Lang::nf($dps, 1).' '.Lang::item('dps').')
    '; // display FeralAttackPower if set if ($fap = $this->getFeralAP()) @@ -1363,8 +1363,8 @@ class ItemList extends BaseType if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms { $average = $extraDPS * $this->curTpl['delay'] / 1000; - $this->templates[$this->id]['dmgMin1'] = number_format(0.7 * $average); - $this->templates[$this->id]['dmgMax1'] = number_format(1.3 * $average); + $this->templates[$this->id]['dmgMin1'] = Lang::nf(0.7 * $average); + $this->templates[$this->id]['dmgMax1'] = Lang::nf(1.3 * $average); } // apply Spell Power from ScalingStatValue if set @@ -1520,8 +1520,8 @@ class ItemList extends BaseType $json['dmgtype1'] = $this->curTpl['dmgType1']; $json['dmgmin1'] = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2']; $json['dmgmax1'] = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']; - $json['speed'] = number_format($this->curTpl['delay'] / 1000, 2); - $json['dps'] = !floatVal($json['speed']) ? 0 : number_format(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1); + $json['speed'] = Lang::nf($this->curTpl['delay'] / 1000, 2); + $json['dps'] = !floatVal($json['speed']) ? 0 : Lang::nf(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1); if (in_array($json['subclass'], [2, 3, 18, 19])) { diff --git a/includes/types/itemset.class.php b/includes/types/itemset.class.php index 3c9cf520..25deb811 100644 --- a/includes/types/itemset.class.php +++ b/includes/types/itemset.class.php @@ -88,7 +88,77 @@ class ItemsetList extends BaseType return $data; } - public function renderTooltip() { } + public function renderTooltip() + { + if (!$this->curTpl) + return array(); + + $x = '
    '; + $x .= ''.Util::jsEscape($this->getField('name', true)).'
    '; + + $nClasses = 0; + if ($_ = $this->getField('classMask')) + { + $cl = Lang::getClassString($_, $__, $nClasses); + $x .= Util::ucFirst($nClasses > 1 ? Lang::game('classes') : Lang::game('class')).Lang::main('colon').$cl.'
    '; + } + + if ($_ = $this->getField('contentGroup')) + $x .= Util::jsEscape(Lang::itemset('notes', $_)).($this->getField('heroic') ? ' ('.Lang::item('heroic').')' : '').'
    '; + + if (!$nClasses || !$this->getField('contentGroup')) + $x.= Lang::itemset('types', $this->getField('type')).'
    '; + + if ($bonuses = $this->getBonuses()) + { + $x .= ''; + + foreach ($bonuses as $b) + $x .= '
    '.$b['bonus'].' '.Lang::itemset('_pieces').Lang::main('colon').''.Util::jsEscape($b['desc']); + + $x .= '
    '; + } + + $x .= '
    '; + + return $x; + } + + public function getBonuses() + { + $spells = []; + for ($i = 1; $i < 9; $i++) + { + $spl = $this->getField('spell'.$i); + $qty = $this->getField('bonus'.$i); + + // cant use spell as index, would change order + if ($spl && $qty) + $spells[] = ['id' => $spl, 'bonus' => $qty]; + } + + // sort by required pieces ASC + usort($spells, function($a, $b) { + if ($a['bonus'] == $b['bonus']) + return 0; + + return ($a['bonus'] > $b['bonus']) ? 1 : -1; + }); + + $setSpells = new SpellList(array(['s.id', array_column($spells, 'id')])); + foreach ($setSpells->iterate() as $spellId => $__) + { + foreach ($spells as &$s) + { + if ($spellId != $s['id']) + continue; + + $s['desc'] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0]; + } + } + + return $spells; + } } diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php index 4ede269f..8164bdb0 100644 --- a/includes/types/spell.class.php +++ b/includes/types/spell.class.php @@ -1174,7 +1174,7 @@ class SpellList extends BaseType // step 3: try to evaluate result $evaled = $this->resolveEvaluation($str); - $return = is_numeric($evaled) ? number_format($evaled, $precision, '.', '') : $evaled; + $return = is_numeric($evaled) ? Lang::nf($evaled, $precision) : $evaled; return $return.$suffix; } diff --git a/includes/utilities.php b/includes/utilities.php index 8b162347..ac0d24a0 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1066,7 +1066,7 @@ class Util else $c = 2 / 52; - $result = number_format($val / Util::$gtCombatRatings[$type] / $c, 2); + $result = Lang::nf($val / Util::$gtCombatRatings[$type] / $c, 2); } if (!in_array($type, array(ITEM_MOD_DEFENSE_SKILL_RATING, ITEM_MOD_EXPERTISE_RATING))) diff --git a/localization/lang.class.php b/localization/lang.class.php index 5f775d37..1fb2ba66 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -322,6 +322,20 @@ class Lang return implode(', ', $tmp); } + + public static function nf($number, $decimals = 0) + { + // [decimal, thousand] + $seps = array( + LOCALE_EN => [',', '.'], + LOCALE_FR => [' ', ','], + LOCALE_DE => ['.', ','], + LOCALE_ES => ['.', ','], + LOCALE_RU => [' ', ','] + ); + + return number_format($number, $decimals, $seps[User::$localeId][1], $seps[User::$localeId][0]); + } } ?> \ No newline at end of file diff --git a/pages/currency.php b/pages/currency.php index 270c5d4b..fcc6706d 100644 --- a/pages/currency.php +++ b/pages/currency.php @@ -21,6 +21,10 @@ class CurrencyPage extends GenericPage { parent::__construct($pageCall, $id); + // temp locale + if ($this->mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) + Util::powerUseLocale($_GET['domain']); + $this->typeId = intVal($id); $this->subject = new CurrencyList(array(['id', $this->typeId])); @@ -52,10 +56,8 @@ class CurrencyPage extends GenericPage $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - if ($this->typeId == 103) // Arena Points - $infobox[] = Lang::currency('cap').Lang::main('colon').'10\'000'; - else if ($this->typeId == 104) // Honor - $infobox[] = Lang::currency('cap').Lang::main('colon').'75\'000'; + if ($_ = $this->subject->getField('cap')) + $infobox[] = Lang::currency('cap').Lang::main('colon').Lang::nf($_); /****************/ /* Main Content */ @@ -69,6 +71,9 @@ class CurrencyPage extends GenericPage BUTTON_LINKS => true ); + if ($_ = $this->subject->getField('description', true)) + $this->extraText = $_; + /**************/ /* Extra Tabs */ /**************/ @@ -224,6 +229,46 @@ class CurrencyPage extends GenericPage } } } + + protected function generateTooltip($asError = false) + { + if ($asError) + return '$WowheadPower.registerCurrency('.$this->typeId.', '.User::$localeId.', {});'; + + $x = '$WowheadPower.registerCurrency('.$this->typeId.', '.User::$localeId.", {\n"; + $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; + $x .= "\ticon: '".urlencode($this->subject->getField('iconString'))."',\n"; + $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."'\n"; + $x .= "});"; + + return $x; + } + + public function display($override = '') + { + if ($this->mode != CACHE_TYPE_TOOLTIP) + return parent::display($override); + + if (!$this->loadCache($tt)) + { + $tt = $this->generateTooltip(); + $this->saveCache($tt); + } + + header('Content-type: application/x-javascript; charset=utf-8'); + die($tt); + } + + public function notFound() + { + if ($this->mode != CACHE_TYPE_TOOLTIP) + return parent::notFound(Lang::game('currency'), Lang::currency('notFound')); + + header('Content-type: application/x-javascript; charset=utf-8'); + echo $this->generateTooltip(true); + exit(); + } + } ?> diff --git a/pages/itemset.php b/pages/itemset.php index 3d5ae9b7..e8983424 100644 --- a/pages/itemset.php +++ b/pages/itemset.php @@ -144,45 +144,6 @@ class ItemsetPage extends GenericPage ); } - // spells - $foo = []; - $spells = []; - for ($i = 1; $i < 9; $i++) - { - $spl = $this->subject->getField('spell'.$i); - $qty = $this->subject->getField('bonus'.$i); - - if ($spl && $qty) - { - $foo[] = $spl; - $spells[] = array( // cant use spell as index, would change order - 'id' => $spl, - 'bonus' => $qty, - 'desc' => '' - ); - } - } - - // sort by required pieces ASC - usort($spells, function($a, $b) { - if ($a['bonus'] == $b['bonus']) - return 0; - - return ($a['bonus'] > $b['bonus']) ? 1 : -1; - }); - - $setSpells = new SpellList(array(['s.id', $foo])); - foreach ($setSpells->iterate() as $spellId => $__) - { - foreach ($spells as &$s) - { - if ($spellId != $s['id']) - continue; - - $s['desc'] = $setSpells->parseText('description')[0]; - } - } - $skill = ''; if ($_sk = $this->subject->getField('skillId')) { @@ -195,7 +156,7 @@ class ItemsetPage extends GenericPage $this->unavailable = $this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE; $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; $this->pieces = $pieces; - $this->spells = $spells; + $this->spells = $this->subject->getBonuses(); $this->expansion = 0; $this->redButtons = array( BUTTON_WOWHEAD => $this->typeId > 0, // bool only @@ -266,6 +227,44 @@ class ItemsetPage extends GenericPage } } } + + protected function generateTooltip($asError = false) + { + if ($asError) + return '$WowheadPower.registerItemSet('.$this->typeId.', '.User::$localeId.', {});'; + + $x = '$WowheadPower.registerItemSet('.$this->typeId.', '.User::$localeId.", {\n"; + $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; + $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."'\n"; + $x .= "});"; + + return $x; + } + + public function display($override = '') + { + if ($this->mode != CACHE_TYPE_TOOLTIP) + return parent::display($override); + + if (!$this->loadCache($tt)) + { + $tt = $this->generateTooltip(); + $this->saveCache($tt); + } + + header('Content-type: application/x-javascript; charset=utf-8'); + die($tt); + } + + public function notFound() + { + if ($this->mode != CACHE_TYPE_TOOLTIP) + return parent::notFound(Lang::game('itemset'), Lang::itemset('notFound')); + + header('Content-type: application/x-javascript; charset=utf-8'); + echo $this->generateTooltip(true); + exit(); + } } diff --git a/pages/npc.php b/pages/npc.php index 6489b5de..566ec361 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -227,33 +227,32 @@ class NpcPage extends GenericPage } // > Stats - $_nf = function ($num) { return number_format($num, 0, '', '.'); }; $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] ? $_nf($health[0]).' - '.$_nf($health[1]) : $_nf($health[0])); + $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] ? $_nf($mana[0]).' - '.$_nf($mana[1]) : $_nf($mana[0])) : null; + $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])) : null; // Armor $armor = $this->subject->getBaseStats('armor'); - $stats['armor'] = Lang::npc('armor').Lang::main('colon').($armor[0] < $armor[1] ? $_nf($armor[0]).' - '.$_nf($armor[1]) : $_nf($armor[0])); + $stats['armor'] = Lang::npc('armor').Lang::main('colon').($armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); // Melee Damage $melee = $this->subject->getBaseStats('melee'); if ($_ = $this->subject->getField('dmgSchool')) // magic damage - $stats['melee'] = Lang::npc('melee').Lang::main('colon').$_nf($melee[0]).' - '.$_nf($melee[1]).' ('.Lang::game('sc', $_).')'; + $stats['melee'] = Lang::npc('melee').Lang::main('colon').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'; else // phys. damage - $stats['melee'] = Lang::npc('melee').Lang::main('colon').$_nf($melee[0]).' - '.$_nf($melee[1]); + $stats['melee'] = Lang::npc('melee').Lang::main('colon').Lang::nf($melee[0]).' - '.Lang::nf($melee[1]); // Ranged Damage $ranged = $this->subject->getBaseStats('ranged'); - $stats['ranged'] = Lang::npc('ranged').Lang::main('colon').$_nf($ranged[0]).' - '.$_nf($ranged[1]); + $stats['ranged'] = Lang::npc('ranged').Lang::main('colon').Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1]); if (in_array($mapType, [1, 2])) // Dungeon or Raid { @@ -268,26 +267,26 @@ class NpcPage extends GenericPage // Health $health = $_altNPCs->getBaseStats('health'); - $modes['health'][] = sprintf($modeRow, $m, $health[0] < $health[1] ? $_nf($health[0]).' - '.$_nf($health[1]) : $_nf($health[0])); + $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 = $_altNPCs->getBaseStats('power'); - $modes['mana'][] = $mana[0] ? sprintf($modeRow, $m, $mana[0] < $mana[1] ? $_nf($mana[0]).' - '.$_nf($mana[1]) : $_nf($mana[0])) : null; + $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 = $_altNPCs->getBaseStats('armor'); - $modes['armor'][] = sprintf($modeRow, $m, $armor[0] < $armor[1] ? $_nf($armor[0]).' - '.$_nf($armor[1]) : $_nf($armor[0])); + $modes['armor'][] = sprintf($modeRow, $m, $armor[0] < $armor[1] ? Lang::nf($armor[0]).' - '.Lang::nf($armor[1]) : Lang::nf($armor[0])); // Melee Damage $melee = $_altNPCs->getBaseStats('melee'); if ($_ = $_altNPCs->getField('dmgSchool')) // magic damage - $modes['melee'][] = sprintf($modeRow, $m, $_nf($melee[0]).' - '.$_nf($melee[1]).' ('.Lang::game('sc', $_).')'); + $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1]).' ('.Lang::game('sc', $_).')'); else // phys. damage - $modes['melee'][] = sprintf($modeRow, $m, $_nf($melee[0]).' - '.$_nf($melee[1])); + $modes['melee'][] = sprintf($modeRow, $m, Lang::nf($melee[0]).' - '.Lang::nf($melee[1])); // Ranged Damage $ranged = $_altNPCs->getBaseStats('ranged'); - $modes['ranged'][] = sprintf($modeRow, $m, $_nf($ranged[0]).' - '.$_nf($ranged[1])); + $modes['ranged'][] = sprintf($modeRow, $m, Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1])); } } } diff --git a/pages/user.php b/pages/user.php index a0ec9730..19117ced 100644 --- a/pages/user.php +++ b/pages/user.php @@ -50,7 +50,7 @@ class UserPage extends GenericPage $infobox[] = Lang::user('lastLogin').Lang::main('colon').'[tooltip name=lastLogin]'.date('l, G:i:s', $this->user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.date(Lang::main('dateFmtShort'), $this->user['prevLogin']).'[/span]'; $infobox[] = Lang::user('userGroups').Lang::main('colon').($groups ? implode(', ', $groups) : Lang::account('groups', -1)); $infobox[] = Lang::user('consecVisits').Lang::main('colon').$this->user['consecutiveVisits']; - $infobox[] = Util::ucFirst(Lang::main('siteRep')).Lang::main('colon').number_format($this->user['sumRep']); + $infobox[] = Util::ucFirst(Lang::main('siteRep')).Lang::main('colon').Lang::nf($this->user['sumRep']); // contrib -> [url=http://www.wowhead.com/client]Data uploads: n [small]([tooltip=tooltip_totaldatauploads]xx.y MB[/tooltip])[/small][/url] diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 1ecf4c50..e4f7af2b 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -513,6 +513,7 @@ CREATE TABLE `aowow_currencies` ( `cuFlags` int(10) unsigned NOT NULL, `iconId` mediumint(9) NOT NULL, `itemId` int(16) NOT NULL, + `cap` mediumint(8) unsigned NOT NULL, `name_loc0` varchar(64) NOT NULL, `name_loc2` varchar(64) NOT NULL, `name_loc3` varchar(64) NOT NULL, diff --git a/setup/tools/filegen/templates/power.js.in b/setup/tools/filegen/templates/power.js.in index 3a8b4248..34367b6b 100644 --- a/setup/tools/filegen/templates/power.js.in +++ b/setup/tools/filegen/templates/power.js.in @@ -27,13 +27,15 @@ if (typeof $WowheadPower == "undefined") { eventAttached = false, - npcs = {}, - objects = {}, - items = {}, - quests = {}, - spells = {}, + npcs = {}, + objects = {}, + items = {}, + quests = {}, + spells = {}, achievements = {}, - profiles = {}, + itemsets = {}, + currencies = {}, + profiles = {}, showLogo = 1, @@ -51,9 +53,11 @@ if (typeof $WowheadPower == "undefined") { TYPE_NPC = 1, TYPE_OBJECT = 2, TYPE_ITEM = 3, + TYPE_ITEMSET = 4, TYPE_QUEST = 5, TYPE_SPELL = 6, TYPE_ACHIEVEMENT = 10, + TYPE_CURRENCY = 17, TYPE_PROFILE = 100, CURSOR_HSPACE = 15, @@ -68,9 +72,11 @@ if (typeof $WowheadPower == "undefined") { 1: [npcs, "npc", "NPC" ], 2: [objects, "object", "Object" ], 3: [items, "item", "Item" ], + 4: [itemsets, "itemset", "Item Set" ], 5: [quests, "quest", "Quest" ], 6: [spells, "spell", "Spell" ], 10: [achievements, "achievement", "Achievement"], + 17: [currencies, "currency", "Currency" ], 100: [profiles, "profile", "Profile" ] }, SCALES = { @@ -90,7 +96,14 @@ if (typeof $WowheadPower == "undefined") { }; if (isRemote) { - var Locale = { id: 0, name: "enus" }; + var Locale = { + getId: function () { + return 0; + }, + getName: function () { + return "enus"; + } + } } function init() { @@ -248,8 +261,8 @@ if (typeof $WowheadPower == "undefined") { i2 = 3; if (t.href.indexOf("http://") == 0 || t.href.indexOf("https://") == 0) { i0 = 1; - // url = t.href.match(/^https?:\/\/(.+?)?\.?wowhead\.com(?:\:\d+)?\/\??(item|quest|spell|achievement|npc|object)=([0-9]+)/); - url = t.href.match(/^https?:\/\/(.*)\/?\??(item|quest|spell|achievement|npc|object)=([0-9]+)/); + // url = t.href.match(/^https?:\/\/(.+?)?\.?wowhead\.com(?:\:\d+)?\/\??(item|quest|spell|achievement|npc|object|itemset|currency)=(-?[0-9]+)/); + url = t.href.match(/^https?:\/\/(.*)\/?\??(item|quest|spell|achievement|npc|object|itemset|currency)=(-?[0-9]+)/); if (url == null) { // url = t.href.match(/^http:\/\/(.+?)?\.?wowhead\.com\/\?(profile)=([^&#]+)/) url = t.href.match(/^https?:\/\/(.*)\/?\??(profile)=([^&#]+)/); @@ -258,7 +271,7 @@ if (typeof $WowheadPower == "undefined") { showLogo = 0; } else { - url = t.href.match(/()\?(item|quest|spell|achievement|npc|object)=([0-9]+)/); + url = t.href.match(/()\?(item|quest|spell|achievement|npc|object|itemset|currency)=(-?[0-9]+)/); if (url == null) { url = t.href.match(/()\?(profile)=([^&#]+)/); } @@ -271,7 +284,7 @@ if (typeof $WowheadPower == "undefined") { i0 = 0; i1 = 1; i2 = 2; - url = t.rel.match(/(item|quest|spell|achievement|npc|object).?([0-9]+)/); + url = t.rel.match(/(item|quest|spell|achievement|npc|object|itemset|currency).?(-?[0-9]+)/); // if (url == null) { // sarjuuk: also matches 'profiler' and 'profiles' which screws with the language-menu workaround // url = t.rel.match(/(profile).?([^&#]+)/); // } @@ -700,6 +713,10 @@ if (typeof $WowheadPower == "undefined") { this.register(TYPE_NPC, id, locale, json); }; + this.registerCurrency = function (id, locale, json) { + this.register(TYPE_CURRENCY, id, locale, json) + }; + this.registerObject = function (id, locale, json) { this.register(TYPE_OBJECT, id, locale, json); }; @@ -708,6 +725,10 @@ if (typeof $WowheadPower == "undefined") { this.register(TYPE_ITEM, id, locale, json); }; + this.registerItemSet = function (id, locale, json) { + this.register(TYPE_ITEMSET, id, locale, json); + }; + this.registerQuest = function (id, locale, json) { this.register(TYPE_QUEST, id, locale, json); }; @@ -740,11 +761,11 @@ if (typeof $WowheadPower == "undefined") { }; this.requestItem = function (id, params) { - this.request(TYPE_ITEM, id, Locale.id, params); + this.request(TYPE_ITEM, id, Locale.getId(), params); }; this.requestSpell = function (id) { - this.request(TYPE_SPELL, id, Locale.id); + this.request(TYPE_SPELL, id, Locale.getId()); }; this.getStatus = function (type, id, locale) { diff --git a/setup/tools/sqlgen/currencies.func.php b/setup/tools/sqlgen/currencies.func.php index 5d45b716..7a2d43ec 100644 --- a/setup/tools/sqlgen/currencies.func.php +++ b/setup/tools/sqlgen/currencies.func.php @@ -18,7 +18,9 @@ $customData = array( 2 => ['cuFlags' => CUSTOM_EXCLUDE_FOR_LISTVIEW, 'category' => 3], 4 => ['cuFlags' => CUSTOM_EXCLUDE_FOR_LISTVIEW, 'category' => 3], 22 => ['cuFlags' => CUSTOM_EXCLUDE_FOR_LISTVIEW, 'category' => 3], - 141 => ['cuFlags' => CUSTOM_EXCLUDE_FOR_LISTVIEW, 'category' => 3] + 141 => ['cuFlags' => CUSTOM_EXCLUDE_FOR_LISTVIEW, 'category' => 3], + 103 => ['cap' => 10000], // Arena Points + 104 => ['cap' => 75000] // Honor Points ); $reqDBC = ['itemdisplayinfo', 'currencytypes']; diff --git a/setup/updates/1436619600_01.sql b/setup/updates/1436619600_01.sql new file mode 100644 index 00000000..1dca8932 --- /dev/null +++ b/setup/updates/1436619600_01.sql @@ -0,0 +1,5 @@ +ALTER TABLE `aowow_currencies` + ADD COLUMN `cap` MEDIUMINT UNSIGNED NOT NULL AFTER `itemId`; + +UPDATE `aowow_currencies` SET `cap` = 10000 WHERE `id` = 103; +UPDATE `aowow_currencies` SET `cap` = 75000 WHERE `id` = 104; diff --git a/static/css/basic.css b/static/css/basic.css index a66bced4..e8728564 100644 --- a/static/css/basic.css +++ b/static/css/basic.css @@ -83,18 +83,19 @@ a span.moneyitem, a span.moneysocketmeta, a span.moneysocketred, a span.moneysoc /* ITEM QUALITY COLORS */ /***********************/ -.q, .q a, .color-q, .wowhead-tooltip .q a { color: #ffd100 !important; } /* Default (yellow) */ -.q0, .q0 a, .color-q0, .wowhead-tooltip .q0 a { color: #9d9d9d !important; } /* Poor */ -.q1, .q1 a, .color-q1, .wowhead-tooltip .q1 a { color: #ffffff !important; } /* Common */ -.q2, .q2 a, .color-q2, .wowhead-tooltip .q2 a { color: #1eff00 !important; } /* Uncommon */ -.q3, .q3 a, .color-q3, .wowhead-tooltip .q3 a { color: #0070dd !important; } /* Rare */ -.q4, .q4 a, .color-q4, .wowhead-tooltip .q4 a { color: #a335ee !important; } /* Epic */ -.q5, .q5 a, .color-q5, .wowhead-tooltip .q5 a { color: #ff8000 !important; } /* Legendary */ -.q6, .q6 a, .color-q6, .wowhead-tooltip .q6 a { color: #e5cc80 !important; } /* Artifact */ -.q7, .q7 a, .color-q7, .wowhead-tooltip .q7 a { color: #e5cc80 !important; } /* Heirloom */ -.q8, .q8 a, .color-q8, .wowhead-tooltip .q8 a { color: #ffff98 !important; } /* Light yellow (item set bonuses) */ -.q9, .q9 a, .color-q9, .wowhead-tooltip .q9 a { color: #71d5ff !important; } /* Light blue (glyph type) */ -.q10, .q10 a, .color-q10, .wowhead-tooltip .q10 a { color: #ff4040 !important; } /* Red (requirement not met, error) */ + .q, .q a, .color-q, .wowhead-tooltip .q a { color: #ffd100 !important } /* Default (yellow) */ + .q0, .q0 a, .color-q0, .wowhead-tooltip .q0 a { color: #9d9d9d !important } /* Poor */ + .q1, .q1 a, .color-q1, .wowhead-tooltip .q1 a { color: #ffffff !important } /* Common */ + .q2, .q2 a, .color-q2, .wowhead-tooltip .q2 a { color: #1eff00 !important } /* Uncommon */ + .q3, .q3 a, .color-q3, .wowhead-tooltip .q3 a { color: #0070dd !important } /* Rare */ + .q4, .q4 a, .color-q4, .wowhead-tooltip .q4 a { color: #a335ee !important } /* Epic */ + .q5, .q5 a, .color-q5, .wowhead-tooltip .q5 a { color: #ff8000 !important } /* Legendary */ + .q6, .q6 a, .color-q6, .wowhead-tooltip .q6 a { color: #e5cc80 !important } /* Artifact */ + .q7, .q7 a, .color-q7, .wowhead-tooltip .q7 a { color: #e5cc80 !important } /* Heirloom */ + .q8, .q8 a, .color-q8, .wowhead-tooltip .q8 a { color: #ffff98 !important } /* Light yellow (item set bonuses) */ + .q9, .q9 a, .color-q9, .wowhead-tooltip .q9 a { color: #71d5ff !important } /* Light blue (glyph type) */ +.q10, .q10 a, .color-q10, .wowhead-tooltip .q10 a { color: #ff4040 !important } /* Red (requirement not met, error) */ +.q13, .q13 a, .color-q13, .wowhead-tooltip .q13 a { color: #ffff98 !important } /*********************/ /* DIFFICULTY COLORS */ From ce82a8f09fa8e109061b19a1f271480aa7ab3d85 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Jul 2015 19:14:47 +0200 Subject: [PATCH 0025/1249] rather enforce interpretation as php for aowow/aowow than hiding it (wich broke filter forms) --- .htaccess | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.htaccess b/.htaccess index e0ea4395..a3fa8fb0 100644 --- a/.htaccess +++ b/.htaccess @@ -1,11 +1,15 @@ Order Deny,Allow - - Deny from all + + Deny from all - Allow from all + Allow from all + + ForceType application/x-httpd-php + + # Block view of some folders Options -Indexes DirectoryIndex index.php @@ -13,8 +17,8 @@ DirectoryIndex index.php # Support for UTF8 AddDefaultCharset utf8 - CharsetDisable on - CharsetRecodeMultipartForms Off + CharsetDisable on + CharsetRecodeMultipartForms Off # 5MB should be enough for the largest screenshots in the land From e9399c169a85a928ad8b628479cc672e9dc47abb Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Jul 2015 19:48:52 +0200 Subject: [PATCH 0026/1249] Config move default_charset from .htaccess to DB --- .htaccess | 3 +-- setup/db_structure.sql | 2 +- setup/updates/1436634000_01.sql | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 setup/updates/1436634000_01.sql diff --git a/.htaccess b/.htaccess index a3fa8fb0..0a1a278f 100644 --- a/.htaccess +++ b/.htaccess @@ -21,8 +21,7 @@ AddDefaultCharset utf8 CharsetRecodeMultipartForms Off -# 5MB should be enough for the largest screenshots in the land - php_value default_charset UTF-8 +# 5MB should be enough for the largest screenshots in the land (cannot be set in config) php_value upload_max_filesize 5M RewriteEngine on diff --git a/setup/db_structure.sql b/setup/db_structure.sql index e4f7af2b..f3334a25 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2284,7 +2284,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_config` WRITE; /*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',0,129,'default: 500 - max results for search'),('sql_limit_default','300',0,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',0,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',0,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',0,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',0,136,' - website title'),('name_short','Aowow',0,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',0,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',0,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',0,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',0,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',0,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',0,129,'default: 50 - vote limit per day'),('force_ssl','0',0,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',0,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',0,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',0,136,' - points js to executable files'),('static_host','',0,136,' - points js to images & scripts'),('cache_decay','25200',1,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',1,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',1,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',2,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',2,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',2,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',2,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',2,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',2,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',3,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',3,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','0',3,200,'default: 0 - probability to remove session data on garbage collection'),('session_cache_dir','',3,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',4,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',4,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',4,129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',4,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',4,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',4,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',4,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',4,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',4,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',4,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',4,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',4,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',4,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',4,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',4,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',4,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',4,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',4,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','4',5,65,' - some derelict code, probably unused'),('memory_limit','2048M',5,200,'default: 2048M - parsing spell.dbc is quite intense'); +INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',0,129,'default: 500 - max results for search'),('sql_limit_default','300',0,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',0,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',0,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',0,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',0,136,' - website title'),('name_short','Aowow',0,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',0,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',0,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',0,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',0,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',0,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',0,129,'default: 50 - vote limit per day'),('force_ssl','0',0,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',0,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',0,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',0,136,' - points js to executable files'),('static_host','',0,136,' - points js to images & scripts'),('cache_decay','25200',1,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',1,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',1,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',2,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',2,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',2,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',2,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',2,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',2,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',3,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',3,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','0',3,200,'default: 0 - probability to remove session data on garbage collection'),('session_cache_dir','',3,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',4,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',4,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',4,129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',4,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',4,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',4,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',4,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',4,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',4,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',4,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',4,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',4,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',4,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',4,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',4,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',4,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',4,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',4,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','4',5,65,' - some derelict code, probably unused'),('memory_limit','2048M',5,200,'default: 2048M - parsing spell.dbc is quite intense'),('default_charset','UTF-8',5,72,'default: UTF-8'); /*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/updates/1436634000_01.sql b/setup/updates/1436634000_01.sql new file mode 100644 index 00000000..db88b9c0 --- /dev/null +++ b/setup/updates/1436634000_01.sql @@ -0,0 +1 @@ +INSERT IGNORE INTO `aowow_config` (`key`, `value`, `flags`, `comment`) VALUES ('default_charset', 'UTF-8', 72, 'default: UTF-8'); From 051334da2215f9f619ca9ee3ba4b3a23c824ea7b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 11 Jul 2015 23:59:55 +0200 Subject: [PATCH 0027/1249] Events drop usage of holidayIds (as far as possible) the obvious change is, that all events are now refenreced by a positive eventId. (?event=375 will probably become ?event=5) Comments fixed malformed db-table. It can now hold negative typeIds. applying this commit will drop any comments related to events without holiday added gain of SiteReputation for comment-replies resyncing dependencies of 'game_event' is required --- includes/ajaxHandler.class.php | 14 +++++++ includes/community.class.php | 6 +-- includes/types/item.class.php | 15 ++++---- includes/types/itemset.class.php | 9 +++-- includes/types/quest.class.php | 3 +- includes/types/worldevent.class.php | 5 +-- pages/event.php | 27 ++++++-------- pages/genericPage.class.php | 22 +---------- pages/item.php | 8 ++-- pages/itemset.php | 12 +++--- pages/npc.php | 2 +- pages/object.php | 2 +- pages/quest.php | 2 +- pages/screenshot.php | 2 - pages/title.php | 7 +++- setup/db_structure.sql | 8 ++-- setup/tools/sqlGen.class.php | 4 +- setup/tools/sqlgen/items.func.php | 9 +++-- setup/tools/sqlgen/itemset.func.php | 6 +++ setup/tools/sqlgen/quests.func.php | 16 ++++---- setup/tools/sqlgen/titles.func.php | 39 ++++++++++++------- setup/updates/1436634000_02.sql | 58 +++++++++++++++++++++++++++++ 22 files changed, 171 insertions(+), 105 deletions(-) create mode 100644 setup/updates/1436634000_02.sql diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index b725a738..62c6c0a7 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -575,6 +575,10 @@ class AjaxHandler if (!$this->post('id') || !User::canUpvote()) break; + $owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->post('id')); + if (!$owner) + break; + $ok = DB::Aowow()->query( 'INSERT INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', $this->post('id'), @@ -583,13 +587,20 @@ class AjaxHandler ); if ($ok) + { + Util::gainSiteReputation($owner, SITEREP_ACTION_UPVOTED, ['id' => $this->post('id'), 'voterId' => User::$id]); User::decrementDailyVotes(); + } break; case 'downvote-reply': if (!$this->post('id') || !User::canUpvote()) break; + $owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->post('id')); + if (!$owner) + break; + $ok = DB::Aowow()->query( 'INSERT INTO ?_comments_rates (commentId, userId, value) VALUES (?d, ?d, ?d)', $this->post('id'), @@ -598,7 +609,10 @@ class AjaxHandler ); if ($ok) + { + Util::gainSiteReputation($owner, SITEREP_ACTION_DOWNVOTED, ['id' => $this->post('id'), 'voterId' => User::$id]); User::decrementDailyVotes(); + } } return json_encode($result, JSON_NUMERIC_CHECK); diff --git a/includes/community.class.php b/includes/community.class.php index 0d459725..d805315c 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -330,11 +330,7 @@ class CommunityContent if (!$ids) continue; - $cnd = [['id', $ids]]; - if ($t == TYPE_WORLDEVENT) // FKIN HOLIDAYS - array_push($cnd, ['holidayId', $ids], 'OR'); - - $tClass = new Util::$typeClasses[$t]($cnd); + $tClass = new Util::$typeClasses[$t](array(['id', $ids])); foreach ($pages as &$p) if ($p['type'] == $t) if ($tClass->getEntry($p['typeId'])) diff --git a/includes/types/item.class.php b/includes/types/item.class.php index 94bc4fdb..36b98a09 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -29,6 +29,7 @@ class ItemList extends BaseType 'ic' => ['j' => ['?_icons ic ON ic.id = -i.displayId', true], 's' => ', ic.iconString'], 'is' => ['j' => ['?_item_stats `is` ON `is`.`id` = `i`.`id`', true], 's' => ', `is`.*'], 's' => ['j' => ['?_spell `s` ON s.effect1CreateItemId = i.id', true], 'g' => 'i.id'], + 'e' => ['j' => ['?_events `e` ON e.id = i.eventId', true], 's' => ', e.holidayId'], 'src' => ['j' => ['?_source src ON type = 3 AND typeId = i.id', true], 's' => ', moreType, moreTypeId, src1, src2, src3, src4, src5, src6, src7, src8, src9, src10, src11, src12, src13, src14, src15, src16, src17, src18, src19, src20, src21, src22, src23, src24'] ); @@ -88,9 +89,9 @@ class ItemList extends BaseType if (empty($this->vendors)) { $itemz = DB::World()->select(' - SELECT nv.item AS ARRAY_KEY1, nv.entry AS ARRAY_KEY2, 0 AS eventId, nv.maxcount, nv.extendedCost FROM npc_vendor nv WHERE {nv.entry IN (?a) AND} nv.item IN (?a) + SELECT nv.item AS ARRAY_KEY1, nv.entry AS ARRAY_KEY2, 0 AS eventId, nv.maxcount, nv.extendedCost FROM npc_vendor nv WHERE {nv.entry IN (?a) AND} nv.item IN (?a) UNION - SELECT genv.item AS ARRAY_KEY1, c.id AS ARRAY_KEY2, IFNULL(IF(ge.holiday, ge.holiday, -ge.eventEntry), 0) AS eventId, genv.maxcount, genv.extendedCost FROM game_event_npc_vendor genv LEFT JOIN game_event ge ON genv.eventEntry = ge.eventEntry JOIN creature c ON c.guid = genv.guid WHERE {c.id IN (?a) AND} genv.item IN (?a)', + SELECT genv.item AS ARRAY_KEY1, c.id AS ARRAY_KEY2, ge.eventEntry AS eventId, genv.maxcount, genv.extendedCost FROM game_event_npc_vendor genv LEFT JOIN game_event ge ON genv.eventEntry = ge.eventEntry JOIN creature c ON c.guid = genv.guid WHERE {c.id IN (?a) AND} genv.item IN (?a)', empty($filter[TYPE_NPC]) || !is_array($filter[TYPE_NPC]) ? DBSIMPLE_SKIP : $filter[TYPE_NPC], array_keys($this->templates), empty($filter[TYPE_NPC]) || !is_array($filter[TYPE_NPC]) ? DBSIMPLE_SKIP : $filter[TYPE_NPC], @@ -543,11 +544,9 @@ class ItemList extends BaseType $x .= "
    ".Lang::game('duration').Lang::main('colon').Util::formatTime(abs($dur) * 1000).($this->curTpl['flagsCustom'] & 0x1 ? ' ('.Lang::item('realTime').')' : null); // required holiday - if ($hId = $this->curTpl['holidayId']) - { - $hDay = DB::Aowow()->selectRow("SELECT * FROM ?_holidays WHERE id = ?", $hId); - $x .= '
    '.sprintf(Lang::game('requires'), ''.Util::localizedString($hDay, 'name').''); - } + if ($eId = $this->curTpl['eventId']) + if ($hName = DB::Aowow()->selectRow('SELECT h.* FROM ?_holidays h JOIN ?_events e ON e.holidayId = h.id WHERE e.id = ?d', $eId)) + $x .= '
    '.sprintf(Lang::game('requires'), ''.Util::localizedString($hName, 'name').''); // item begins a quest if ($this->curTpl['startQuest']) @@ -1657,7 +1656,7 @@ class ItemListFilter extends Filter 99 => [FILTER_CR_ENUM, 'requiredSkill' ], // requiresprof 66 => [FILTER_CR_ENUM, 'requiredSpell' ], // requiresprofspec 17 => [FILTER_CR_ENUM, 'requiredFaction' ], // requiresrepwith - 169 => [FILTER_CR_ENUM, 'holidayId' ], // requiresevent + 169 => [FILTER_CR_ENUM, 'e.holidayId' ], // requiresevent 21 => [FILTER_CR_NUMERIC, 'is.agi', null, true], // agi 23 => [FILTER_CR_NUMERIC, 'is.int', null, true], // int 22 => [FILTER_CR_NUMERIC, 'is.sta', null, true], // sta diff --git a/includes/types/itemset.class.php b/includes/types/itemset.class.php index 25deb811..689eb5e6 100644 --- a/includes/types/itemset.class.php +++ b/includes/types/itemset.class.php @@ -14,8 +14,11 @@ class ItemsetList extends BaseType public $pieceToSet = []; // used to build g_items and search private $classes = []; // used to build g_classes - protected $queryBase = 'SELECT *, id AS ARRAY_KEY FROM ?_itemset `set`'; - protected $queryOpts = ['set' => ['o' => 'maxlevel DESC']]; + protected $queryBase = 'SELECT `set`.*, `set`.id AS ARRAY_KEY FROM ?_itemset `set`'; + protected $queryOpts = array( + 'set' => ['o' => 'maxlevel DESC'], + 'e' => ['j' => ['?_events e ON e.id = `set`.eventId', true], 's' => ', e.holidayId'] + ); public function __construct($conditions = []) { @@ -171,7 +174,7 @@ class ItemsetListFilter extends Filter 3 => [FILTER_CR_NUMERIC, 'npieces', ], // pieces 4 => [FILTER_CR_STRING, 'bonusText', true ], // bonustext 5 => [FILTER_CR_BOOLEAN, 'heroic', ], // heroic - 6 => [FILTER_CR_ENUM, 'holidayId', ], // relatedevent + 6 => [FILTER_CR_ENUM, 'e.holidayId', ], // relatedevent 8 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments 9 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots 10 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos diff --git a/includes/types/quest.class.php b/includes/types/quest.class.php index ebbc81e1..1f1a852e 100644 --- a/includes/types/quest.class.php +++ b/includes/types/quest.class.php @@ -18,6 +18,7 @@ class QuestList extends BaseType 'q' => [], 'rsc' => ['j' => '?_spell rsc ON q.rewardSpellCast = rsc.id'], // limit rewardSpellCasts 'qse' => ['j' => '?_quests_startend qse ON q.id = qse.questId', 's' => ', qse.method'], // groupConcat..? + 'e' => ['j' => ['?_events e ON e.id = `q`.eventId', true], 's' => ', e.holidayId'] ); public function __construct($conditions = [], $miscData = null) @@ -433,7 +434,7 @@ class QuestListFilter extends Filter 45 => [FILTER_CR_BOOLEAN, 'rewardTitleId', ], // titlerewarded 2 => [FILTER_CR_NUMERIC, 'rewardXP', ], // experiencegained 3 => [FILTER_CR_NUMERIC, 'rewardOrReqMoney', ], // moneyrewarded - 33 => [FILTER_CR_ENUM, 'holidayId', ], // relatedevent + 33 => [FILTER_CR_ENUM, 'e.holidayId', ], // relatedevent 25 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments 18 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots 36 => [FILTER_CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos diff --git a/includes/types/worldevent.class.php b/includes/types/worldevent.class.php index d5ec19ff..6021cca6 100644 --- a/includes/types/worldevent.class.php +++ b/includes/types/worldevent.class.php @@ -9,7 +9,7 @@ class WorldEventList extends BaseType public static $type = TYPE_WORLDEVENT; public static $brickFile = 'event'; - protected $queryBase = 'SELECT *, -e.id as id, -e.id AS ARRAY_KEY FROM ?_events e'; + protected $queryBase = 'SELECT *, e.id as id, e.id AS ARRAY_KEY FROM ?_events e'; protected $queryOpts = array( 'e' => [['h']], 'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC'] @@ -40,12 +40,9 @@ class WorldEventList extends BaseType if ($this->curTpl['requires']) $this->curTpl['requires'] = explode(' ', $this->curTpl['requires']); - $this->curTpl['eventBak'] = -$this->curTpl['id']; - // change Ids if holiday is set if ($this->curTpl['holidayId'] > 0) { - $this->curTpl['id'] = $this->curTpl['holidayId']; $this->curTpl['name'] = $this->getField('name', true); $replace[$this->id] = $this->curTpl; unset($this->curTpl['description']); diff --git a/pages/event.php b/pages/event.php index 0a3621c8..9d0aec73 100644 --- a/pages/event.php +++ b/pages/event.php @@ -26,18 +26,12 @@ class EventPage extends GenericPage $this->typeId = intVal($id); - $conditions = $this->typeId < 0 ? [['id', -$this->typeId]] : [['holidayId', $this->typeId]]; - - $this->subject = new WorldEventList($conditions); + $this->subject = new WorldEventList(array(['id', $this->typeId])); if ($this->subject->error) $this->notFound(Lang::game('event'), Lang::event('notFound')); $this->hId = $this->subject->getField('holidayId'); - $this->eId = $this->subject->getField('eventBak'); - - // redirect if associated with a holiday - if ($this->hId && $this->typeId != $this->hId) - header('Location: '.HOST_URL.'?event='.$this->hId, true, 302); + $this->eId = $this->typeId; $this->name = $this->subject->getField('name', true); } @@ -86,7 +80,7 @@ class EventPage extends GenericPage $this->headIcons = [$this->subject->getField('iconString')]; $this->redButtons = array( - BUTTON_WOWHEAD => $this->typeId > 0, + BUTTON_WOWHEAD => $this->hId > 0, BUTTON_LINKS => true ); $this->dates = array( @@ -163,11 +157,11 @@ class EventPage extends GenericPage { $itemCnd = array( 'OR', - ['holidayId', $this->hId], // direct requirement on item + ['eventId', $this->eId], // direct requirement on item ); // tab: quests (by table, go & creature) - $quests = new QuestList(array(['holidayId', $this->hId])); + $quests = new QuestList(array(['eventId', $this->eId])); if (!$quests->error) { $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); @@ -217,7 +211,7 @@ class EventPage extends GenericPage } // tab: see also (event conditions) - if ($rel = DB::World()->selectCol('SELECT IF(eventEntry = prerequisite_event, NULL, IF(eventEntry = ?d, -prerequisite_event, eventEntry)) FROM game_event_prerequisite WHERE prerequisite_event = ?d OR eventEntry = ?d', $this->eId, $this->eId, $this->eId)) + if ($rel = DB::World()->selectCol('SELECT IF(eventEntry = prerequisite_event, NULL, IF(eventEntry = ?d, prerequisite_event, -eventEntry)) FROM game_event_prerequisite WHERE prerequisite_event = ?d OR eventEntry = ?d', $this->eId, $this->eId, $this->eId)) { $list = []; array_walk($rel, function($v, $k) use (&$list) { @@ -233,18 +227,18 @@ class EventPage extends GenericPage $this->extendGlobalData($relEvents->getJSGlobals()); $relData = $relEvents->getListviewData(); foreach ($relEvents->getFoundIDs() as $id) - $relData[$id]['condition'][0][$this->typeId][] = [[-CND_ACTIVE_EVENT, -$this->eId]]; + $relData[$id]['condition'][0][$this->typeId][] = [[-CND_ACTIVE_EVENT, $this->eId]]; $this->extendGlobalData($this->subject->getJSGlobals()); foreach ($rel as $r) { - if ($r >= 0) + if ($r <= 0) continue; $this->extendGlobalIds(TYPE_WORLDEVENT, $r); $d = $this->subject->getListviewData(); - $d[-$this->eId]['condition'][0][$this->typeId][] = [[-CND_ACTIVE_EVENT, $r]]; + $d[$this->eId]['condition'][0][$this->typeId][] = [[-CND_ACTIVE_EVENT, $r]]; $relData = array_merge($relData, $d); } @@ -265,6 +259,9 @@ class EventPage extends GenericPage protected function postCache() { + if ($this->hId) + Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/event='.$this->hId; + /********************/ /* finalize infobox */ /********************/ diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index 5c55ffce..31821a48 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -634,27 +634,7 @@ class GenericPage $this->initJSGlobal($type); - // todo (med): properly distinguish holidayId and eventId - $cnd = [CFG_SQL_LIMIT_NONE]; - if ($type == TYPE_WORLDEVENT) - { - $hIds = array_filter($ids, function($v) { return $v > 0; }); - $eIds = array_filter($ids, function($v) { return $v < 0; }); - - if ($hIds) - $cnd[] = ['holidayId', array_unique($hIds, SORT_NUMERIC)]; - - if ($eIds) - { - array_walk($eIds, function(&$v) { $v = abs($v);}); - $cnd[] = ['e.id', array_unique($eIds, SORT_NUMERIC)]; - } - - if ($eIds && $hIds) - $cnd[] = 'OR'; - } - else - $cnd [] = ['id', array_unique($ids, SORT_NUMERIC)]; + $cnd = [CFG_SQL_LIMIT_NONE, ['id', array_unique($ids, SORT_NUMERIC)]]; switch ($type) { diff --git a/pages/item.php b/pages/item.php index 4a27af60..0c55a37c 100644 --- a/pages/item.php +++ b/pages/item.php @@ -167,9 +167,11 @@ class ItemPage extends genericPage } // related holiday - if ($hId = $this->subject->getField('holidayId')) - if ($hName = DB::Aowow()->selectRow('SELECT * FROM ?_holidays WHERE id = ?d', $hId)) - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[url=?event='.$hId.']'.Util::localizedString($hName, 'name').'[/url]'; + if ($eId = $this->subject->getField('eventId')) + { + $this->extendGlobalIds(TYPE_WORLDEVENT, $eId); + $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$eId.']'; + } // tool if ($tId = $this->subject->getField('totemCategory')) diff --git a/pages/itemset.php b/pages/itemset.php index e8983424..fbe472a6 100644 --- a/pages/itemset.php +++ b/pages/itemset.php @@ -68,11 +68,11 @@ class ItemsetPage extends GenericPage if ($this->subject->getField('cuFlags') & CUSTOM_UNAVAILABLE) $infobox[] = Lang::main('unavailable'); - // holiday - if ($h = $this->subject->getField('holidayId')) + // worldevent + if ($e = $this->subject->getField('eventId')) { - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$h.']'; - $this->extendGlobalIds(TYPE_WORLDEVENT, $h); + $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$e.']'; + $this->extendGlobalIds(TYPE_WORLDEVENT, $e); } // itemLevel @@ -184,10 +184,10 @@ class ItemsetPage extends GenericPage $rel[] = ['classMask', 1 << (end($this->path) - 1), '&']; $rel[] = ['contentGroup', (int)$_ta]; } - else if ($this->subject->getField('holidayId')) + else if ($this->subject->getField('eventId')) { $rel[] = ['id', $this->typeId, '!']; - $rel[] = ['holidayId', 0, '!']; + $rel[] = ['eventId', 0, '!']; } else if ($this->subject->getField('skillId')) { diff --git a/pages/npc.php b/pages/npc.php index 566ec361..6ca91efe 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -110,7 +110,7 @@ class NpcPage extends GenericPage $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // Event (ignore events, where the object only gets removed) - if ($_ = DB::World()->selectCol('SELECT DISTINCT IF(ge.holiday, ge.holiday, -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)) + 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 = []; diff --git a/pages/object.php b/pages/object.php index 96d5ce2e..27351ae5 100644 --- a/pages/object.php +++ b/pages/object.php @@ -63,7 +63,7 @@ class ObjectPage extends GenericPage $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // Event (ignore events, where the object only gets removed) - if ($_ = DB::World()->selectCol('SELECT DISTINCT IF(ge.holiday, ge.holiday, -ge.eventEntry) FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.eventEntry = geg.eventEntry AND g.guid = geg.guid AND g.id = ?d', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.eventEntry FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.eventEntry = geg.eventEntry AND g.guid = geg.guid AND g.id = ?d', $this->typeId)) { $this->extendGlobalIds(TYPE_WORLDEVENT, $_); $ev = []; diff --git a/pages/quest.php b/pages/quest.php index 24695c9c..eec62ee4 100644 --- a/pages/quest.php +++ b/pages/quest.php @@ -63,7 +63,7 @@ class QuestPage extends GenericPage $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // event (todo: assign eventData) - if ($_ = $this->subject->getField('holidayId')) + if ($_ = $this->subject->getField('eventId')) { $this->extendGlobalIds(TYPE_WORLDEVENT, $_); $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$_.']'; diff --git a/pages/screenshot.php b/pages/screenshot.php index b98fb30e..57632c7f 100644 --- a/pages/screenshot.php +++ b/pages/screenshot.php @@ -49,8 +49,6 @@ class ScreenshotPage extends GenericPage $t = Util::$typeClasses[$m[1]]; $c = [['id', intVal($m[2])]]; - if ($m[1] == TYPE_WORLDEVENT && $m[2] < 0) // ohforfsake.. - $c = [['id', -intVal($m[2])]]; $this->destination = new $t($c); diff --git a/pages/title.php b/pages/title.php index 20029899..d9f49bd3 100644 --- a/pages/title.php +++ b/pages/title.php @@ -61,8 +61,11 @@ class TitlePage extends GenericPage if ($g = $this->subject->getField('gender')) $infobox[] = Lang::main('gender').Lang::main('colon').'[span class=icon-'.($g == 2 ? 'female' : 'male').']'.Lang::main('sex', $g).'[/span]'; - if ($e = $this->subject->getField('holidayId')) - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[url=?event='.$e.']'.WorldEventList::getName($e).'[/url]'; + if ($eId = $this->subject->getField('eventId')) + { + $this->extendGlobalIds(TYPE_WORLDEVENT, $eId); + $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$eId.']'; + } /****************/ /* Main Content */ diff --git a/setup/db_structure.sql b/setup/db_structure.sql index f3334a25..2cc2694d 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -1080,7 +1080,7 @@ CREATE TABLE `aowow_items` ( `disenchantId` mediumint(8) unsigned NOT NULL DEFAULT '0', `duration` int(10) unsigned NOT NULL DEFAULT '0', `itemLimitCategory` smallint(6) NOT NULL DEFAULT '0', - `holidayId` int(11) unsigned NOT NULL DEFAULT '0', + `eventId` smallint(5) unsigned NOT NULL, `scriptName` varchar(64) NOT NULL DEFAULT '', `foodType` tinyint(3) unsigned NOT NULL DEFAULT '0', `gemEnchantmentId` mediumint(8) NOT NULL, @@ -1152,7 +1152,7 @@ CREATE TABLE `aowow_itemset` ( `quality` tinyint(4) NOT NULL, `type` smallint(6) NOT NULL COMMENT 'g_itemset_types', `contentGroup` smallint(6) NOT NULL COMMENT 'g_itemset_notes', - `holidayId` smallint(3) NOT NULL, + `eventId` smallint(5) unsigned NOT NULL, `skillId` smallint(3) unsigned NOT NULL, `skillLevel` smallint(3) unsigned NOT NULL, PRIMARY KEY (`id`) @@ -1349,7 +1349,7 @@ CREATE TABLE `aowow_quests` ( `type` smallint(5) unsigned NOT NULL DEFAULT '0', `suggestedPlayers` tinyint(3) unsigned NOT NULL DEFAULT '0', `timeLimit` int(10) unsigned NOT NULL DEFAULT '0', - `holidayId` smallint(6) NOT NULL DEFAULT '0', + `eventId` smallint(5) unsigned NOT NULL, `prevQuestId` mediumint(8) NOT NULL DEFAULT '0', `nextQuestId` mediumint(8) NOT NULL DEFAULT '0', `exclusiveGroup` mediumint(8) NOT NULL DEFAULT '0', @@ -2134,7 +2134,7 @@ CREATE TABLE `aowow_titles` ( `side` tinyint(3) unsigned NOT NULL, `expansion` tinyint(3) unsigned NOT NULL, `src12Ext` mediumint(9) unsigned NOT NULL, - `holidayId` smallint(5) unsigned NOT NULL, + `eventId` smallint(5) unsigned NOT NULL, `male_loc0` varchar(33) NOT NULL, `male_loc2` varchar(35) NOT NULL, `male_loc3` varchar(37) NOT NULL, diff --git a/setup/tools/sqlGen.class.php b/setup/tools/sqlGen.class.php index 6ab60e12..d6d126b5 100644 --- a/setup/tools/sqlGen.class.php +++ b/setup/tools/sqlGen.class.php @@ -57,10 +57,10 @@ class SqlGen 'spelldifficulty' => [null, null, null, ['spelldifficulty_dbc']], 'taxi' /* nodes + paths */ => [null, null, null, ['creature_template', 'creature']], 'titles' => [null, null, null, ['quest_template', 'game_event_seasonal_questrelation', 'game_event', 'achievement_reward']], - 'items' => [null, null, null, ['item_template', 'locales_item', 'spell_group']], + 'items' => [null, null, null, ['item_template', 'locales_item', 'spell_group', 'game_event']], 'spawns' /* + waypoints */ => [null, null, null, ['creature', 'creature_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'script_waypoint', 'waypoints', 'waypoint_data']], 'zones' => [null, null, null, ['access_requirement']], - 'itemset' => [null, null, ['spell'], ['item_template']], + 'itemset' => [null, null, ['spell'], ['item_template', 'game_event']], 'item_stats' => [null, null, ['items', 'spell'], null], 'source' => [null, null, ['spell', 'achievements'], ['npc_vendor', 'game_event_npc_vendor', 'creature', 'quest_template', 'playercreateinfo_item', 'npc_trainer', 'skill_discovery_template', 'playercreateinfo_spell', 'achievement_reward']] ); diff --git a/setup/tools/sqlgen/items.func.php b/setup/tools/sqlgen/items.func.php index 9e150614..73635b67 100644 --- a/setup/tools/sqlgen/items.func.php +++ b/setup/tools/sqlgen/items.func.php @@ -11,6 +11,7 @@ if (!CLI) * item_template * locales_item * spell_group + * game_event */ $customData = array( @@ -80,7 +81,7 @@ function items(array $ids = []) spellid_4, spelltrigger_4, spellcharges_4, spellppmRate_4, spellcooldown_4, spellcategory_4, spellcategorycooldown_4, spellid_5, spelltrigger_5, spellcharges_5, spellppmRate_5, spellcooldown_5, spellcategory_5, spellcategorycooldown_5, bonding, - description, description_loc2, description_loc3, description_loc6, description_loc8, + it.description, description_loc2, description_loc3, description_loc6, description_loc8, PageText, LanguageID, startquest, @@ -101,7 +102,7 @@ function items(array $ids = []) DisenchantID, duration, ItemLimitCategory, - HolidayId, + IFNULL(ge.eventEntry, 0), ScriptName, FoodType, 0 AS gemEnchantmentId, @@ -113,6 +114,8 @@ function items(array $ids = []) locales_item li ON li.entry = it.entry LEFT JOIN spell_group sg ON sg.spell_id = it.spellid_1 AND it.class = 0 AND it.subclass = 2 AND sg.id IN (1, 2) + LEFT JOIN + game_event ge ON ge.holiday = it.HolidayId AND it.HolidayId > 0 { WHERE ct.entry IN (?a) @@ -182,7 +185,7 @@ function items(array $ids = []) DB::Aowow()->query('UPDATE ?_items SET subClass = -2 WHERE quality = 4 AND class = 15 AND subClassBak = 0 AND requiredClass AND (requiredClass & 0x5FF) <> 0x5FF'); // move some junk to holiday if it requires one - DB::Aowow()->query('UPDATE ?_items SET subClass = 3 WHERE classBak = 15 AND subClassBak = 0 AND holidayId <> 0'); + DB::Aowow()->query('UPDATE ?_items SET subClass = 3 WHERE classBak = 15 AND subClassBak = 0 AND eventId <> 0'); // move misc items that start quests to class: quest (except Sayges scrolls for consistency) DB::Aowow()->query('UPDATE ?_items SET class = 12 WHERE classBak = 15 AND startQuest <> 0 AND name_loc0 NOT LIKE "sayge\'s fortune%"'); diff --git a/setup/tools/sqlgen/itemset.func.php b/setup/tools/sqlgen/itemset.func.php index f82b84a5..7fbcc104 100644 --- a/setup/tools/sqlgen/itemset.func.php +++ b/setup/tools/sqlgen/itemset.func.php @@ -15,6 +15,7 @@ if (!CLI) /* deps: * item_template + * game_event */ @@ -33,6 +34,11 @@ function itemset() 812 => 181, // Noblegarden ); + // find events associated with holidayIds + if ($pairs = DB::World()->selectCol('SELECT holiday AS ARRAY_KEY, eventEntry FROM game_event WHERE holiday IN (?a)', array_values($setToHoliday))) + foreach ($setToHoliday as &$hId) + $hId = !empty($pairs[$hId]) ? $pairs[$hId] : 0; + // tags where refId == virtualId // in pve sets are not recycled beyond the contentGroup $tagsById = array( diff --git a/setup/tools/sqlgen/quests.func.php b/setup/tools/sqlgen/quests.func.php index 358c403e..cf5e67a6 100644 --- a/setup/tools/sqlgen/quests.func.php +++ b/setup/tools/sqlgen/quests.func.php @@ -33,7 +33,7 @@ function quests(array $ids = []) Type, SuggestedPlayers, LimitTime, - 0 AS holidayId, -- holidayId + IFNULL(gesqr.eventEntry, 0) AS eventId, PrevQuestId, NextQuestId, ExclusiveGroup, @@ -90,6 +90,8 @@ function quests(array $ids = []) quest_template q LEFT JOIN locales_quest lq ON q.Id = lq.Id + LEFT JOIN + game_event_seasonal_questrelation gesqr ON gesqr.questId = q.Id { WHERE q.Id IN (?a) @@ -155,7 +157,7 @@ function quests(array $ids = []) DB::Aowow()->query($repQuery, $i, $i, $i, $i, $ids ?: DBSIMPLE_SKIP); // update zoneOrSort .. well .. now "not documenting" bites me in the ass .. ~700 quests were changed, i don't know by what method - $questByHoliday = DB::World()->selectCol('SELECT sq.questId AS ARRAY_KEY, ge.holiday FROM game_event_seasonal_questrelation sq JOIN game_event ge ON ge.eventEntry = sq.eventEntry'); + $eventSet = DB::World()->selectCol('SELECT holiday AS ARRAY_KEY, eventEntry FROM game_event WHERE holiday <> 0'); $holidaySorts = array( 141 => -1001, 181 => -374, 201 => -1002, 301 => -101, 321 => -1005, 324 => -1003, @@ -163,9 +165,9 @@ function quests(array $ids = []) 374 => -364, 376 => -364, 404 => -375, 409 => -41, 423 => -376, 424 => -101 ); - foreach ($questByHoliday as $qId => $hId) - if ($hId) - DB::Aowow()->query('UPDATE ?_quests SET zoneOrSort = ?d WHERE id = ?d{ AND id IN (?a)}', $holidaySorts[$hId], $qId, $ids ?: DBSIMPLE_SKIP); + foreach ($holidaySorts as $hId => $sort) + if (!empty($eventSet[$hId])) + DB::Aowow()->query('UPDATE ?_quests SET zoneOrSort = ?d WHERE eventId = ?d{ AND id IN (?a)}', $sort, $eventSet[$hId], $ids ?: DBSIMPLE_SKIP); /* zoneorsort for quests will need updating @@ -193,10 +195,6 @@ function quests(array $ids = []) // dungeon quests to Misc/Dungeon Finder DB::Aowow()->query('UPDATE ?_quests SET zoneOrSort = ?d WHERE (specialFlags & ?d OR id IN (?a)){ AND id IN (?a)}', -1010, QUEST_FLAG_SPECIAL_DUNGEON_FINDER, [24789, 24791, 24923], $ids ?: DBSIMPLE_SKIP); - // finally link related events (after zoneorSort has been updated) - foreach ($holidaySorts as $hId => $sort) - DB::Aowow()->query('UPDATE ?_quests SET holidayId = ?d WHERE zoneOrSort = ?d{ AND id IN (?a)}', $hId, $sort, $ids ?: DBSIMPLE_SKIP); - return true; } diff --git a/setup/tools/sqlgen/titles.func.php b/setup/tools/sqlgen/titles.func.php index 1ea1aaa4..a084930b 100644 --- a/setup/tools/sqlgen/titles.func.php +++ b/setup/tools/sqlgen/titles.func.php @@ -14,27 +14,32 @@ if (!CLI) */ $customData = array( - 137 => ['holidayId' => 201, 'gender' => 2], - 138 => ['holidayId' => 201, 'gender' => 1], - 124 => ['holidayId' => 324], - 135 => ['holidayId' => 423], - 155 => ['holidayId' => 181], - 133 => ['holidayId' => 372], - 74 => ['holidayId' => 327], - 75 => ['holidayId' => 341], - 76 => ['holidayId' => 341], - 134 => ['holidayId' => 141], - 168 => ['holidayId' => 404] + 137 => ['gender' => 2], + 138 => ['gender' => 1], ); $reqDBC = ['chartitles']; function titles() { + $titleHoliday = array( + 137 => 201, + 138 => 201, + 124 => 324, + 135 => 423, + 155 => 181, + 133 => 372, + 74 => 327, + 75 => 341, + 76 => 341, + 134 => 141, + 168 => 404 + ); + $questQuery = ' SELECT qt.RewardTitleId AS ARRAY_KEY, qt.RequiredRaces, - ge.holiday + ge.eventEntry FROM quest_template qt LEFT JOIN @@ -61,13 +66,19 @@ function titles() DB::Aowow()->query('UPDATE ?_titles SET category = 4 WHERE id IN (81, 125)'); DB::Aowow()->query('UPDATE ?_titles SET category = 3 WHERE id IN (53, 64, 120, 121, 122, 129, 139, 140, 141, 142) OR (id >= 158 AND category = 0)'); + // update event + if ($assoc = DB::World()->selectCol('SELECT holiday AS ARRAY_KEY, eventEntry FROM game_event WHERE holiday IN (?a)', array_values($titleHoliday))) + foreach ($titleHoliday as $tId => $hId) + if (!empty($assoc[$hId])) + DB::Aowow()->query('UPDATE ?_titles SET eventId = ?d WHERE id = ?d', $assoc[$hId], $tId); + // update side $questInfo = DB::World()->select($questQuery); $sideUpd = DB::World()->selectCol('SELECT IF (title_A, title_A, title_H) AS ARRAY_KEY, BIT_OR(IF(title_A, 1, 2)) AS side FROM achievement_reward WHERE (title_A <> 0 AND title_H = 0) OR (title_H <> 0 AND title_A = 0) GROUP BY ARRAY_KEY HAVING side <> 3'); foreach ($questInfo as $tId => $data) { - if ($data['holiday']) - DB::Aowow()->query('UPDATE ?_titles SET holidayId = ?d WHERE id = ?d', $data['holiday'], $tId); + if ($data['eventEntry']) + DB::Aowow()->query('UPDATE ?_titles SET eventId = ?d WHERE id = ?d', $data['eventEntry'], $tId); $side = Util::sideByRaceMask($data['RequiredRaces']); if ($side == 3) diff --git a/setup/updates/1436634000_02.sql b/setup/updates/1436634000_02.sql new file mode 100644 index 00000000..d6432249 --- /dev/null +++ b/setup/updates/1436634000_02.sql @@ -0,0 +1,58 @@ +ALTER TABLE `aowow_items` + ALTER `holidayId` DROP DEFAULT; +ALTER TABLE `aowow_items` + CHANGE COLUMN `holidayId` `eventId` SMALLINT(5) UNSIGNED NOT NULL AFTER `itemLimitCategory`; + +ALTER TABLE `aowow_itemset` + ALTER `holidayId` DROP DEFAULT; +ALTER TABLE `aowow_itemset` + CHANGE COLUMN `holidayId` `eventId` SMALLINT(5) UNSIGNED NOT NULL AFTER `contentGroup`; + +ALTER TABLE `aowow_quests` + ALTER `holidayId` DROP DEFAULT; +ALTER TABLE `aowow_quests` + CHANGE COLUMN `holidayId` `eventId` SMALLINT(5) UNSIGNED NOT NULL AFTER `timeLimit`; + +ALTER TABLE `aowow_titles` + ALTER `holidayId` DROP DEFAULT; +ALTER TABLE `aowow_titles` + CHANGE COLUMN `holidayId` `eventId` SMALLINT(5) UNSIGNED NOT NULL AFTER `src12Ext`; + +ALTER TABLE `aowow_comments` + ALTER `typeId` DROP DEFAULT; +ALTER TABLE `aowow_comments` + CHANGE COLUMN `typeId` `typeId` INT(10) NOT NULL COMMENT 'ID Of Page' AFTER `type`; + +-- --------------- +-- try to reconstruct CommunityContent for TYPE_WORLDEVENT (12) +-- --------------- +UPDATE `aowow_comments` c, `aowow_events` e SET c.`typeId` = e.`id` WHERE c.`type` = 12 AND c.`typeId` > 0 AND c.`typeId` = e.`holidayId`; +UPDATE `aowow_comments` SET `typeId` = -`typeId` WHERE `type` = 12 AND `typeId` < 0; +UPDATE `aowow_screenshots` s, `aowow_events` e SET s.`typeId` = e.`id` WHERE s.`type` = 12 AND s.`typeId` > 0 AND s.`typeId` = e.`holidayId`; +UPDATE `aowow_screenshots` SET `typeId` = -`typeId` WHERE `type` = 12 AND `typeId` < 0; +UPDATE `aowow_videos` v, `aowow_events` e SET v.`typeId` = e.`id` WHERE v.`type` = 12 AND v.`typeId` > 0 AND v.`typeId` = e.`holidayId`; +UPDATE `aowow_videos` SET `typeId` = -`typeId` WHERE `type` = 12 AND `typeId` < 0; + +-- --------------- +-- drop not recoverable comments +-- --------------- +DELETE FROM `aowow_account_reputation` WHERE `action` IN (3, 4, 5) AND `sourceA` IN ( + SELECT x.`id` FROM ( + SELECT c2.id FROM `aowow_comments` c1 JOIN `aowow_comments` c2 ON c2.`replyTo` = c1.`id` WHERE c1.`type` = 12 AND c1.`typeId` = 0 UNION + SELECT id FROM `aowow_comments` WHERE `type` = 12 AND `typeId` = 0 + ) AS x +) + +DELETE FROM `aowow_comments_rates` WHERE `commentId` IN ( + SELECT x.`id` FROM ( + SELECT c2.id FROM `aowow_comments` c1 JOIN `aowow_comments` c2 ON c2.`replyTo` = c1.`id` WHERE c1.`type` = 12 AND c1.`typeId` = 0 UNION + SELECT id FROM `aowow_comments` WHERE `type` = 12 AND `typeId` = 0 + ) AS x +) + +DELETE FROM `aowow_comments` WHERE `id` IN ( + SELECT x.`id` FROM ( + SELECT c2.id FROM `aowow_comments` c1 JOIN `aowow_comments` c2 ON c2.`replyTo` = c1.`id` WHERE c1.`type` = 12 AND c1.`typeId` = 0 UNION + SELECT id FROM `aowow_comments` WHERE `type` = 12 AND `typeId` = 0 + ) AS x +) From dd0bb304812917bfb2effd5abcda42f0d7a2ba8d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Jul 2015 01:29:21 +0200 Subject: [PATCH 0028/1249] Comments/Replies * fixed reply editing * fixed displaying error messages --- includes/ajaxHandler.class.php | 50 +++++++++++++++------------------- includes/loot.class.php | 2 +- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index 62c6c0a7..b68d6712 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -403,9 +403,10 @@ class AjaxHandler } if ($votes = DB::Aowow()->selectRow('SELECT 1 AS success, SUM(IF(value > 0, value, 0)) AS up, SUM(IF(value < 0, -value, 0)) AS down FROM ?_comments_rates WHERE commentId = ?d GROUP BY commentId', $this->get('id'))) - return json_encode($votes, JSON_NUMERIC_CHECK); + $result = $votes; + else + $result = ['success' => 1, 'up' => 0, 'down' => 0]; - $result = ['success' => 1, 'up' => 0, 'down' => 0]; break; case 'vote': // up, down and remove if (!User::$id || !$this->get('id') || !$this->get('rating')) @@ -504,44 +505,37 @@ class AjaxHandler break; case 'add-reply': // also returns all replies on success if (!User::canComment()) - $result = 'You are not allowed to reply.'; + return 'You are not allowed to reply.'; else if (!$this->post('body') || mb_strlen($this->post('body')) < $_minRpl || mb_strlen($this->post('body')) > $_maxRpl) - $result = 'Your reply has '.mb_strlen($this->post('body')).' characters and must have at least '.$_minRpl.' and at most '.$_maxRpl.'.'; + return 'Your reply has '.mb_strlen($this->post('body')).' characters and must have at least '.$_minRpl.' and at most '.$_maxRpl.'.'; else if (!$this->post('commentId') || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE id = ?d', $this->post('commentId'))) - $result = Lang::main('genericError'); + return Lang::main('genericError'); else if (DB::Aowow()->query('INSERT INTO ?_comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (?d, ?d, ?, UNIX_TIMESTAMP(), ?d)', User::$id, User::$groups, $this->post('body'), $this->post('commentId'))) $result = CommunityContent::getCommentReplies($this->post('commentId')); else - $result = Lang::main('genericError'); + return Lang::main('genericError'); break; case 'edit-reply': // also returns all replies on success if (!User::canComment()) - $result = 'You are not allowed to reply.'; + return 'You are not allowed to reply.'; - else if (!$this->post('replyId') || $this->post('commentId')) - $result = Lang::main('genericError'); + else if (!$this->post('replyId') || !$this->post('commentId')) + return Lang::main('genericError'); else if (!$this->post('body') || mb_strlen($this->post('body')) < $_minRpl || mb_strlen($this->post('body')) > $_maxRpl) - $result = 'Your reply has '.mb_strlen($this->post('body')).' characters and must have at least '.$_minRpl.' and at most '.$_maxRpl.'.'; + return 'Your reply has '.mb_strlen($this->post('body')).' characters and must have at least '.$_minRpl.' and at most '.$_maxRpl.'.'; - if ($result) - break; + if (DB::Aowow()->query('UPDATE ?_comments SET body = ?, editUserId = ?d, editDate = UNIX_TIMESTAMP(), editCount = editCount + 1 WHERE id = ?d AND replyTo = ?d{ AND userId = ?d}', + $this->post('body'), User::$id, $this->post('replyId'), $this->post('commentId'), User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) + $result = CommunityContent::getCommentReplies($this->post('commentId')); + else + return Lang::main('genericError'); - $ok = DB::Aowow()->query( - 'UPDATE ?_comments SET body = ?, editUserId = ?d, editDate = UNIX_TIMESTAMP(), editCount = editCount + 1 WHERE id = ?d AND replyTo = ?d{ AND userId = ?d}', - $this->post('body'), - User::$id, - $this->post('replyId'), - $this->post('commentId'), - User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id - ); - - $result = $ok ? CommunityContent::getCommentReplies($this->post('commentId')) : Lang::main('genericError'); break; case 'detach-reply': if (!User::isInGroup(U_GROUP_MODERATOR) || !$this->post('id')) @@ -615,7 +609,7 @@ class AjaxHandler } } - return json_encode($result, JSON_NUMERIC_CHECK); + return Util::toJSON($result); } private function handleLocale() // not sure if this should be here.. @@ -945,7 +939,7 @@ class AjaxHandler // get and apply inventory foreach ($itemz->iterate() as $iId => $__) - $buff .= 'g_items.add('.$iId.', {name_'.User::$localeString.":'".Util::jsEscape($itemz->getField('name', true))."', quality:".$itemz->getField('quality').", icon:'".$itemz->getField('iconString')."', jsonequip:".json_encode($data[$iId], JSON_NUMERIC_CHECK)."});\n"; + $buff .= 'g_items.add('.$iId.', {name_'.User::$localeString.":'".Util::jsEscape($itemz->getField('name', true))."', quality:".$itemz->getField('quality').", icon:'".$itemz->getField('iconString')."', jsonequip:".Util::toJSON($data[$iId])."});\n"; $buff .= "\n"; } @@ -971,7 +965,7 @@ class AjaxHandler } } - $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(substr($data['name'], 1))."', icon:'".$data['icon']."', modifier:".json_encode($mods, JSON_NUMERIC_CHECK)."});\n"; + $buff .= 'g_spells.add('.$id.", {id:".$id.", name:'".Util::jsEscape(substr($data['name'], 1))."', icon:'".$data['icon']."', modifier:".Util::toJSON($mods)."});\n"; } $buff .= "\n"; } @@ -1010,7 +1004,7 @@ class AjaxHandler // $buff .= "\n\ng_excludes = {};"; // add profile to buffer - $buff .= "\n\n\$WowheadProfiler.registerProfile(".json_encode($char->getEntry(2)).");"; // can't use JSON_NUMERIC_CHECK or the talent-string becomes a float + $buff .= "\n\n\$WowheadProfiler.registerProfile(".Util::toJSON($char->getEntry(2)).");"; // can't use JSON_NUMERIC_CHECK or the talent-string becomes a float return $buff."\n"; } @@ -1122,7 +1116,7 @@ class AjaxHandler // ssm_numPagesFound $pages = CommunityContent::getScreenshotPagesForManager(isset($this->get['all']), $nPages); - $buff = 'ssm_screenshotPages = '.json_encode($pages, JSON_NUMERIC_CHECK).";\n"; + $buff = 'ssm_screenshotPages = '.Util::toJSON($pages).";\n"; $buff .= 'ssm_numPagesFound = '.$nPages.';'; return $buff; @@ -1140,7 +1134,7 @@ class AjaxHandler if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', strtolower(urldecode($this->get('user'))))) $res = CommunityContent::getScreenshotsForManager(0, 0, $uId); - return 'ssm_screenshotData = '.json_encode($res, JSON_NUMERIC_CHECK); + return 'ssm_screenshotData = '.Util::toJSON($res); } // get: id => SSid diff --git a/includes/loot.class.php b/includes/loot.class.php index c9cdfe26..51ead30e 100644 --- a/includes/loot.class.php +++ b/includes/loot.class.php @@ -63,7 +63,7 @@ class Loot $stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3); // yes, it wants a string .. how weired is that.. - return json_encode($stack, JSON_NUMERIC_CHECK); + return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON ! } private function storeJSGlobals($data) From 8b317d4b212778250ff58dbdb12c52d3a6637294 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 12 Jul 2015 23:19:07 +0200 Subject: [PATCH 0029/1249] fixup add forgtten categories to config values --- setup/updates/1436735830_01.sql | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 setup/updates/1436735830_01.sql diff --git a/setup/updates/1436735830_01.sql b/setup/updates/1436735830_01.sql new file mode 100644 index 00000000..aaaa936d --- /dev/null +++ b/setup/updates/1436735830_01.sql @@ -0,0 +1,52 @@ +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'sql_limit_search'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'sql_limit_default'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'sql_limit_quicksearch'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'sql_limit_none'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'ttl_rss'; +UPDATE `aowow_config` SET `cat` = 1 WHERE `key` = 'cache_decay'; +UPDATE `aowow_config` SET `cat` = 3 WHERE `key` = 'session_timeout_delay'; +UPDATE `aowow_config` SET `cat` = 2 WHERE `key` = 'acc_failed_auth_block'; +UPDATE `aowow_config` SET `cat` = 2 WHERE `key` = 'acc_failed_auth_count'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'name'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'name_short'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'board_url'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'contact_email'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'battlegroup'; +UPDATE `aowow_config` SET `cat` = 2 WHERE `key` = 'acc_allow_register'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'debug'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'maintenance'; +UPDATE `aowow_config` SET `cat` = 2 WHERE `key` = 'acc_auth_mode'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_upvote'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_downvote'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_comment'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_supervote'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_votemore_base'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_register'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_upvoted'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_downvoted'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_good_report'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_bad_report'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_dailyvisit'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_user_warned'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_comment'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_premium'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_upload'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_article'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_reward_user_suspended'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'user_max_votes'; +UPDATE `aowow_config` SET `cat` = 4 WHERE `key` = 'rep_req_votemore_add'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'force_ssl'; +UPDATE `aowow_config` SET `cat` = 1 WHERE `key` = 'cache_mode'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'locales'; +UPDATE `aowow_config` SET `cat` = 2 WHERE `key` = 'acc_create_save_decay'; +UPDATE `aowow_config` SET `cat` = 2 WHERE `key` = 'acc_recovery_decay'; +UPDATE `aowow_config` SET `cat` = 5 WHERE `key` = 'serialize_precision'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'screenshot_min_size'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'site_host'; +UPDATE `aowow_config` SET `cat` = 0 WHERE `key` = 'static_host'; +UPDATE `aowow_config` SET `cat` = 5 WHERE `key` = 'memory_limit'; +UPDATE `aowow_config` SET `cat` = 3 WHERE `key` = 'session.gc_maxlifetime'; +UPDATE `aowow_config` SET `cat` = 3 WHERE `key` = 'session.gc_probability'; +UPDATE `aowow_config` SET `cat` = 5 WHERE `key` = 'default_charset'; +UPDATE `aowow_config` SET `cat` = 3 WHERE `key` = 'session_cache_dir'; +UPDATE `aowow_config` SET `cat` = 1 WHERE `key` = 'cache_dir'; From cea81d1810f5016c4dbddbcaebfd7921291f0483 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 13 Jul 2015 00:28:07 +0200 Subject: [PATCH 0030/1249] fixup#2 * save sessions under absolute path * somehow forgot to add garbage collect - divisor to config --- includes/kernel.php | 2 +- setup/db_structure.sql | 2 +- setup/updates/1436739821_01.sql | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 setup/updates/1436739821_01.sql diff --git a/includes/kernel.php b/includes/kernel.php index a6b61e62..5fb87fff 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -158,7 +158,7 @@ if (!CLI) // Setup Session if (CFG_SESSION_CACHE_DIR && Util::checkOrCreateDirectory(CFG_SESSION_CACHE_DIR)) - session_save_path(CFG_SESSION_CACHE_DIR); + session_save_path(getcwd().'/'.CFG_SESSION_CACHE_DIR); session_set_cookie_params(15 * YEAR, '/', '', $secure, true); session_cache_limiter('private'); diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 2cc2694d..c88cd390 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2284,7 +2284,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_config` WRITE; /*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',0,129,'default: 500 - max results for search'),('sql_limit_default','300',0,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',0,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',0,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',0,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',0,136,' - website title'),('name_short','Aowow',0,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',0,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',0,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',0,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',0,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',0,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',0,129,'default: 50 - vote limit per day'),('force_ssl','0',0,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',0,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',0,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',0,136,' - points js to executable files'),('static_host','',0,136,' - points js to images & scripts'),('cache_decay','25200',1,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',1,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',1,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',2,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',2,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',2,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',2,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',2,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',2,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',3,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',3,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','0',3,200,'default: 0 - probability to remove session data on garbage collection'),('session_cache_dir','',3,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',4,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',4,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',4,129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',4,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',4,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',4,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',4,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',4,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',4,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',4,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',4,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',4,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',4,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',4,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',4,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',4,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',4,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',4,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','4',5,65,' - some derelict code, probably unused'),('memory_limit','2048M',5,200,'default: 2048M - parsing spell.dbc is quite intense'),('default_charset','UTF-8',5,72,'default: UTF-8'); +INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',0,129,'default: 500 - max results for search'),('sql_limit_default','300',0,129,'default: 300 - max results for listviews'),('sql_limit_quicksearch','10',0,129,'default: 10 - max results for suggestions'),('sql_limit_none','0',0,129,'default: 0 - unlimited results (i wouldn\'t change that mate)'),('ttl_rss','60',0,129,'default: 60 - time to live for RSS (in seconds)'),('name','Aowow Database Viewer (ADV)',0,136,' - website title'),('name_short','Aowow',0,136,' - feed title'),('board_url','http://www.wowhead.com/forums?board=',0,136,' - another halfbaked javascript thing..'),('contact_email','feedback@aowow.org',0,136,' - displayed sender for auth-mails, ect'),('battlegroup','Pure Pwnage',0,136,' - pretend, we belong to a battlegroup to satisfy profiler-related Jscripts'),('debug','0',0,132,'default: 0 - disable cache, enable sql-errors, enable error_reporting'),('maintenance','1',0,132,'default: 0 - display brb gnomes and block access for non-staff'),('user_max_votes','50',0,129,'default: 50 - vote limit per day'),('force_ssl','0',0,132,'default: 0 - enforce SSL, if the server is behind a load balancer'),('locales','333',0,161,'default: 0x14D - allowed locales - 0:English, 2:French, 3:German, 6:Spanish, 8:Russian'),('screenshot_min_size','200',0,129,'default: 200 - minimum dimensions of uploaded screenshots in px (yes, it\'s square)'),('site_host','',0,136,' - points js to executable files'),('static_host','',0,136,' - points js to images & scripts'),('cache_decay','25200',1,129,'default: 60 * 60 * 7 - time to keep cache in seconds'),('cache_mode','1',1,161,'default: 1 - set cache method - 0:filecache, 1:memcached'),('cache_dir','',1,136,'default: cache/template - generated pages are saved here (requires CACHE_MODE: filecache)'),('acc_failed_auth_block','900',2,129,'default: 15 * 60 - how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5',2,129,'default: 5 - how often invalid passwords are tolerated'),('acc_allow_register','1',2,132,'default: 1 - allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0',2,145,'default: 0 - source to auth against - 0:aowow, 1:TC auth-table, 2:external script'),('acc_create_save_decay','604800',2,129,'default: 604800 - time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_recovery_decay','300',2,129,'default: 300 - time to recover your account and new recovery requests are blocked'),('session_timeout_delay','3600',3,129,'default: 60 * 60 - non-permanent session times out in time() + X'),('session.gc_maxlifetime','604800',3,200,'default: 7*24*60*60 - lifetime of session data'),('session.gc_probability','1',3,200,'default: 0 - probability to remove session data on garbage collection'),('session.gc_divisor', 100, 3, 200, 'default: 100 - probability to remove session data on garbage collection'),('session_cache_dir','',3,136,'default: - php sessions are saved here. Leave empty to use php default directory.'),('rep_req_upvote','125',4,129,'default: 125 - required reputation to upvote comments'),('rep_req_downvote','250',4,129,'default: 250 - required reputation to downvote comments'),('rep_req_comment','75',4,129,'default: 75 - required reputation to write a comment / reply'),('rep_req_supervote','2500',4,129,'default: 2500 - required reputation for double vote effect'),('rep_req_votemore_base','2000',4,129,'default: 2000 - gains more votes past this threshold'),('rep_reward_register','100',4,129,'default: 100 - activated an account'),('rep_reward_upvoted','5',4,129,'default: 5 - comment received upvote'),('rep_reward_downvoted','0',4,129,'default: 0 - comment received downvote'),('rep_reward_good_report','10',4,129,'default: 10 - filed an accepted report'),('rep_reward_bad_report','0',4,129,'default: 0 - filed a rejected report'),('rep_reward_dailyvisit','5',4,129,'default: 5 - daily visit'),('rep_reward_user_warned','-50',4,129,'default: -50 - moderator imposed a warning'),('rep_reward_comment','1',4,129,'default: 1 - created a comment (not a reply) '),('rep_req_premium','25000',4,129,'default: 25000 - required reputation for premium status through reputation'),('rep_reward_upload','10',4,129,'default: 10 - suggested / uploaded video / screenshot was approved'),('rep_reward_article','100',4,129,'default: 100 - submitted an approved article/guide'),('rep_reward_user_suspended','-200',4,129,'default: -200 - moderator revoked rights'),('rep_req_votemore_add','250',4,129,'default: 250 - required reputation per additional vote past threshold'),('serialize_precision','4',5,65,' - some derelict code, probably unused'),('memory_limit','2048M',5,200,'default: 2048M - parsing spell.dbc is quite intense'),('default_charset','UTF-8',5,72,'default: UTF-8'); /*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/updates/1436739821_01.sql b/setup/updates/1436739821_01.sql new file mode 100644 index 00000000..ef35abfa --- /dev/null +++ b/setup/updates/1436739821_01.sql @@ -0,0 +1 @@ +INSERT IGNORE INTO aowow_config (`key`, `value`, `cat`, `flags`, `comment`) VALUES ('session.gc_divisor', 100, 3, 200, 'default: 100 - probability to remove session data on garbage collection'); From 20732e38d82a02cf19389098f71369d62c854ae1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 14 Jul 2015 21:51:20 +0200 Subject: [PATCH 0031/1249] DB/Structure added proper keys to ?_comments --- setup/db_structure.sql | 3 ++- setup/updates/1436903207_01.sql | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 setup/updates/1436903207_01.sql diff --git a/setup/db_structure.sql b/setup/db_structure.sql index c88cd390..10b46f1c 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -346,7 +346,8 @@ CREATE TABLE `aowow_comments` ( `responseUserId` int(10) unsigned NOT NULL DEFAULT '0', `responseBody` text, `responseRoles` smallint(5) unsigned NOT NULL DEFAULT '0', - UNIQUE KEY `id` (`id`) + PRIMARY KEY (`id`), + INDEX `type_typeId` (`type`, `typeId`) ) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/setup/updates/1436903207_01.sql b/setup/updates/1436903207_01.sql new file mode 100644 index 00000000..11eacb0f --- /dev/null +++ b/setup/updates/1436903207_01.sql @@ -0,0 +1,4 @@ +ALTER TABLE `aowow_comments` + DROP INDEX `id`, + ADD PRIMARY KEY (`id`), + ADD INDEX `type_typeId` (`type`, `typeId`); From c1f5d2ea9b13b5bc68b1693cac4d391380d8ad23 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 17 Jul 2015 23:04:34 +0200 Subject: [PATCH 0032/1249] Setup/Spawns: * only priorize captials over their surrounding if there is no alphaMap present * Dalaran and Shattrath are capitals * truncate old data as it may not get overwitten --- includes/shared.php | 2 +- setup/tools/sqlgen/spawns.func.php | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 09fe6d84..5e495c30 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ $q) $result = [$q, $res]; } + else if (in_array($res['areaId'], $capitals)) // capitals (auto-discovered) and no hand-made alphaMap available + return $res; else if (empty($result)) // add with lowest quality if alpha map is missing $result = [1.0, $res]; } @@ -121,6 +122,14 @@ function spawns() // and waypoints 'ORDER BY quality ASC'; + /*********************/ + /* truncate old data */ + /*********************/ + + DB::Aowow()->query('TRUNCATE TABLE ?_spawns'); + DB::Aowow()->query('TRUNCATE TABLE ?_creature_waypoints'); + + /**************************/ /* offsets for transports */ /**************************/ @@ -205,6 +214,7 @@ function spawns() // and waypoints } } + /*****************************/ /* spawn vehicle accessories */ /*****************************/ @@ -248,6 +258,7 @@ function spawns() // and waypoints if ($accessories) CLISetup::log(count($accessories).' accessories could not be fitted onto a spawned vehicle.', CLISetup::LOG_WARN); + /********************************/ /* restrict difficulty displays */ /********************************/ From 111e53aaef534dffcba88ee3096a8051fd1e2e1a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 18 Jul 2015 01:47:22 +0200 Subject: [PATCH 0033/1249] Spells/Tooltips * fixed text-parsing for spells with a combat rating in the tooltip, that is inside a formula (e.g. spell 24574) * fixed client tooltip updates when modifying level for affected tooltips * fixed spell-links on item tooltips with multiple combat ratings --- includes/shared.php | 2 +- includes/types/item.class.php | 8 +- includes/types/spell.class.php | 301 ++++++++++++++++----------------- static/js/basic.js | 12 ++ 4 files changed, 167 insertions(+), 156 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 5e495c30..f76b2078 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ id.'">%s'; - $parsed = preg_replace_callback('/^(.*)( .*<\/small>)(.*)$/i', function($m) use($link) { - $m[1] = sprintf($link, $m[1]); - $m[3] = sprintf($link, $m[3]); + $parsed = preg_replace_callback('/([^;]*)( .*?<\/small>)([^&]*)/i', function($m) use($link) { + $m[1] = $m[1] ? sprintf($link, $m[1]) : ''; + $m[3] = $m[3] ? sprintf($link, $m[3]) : ''; return $m[1].$m[2].$m[3]; }, $parsed, -1, $nMatches ); if (!$nMatches) - $parsed = sprintF($link, $parsed); + $parsed = sprintf($link, $parsed); } $green[] = Lang::item('trigger', $itemSpellsAndTrigger[$itemSpells->id][0]).$parsed.$itemSpellsAndTrigger[$itemSpells->id][1]; diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php index 8164bdb0..76826529 100644 --- a/includes/types/spell.class.php +++ b/includes/types/spell.class.php @@ -798,6 +798,7 @@ class SpellList extends BaseType } // description-, buff-parsing component + // returns [min, max, minFulltext, maxFulltext, ratingId] private function resolveVariableString($variable, &$usesScalingRating) { $signs = ['+', '-', '/', '*', '%', '^']; @@ -809,8 +810,10 @@ class SpellList extends BaseType $effIdx = $variable[6] ? null : $variable[9]; $switch = $variable[7] ? explode(':', $variable[7]) : null; + $result = [null]; + if (!$var) - return; + return $result; if (!$effIdx) // if EffectIdx is omitted, assume EffectIdx: 1 $effIdx = 1; @@ -819,111 +822,92 @@ class SpellList extends BaseType if ($lookup && !isset($this->refSpells[$lookup])) $this->refSpells[$lookup] = new SpellList(array(['s.id', $lookup])); + $srcSpell = $lookup ? $this->refSpells[$lookup] : $this; + switch ($var) { case 'a': // EffectRadiusMin case 'A': // EffectRadiusMax - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'RadiusMax'); - else - $base = $this->getField('effect'.$effIdx.'RadiusMax'); + $base = $srcSpell->getField('effect'.$effIdx.'RadiusMax'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'b': // PointsPerComboPoint case 'B': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'PointsPerComboPoint'); - else - $base = $this->getField('effect'.$effIdx.'PointsPerComboPoint'); + $base = $srcSpell->getField('effect'.$effIdx.'PointsPerComboPoint'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'd': // SpellDuration case 'D': // todo (med): min/max?; /w unit? - if ($lookup) - $base = $this->refSpells[$lookup]->getField('duration'); - else - $base = $this->getField('duration'); + $base = $srcSpell->getField('duration'); if ($base <= 0) - return Lang::spell('untilCanceled'); + $result[2] = Lang::spell('untilCanceled'); + else + $result[2] = Util::formatTime($base, true); - if ($op && is_numeric($oparg) && is_numeric($base)) + if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return explode(' ', Util::formatTime(abs($base), true)); + $result[0] = $base < 0 ? 0 : $base; + break; case 'e': // EffectValueMultiplier case 'E': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'ValueMultiplier'); - else - $base = $this->getField('effect'.$effIdx.'ValueMultiplier'); + $base = $srcSpell->getField('effect'.$effIdx.'ValueMultiplier'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'f': // EffectDamageMultiplier case 'F': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'DamageMultiplier'); - else - $base = $this->getField('effect'.$effIdx.'DamageMultiplier'); + $base = $srcSpell->getField('effect'.$effIdx.'DamageMultiplier'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'g': // boolean choice with casters gender as condition $gX:Y; case 'G': - return '<'.$switch[0].'/'.$switch[1].'>'; + $result[2] = '<'.$switch[0].'/'.$switch[1].'>'; + break; case 'h': // ProcChance case 'H': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('procChance'); - else - $base = $this->getField('procChance'); + $base = $srcSpell->getField('procChance'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'i': // MaxAffectedTargets case 'I': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('maxAffectedTargets'); - else - $base = $this->getField('maxAffectedTargets'); + $base = $srcSpell->getField('maxAffectedTargets'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'l': // boolean choice with last value as condition $lX:Y; case 'L': - return '$l'.$switch[0].':'.$switch[1]; // resolve later by backtracking + $result[2] = '$l'.$switch[0].':'.$switch[1];// resolve later by backtracking + break; case 'm': // BasePoints (minValue) case 'M': // BasePoints (maxValue) - if ($lookup) - { - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'BasePoints'); - $add = $this->refSpells[$lookup]->getField('effect'.$effIdx.'DieSides'); - $mv = $this->refSpells[$lookup]->getField('effect'.$effIdx.'MiscValue'); - $aura = $this->refSpells[$lookup]->getField('effect'.$effIdx.'AuraId'); - - } - else - { - $base = $this->getField('effect'.$effIdx.'BasePoints'); - $add = $this->getField('effect'.$effIdx.'DieSides'); - $mv = $this->getField('effect'.$effIdx.'MiscValue'); - $aura = $this->getField('effect'.$effIdx.'AuraId'); - } + $base = $srcSpell->getField('effect'.$effIdx.'BasePoints'); + $add = $srcSpell->getField('effect'.$effIdx.'DieSides'); + $mv = $srcSpell->getField('effect'.$effIdx.'MiscValue'); + $aura = $srcSpell->getField('effect'.$effIdx.'AuraId'); if (ctype_lower($var)) $add = 1; @@ -940,37 +924,28 @@ class SpellList extends BaseType $usesScalingRating = true; // Aura end - if ($rType && $this->interactive && $aura == 189) - return ''.abs($base).' ('.sprintf(Util::$setRatingLevelString, $this->charLevel, $rType, abs($base), Util::setRatingLevel($this->charLevel, $rType, abs($base))).')'; - else if ($rType && $aura == 189) - return ''.abs($base).' ('.Util::setRatingLevel($this->charLevel, $rType, abs($base)).')'; - else - return $base; + if ($rType) + { + $result[2] = '%s (%s)'; + $result[4] = $rType; + } + + $result[0] = $base; + break; case 'n': // ProcCharges case 'N': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('procCharges'); - else - $base = $this->getField('procCharges'); + $base = $srcSpell->getField('procCharges'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'o': // TotalAmount for periodic auras (with variance) case 'O': - if ($lookup) - { - list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $this->refSpells[$lookup]); - $periode = $this->refSpells[$lookup]->getField('effect'.$effIdx.'Periode'); - $duration = $this->refSpells[$lookup]->getField('duration'); - } - else - { - list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx); - $periode = $this->getField('effect'.$effIdx.'Periode'); - $duration = $this->getField('duration'); - } + list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $srcSpell); + $periode = $srcSpell->getField('effect'.$effIdx.'Periode'); + $duration = $srcSpell->getField('duration'); if (!$periode) $periode = 3000; @@ -980,49 +955,45 @@ class SpellList extends BaseType $equal = $min == $max; if (in_array($op, $signs) && is_numeric($oparg)) - if ($equal) - eval("\$min = $min $op $oparg;"); + { + eval("\$min = $min $op $oparg;"); + if (!$equal) + eval("\$max = $max $op $oparg;"); + } if ($this->interactive) - return $modStrMin.$min . (!$equal ? Lang::game('valueDelim') . $modStrMax.$max : null); - else - return $min . (!$equal ? Lang::game('valueDelim') . $max : null); + { + $result[2] = $modStrMin.'%s'; + $result[3] = $modStrMax.'%s'; + } + + $result[0] = $min; + $result[1] = $max; + break; case 'q': // EffectMiscValue case 'Q': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'MiscValue'); - else - $base = $this->getField('effect'.$effIdx.'MiscValue'); + $base = $srcSpell->getField('effect'.$effIdx.'MiscValue'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'r': // SpellRange case 'R': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('rangeMaxHostile'); - else - $base = $this->getField('rangeMaxHostile'); + $base = $srcSpell->getField('rangeMaxHostile'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 's': // BasePoints (with variance) case 'S': - if ($lookup) - { - list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $this->refSpells[$lookup]); - $mv = $this->refSpells[$lookup]->getField('effect'.$effIdx.'MiscValue'); - $aura = $this->refSpells[$lookup]->getField('effect'.$effIdx.'AuraId'); - } - else - { - list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx); - $mv = $this->getField('effect'.$effIdx.'MiscValue'); - $aura = $this->getField('effect'.$effIdx.'AuraId'); - } + list($min, $max, $modStrMin, $modStrMax) = $this->calculateAmountForCurrent($effIdx, $srcSpell); + $mv = $srcSpell->getField('effect'.$effIdx.'MiscValue'); + $aura = $srcSpell->getField('effect'.$effIdx.'AuraId'); + $equal = $min == $max; if (in_array($op, $signs) && is_numeric($oparg)) @@ -1039,66 +1010,70 @@ class SpellList extends BaseType $usesScalingRating = true; // Aura end - if ($rType && $equal && $this->interactive && $aura == 189) - return ''.$min.' ('.sprintf(Util::$setRatingLevelString, $this->charLevel, $rType, $min, Util::setRatingLevel($this->charLevel, $rType, $min)).')'; - else if ($rType && $equal && $aura == 189) - return ''.$min.' ('.Util::setRatingLevel($this->charLevel, $rType, $min).')'; - else if ($this->interactive && $aura == 189) - return $modStrMin.$min . (!$equal ? Lang::game('valueDelim') . $modStrMax.$max : null); - else - return $min . (!$equal ? Lang::game('valueDelim') . $max : null); + if ($rType) + { + $result[2] = '%s (%s)'; + $result[4] = $rType; + } + else if ($aura == 189 && $this->interactive) + { + $result[2] = $modStrMin.'%s'; + $result[3] = $modStrMax.'%s'; + } + + $result[0] = $min; + $result[1] = $max; + break; case 't': // Periode case 'T': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'Periode') / 1000; - else - $base = $this->getField('effect'.$effIdx.'Periode') / 1000; + $base = $srcSpell->getField('effect'.$effIdx.'Periode') / 1000; if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'u': // StackCount case 'U': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('stackAmount'); - else - $base = $this->getField('stackAmount'); + $base = $srcSpell->getField('stackAmount'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'v': // MaxTargetLevel case 'V': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('MaxTargetLevel'); - else - $base = $this->getField('MaxTargetLevel'); + $base = $srcSpell->getField('MaxTargetLevel'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'x': // ChainTargetCount case 'X': - if ($lookup) - $base = $this->refSpells[$lookup]->getField('effect'.$effIdx.'ChainTarget'); - else - $base = $this->getField('effect'.$effIdx.'ChainTarget'); + $base = $srcSpell->getField('effect'.$effIdx.'ChainTarget'); if (in_array($op, $signs) && is_numeric($oparg) && is_numeric($base)) eval("\$base = $base $op $oparg;"); - return $base; + $result[0] = $base; + break; case 'z': // HomeZone - return Lang::spell('home'); + $result[2] = Lang::spell('home'); + break; } + + return $result; } // description-, buff-parsing component private function resolveFormulaString($formula, $precision = 0, &$scaling) { + $fSuffix = '%s'; + $fRating = 0; + // step 1: formula unpacking redux while (($formStartPos = strpos($formula, '${')) !== false) { @@ -1133,7 +1108,7 @@ class SpellList extends BaseType ++$formCurPos; // for some odd reason the precision decimal survives if we dont increment further.. } - $formOutStr = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); + list($formOutStr, $fSuffix, $fRating) = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); $formula = substr_replace($formula, $formOutStr, $formStartPos, ($formCurPos - $formStartPos)); } @@ -1141,7 +1116,6 @@ class SpellList extends BaseType // step 2: resolve variables $pos = 0; // continue strpos-search from this offset $str = ''; - $suffix = ''; while (($npos = strpos($formula, '$', $pos)) !== false) { if ($npos != $pos) @@ -1159,14 +1133,17 @@ class SpellList extends BaseType } $pos += strlen($result[0]); - $var = $this->resolveVariableString($result, $scaling); - if (is_array($var)) - { - $str .= $var[0]; - $suffix = ' '.$var[1]; - } - else - $str .= $var; + // we are resolving a formula -> omit ranges + $var = $this->resolveVariableString($result, $scaling); + $str .= $var[0]; + + // overwrite eventually inherited strings + if (isset($var[2])) + $fSuffix = $var[2]; + + // overwrite eventually inherited ratings + if (isset($var[4])) + $fRating = $var[4]; } $str .= substr($formula, $pos); $str = str_replace('#', '$', $str); // reset marks @@ -1175,7 +1152,7 @@ class SpellList extends BaseType $evaled = $this->resolveEvaluation($str); $return = is_numeric($evaled) ? Lang::nf($evaled, $precision) : $evaled; - return $return.$suffix; + return [$return, $fSuffix, $fRating]; } // should probably used only once to create ?_spell. come to think of it, it yields the same results every time.. it absolutely has to! @@ -1432,9 +1409,16 @@ class SpellList extends BaseType $formPrecision = $data[$formCurPos + 1]; $formCurPos += 2; } - $formOutStr = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); + list($formOutVal, $formOutStr, $ratingId) = $this->resolveFormulaString($formOutStr, $formPrecision, $scaling); - $data = substr_replace($data, $formOutStr, $formStartPos, ($formCurPos - $formStartPos)); + if ($ratingId && is_numeric($formOutVal) && $this->interactive) + $resolved = sprintf($formOutStr, $ratingId, abs($formOutVal), sprintf(Util::$setRatingLevelString, $this->charLevel, $ratingId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $ratingId, abs($formOutVal)))); + else if ($ratingId && is_numeric($formOutVal)) + $resolved = sprintf($formOutStr, $ratingId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $ratingId, abs($formOutVal))); + else + $resolved = sprintf($formOutStr, is_numeric($formOutVal) ? abs($formOutVal) : $formOutVal); + + $data = substr_replace($data, $resolved, $formStartPos, ($formCurPos - $formStartPos)); } // step 4: find and eliminate regular variables @@ -1460,10 +1444,25 @@ class SpellList extends BaseType $pos += strlen($result[0]); $var = $this->resolveVariableString($result, $scaling); - $resolved = is_array($var) ? $var[0] : $var; - $str .= is_numeric($resolved) ? abs($resolved) : $resolved; - if (is_array($var)) - $str .= ' '.$var[1]; + $resolved = is_numeric($var[0]) ? abs($var[0]) : $var[0]; + if (isset($var[2])) + { + if (isset($var[4]) && $this->interactive) + $resolved = sprintf($var[2], $var[4], abs($var[0]), sprintf(Util::$setRatingLevelString, $this->charLevel, $var[4], abs($var[0]), Util::setRatingLevel($this->charLevel, $var[4], abs($var[0])))); + else if (isset($var[4])) + $resolved = sprintf($var[2], $var[4], abs($var[0]), Util::setRatingLevel($this->charLevel, $var[4], abs($var[0]))); + else + $resolved = sprintf($var[2], $resolved); + } + + if (isset($var[1]) && $var[0] != $var[1] && !isset($var[4])) + { + $_ = is_numeric($var[0]) ? abs($var[0]) : $var[0]; + $resolved .= Lang::game('valueDelim'); + $resolved .= isset($var[3]) ? sprintf($var[3], $_) : $_; + } + + $str .= $resolved; } $str .= substr($data, $pos); $str = str_replace('#', '$', $str); // reset marker diff --git a/static/js/basic.js b/static/js/basic.js index 58a85400..4612ae27 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -1458,13 +1458,25 @@ $WH.g_setTooltipLevel = function(tooltip, level) { }); // Rating to percent + nMatch = []; tooltip = tooltip.replace(/()([\.0-9]+)/g, function(_all, prefix, ratingId, percent) { +/* sarjuuk: fix tooltips with multiple occurences of the same rating _ = tooltip.match(new RegExp('(\\d+)')); if (!_) { return _all; } return prefix + Math.round($WH.g_convertRatingToPercent(level, ratingId, _[1]) * 100) / 100; +*/ + if (!nMatch[ratingId]) + nMatch[ratingId] = 0; + + _ = tooltip.match(new RegExp('(\\d+)', 'g'))[nMatch[ratingId]++]; + if (!_) { + return _all; + } + + return prefix + Math.round($WH.g_convertRatingToPercent(level, ratingId, _.split('>')[1]) * 100) / 100; }); // Level From 3e7a34a2ea9e4fdc4b2ccd6114e441557d6f75e9 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 18 Jul 2015 12:58:01 +0200 Subject: [PATCH 0034/1249] Spells/Tooltips * implemented 'know'-parameter (modifies tooltips based on known spells) * displayed on spell detail page * usale as urlParam (&know=) or rel-attribute (rel="know=) --- includes/shared.php | 2 +- includes/types/spell.class.php | 283 +++++++++++++++++++-------------- static/js/basic.js | 4 +- 3 files changed, 170 insertions(+), 119 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index f76b2078..65ebaad2 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ interactive) @@ -999,10 +998,8 @@ class SpellList extends BaseType if (in_array($op, $signs) && is_numeric($oparg)) { eval("\$min = $min $op $oparg;"); - if (!$equal) - eval("\$max = $max $op $oparg;"); + eval("\$max = $max $op $oparg;"); } - // Aura giving combat ratings $rType = 0; if ($aura == 189) @@ -1065,6 +1062,10 @@ class SpellList extends BaseType break; } + // handle excessively precise floats + if (is_float($result[0])) + $result[0] = round($result[0], 2); + return $result; } @@ -1134,7 +1135,15 @@ class SpellList extends BaseType $pos += strlen($result[0]); // we are resolving a formula -> omit ranges - $var = $this->resolveVariableString($result, $scaling); + $var = $this->resolveVariableString($result, $scaling); + + // time within formula -> rebase to seconds and omit timeUnit + if (strtolower($result[6] ?: $result[8]) == 'd') + { + $var[0] /= 1000; + unset($var[2]); + } + $str .= $var[0]; // overwrite eventually inherited strings @@ -1152,6 +1161,7 @@ class SpellList extends BaseType $evaled = $this->resolveEvaluation($str); $return = is_numeric($evaled) ? Lang::nf($evaled, $precision) : $evaled; + return [$return, $fSuffix, $fRating]; } @@ -1240,7 +1250,7 @@ class SpellList extends BaseType // step 0: get text $data = $this->getField($type, true); if (empty($data) || $data == "[]") // empty tooltip shouldn't be displayed anyway - return array("", []); + return ['', []]; // step 1: if the text is supplemented with text-variables, get and replace them if ($this->curTpl['spellDescriptionVariableId'] > 0) @@ -1282,96 +1292,36 @@ class SpellList extends BaseType b) elseif - $?cond[A]?cond[B]..[C] // can probably be repeated as often as you wanted c) recursive - $?cond[A][$?cond[B][..]] // can probably be stacked as deep as you wanted - only case a) can be used for KNOW-parameter IF, AND ONLY IF it is not containing further variables ($) AND it has a simple spell-condition ( \(?!?[as]\d+\)? ) - - _[100].tooltip_enus = '
    Charge
    8 - 25 yd range
    Instant20 sec cooldown
    Requires Warrior
    Requires level 3
    Charge to an enemy, stunning it for 1 sec. Generates 20 Rage.
    '; - _[100].buff_enus = ''; - _[100].spells_enus = {"58377": [["", "and 2 additional nearby targets "]], "103828": [["1 sec", "3 sec and reducing movement speed by 50% for 15 sec"]]}; - _[100].buffspells_enus = {}; - - Turns the Shaman into a Ghost Wolf, increasing speed by $s2%$?s59289[ and regenerating $59289s1% of your maximum health every 5 sec][]. - Lasts 5 min. $?$gte($pl,68)[][Cannot be used on items level 138 and higher.] + only case a) can be used for KNOW-parameter */ - // \1: full pattern match; \2: any sequence, that may include an aura/spell-ref; \3: any other sequence, between "?$" and "[" - while (preg_match('/\$\?(([\W\D]*[as]\d+)|([^\[]*))/i', $data, $matches)) - { - $condBrktCnt = 0; - $targetPart = 3; // we usually want the second pair of brackets - $curPart = 0; // parts: $? 0 [ 1 ] 2 [ 3 ] 4 - $relSpells = []; // see spells_enus - - $condOutStr = ''; - - if (!empty($matches[3])) // we can do this! -> eval - { - $cnd = $this->resolveEvaluation($matches[3]); - if ((is_numeric($cnd) || is_bool($cnd)) && $cnd) // only case, deviating from normal; positive result -> use [true] - $targetPart = 1; - - $condStartPos = strpos($data, $matches[3]) - 2; - $condCurPos = $condStartPos; - - } - - else if (!empty($matches[2])) - { - $condStartPos = strpos($data, $matches[2]) - 2; - $condCurPos = $condStartPos; - } - else // empty too? WTF?! GTFO! - die('what a terrible failure'); - - while ($condCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ - { - // we're through with this condition. replace with found result and continue - if ($curPart == 4 || $condCurPos == strlen($data)) - { - $data = substr_replace($data, $condOutStr, $condStartPos, ($condCurPos - $condStartPos)); - break; - } - - $char = $data[$condCurPos]; - - // advance position - $condCurPos++; - - if ($char == '[') - { - if (!$condBrktCnt) - $curPart++; - - $condBrktCnt++; - - if ($condBrktCnt == 1) - continue; - } - else if ($char == ']') - { - if ($condBrktCnt == 1) - $curPart++; - - $condBrktCnt--; - - if (!$condBrktCnt) - continue; - } - - // we got an elseif .. since they are self-containing we can just remove everything we've got up to here and restart the iteration - if ($curPart == 2 && $char == '?') - { - $replace = $targetPart == 1 ? $condOutStr.' $' : '$'; - $data = substr_replace($data, $replace, $condStartPos, ($condCurPos - $condStartPos) - 1); - break; - } - - if ($curPart == $targetPart) - $condOutStr .= $char; - - } - } + $relSpells = []; + $data = $this->handleConditions($data, $scaling, $relSpells); // step 3: unpack formulas ${ .. }.X + $data = $this->handleFormulas($data, $scaling); + + // step 4: find and eliminate regular variables + $data = $this->handleVariables($data, $scaling); + + // step 5: variable-dependant variable-text + // special case $lONE:ELSE; + // todo (low): russian uses THREE (wtf?! oO) cases ($l[singular]:[plural1]:[plural2]) .. explode() chooses always the first plural option :/ + while (preg_match('/([\d\.]+)([^\d]*)(\$l:*)([^:]*):([^;]*);/i', $data, $m)) + $data = str_ireplace($m[1].$m[2].$m[3].$m[4].':'.$m[5].';', $m[1].$m[2].($m[1] == 1 ? $m[4] : explode(':', $m[5])[0]), $data); + + // step 6: HTMLize + // colors + $data = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $data); + + // line endings + $data = strtr($data, ["\r" => '', "\n" => '
    ']); + + return [$data, $relSpells]; + } + + private function handleFormulas($data, &$scaling) + { // they are stacked recursively but should be balanced .. hf while (($formStartPos = strpos($data, '${')) !== false) { @@ -1421,7 +1371,11 @@ class SpellList extends BaseType $data = substr_replace($data, $resolved, $formStartPos, ($formCurPos - $formStartPos)); } - // step 4: find and eliminate regular variables + return $data; + } + + private function handleVariables($data, &$scaling) + { $pos = 0; // continue strpos-search from this offset $str = ''; while (($npos = strpos($data, '$', $pos)) !== false) @@ -1467,30 +1421,125 @@ class SpellList extends BaseType $str .= substr($data, $pos); $str = str_replace('#', '$', $str); // reset marker - // step 5: variable-dependant variable-text - // special case $lONE:ELSE; - // todo (low): russian uses THREE (wtf?! oO) cases ($l[singular]:[plural1]:[plural2]) .. explode() chooses always the first plural option :/ - while (preg_match('/([\d\.]+)([^\d]*)(\$l:*)([^:]*):([^;]*);/i', $str, $m)) - $str = str_ireplace($m[1].$m[2].$m[3].$m[4].':'.$m[5].';', $m[1].$m[2].($m[1] == 1 ? $m[4] : explode(':', $m[5])[0]), $str); + return $str; + } - // step 6: HTMLize - // colors - $str = preg_replace('/\|cff([a-f0-9]{6})(.+?)\|r/i', '$2', $str); + private function handleConditions($data, &$scaling, &$relSpells, $dontKnow = false) + { + while (($condStartPos = strpos($data, '$?')) !== false) + { + $condBrktCnt = 0; + $condCurPos = $condStartPos + 2; // after the '$?' + $targetPart = 3; // we usually want the second pair of brackets + $curPart = 0; // parts: $? 0 [ 1 ] 2 [ 3 ] 4 ... + $condParts = []; + $isLastElse = false; - // line endings - $str = strtr($str, ["\r" => '', "\n" => '
    ']); + while ($condCurPos <= strlen($data)) // only hard-exit condition, we'll hit those breaks eventually^^ + { + $char = $data[$condCurPos]; - return array($str, []/*$relSpells*/); + // advance position + $condCurPos++; + + if ($char == '[') + { + $condBrktCnt++; + + if ($condBrktCnt == 1) + $curPart++; + + // previously there was no condition -> last else + if ($condBrktCnt == 1) + if (($curPart && ($curPart % 2)) && (!isset($condParts[$curPart - 1]) || empty(trim($condParts[$curPart - 1])))) + $isLastElse = true; + + if (empty($condParts[$curPart])) + continue; + } + + if (empty($condParts[$curPart])) + $condParts[$curPart] = $char; + else + $condParts[$curPart] .= $char; + + if ($char == ']') + { + $condBrktCnt--; + + if (!$condBrktCnt) + { + $condParts[$curPart] = substr($condParts[$curPart], 0, -1); + $curPart++; + } + + if ($condBrktCnt) + continue; + + if ($isLastElse) + break; + } + } + + // check if it is know-compatible + $know = 0; + if (preg_match('/\(?(\!?)[as](\d+)\)?$/i', $condParts[0], $m)) + { + if (!strstr($condParts[1], '$?')) + if (!strstr($condParts[3], '$?')) + if (!isset($condParts[5])) + $know = $m[2]; + + // found a negation -> switching condition target + if ($m[1] == '!') + $targetPart = 1; + } + // if not, what part of the condition should be used? + else if (preg_match('/(([\W\D]*[as]\d+)|([^\[]*))/i', $condParts[0], $m) && !empty($m[3])) + { + $cnd = $this->resolveEvaluation($m[3]); + if ((is_numeric($cnd) || is_bool($cnd)) && $cnd) // only case, deviating from normal; positive result -> use [true] + $targetPart = 1; + } + + // recursive conditions + if (strstr($condParts[$targetPart], '$?')) + $condParts[$targetPart] = $this->handleConditions($condParts[$targetPart], $scaling, $relSpells, true); + + if ($know && !$dontKnow) + { + foreach ([1, 3] as $pos) + { + if (strstr($condParts[$pos], '${')) + $condParts[$pos] = $this->handleFormulas($condParts[$pos], $scaling); + + if (strstr($condParts[$pos], '$')) + $condParts[$pos] = $this->handleVariables($condParts[$pos], $scaling); + } + + // false condition first + if (!isset($relSpells[$know])) + $relSpells[$know] = []; + + $relSpells[$know][] = [$condParts[3], $condParts[1]]; + + $data = substr_replace($data, ''.$condParts[$targetPart].'', $condStartPos, ($condCurPos - $condStartPos)); + } + else + $data = substr_replace($data, $condParts[$targetPart], $condStartPos, ($condCurPos - $condStartPos)); + } + + return $data; } public function renderBuff($level = MAX_LEVEL, $interactive = false) { if (!$this->curTpl) - return array(); + return ['', []]; // doesn't have a buff if (!$this->getField('buff', true)) - return array(); + return ['', []]; $this->interactive = $interactive; @@ -1521,13 +1570,13 @@ class SpellList extends BaseType // scaling information - spellId:min:max:curr $x .= ''; - return array($x, $btt[1]); + return [$x, $btt[1]]; } public function renderTooltip($level = MAX_LEVEL, $interactive = false) { if (!$this->curTpl) - return array(); + return ['', []]; $this->interactive = $interactive; @@ -1670,7 +1719,7 @@ class SpellList extends BaseType // scaling information - spellId:min:max:curr $x .= ''; - return array($x, $desc ? $desc[1] : null); + return [$x, $desc[1]]; } public function getTalentHeadForCurrent() @@ -1857,15 +1906,17 @@ class SpellList extends BaseType if ($addMask & GLOBALINFO_EXTRA) { -/* -spells / buffspells = { - "58377": [["", "and 2 additional nearby targets "]], - "103828": [["stunning", "rooting"], ["1 sec", "4 sec and reducing movement speed by 50% for 15 sec"]] -}; -*/ $buff = $this->renderBuff(MAX_LEVEL, true); $tTip = $this->renderTooltip(MAX_LEVEL, true); + foreach ($tTip[1] as $relId => $_) + if (empty($data[TYPE_SPELL][$relId])) + $data[TYPE_SPELL][$relId] = $relId; + + foreach ($buff[1] as $relId => $_) + if (empty($data[TYPE_SPELL][$relId])) + $data[TYPE_SPELL][$relId] = $relId; + $extra[$id] = array( 'id' => $id, 'tooltip' => $tTip[0], diff --git a/static/js/basic.js b/static/js/basic.js index 4612ae27..392177f1 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -1489,7 +1489,7 @@ $WH.g_setTooltipLevel = function(tooltip, level) { $WH.g_setTooltipSpells = function(tooltip, spells, spellData, position) { var known = {}, - regex = '.+?', + regex = '.*?', effects; if (spells == null) { @@ -1521,7 +1521,7 @@ $WH.g_setTooltipSpells = function(tooltip, spells, spellData, position) { } var effect = spellData[spellId][position[spellId]][known[spellId]]; - effect = $WH.g_setTooltipSpells(effect, spells, spellData, position); + effect = $WH.g_setTooltipSpells(effect.toString(), spells, spellData, position); tooltip = tooltip.replace(effects[i], '' + effect + ''); } From 58a235e2e3856d66f36f7ebcfea470ad5d8288cc Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 18 Jul 2015 15:16:35 +0200 Subject: [PATCH 0035/1249] PageText handle cases where pageTextId is given but pageText is not in DB --- pages/item.php | 23 +++++++++++++++++------ pages/object.php | 13 ++++++++++--- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/pages/item.php b/pages/item.php index 0c55a37c..c440c2c6 100644 --- a/pages/item.php +++ b/pages/item.php @@ -346,17 +346,28 @@ class ItemPage extends genericPage // pageText if ($next = $this->subject->getField('pageTextId')) { - $this->addJS('Book.js'); - $this->addCSS(['path' => 'Book.css']); - while ($next) { - $row = DB::World()->selectRow('SELECT *, text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.entry = lpt.entry WHERE pt.entry = ?d', $next); - $next = $row['next_page']; - $this->pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); + if ($row = DB::World()->selectRow('SELECT *, text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.entry = lpt.entry WHERE pt.entry = ?d', $next)) + { + $next = $row['next_page']; + $this->pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); + } + else + { + Util::addNote(U_GROUP_STAFF, 'Referenced PageTextId #'.$next.' is not in DB'); + break; + } } } + // add conditional js & css + if ($this->pageText) + { + $this->addJS('Book.js'); + $this->addCSS(['path' => 'Book.css']); + } + // subItems $this->subject->initSubItems(); if (!empty($this->subject->subItems[$this->typeId])) diff --git a/pages/object.php b/pages/object.php index 27351ae5..5666f7df 100644 --- a/pages/object.php +++ b/pages/object.php @@ -206,9 +206,16 @@ class ObjectPage extends GenericPage { while ($next) { - $row = DB::World()->selectRow('SELECT *, text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.entry = lpt.entry WHERE pt.entry = ?d', $next); - $next = $row['next_page']; - $pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); + if ($row = DB::World()->selectRow('SELECT *, text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.entry = lpt.entry WHERE pt.entry = ?d', $next)) + { + $next = $row['next_page']; + $pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); + } + else + { + Util::addNote(U_GROUP_STAFF, 'Referenced PageTextId #'.$next.' is not in DB'); + break; + } } } From 998763be7b653bd30e48afbbb6998db7f151156c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 18 Jul 2015 16:45:11 +0200 Subject: [PATCH 0036/1249] Errors/Logging * log addNotes to DB as php user-errors * always skip the native php error handler --- includes/community.class.php | 4 ++-- includes/kernel.php | 27 ++++++++++----------------- includes/loot.class.php | 10 +++++----- includes/utilities.php | 16 ++++++++++------ localization/lang.class.php | 8 ++++---- pages/event.php | 2 +- pages/genericPage.class.php | 2 ++ pages/item.php | 2 +- pages/npc.php | 2 +- pages/object.php | 2 +- pages/screenshot.php | 14 +++++++------- 11 files changed, 44 insertions(+), 45 deletions(-) diff --git a/includes/community.class.php b/includes/community.class.php index d805315c..e206359e 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -190,7 +190,7 @@ class CommunityContent } else { - Util::addNote(U_GROUP_STAFF, 'CommunityClass::getCommentPreviews - comment '.$c['id'].' belongs to nonexistant subject'); + Util::logError('Comment '.$c['id'].' belongs to nonexistant subject.', E_USER_NOTICE); unset($comments[$idx]); } } @@ -341,7 +341,7 @@ class CommunityContent { if (empty($p['name'])) { - Util::addNote(U_GROUP_STAFF | U_GROUP_SCREENSHOT, 'AdminPage::handleScreenshots() - Screenshot linked to nonexistant type/typeId combination '.$p['type'].'/'.$p['typeId']); + Util::logError('Screenshot linked to nonexistant type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); unset($p); } else diff --git a/includes/kernel.php b/includes/kernel.php index 5fb87fff..8efaa41b 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -73,7 +73,7 @@ foreach ($sets as $k => $v) // this should not have been possible if (!strlen($v['value']) && !($v['flags'] & CON_FLAG_TYPE_STRING) && !$php) { - Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!'); + Util::logError('Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!', E_USER_ERROR); continue; } @@ -87,12 +87,12 @@ foreach ($sets as $k => $v) $val = preg_replace('/[^\p{L}0-9~\s_\-\'\/\.:,]/ui', '', $v['value']); else if ($php) { - Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: PHP config value '.strtolower($k).' has no type set - config will not be used!'); + Util::logError('PHP config value '.strtolower($k).' has no type set - config will not be used!', E_USER_ERROR); continue; } else // if (!$php) { - Util::addNote(U_GROUP_ADMIN | U_GROUP_DEV, 'Kernel: Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!'); + Util::logError('Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!', E_USER_ERROR); $val = 0; } @@ -105,9 +105,10 @@ foreach ($sets as $k => $v) // handle occuring errors error_reporting(!empty($AoWoWconf['aowow']) && CFG_DEBUG ? (E_ALL & ~(E_DEPRECATED | E_USER_DEPRECATED | E_STRICT)) : 0); -$errHandled = false; -set_error_handler(function($errNo, $errStr, $errFile, $errLine) use (&$errHandled) { +set_error_handler(function($errNo, $errStr, $errFile, $errLine) { $errName = 'unknown error'; // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored + $uGroup = U_GROUP_EMPLOYEE; + if ($errNo == E_WARNING) // 0x0002 $errName = 'E_WARNING'; else if ($errNo == E_PARSE) // 0x0004 @@ -120,26 +121,18 @@ set_error_handler(function($errNo, $errStr, $errFile, $errLine) use (&$errHandle $errName = 'E_USER_WARNING'; else if ($errNo == E_USER_NOTICE) // 0x0400 $errName = 'E_USER_NOTICE'; + $uGroup = U_GROUP_STAFF; else if ($errNo == E_RECOVERABLE_ERROR) // 0x1000 $errName = 'E_RECOVERABLE_ERROR'; - if (User::isInGroup(U_GROUP_STAFF)) - { - if (!$errHandled) - { - Util::addNote(U_GROUP_STAFF, 'one or more php related error occured, while generating this page.'); - $errHandled = true; - } - - Util::addNote(U_GROUP_STAFF, $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine); - } + Util::addNote($uGroup, $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine); if (DB::isConnectable(DB_AOWOW)) DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : $_SERVER['QUERY_STRING'], User::$groups, $errStr ); - return !((User::isInGroup(U_GROUP_STAFF) && defined('CFG_DEBUG') && CFG_DEBUG) || CLI); + return true; }, E_ALL & ~(E_DEPRECATED | E_USER_DEPRECATED | E_STRICT)); @@ -158,7 +151,7 @@ if (!CLI) // Setup Session if (CFG_SESSION_CACHE_DIR && Util::checkOrCreateDirectory(CFG_SESSION_CACHE_DIR)) - session_save_path(getcwd().'/'.CFG_SESSION_CACHE_DIR); + session_save_path(CFG_SESSION_CACHE_DIR); session_set_cookie_params(15 * YEAR, '/', '', $secure, true); session_cache_limiter('private'); diff --git a/includes/loot.class.php b/includes/loot.class.php index 51ead30e..8cd0d500 100644 --- a/includes/loot.class.php +++ b/includes/loot.class.php @@ -175,7 +175,7 @@ class Loot } else // shouldn't have happened { - Util::addNote(U_GROUP_EMPLOYEE, 'Loot::getByContainerRecursive: unhandled case in calculating chance for item '.$entry['Item'].'!'); + Util::logError('Unhandled case in calculating chance for item '.$entry['Item'].'!'); continue; } @@ -189,7 +189,7 @@ class Loot $sum = 0; else if ($sum >= 100.01) { - Util::addNote(U_GROUP_EMPLOYEE, 'Loot::getByContainerRecursive: entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!'); + Util::logError('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!'); $sum = 100; } @@ -378,13 +378,13 @@ class Loot { // check for possible database inconsistencies if (!$ref['chance'] && !$ref['isGrouped']) - Util::addNote(U_GROUP_EMPLOYEE, 'Loot by Item: ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!'); + Util::logError('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!'); if ($ref['isGrouped'] && $ref['sumChance'] > 100) - Util::addNote(U_GROUP_EMPLOYEE, 'Loot by Item: group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!'); + Util::logError('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!'); if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) - Util::addNote(U_GROUP_EMPLOYEE, 'Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!'); + Util::logError('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!'); $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100; diff --git a/includes/utilities.php b/includes/utilities.php index ac0d24a0..3c7ea8cc 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -698,19 +698,23 @@ class Util public static $wowheadLink = ''; private static $notes = []; - // creates an announcement; use if minor issues arise + public static function logError($errStr, $mode = E_USER_WARNING) + { + // handled by set_error_handler + trigger_error($errStr, $mode); + } + public static function addNote($uGroupMask, $str) { - // todo (med): log all those errors to DB self::$notes[] = [$uGroupMask, $str]; } - public static function getNotes($restricted = true) + public static function getNotes() { $notes = []; foreach (self::$notes as $data) - if (!$restricted || !$data[0] || User::isInGroup($data[0])) + if (!$data[0] || User::isInGroup($data[0])) $notes[] = $data[1]; return $notes; @@ -1718,9 +1722,9 @@ class Util $path = preg_replace('|/+|', '/', $path); if (!is_dir($path) && !@mkdir($path, self::FILE_ACCESS, true)) - self::addNote(U_GROUP_EMPLOYEE, 'could not create directory: '.$path); + self::logError('Could not create directory: '.$path, E_USER_ERROR); else if (!is_writable($path) && !@chmod($path, self::FILE_ACCESS)) - self::addNote(U_GROUP_EMPLOYEE, 'cannot write into directory: '.$path); + self::logError('Cannot write into directory: '.$path, E_USER_ERROR); else return true; diff --git a/localization/lang.class.php b/localization/lang.class.php index 1fb2ba66..7226cca1 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -49,7 +49,7 @@ class Lang { if (!isset(self::$$prop)) { - Util::addNote(U_GROUP_STAFF, 'Lang::__callStatic() - tried to use undefined property Lang::$'.$prop); + Util::logError('Lang - tried to use undefined property Lang::$'.$prop); return null; } @@ -58,7 +58,7 @@ class Lang { if (!isset($var[$key])) { - Util::addNote(U_GROUP_STAFF, 'Lang::__callStatic() - undefined key "'.$key.'" in property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\']'); + Util::logError('Lang - undefined key "'.$key.'" in property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\']'); return null; } @@ -73,14 +73,14 @@ class Lang if (!isset(self::$$prop)) { - Util::addNote(U_GROUP_STAFF, 'Lang::sort() - tried to use undefined property Lang::$'.$prop); + Util::logError('Lang::sort - tried to use undefined property Lang::$'.$prop); return null; } $var = &self::$$prop; if (!isset($var[$group])) { - Util::addNote(U_GROUP_STAFF, 'Lang::sort() - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']'); + Util::logError('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']'); return null; } diff --git a/pages/event.php b/pages/event.php index 9d0aec73..1ca8a2d7 100644 --- a/pages/event.php +++ b/pages/event.php @@ -218,7 +218,7 @@ class EventPage extends GenericPage if ($v > 0) $list[] = $v; else if ($v === null) - Util::addNote(U_GROUP_EMPLOYEE, 'game_event_prerequisite: this event has itself as prerequisite'); + Util::logError('game_event_prerequisite: this event has itself as prerequisite'); }); if ($list) diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index 31821a48..add97605 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -323,6 +323,8 @@ class GenericPage // display occured notices if ($_ = Util::getNotes()) { + array_unshift($_, 'One or more errors occured, while generating this page.'); + $this->announcements[0] = array( 'parent' => 'announcement-0', 'id' => 0, diff --git a/pages/item.php b/pages/item.php index c440c2c6..64c8ed7b 100644 --- a/pages/item.php +++ b/pages/item.php @@ -355,7 +355,7 @@ class ItemPage extends genericPage } else { - Util::addNote(U_GROUP_STAFF, 'Referenced PageTextId #'.$next.' is not in DB'); + Util::logError('Referenced PageTextId #'.$next.' is not in DB'); break; } } diff --git a/pages/npc.php b/pages/npc.php index 6ca91efe..84547d53 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -509,7 +509,7 @@ class NpcPage extends GenericPage } } else - Util::addNote(U_GROUP_EMPLOYEE, 'NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set'); + Util::logError('NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set'); } // tab: sells diff --git a/pages/object.php b/pages/object.php index 5666f7df..62bfd490 100644 --- a/pages/object.php +++ b/pages/object.php @@ -213,7 +213,7 @@ class ObjectPage extends GenericPage } else { - Util::addNote(U_GROUP_STAFF, 'Referenced PageTextId #'.$next.' is not in DB'); + Util::logError('Referenced PageTextId #'.$next.' is not in DB'); break; } } diff --git a/pages/screenshot.php b/pages/screenshot.php index 57632c7f..98cbb788 100644 --- a/pages/screenshot.php +++ b/pages/screenshot.php @@ -35,7 +35,7 @@ class ScreenshotPage extends GenericPage if ($this->minSize <= 0) { - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::__construct() - config error: dimensions for uploaded screenshots egual or less than zero. Value forced to 200'); + Util::logError('config error: dimensions for uploaded screenshots equal or less than zero. Value forced to 200'); $this->minSize = 200; } @@ -276,26 +276,26 @@ class ScreenshotPage extends GenericPage switch ($_FILES['screenshotfile']['error']) { case 1: - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - the file exceeds the maximum size of '.ini_get('upload_max_filesize')); + Util::logError('validateScreenshot - the file exceeds the maximum size of '.ini_get('upload_max_filesize')); return Lang::screenshot('error', 'selectSS'); case 3: - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - upload was interrupted'); + Util::logError('validateScreenshot - upload was interrupted'); return Lang::screenshot('error', 'selectSS'); case 4: - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - no file was received'); + Util::logError('validateScreenshot() - no file was received'); return Lang::screenshot('error', 'selectSS'); case 6: - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - temporary upload directory is not set'); + Util::logError('validateScreenshot - temporary upload directory is not set'); return Lang::main('intError'); case 7: - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - could not write temporary file to disk'); + Util::logError('validateScreenshot - could not write temporary file to disk'); return Lang::main('intError'); } // points to invalid file (hack attempt) if (!is_uploaded_file($_FILES['screenshotfile']['tmp_name'])) { - Util::addNote(U_GROUP_EMPLOYEE, 'ScreenshotPage::validateScreenshot() - uploaded file not in upload directory'); + Util::logError('validateScreenshot - uploaded file not in upload directory'); return Lang::main('intError'); } From 11ab3e0f19573b8d852d35674ce5b703ca417ec6 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 18 Jul 2015 16:51:28 +0200 Subject: [PATCH 0037/1249] typo fix --- pages/utility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/utility.php b/pages/utility.php index 2ca1d815..5f12ef43 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -214,7 +214,7 @@ class UtilityPage extends GenericPage continue; $comments = DB::Aowow()->selectCol(' - SELECT `typeId` AS ARRAY_KEY, count(1) AS nComments FROM ?_comments + SELECT `typeId` AS ARRAY_KEY, count(1) AS ncomments FROM ?_comments WHERE `replyTo` = 0 AND (`flags` & ?d) = 0 AND `type`= ?d AND `date` > (UNIX_TIMESTAMP() - ?d) GROUP BY `type`, `typeId` LIMIT 100', From 5b8a862df9c3452ffffcf6ce0e4e12974988eee1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 19 Jul 2015 19:36:46 +0200 Subject: [PATCH 0038/1249] DB/Structure (thx @Carbenium for the heads up & suggestions) * adapted structure to TDB 335.59 * dropped questItem[1-6] fields from ?_creature and ?_object (why were they even there) - removed a function-stub from Util (already forgot, what it was supposed to achieve) --- README.md | Bin 9230 -> 9230 bytes includes/shared.php | 2 +- includes/utilities.php | 10 --- pages/item.php | 4 +- pages/npc.php | 14 ++-- pages/object.php | 4 +- pages/skill.php | 8 +-- pages/spell.php | 8 +-- setup/db_structure.sql | 12 ---- setup/tools/sqlGen.class.php | 2 +- setup/tools/sqlgen/creature.func.php | 1 - setup/tools/sqlgen/objects.func.php | 56 ++++++++-------- setup/tools/sqlgen/quests.func.php | 94 +++++++++++++-------------- setup/tools/sqlgen/source.func.php | 88 ++++++++++++------------- setup/tools/sqlgen/spawns.func.php | 2 +- setup/tools/sqlgen/spell.func.php | 4 +- setup/tools/sqlgen/titles.func.php | 6 +- setup/updates/1437329787_01.sql | 15 +++++ 18 files changed, 163 insertions(+), 167 deletions(-) create mode 100644 setup/updates/1437329787_01.sql diff --git a/README.md b/README.md index 8b15c2973550876be5a2d8a11f59217b0765328c..009beae1327fa1ec71f0b14f9d179bdd5428060c 100644 GIT binary patch delta 24 fcmeD4==0bR#?ELt*_Km#aua(Lqvhti?1nM`WB><@ delta 24 fcmeD4==0bR#?ELl*_Km#aua(Lqs8XC?1nM`W9kQr diff --git a/includes/shared.php b/includes/shared.php index 65ebaad2..06367e18 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ selectRow('SELECT *, text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.entry = lpt.entry WHERE pt.entry = ?d', $next)) + if ($row = DB::World()->selectRow('SELECT *, Text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.ID = lpt.entry WHERE pt.ID = ?d', $next)) { - $next = $row['next_page']; + $next = $row['NextPageID']; $this->pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); } else diff --git a/pages/npc.php b/pages/npc.php index 84547d53..f0689583 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -451,14 +451,14 @@ class NpcPage extends GenericPage if ($this->subject->getField('npcflag') & NPC_FLAG_TRAINER) { $teachQuery = ' - SELECT IFNULL(t2.spell, t1.spell) AS ARRAY_KEY, - IFNULL(t2.spellcost, t1.spellcost) AS cost, - IFNULL(t2.reqskill, t1.reqskill) AS reqSkillId, - IFNULL(t2.reqskillvalue, t1.reqskillvalue) AS reqSkillValue, - IFNULL(t2.reqlevel, t1.reqlevel) AS reqLevel + SELECT IFNULL(t2.SpellID, t1.SpellID) AS ARRAY_KEY, + IFNULL(t2.MoneyCost, t1.MoneyCost) AS cost, + IFNULL(t2.ReqSkillLine, t1.ReqSkillLine) AS reqSkillId, + IFNULL(t2.ReqSkillRank, t1.ReqSkillRank) AS reqSkillValue, + IFNULL(t2.ReqLevel, t1.ReqLevel) AS reqLevel FROM npc_trainer t1 - LEFT JOIN npc_trainer t2 ON t2.entry = IF(t1.spell < 0, -t1.spell, null) - WHERE t1.entry = ?d + LEFT JOIN npc_trainer t2 ON t2.ID = IF(t1.SpellID < 0, -t1.SpellID, null) + WHERE t1.ID = ?d '; if ($tSpells = DB::World()->select($teachQuery, $this->typeId)) diff --git a/pages/object.php b/pages/object.php index 62bfd490..a57f5074 100644 --- a/pages/object.php +++ b/pages/object.php @@ -206,9 +206,9 @@ class ObjectPage extends GenericPage { while ($next) { - if ($row = DB::World()->selectRow('SELECT *, text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.entry = lpt.entry WHERE pt.entry = ?d', $next)) + if ($row = DB::World()->selectRow('SELECT *, Text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.ID = lpt.entry WHERE pt.ID = ?d', $next)) { - $next = $row['next_page']; + $next = $row['NextPageID']; $pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); } else diff --git a/pages/skill.php b/pages/skill.php index 53f37e14..17570054 100644 --- a/pages/skill.php +++ b/pages/skill.php @@ -250,7 +250,7 @@ class SkillPage extends GenericPage { $list = []; if (!empty(Util::$trainerTemplates[TYPE_SKILL][$this->typeId])) - $list = DB::World()->selectCol('SELECT DISTINCT entry FROM npc_trainer WHERE spell IN (?a) AND entry < 200000', Util::$trainerTemplates[TYPE_SKILL][$this->typeId]); + $list = DB::World()->selectCol('SELECT DISTINCT ID FROM npc_trainer WHERE SpellID IN (?a) AND ID < 200000', Util::$trainerTemplates[TYPE_SKILL][$this->typeId]); else { $mask = 0; @@ -266,10 +266,10 @@ class SkillPage extends GenericPage ); $list = $spellIds ? DB::World()->selectCol(' - SELECT IF(t1.entry > 200000, t2.entry, t1.entry) + SELECT IF(t1.ID > 200000, t2.ID, t1.ID) FROM npc_trainer t1 - LEFT JOIN npc_trainer t2 ON t2.spell = -t1.entry - WHERE t1.spell IN (?a)', + LEFT JOIN npc_trainer t2 ON t2.SpellID = -t1.ID + WHERE t1.SpellID IN (?a)', $spellIds ) : []; } diff --git a/pages/spell.php b/pages/spell.php index 07b2854b..3c4fe5cc 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -1010,7 +1010,7 @@ class SpellPage extends GenericPage } if ($tt) - $list = DB::World()->selectCol('SELECT DISTINCT entry FROM npc_trainer WHERE spell IN (?a) AND entry < 200000', $tt); + $list = DB::World()->selectCol('SELECT DISTINCT ID FROM npc_trainer WHERE SpellID IN (?a) AND ID < 200000', $tt); else { $mask = 0; @@ -1019,10 +1019,10 @@ class SpellPage extends GenericPage $mask |= 1 << $idx; $list = DB::World()->selectCol(' - SELECT IF(t1.entry > 200000, t2.entry, t1.entry) + SELECT IF(t1.ID > 200000, t2.ID, t1.ID) FROM npc_trainer t1 - LEFT JOIN npc_trainer t2 ON t2.spell = -t1.entry - WHERE t1.spell = ?d', + LEFT JOIN npc_trainer t2 ON t2.SpellID = -t1.ID + WHERE t1.SpellID IN (?a)', $this->typeId ); } diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 10b46f1c..abba0c58 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -468,12 +468,6 @@ CREATE TABLE `aowow_creature` ( `armorMin` mediumint(8) unsigned NOT NULL DEFAULT '1', `armorMax` mediumint(8) unsigned NOT NULL DEFAULT '1', `racialLeader` tinyint(3) unsigned NOT NULL DEFAULT '0', - `questItem1` int(10) unsigned NOT NULL DEFAULT '0', - `questItem2` int(10) unsigned NOT NULL DEFAULT '0', - `questItem3` int(10) unsigned NOT NULL DEFAULT '0', - `questItem4` int(10) unsigned NOT NULL DEFAULT '0', - `questItem5` int(10) unsigned NOT NULL DEFAULT '0', - `questItem6` int(10) unsigned NOT NULL DEFAULT '0', `mechanicImmuneMask` int(10) unsigned NOT NULL DEFAULT '0', `flagsExtra` int(10) unsigned NOT NULL DEFAULT '0', `scriptName` varchar(50) NOT NULL DEFAULT '', @@ -1273,12 +1267,6 @@ CREATE TABLE `aowow_objects` ( `faction` smallint(5) unsigned NOT NULL DEFAULT '0', `flags` int(10) unsigned NOT NULL DEFAULT '0', `cuFlags` int(10) unsigned NOT NULL DEFAULT '0', - `questItem1` int(11) unsigned NOT NULL DEFAULT '0', - `questItem2` int(11) unsigned NOT NULL DEFAULT '0', - `questItem3` int(11) unsigned NOT NULL DEFAULT '0', - `questItem4` int(11) unsigned NOT NULL DEFAULT '0', - `questItem5` int(11) unsigned NOT NULL DEFAULT '0', - `questItem6` int(11) unsigned NOT NULL DEFAULT '0', `lootId` mediumint(8) unsigned NOT NULL DEFAULT '0', `lockId` smallint(5) unsigned NOT NULL DEFAULT '0', `reqSkill` smallint(5) unsigned NOT NULL DEFAULT '0', diff --git a/setup/tools/sqlGen.class.php b/setup/tools/sqlGen.class.php index d6d126b5..9bc7445e 100644 --- a/setup/tools/sqlGen.class.php +++ b/setup/tools/sqlGen.class.php @@ -49,7 +49,7 @@ class SqlGen 'creature' => [null, null, null, ['creature_template', 'locales_creature', 'creature_classlevelstats', 'instance_encounters']], 'currencies' => [null, null, null, ['item_template', 'locales_item']], 'events' => [null, null, null, ['game_event', 'game_event_prerequisite']], - 'objects' => [null, null, null, ['gameobject_template', 'locales_gameobject']], + 'objects' => [null, null, null, ['gameobject_template', 'locales_gameobject', 'gameobject_questitem']], 'pet' => [null, null, null, ['creature_template', 'creature']], 'quests' => [null, null, null, ['quest_template', 'locales_quest', 'game_event', 'game_event_seasonal_questrelation']], 'quests_startend' => [null, null, null, ['creature_queststarter', 'creature_questender', 'game_event_creature_quest', 'gameobject_queststarter', 'gameobject_questender', 'game_event_gameobject_quest', 'item_template']], diff --git a/setup/tools/sqlgen/creature.func.php b/setup/tools/sqlgen/creature.func.php index adc958b3..8fe0fccb 100644 --- a/setup/tools/sqlgen/creature.func.php +++ b/setup/tools/sqlgen/creature.func.php @@ -72,7 +72,6 @@ function creature(array $ids = []) min.basearmor * ct.ArmorModifier AS armorMin, max.basearmor * ct.ArmorModifier AS armorMax, RacialLeader, - questItem1, questItem2, questItem3, questItem4, questItem5, questItem6, mechanic_immune_mask, flags_extra, ScriptName diff --git a/setup/tools/sqlgen/objects.func.php b/setup/tools/sqlgen/objects.func.php index 9c0b2e69..b455fcb6 100644 --- a/setup/tools/sqlgen/objects.func.php +++ b/setup/tools/sqlgen/objects.func.php @@ -10,6 +10,7 @@ if (!CLI) /* deps: * gameobject_template * locales_gameobject + * gameobject_questitem */ @@ -24,8 +25,8 @@ function objects(array $ids = []) go.entry, `type`, IF(`type` = 2, -2, -- quests 1 - IF(`type` = 8 AND data0 IN (1, 2, 3, 4, 1552), -6, -- tools - IF(`type` = 3 AND questitem1 <> 0, -2, -- quests 2 + IF(`type` = 8 AND Data0 IN (1, 2, 3, 4, 1552), -6, -- tools + IF(`type` = 3 AND IFNULL(gqi.ItemId, 0) <> 0, -2, -- quests 2 IF(`type` IN (3, 9, 25), `type`, 0)))), -- regular chests, books, pools 0 AS event, -- linked worldevent displayId, @@ -33,40 +34,43 @@ function objects(array $ids = []) faction, flags, 0 AS cuFlags, -- custom Flags - questItem1, questItem2, questItem3, questItem4, questItem5, questItem6, - IF(`type` IN (3, 25), data1, 0), -- lootId - IF(`type` IN (2, 3, 6, 10, 13, 24, 26), data0, IF(`type` IN (0, 1), data1, 0)), -- lockId + IF(`type` IN (3, 25), Data1, 0), -- lootId + IF(`type` IN (2, 3, 6, 10, 13, 24, 26), Data0, IF(`type` IN (0, 1), Data1, 0)), -- lockId 0 AS reqSkill, -- reqSkill - IF(`type` = 9, data0, IF(`type` = 10, data7, 0)), -- pageTextId - IF(`type` = 1, data3, -- linkedTrapIds - IF(`type` = 3, data7, - IF(`type` = 10, data12, - IF(`type` = 8, data2, 0)))), - IF(`type` = 5, data5, -- reqQuest - IF(`type` = 3, data8, - IF(`type` = 10, data1, - IF(`type` = 8, data4, 0)))), - IF(`type` = 8, data0, 0), -- spellFocusId - IF(`type` = 10, data10, -- onUseSpell - IF(`type` IN (18, 24), data1, - IF(`type` = 26, data2, - IF(`type` = 22, data0, 0)))), - IF(`type` = 18, data4, 0), -- onSuccessSpell - IF(`type` = 18, data2, IF(`type` = 24, data3, 0)), -- auraSpell - IF(`type` = 30, data2, IF(`type` = 24, data4, IF(`type` = 6, data3, 0))), -- triggeredSpell - IF(`type` = 29, CONCAT_WS(" ", data14, data15, data16, data17, data0), -- miscInfo: capturePoint - IF(`type` = 3, CONCAT_WS(" ", data4, data5, data2), -- miscInfo: loot v - IF(`type` = 25, CONCAT_WS(" ", data2, data3, 0), - IF(`type` = 23, CONCAT_WS(" ", data0, data1, data2), "")))), -- miscInfo: meetingStone + IF(`type` = 9, Data0, IF(`type` = 10, Data7, 0)), -- pageTextId + IF(`type` = 1, Data3, -- linkedTrapIds + IF(`type` = 3, Data7, + IF(`type` = 10, Data12, + IF(`type` = 8, Data2, 0)))), + IF(`type` = 5, Data5, -- reqQuest + IF(`type` = 3, Data8, + IF(`type` = 10, Data1, + IF(`type` = 8, Data4, 0)))), + IF(`type` = 8, Data0, 0), -- spellFocusId + IF(`type` = 10, Data10, -- onUseSpell + IF(`type` IN (18, 24), Data1, + IF(`type` = 26, Data2, + IF(`type` = 22, Data0, 0)))), + IF(`type` = 18, Data4, 0), -- onSuccessSpell + IF(`type` = 18, Data2, IF(`type` = 24, Data3, 0)), -- auraSpell + IF(`type` = 30, Data2, IF(`type` = 24, Data4, IF(`type` = 6, Data3, 0))), -- triggeredSpell + IF(`type` = 29, CONCAT_WS(" ", Data14, Data15, Data16, Data17, Data0), -- miscInfo: capturePoint + IF(`type` = 3, CONCAT_WS(" ", Data4, Data5, Data2), -- miscInfo: loot v + IF(`type` = 25, CONCAT_WS(" ", Data2, Data3, 0), + IF(`type` = 23, CONCAT_WS(" ", Data0, Data1, Data2), "")))), -- miscInfo: meetingStone IF(ScriptName <> "", ScriptName, AIName) FROM gameobject_template go LEFT JOIN locales_gameobject lgo ON go.entry = lgo.entry + LEFT JOIN + gameobject_questitem gqi ON gqi.GameObjectEntry = go.entry { WHERE go.entry IN (?a) } + GROUP BY + go.entry LIMIT ?d, ?d'; diff --git a/setup/tools/sqlgen/quests.func.php b/setup/tools/sqlgen/quests.func.php index cf5e67a6..7df3116f 100644 --- a/setup/tools/sqlgen/quests.func.php +++ b/setup/tools/sqlgen/quests.func.php @@ -23,15 +23,15 @@ function quests(array $ids = []) { $baseQuery = ' SELECT - q.Id, + q.ID, Method, - Level, + QuestLevel, MinLevel, MaxLevel, - ZoneOrSort, - ZoneOrSort AS zoneOrSortBak, -- ZoneOrSortBak - Type, - SuggestedPlayers, + QuestSortID, + QuestSortID AS zoneOrSortBak, -- ZoneOrSortBak + QuestType, + SuggestedGroupNum, LimitTime, IFNULL(gesqr.eventEntry, 0) AS eventId, PrevQuestId, @@ -41,60 +41,60 @@ function quests(array $ids = []) Flags, SpecialFlags, 0 AS cuFlags, -- cuFlags - RequiredClasses, RequiredRaces, - RequiredSkillId, RequiredSkillPoints, - RequiredFactionId1, RequiredFactionId2, - RequiredFactionValue1, RequiredFactionValue2, - RequiredMinRepFaction, RequiredMaxRepFaction, - RequiredMinRepValue, RequiredMaxRepValue, + RequiredClasses, RequiredRaces, + RequiredSkillId, RequiredSkillPoints, + RequiredFactionId1, RequiredFactionId2, + RequiredFactionValue1, RequiredFactionValue2, + RequiredMinRepFaction, RequiredMaxRepFaction, + RequiredMinRepValue, RequiredMaxRepValue, RequiredPlayerKills, - SourceItemId, SourceItemCount, + SourceItemId, SourceItemCount, SourceSpellId, RewardXPId, -- QuestXP.dbc x level RewardOrRequiredMoney, RewardMoneyMaxLevel, - RewardSpell, RewardSpellCast, + RewardSpell, RewardSpellCast, RewardHonor * 124 * RewardHonorMultiplier, -- alt calculation in QuestDef.cpp -> Quest::CalculateHonorGain(playerLevel) - RewardMailTemplateId, RewardMailDelay, - RewardTitleId, + RewardMailTemplateId, RewardMailDelay, + RewardTitle, RewardTalents, RewardArenaPoints, - RewardItemId1, RewardItemId2, RewardItemId3, RewardItemId4, - RewardItemCount1, RewardItemCount2, RewardItemCount3, RewardItemCount4, - RewardChoiceItemId1, RewardChoiceItemId2, RewardChoiceItemId3, RewardChoiceItemId4, RewardChoiceItemId5, RewardChoiceItemId6, - RewardChoiceItemCount1, RewardChoiceItemCount2, RewardChoiceItemCount3, RewardChoiceItemCount4, RewardChoiceItemCount5, RewardChoiceItemCount6, - RewardFactionId1, RewardFactionId2, RewardFactionId3, RewardFactionId4, RewardFactionId5, - IF (RewardFactionValueIdOverride1 <> 0, RewardFactionValueIdOverride1 / 100, RewardFactionValueId1), - IF (RewardFactionValueIdOverride2 <> 0, RewardFactionValueIdOverride2 / 100, RewardFactionValueId2), - IF (RewardFactionValueIdOverride3 <> 0, RewardFactionValueIdOverride3 / 100, RewardFactionValueId3), - IF (RewardFactionValueIdOverride4 <> 0, RewardFactionValueIdOverride4 / 100, RewardFactionValueId4), - IF (RewardFactionValueIdOverride5 <> 0, RewardFactionValueIdOverride5 / 100, RewardFactionValueId5), - Title, Title_loc2, Title_loc3, Title_loc6, Title_loc8, - Objectives, Objectives_loc2, Objectives_loc3, Objectives_loc6, Objectives_loc8, - Details, Details_loc2, Details_loc3, Details_loc6, Details_loc8, - EndText, EndText_loc2, EndText_loc3, EndText_loc6, EndText_loc8, - OfferRewardText, OfferRewardText_loc2, OfferRewardText_loc3, OfferRewardText_loc6, OfferRewardText_loc8, - RequestItemsText, RequestItemsText_loc2, RequestItemsText_loc3, RequestItemsText_loc6, RequestItemsText_loc8, - CompletedText, CompletedText_loc2, CompletedText_loc3, CompletedText_loc6, CompletedText_loc8, - RequiredNpcOrGo1, RequiredNpcOrGo2, RequiredNpcOrGo3, RequiredNpcOrGo4, - RequiredNpcOrGoCount1, RequiredNpcOrGoCount2, RequiredNpcOrGoCount3, RequiredNpcOrGoCount4, - RequiredSourceItemId1, RequiredSourceItemId2, RequiredSourceItemId3, RequiredSourceItemId4, - RequiredSourceItemCount1,RequiredSourceItemCount2,RequiredSourceItemCount3,RequiredSourceItemCount4, - RequiredItemId1, RequiredItemId2, RequiredItemId3, RequiredItemId4, RequiredItemId5, RequiredItemId6, - RequiredItemCount1, RequiredItemCount2, RequiredItemCount3, RequiredItemCount4, RequiredItemCount5, RequiredItemCount6, - ObjectiveText1, ObjectiveText1_loc2, ObjectiveText1_loc3, ObjectiveText1_loc6, ObjectiveText1_loc8, - ObjectiveText2, ObjectiveText2_loc2, ObjectiveText2_loc3, ObjectiveText2_loc6, ObjectiveText2_loc8, - ObjectiveText3, ObjectiveText3_loc2, ObjectiveText3_loc3, ObjectiveText3_loc6, ObjectiveText3_loc8, - ObjectiveText4, ObjectiveText4_loc2, ObjectiveText4_loc3, ObjectiveText4_loc6, ObjectiveText4_loc8 + RewardItem1, RewardItem2, RewardItem3, RewardItem4, + RewardAmount1, RewardAmount2, RewardAmount3, RewardAmount4, + RewardChoiceItemID1, RewardChoiceItemID2, RewardChoiceItemID3, RewardChoiceItemID4, RewardChoiceItemID5, RewardChoiceItemID6, + RewardChoiceItemQuantity1, RewardChoiceItemQuantity2, RewardChoiceItemQuantity3, RewardChoiceItemQuantity4, RewardChoiceItemQuantity5, RewardChoiceItemQuantity6, + RewardFactionID1, RewardFactionID2, RewardFactionID3, RewardFactionID4, RewardFactionID5, + IF (RewardFactionOverride1 <> 0, RewardFactionOverride1 / 100, RewardFactionValue1), + IF (RewardFactionOverride2 <> 0, RewardFactionOverride2 / 100, RewardFactionValue2), + IF (RewardFactionOverride3 <> 0, RewardFactionOverride3 / 100, RewardFactionValue3), + IF (RewardFactionOverride4 <> 0, RewardFactionOverride4 / 100, RewardFactionValue4), + IF (RewardFactionOverride5 <> 0, RewardFactionOverride5 / 100, RewardFactionValue5), + LogTitle, Title_loc2, Title_loc3, Title_loc6, Title_loc8, + LogDescription, Objectives_loc2, Objectives_loc3, Objectives_loc6, Objectives_loc8, + QuestDescription, Details_loc2, Details_loc3, Details_loc6, Details_loc8, + EndText, EndText_loc2, EndText_loc3, EndText_loc6, EndText_loc8, + OfferRewardText, OfferRewardText_loc2, OfferRewardText_loc3, OfferRewardText_loc6, OfferRewardText_loc8, + RequestItemsText, RequestItemsText_loc2, RequestItemsText_loc3, RequestItemsText_loc6, RequestItemsText_loc8, + QuestCompletionLog, CompletedText_loc2, CompletedText_loc3, CompletedText_loc6, CompletedText_loc8, + RequiredNpcOrGo1, RequiredNpcOrGo2, RequiredNpcOrGo3, RequiredNpcOrGo4, + RequiredNpcOrGoCount1, RequiredNpcOrGoCount2, RequiredNpcOrGoCount3, RequiredNpcOrGoCount4, + RequiredSourceItemId1, RequiredSourceItemId2, RequiredSourceItemId3, RequiredSourceItemId4, + RequiredSourceItemCount1, RequiredSourceItemCount2, RequiredSourceItemCount3, RequiredSourceItemCount4, + RequiredItemId1, RequiredItemId2, RequiredItemId3, RequiredItemId4, RequiredItemId5, RequiredItemId6, + RequiredItemCount1, RequiredItemCount2, RequiredItemCount3, RequiredItemCount4, RequiredItemCount5, RequiredItemCount6, + ObjectiveText1, ObjectiveText1_loc2, ObjectiveText1_loc3, ObjectiveText1_loc6, ObjectiveText1_loc8, + ObjectiveText2, ObjectiveText2_loc2, ObjectiveText2_loc3, ObjectiveText2_loc6, ObjectiveText2_loc8, + ObjectiveText3, ObjectiveText3_loc2, ObjectiveText3_loc3, ObjectiveText3_loc6, ObjectiveText3_loc8, + ObjectiveText4, ObjectiveText4_loc2, ObjectiveText4_loc3, ObjectiveText4_loc6, ObjectiveText4_loc8 FROM quest_template q LEFT JOIN - locales_quest lq ON q.Id = lq.Id + locales_quest lq ON q.ID = lq.Id LEFT JOIN - game_event_seasonal_questrelation gesqr ON gesqr.questId = q.Id + game_event_seasonal_questrelation gesqr ON gesqr.questId = q.ID { WHERE - q.Id IN (?a) + q.ID IN (?a) } LIMIT ?d, ?d'; @@ -156,7 +156,7 @@ function quests(array $ids = []) for ($i = 1; $i < 6; $i++) DB::Aowow()->query($repQuery, $i, $i, $i, $i, $ids ?: DBSIMPLE_SKIP); - // update zoneOrSort .. well .. now "not documenting" bites me in the ass .. ~700 quests were changed, i don't know by what method + // update zoneOrSort/QuestSortID .. well .. now "not documenting" bites me in the ass .. ~700 quests were changed, i don't know by what method $eventSet = DB::World()->selectCol('SELECT holiday AS ARRAY_KEY, eventEntry FROM game_event WHERE holiday <> 0'); $holidaySorts = array( 141 => -1001, 181 => -374, 201 => -1002, diff --git a/setup/tools/sqlgen/source.func.php b/setup/tools/sqlgen/source.func.php index e290876f..8f34be00 100644 --- a/setup/tools/sqlgen/source.func.php +++ b/setup/tools/sqlgen/source.func.php @@ -8,32 +8,32 @@ if (!CLI) /* deps: - * reference_loot_template - * item_loot_template - * creature_loot_template - * gameobject_loot_template - * mail_loot_template - * disenchant_loot_template - * fishing_loot_template - * skinning_loot_template - * milling_loot_template - * prospecting_loot_template - * pickpocketing_loot_template + * reference_loot_template + * item_loot_template + * creature_loot_template + * gameobject_loot_template + * mail_loot_template + * disenchant_loot_template + * fishing_loot_template + * skinning_loot_template + * milling_loot_template + * prospecting_loot_template + * pickpocketing_loot_template - * item_template - * creature_template - * gameobject_template - * quest_template + * item_template + * creature_template + * gameobject_template + * quest_template - * npc_trainer - * npc_vendor - * game_event_npc_vendor - * creature + * npc_trainer + * npc_vendor + * game_event_npc_vendor + * creature - * playercreateinfo_item - * playercreateinfo_spell - * achievement_reward - * skill_discovery_template + * playercreateinfo_item + * playercreateinfo_spell + * achievement_reward + * skill_discovery_template */ $customData = array( @@ -250,11 +250,11 @@ function source(array $ids = []) FROM gameobject_loot_template glt JOIN - gameobject_template gt ON glt.entry = gt.data1 + gameobject_template gt ON glt.entry = gt.Data1 LEFT JOIN item_template it ON it.entry = glt.Item AND glt.Reference <= 0 WHERE - `type` = 3 AND gt.data1 > 0 AND gt.data0 NOT IN (?a) + `type` = 3 AND gt.Data1 > 0 AND gt.Data0 NOT IN (?a) GROUP BY ARRAY_KEY', $exclLocks @@ -388,17 +388,17 @@ function source(array $ids = []) $spellBuff = []; $itemBuff = []; $quests = DB::World()->select( - 'SELECT n.item AS ARRAY_KEY, n.Id AS quest, SUM(n.qty) AS qty, BIT_OR(n.side) AS side, it.class, it.subclass, it.spellid_1, it.spelltrigger_1, it.spellid_2, it.spelltrigger_2 FROM ( - SELECT rewardChoiceItemId1 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardChoiceItemId1 > 0 GROUP BY item UNION - SELECT rewardChoiceItemId2 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardChoiceItemId2 > 0 GROUP BY item UNION - SELECT rewardChoiceItemId3 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardChoiceItemId3 > 0 GROUP BY item UNION - SELECT rewardChoiceItemId4 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardChoiceItemId4 > 0 GROUP BY item UNION - SELECT rewardChoiceItemId5 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardChoiceItemId5 > 0 GROUP BY item UNION - SELECT rewardChoiceItemId6 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardChoiceItemId6 > 0 GROUP BY item UNION - SELECT rewardItemId1 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardItemId1 > 0 GROUP BY item UNION - SELECT rewardItemId2 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardItemId2 > 0 GROUP BY item UNION - SELECT rewardItemId3 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardItemId3 > 0 GROUP BY item UNION - SELECT rewardItemId4 AS item, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE rewardItemId4 > 0 GROUP BY item + 'SELECT n.item AS ARRAY_KEY, n.ID AS quest, SUM(n.qty) AS qty, BIT_OR(n.side) AS side, it.class, it.subclass, it.spellid_1, it.spelltrigger_1, it.spellid_2, it.spelltrigger_2 FROM ( + SELECT RewardChoiceItemID1 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardChoiceItemID1 > 0 GROUP BY item UNION + SELECT RewardChoiceItemID2 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardChoiceItemID2 > 0 GROUP BY item UNION + SELECT RewardChoiceItemID3 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardChoiceItemID3 > 0 GROUP BY item UNION + SELECT RewardChoiceItemID4 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardChoiceItemID4 > 0 GROUP BY item UNION + SELECT RewardChoiceItemID5 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardChoiceItemID5 > 0 GROUP BY item UNION + SELECT RewardChoiceItemID6 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardChoiceItemID6 > 0 GROUP BY item UNION + SELECT RewardItem1 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardItem1 > 0 GROUP BY item UNION + SELECT RewardItem2 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardItem2 > 0 GROUP BY item UNION + SELECT RewardItem3 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardItem3 > 0 GROUP BY item UNION + SELECT RewardItem4 AS item, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE RewardItem4 > 0 GROUP BY item ) n JOIN item_template it ON it.entry = n.item GROUP BY item' ); @@ -413,7 +413,7 @@ function source(array $ids = []) $mailLoot = DB::World()->select(' SELECT IF(mlt.Reference > 0, -mlt.Reference, mlt.Item) AS ARRAY_KEY, - qt.Id AS entry, + qt.ID AS entry, it.class, it.subclass, it.spellid_1, it.spelltrigger_1, it.spellid_2, it.spelltrigger_2, count(1) AS qty, BIT_OR(IF(qt.RequiredRaces & 0x2B2 AND !(qt.RequiredRaces & 0x44D), 2, IF(qt.RequiredRaces & 0x44D AND !(qt.RequiredRaces & 0x2B2), 1, 3))) AS side @@ -635,7 +635,7 @@ function source(array $ids = []) count(1) AS qty FROM ( SELECT 0 AS entry, IF(flt.Reference > 0, -flt.Reference, flt.Item) itemOrRef FROM fishing_loot_template flt UNION - SELECT gt.entry, IF(glt.Reference > 0, -glt.Reference, glt.Item) itemOrRef FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.entry = gt.data1 WHERE `type` = 25 AND gt.data1 > 0 + SELECT gt.entry, IF(glt.Reference > 0, -glt.Reference, glt.Item) itemOrRef FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.entry = gt.Data1 WHERE `type` = 25 AND gt.Data1 > 0 ) src LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.itemOrRef = it.entry @@ -688,7 +688,7 @@ function source(array $ids = []) src.srcType FROM ( SELECT ct.entry, IF(slt.Reference > 0, -slt.Reference, slt.Item) itemOrRef, ?d AS srcType FROM creature_template ct JOIN skinning_loot_template slt ON slt.entry = ct.skinloot WHERE (type_flags & ?d) AND ct.skinloot > 0 UNION - SELECT gt.entry, IF(glt.Reference > 0, -glt.Reference, glt.Item) itemOrRef, ?d AS srcType FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.entry = gt.data1 WHERE gt.`type` = 3 AND gt.data1 > 0 AND data0 IN (?a) + SELECT gt.entry, IF(glt.Reference > 0, -glt.Reference, glt.Item) itemOrRef, ?d AS srcType FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.entry = gt.Data1 WHERE gt.`type` = 3 AND gt.Data1 > 0 AND Data0 IN (?a) ) src LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.itemOrRef = it.entry @@ -794,7 +794,7 @@ function source(array $ids = []) src.srcType FROM ( SELECT ct.entry, IF(slt.Reference > 0, -slt.Reference, slt.Item) itemOrRef, ?d AS srcType FROM creature_template ct JOIN skinning_loot_template slt ON slt.entry = ct.skinloot WHERE (type_flags & ?d) AND ct.skinloot > 0 UNION - SELECT gt.entry, IF(glt.Reference > 0, -glt.Reference, glt.Item) itemOrRef, ?d AS srcType FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.entry = gt.data1 WHERE gt.`type` = 3 AND gt.data1 > 0 AND data0 IN (?a) + SELECT gt.entry, IF(glt.Reference > 0, -glt.Reference, glt.Item) itemOrRef, ?d AS srcType FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.entry = gt.Data1 WHERE gt.`type` = 3 AND gt.Data1 > 0 AND Data0 IN (?a) ) src LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.itemOrRef = it.entry @@ -1045,9 +1045,9 @@ function source(array $ids = []) CLISetup::log(' * #4 Quest'); $quests = DB::World()->select(' SELECT spell AS ARRAY_KEY, id, SUM(qty) AS qty, BIT_OR(side) AS side FROM ( - SELECT IF(rewardSpellCast = 0, rewardSpell, rewardSpellCast) AS spell, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE IF(rewardSpellCast = 0, rewardSpell, rewardSpellCast) > 0 GROUP BY spell + SELECT IF(rewardSpellCast = 0, rewardSpell, rewardSpellCast) AS spell, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE IF(rewardSpellCast = 0, rewardSpell, rewardSpellCast) > 0 GROUP BY spell UNION - SELECT SourceSpellId AS spell, Id, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE SourceSpellId > 0 GROUP BY spell + SELECT SourceSpellId AS spell, ID, COUNT(1) AS qty, IF(RequiredRaces & 0x2B2 AND !(RequiredRaces & 0x44D), 2, IF(RequiredRaces & 0x44D AND !(RequiredRaces & 0x2B2), 1, 3)) AS side FROM quest_template WHERE SourceSpellId > 0 GROUP BY spell ) t GROUP BY spell'); if ($quests) @@ -1071,7 +1071,7 @@ function source(array $ids = []) # 6: Trainer CLISetup::log(' * #6 Trainer'); - if ($tNpcs = DB::World()->select('SELECT spell AS ARRAY_KEY, entry, COUNT(1) AS qty FROM npc_trainer WHERE spell > 0 GROUP BY ARRAY_KEY')) + if ($tNpcs = DB::World()->select('SELECT SpellID AS ARRAY_KEY, ID AS entry, COUNT(1) AS qty FROM npc_trainer WHERE SpellID > 0 GROUP BY ARRAY_KEY')) { $tSpells = DB::Aowow()->select('SELECT Id AS ARRAY_KEY, effect1Id, effect2Id, effect3Id, effect1TriggerSpell, effect2TriggerSpell, effect3TriggerSpell FROM dbc_spell WHERE Id IN (?a)', array_keys($tNpcs)); $buff = []; @@ -1167,7 +1167,7 @@ function source(array $ids = []) # 4: Quest CLISetup::log(' * #4 Quest'); - if ($quests = DB::World()->select('SELECT ?d, RewardTitleId, 1, ?d, Id FROM quest_template WHERE RewardTitleId > 0', TYPE_TITLE, TYPE_QUEST)) + if ($quests = DB::World()->select('SELECT ?d, RewardTitle, 1, ?d, ID FROM quest_template WHERE RewardTitle > 0', TYPE_TITLE, TYPE_QUEST)) DB::Aowow()->query(queryfy('[V]', $quests, $insMore), 4, 4, 4); # 12: Achievement diff --git a/setup/tools/sqlgen/spawns.func.php b/setup/tools/sqlgen/spawns.func.php index d7fdd919..ee0d6bc7 100644 --- a/setup/tools/sqlgen/spawns.func.php +++ b/setup/tools/sqlgen/spawns.func.php @@ -134,7 +134,7 @@ function spawns() // and waypoints /* offsets for transports */ /**************************/ - $transports = DB::World()->selectCol('SELECT data0 AS pathId, data6 AS ARRAY_KEY FROM gameobject_template WHERE type = 15 AND data6 <> 0'); + $transports = DB::World()->selectCol('SELECT Data0 AS pathId, Data6 AS ARRAY_KEY FROM gameobject_template WHERE type = 15 AND Data6 <> 0'); foreach ($transports as &$t) $t = DB::Aowow()->selectRow('SELECT posX, posY, mapId FROM dbc_taxipathnode tpn WHERE tpn.pathId = ?d AND nodeIdx = 0', $t); diff --git a/setup/tools/sqlgen/spell.func.php b/setup/tools/sqlgen/spell.func.php index 60ef5423..f5b30bc9 100644 --- a/setup/tools/sqlgen/spell.func.php +++ b/setup/tools/sqlgen/spell.func.php @@ -353,7 +353,7 @@ function spell() } // fill learnedAt, trainingCost from trainer - if ($trainer = DB::World()->select('SELECT spell AS ARRAY_KEY, MIN(reqskillvalue) AS reqSkill, MIN(spellcost) AS cost, COUNT(*) as count FROM npc_trainer GROUP BY spell')) + if ($trainer = DB::World()->select('SELECT SpellID AS ARRAY_KEY, MIN(ReqSkillRank) AS reqSkill, MIN(MoneyCost) AS cost, COUNT(*) AS count FROM npc_trainer GROUP BY SpellID')) { $spells = DB::Aowow()->select('SELECT Id AS ARRAY_KEY, effect1Id, effect2Id, effect3Id, effect1TriggerSpell, effect2TriggerSpell, effect3TriggerSpell FROM dbc_spell WHERE Id IN (?a)', array_keys($trainer)); $links = []; @@ -489,7 +489,7 @@ function spell() 201032 => 10658 ); foreach ($specs as $tt => $req) - if ($spells = DB::World()->selectCol('SELECT spell FROM npc_trainer WHERE entry = ?d', $tt)) + if ($spells = DB::World()->selectCol('SELECT SpellID FROM npc_trainer WHERE ID = ?d', $tt)) DB::Aowow()->query('UPDATE ?_spell SET reqSpellId = ?d WHERE id IN (?a)', $req, $spells); $itemReqs = DB::World()->selectCol('SELECT entry AS ARRAY_KEY, requiredSpell FROM item_template WHERE requiredSpell NOT IN (?a)', [0, 34090, 34091]); // not riding diff --git a/setup/tools/sqlgen/titles.func.php b/setup/tools/sqlgen/titles.func.php index a084930b..21382c03 100644 --- a/setup/tools/sqlgen/titles.func.php +++ b/setup/tools/sqlgen/titles.func.php @@ -37,17 +37,17 @@ function titles() $questQuery = ' SELECT - qt.RewardTitleId AS ARRAY_KEY, + qt.RewardTitle AS ARRAY_KEY, qt.RequiredRaces, ge.eventEntry FROM quest_template qt LEFT JOIN - game_event_seasonal_questrelation sq ON sq.questId = qt.id + game_event_seasonal_questrelation sq ON sq.questId = qt.ID LEFT JOIN game_event ge ON ge.eventEntry = sq.eventEntry WHERE - qt.RewardTitleId <> 0'; + qt.RewardTitle <> 0'; DB::Aowow()->query('REPLACE INTO ?_titles SELECT Id, 0, 0, 0, 0, 0, 0, 0, male_loc0, male_loc2, male_loc3, male_loc6, male_loc8, female_loc0, female_loc2, female_loc3, female_loc6, female_loc8 FROM dbc_chartitles'); diff --git a/setup/updates/1437329787_01.sql b/setup/updates/1437329787_01.sql new file mode 100644 index 00000000..4d1e8434 --- /dev/null +++ b/setup/updates/1437329787_01.sql @@ -0,0 +1,15 @@ +ALTER TABLE `aowow_objects` + DROP COLUMN `questItem1`, + DROP COLUMN `questItem2`, + DROP COLUMN `questItem3`, + DROP COLUMN `questItem4`, + DROP COLUMN `questItem5`, + DROP COLUMN `questItem6`; + +ALTER TABLE `aowow_creature` + DROP COLUMN `questItem1`, + DROP COLUMN `questItem2`, + DROP COLUMN `questItem3`, + DROP COLUMN `questItem4`, + DROP COLUMN `questItem5`, + DROP COLUMN `questItem6`; From 022ceba20d80a9f864be318733c8415c48c61a96 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 20 Jul 2015 11:33:48 +0200 Subject: [PATCH 0039/1249] fixup to 998763b so .. basicly everything was wrong with this one --- includes/community.class.php | 4 ++-- includes/kernel.php | 10 ++++++---- includes/loot.class.php | 10 +++++----- includes/utilities.php | 10 ++-------- localization/lang.class.php | 8 ++++---- pages/event.php | 2 +- pages/item.php | 2 +- pages/npc.php | 2 +- pages/object.php | 2 +- pages/screenshot.php | 14 +++++++------- 10 files changed, 30 insertions(+), 34 deletions(-) diff --git a/includes/community.class.php b/includes/community.class.php index e206359e..77afd721 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -190,7 +190,7 @@ class CommunityContent } else { - Util::logError('Comment '.$c['id'].' belongs to nonexistant subject.', E_USER_NOTICE); + trigger_error('Comment '.$c['id'].' belongs to nonexistant subject.', E_USER_NOTICE); unset($comments[$idx]); } } @@ -341,7 +341,7 @@ class CommunityContent { if (empty($p['name'])) { - Util::logError('Screenshot linked to nonexistant type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); + trigger_error('Screenshot linked to nonexistant type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); unset($p); } else diff --git a/includes/kernel.php b/includes/kernel.php index 8efaa41b..617e40d3 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -73,7 +73,7 @@ foreach ($sets as $k => $v) // this should not have been possible if (!strlen($v['value']) && !($v['flags'] & CON_FLAG_TYPE_STRING) && !$php) { - Util::logError('Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!', E_USER_ERROR); + trigger_error('Aowow config value CFG_'.strtoupper($k).' is empty - config will not be used!', E_USER_ERROR); continue; } @@ -87,12 +87,12 @@ foreach ($sets as $k => $v) $val = preg_replace('/[^\p{L}0-9~\s_\-\'\/\.:,]/ui', '', $v['value']); else if ($php) { - Util::logError('PHP config value '.strtolower($k).' has no type set - config will not be used!', E_USER_ERROR); + trigger_error('PHP config value '.strtolower($k).' has no type set - config will not be used!', E_USER_ERROR); continue; } else // if (!$php) { - Util::logError('Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!', E_USER_ERROR); + trigger_error('Aowow config value CFG_'.strtoupper($k).' has no type set - value forced to 0!', E_USER_ERROR); $val = 0; } @@ -120,8 +120,10 @@ set_error_handler(function($errNo, $errStr, $errFile, $errLine) { else if ($errNo == E_USER_WARNING) // 0x0200 $errName = 'E_USER_WARNING'; else if ($errNo == E_USER_NOTICE) // 0x0400 + { $errName = 'E_USER_NOTICE'; $uGroup = U_GROUP_STAFF; + } else if ($errNo == E_RECOVERABLE_ERROR) // 0x1000 $errName = 'E_RECOVERABLE_ERROR'; @@ -151,7 +153,7 @@ if (!CLI) // Setup Session if (CFG_SESSION_CACHE_DIR && Util::checkOrCreateDirectory(CFG_SESSION_CACHE_DIR)) - session_save_path(CFG_SESSION_CACHE_DIR); + session_save_path(getcwd().'/'.CFG_SESSION_CACHE_DIR); session_set_cookie_params(15 * YEAR, '/', '', $secure, true); session_cache_limiter('private'); diff --git a/includes/loot.class.php b/includes/loot.class.php index 8cd0d500..dcf2f631 100644 --- a/includes/loot.class.php +++ b/includes/loot.class.php @@ -175,7 +175,7 @@ class Loot } else // shouldn't have happened { - Util::logError('Unhandled case in calculating chance for item '.$entry['Item'].'!'); + trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING); continue; } @@ -189,7 +189,7 @@ class Loot $sum = 0; else if ($sum >= 100.01) { - Util::logError('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!'); + trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING); $sum = 100; } @@ -378,13 +378,13 @@ class Loot { // check for possible database inconsistencies if (!$ref['chance'] && !$ref['isGrouped']) - Util::logError('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!'); + trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING); if ($ref['isGrouped'] && $ref['sumChance'] > 100) - Util::logError('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!'); + trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING); if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) - Util::logError('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!'); + trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING); $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100; diff --git a/includes/utilities.php b/includes/utilities.php index 2105d182..74e7a1f9 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -698,12 +698,6 @@ class Util public static $wowheadLink = ''; private static $notes = []; - public static function logError($errStr, $mode = E_USER_WARNING) - { - // handled by set_error_handler - trigger_error($errStr, $mode); - } - public static function addNote($uGroupMask, $str) { self::$notes[] = [$uGroupMask, $str]; @@ -1712,9 +1706,9 @@ class Util $path = preg_replace('|/+|', '/', $path); if (!is_dir($path) && !@mkdir($path, self::FILE_ACCESS, true)) - self::logError('Could not create directory: '.$path, E_USER_ERROR); + trigger_error('Could not create directory: '.$path, E_USER_ERROR); else if (!is_writable($path) && !@chmod($path, self::FILE_ACCESS)) - self::logError('Cannot write into directory: '.$path, E_USER_ERROR); + trigger_error('Cannot write into directory: '.$path, E_USER_ERROR); else return true; diff --git a/localization/lang.class.php b/localization/lang.class.php index 7226cca1..b5c92723 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -49,7 +49,7 @@ class Lang { if (!isset(self::$$prop)) { - Util::logError('Lang - tried to use undefined property Lang::$'.$prop); + trigger_error('Lang - tried to use undefined property Lang::$'.$prop, E_USER_WARNING); return null; } @@ -58,7 +58,7 @@ class Lang { if (!isset($var[$key])) { - Util::logError('Lang - undefined key "'.$key.'" in property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\']'); + trigger_error('Lang - undefined key "'.$key.'" in property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\']', E_USER_WARNING); return null; } @@ -73,14 +73,14 @@ class Lang if (!isset(self::$$prop)) { - Util::logError('Lang::sort - tried to use undefined property Lang::$'.$prop); + trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop, E_USER_WARNING); return null; } $var = &self::$$prop; if (!isset($var[$group])) { - Util::logError('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']'); + trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']', E_USER_WARNING); return null; } diff --git a/pages/event.php b/pages/event.php index 1ca8a2d7..e8f68340 100644 --- a/pages/event.php +++ b/pages/event.php @@ -218,7 +218,7 @@ class EventPage extends GenericPage if ($v > 0) $list[] = $v; else if ($v === null) - Util::logError('game_event_prerequisite: this event has itself as prerequisite'); + trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); }); if ($list) diff --git a/pages/item.php b/pages/item.php index 7b84b77e..744d7091 100644 --- a/pages/item.php +++ b/pages/item.php @@ -355,7 +355,7 @@ class ItemPage extends genericPage } else { - Util::logError('Referenced PageTextId #'.$next.' is not in DB'); + trigger_error('Referenced PageTextId #'.$next.' is not in DB', E_USER_WARNING); break; } } diff --git a/pages/npc.php b/pages/npc.php index f0689583..e9c8a776 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -509,7 +509,7 @@ class NpcPage extends GenericPage } } else - Util::logError('NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set'); + trigger_error('NPC '.$this->typeId.' is flagged as trainer, but doesn\'t have any spells set', E_USER_WARNING); } // tab: sells diff --git a/pages/object.php b/pages/object.php index a57f5074..882bd924 100644 --- a/pages/object.php +++ b/pages/object.php @@ -213,7 +213,7 @@ class ObjectPage extends GenericPage } else { - Util::logError('Referenced PageTextId #'.$next.' is not in DB'); + trigger_error('Referenced PageTextId #'.$next.' is not in DB', E_USER_WARNING); break; } } diff --git a/pages/screenshot.php b/pages/screenshot.php index 98cbb788..3aa653cb 100644 --- a/pages/screenshot.php +++ b/pages/screenshot.php @@ -35,7 +35,7 @@ class ScreenshotPage extends GenericPage if ($this->minSize <= 0) { - Util::logError('config error: dimensions for uploaded screenshots equal or less than zero. Value forced to 200'); + trigger_error('config error: dimensions for uploaded screenshots equal or less than zero. Value forced to 200', E_USER_WARNING); $this->minSize = 200; } @@ -276,26 +276,26 @@ class ScreenshotPage extends GenericPage switch ($_FILES['screenshotfile']['error']) { case 1: - Util::logError('validateScreenshot - the file exceeds the maximum size of '.ini_get('upload_max_filesize')); + trigger_error('validateScreenshot - the file exceeds the maximum size of '.ini_get('upload_max_filesize'), E_USER_WARNING); return Lang::screenshot('error', 'selectSS'); case 3: - Util::logError('validateScreenshot - upload was interrupted'); + trigger_error('validateScreenshot - upload was interrupted'); return Lang::screenshot('error', 'selectSS'); case 4: - Util::logError('validateScreenshot() - no file was received'); + trigger_error('validateScreenshot() - no file was received', E_USER_WARNING); return Lang::screenshot('error', 'selectSS'); case 6: - Util::logError('validateScreenshot - temporary upload directory is not set'); + trigger_error('validateScreenshot - temporary upload directory is not set', E_USER_WARNING); return Lang::main('intError'); case 7: - Util::logError('validateScreenshot - could not write temporary file to disk'); + trigger_error('validateScreenshot - could not write temporary file to disk', E_USER_WARNING); return Lang::main('intError'); } // points to invalid file (hack attempt) if (!is_uploaded_file($_FILES['screenshotfile']['tmp_name'])) { - Util::logError('validateScreenshot - uploaded file not in upload directory'); + trigger_error('validateScreenshot - uploaded file not in upload directory', E_USER_WARNING); return Lang::main('intError'); } From 7276fed9e7995b286ab5050d1bea814e03d28c97 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 20 Jul 2015 15:33:23 +0200 Subject: [PATCH 0040/1249] Setup/Quests * fixed wrong offset on questXP.dbc and factionReward.dbc * xp & reputation should be correct after running `aowow --sql=quests` --- includes/shared.php | 2 +- setup/tools/sqlgen/quests.func.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 06367e18..334e4e22 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ 0, 1, 2) SET rewardFactionValue?d = (CASE ABS(rewardFactionValue?d) - WHEN 1 THEN rep.Field1 WHEN 2 THEN rep.Field2 WHEN 3 THEN rep.Field3 WHEN 4 THEN rep.Field4 WHEN 5 THEN rep.Field5 - WHEN 6 THEN rep.Field6 WHEN 7 THEN rep.Field7 WHEN 8 THEN rep.Field8 WHEN 9 THEN rep.Field9 WHEN 10 THEN rep.Field10 + WHEN 0 THEN rep.Field1 WHEN 1 THEN rep.Field2 WHEN 2 THEN rep.Field3 WHEN 3 THEN rep.Field4 WHEN 4 THEN rep.Field5 + WHEN 5 THEN rep.Field6 WHEN 6 THEN rep.Field7 WHEN 7 THEN rep.Field8 WHEN 8 THEN rep.Field9 WHEN 9 THEN rep.Field10 + ELSE 0 END) WHERE ABS(rewardFactionValue?d) BETWEEN 1 AND 10 { AND From 5239cbd293bbe85059c1d127ad15cf1dfc288faa Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 20 Jul 2015 17:01:39 +0200 Subject: [PATCH 0041/1249] NPCs / Quests display reputattion change in list when using corresponding filters --- includes/defines.php | 1 + includes/shared.php | 2 +- includes/types/basetype.class.php | 10 ++++++---- includes/types/creature.class.php | 28 +++++++++++++++++++++++++++- includes/types/quest.class.php | 6 ++++++ pages/npcs.php | 8 ++++++-- pages/quests.php | 4 +++- 7 files changed, 50 insertions(+), 9 deletions(-) diff --git a/includes/defines.php b/includes/defines.php index f1edd788..84d6a481 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -183,6 +183,7 @@ define('ITEMINFO_MODEL', 0x20); define('NPCINFO_TAMEABLE', 0x1); define('NPCINFO_MODEL', 0x2); +define('NPCINFO_REP', 0x4); define('ACHIEVEMENTINFO_PROFILE', 0x1); diff --git a/includes/shared.php b/includes/shared.php index 334e4e22..b134a807 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ [], 'v' =>[]]; protected $formData = array( // data to fill form fields - 'form' => [], // base form - unsanitized - 'setCriteria' => [], // dynamic criteria list - index checked - 'setWeights' => [], // dynamic weights list - index checked - 'extraCols' => [] // extra columns for LV - added as required + 'form' => [], // base form - unsanitized + 'setCriteria' => [], // dynamic criteria list - index checked + 'setWeights' => [], // dynamic weights list - index checked + 'extraCols' => [], // extra columns for LV - added as required + 'reputationCols' => [] // simlar and exclusive to extraCols - added as required ); // parse the provided request into a usable format; recall self with GET-params if nessecary @@ -856,6 +857,7 @@ abstract class Filter $form[$name] = $raw ? $data : 'fi_setWeights('.Util::toJSON($data).', 0, 1, 1);'; break; case 'form': + case 'reputationCols': if ($key == $name) // only if explicitely specified $form[$name] = $data; break; diff --git a/includes/types/creature.class.php b/includes/types/creature.class.php index 0356fb6d..58256930 100644 --- a/includes/types/creature.class.php +++ b/includes/types/creature.class.php @@ -161,9 +161,21 @@ class CreatureList extends BaseType * * NPCINFO_TAMEABLE (0x1): include texture & react * NPCINFO_MODEL (0x2): + * NPCINFO_REP (0x4): include repreward */ - $data = []; + $data = []; + $rewRep = []; + + if ($addInfoMask & NPCINFO_REP) + { + $rewRep = DB::World()->selectCol(' + SELECT creature_id AS ARRAY_KEY, RewOnKillRepFaction1 AS ARRAY_KEY2, RewOnKillRepValue1 FROM creature_onkill_reputation WHERE creature_id IN (?a) AND RewOnKillRepFaction1 > 0 UNION + SELECT creature_id AS ARRAY_KEY, RewOnKillRepFaction2 AS ARRAY_KEY2, RewOnKillRepValue2 FROM creature_onkill_reputation WHERE creature_id IN (?a) AND RewOnKillRepFaction2 > 0', + $this->getFoundIDs(), + $this->getFoundIDs() + ); + } foreach ($this->iterate() as $__) { @@ -215,6 +227,14 @@ class CreatureList extends BaseType if ($addInfoMask & NPCINFO_TAMEABLE) // only first skin of first model ... we're omitting potentially 11 skins here .. but the lv accepts only one .. w/e $data[$this->id]['skin'] = $this->curTpl['textureString']; + + if ($addInfoMask & NPCINFO_REP) + { + $data[$this->id]['reprewards'] = []; + if ($rewRep[$this->id]) + foreach ($rewRep[$this->id] as $fac => $val) + $data[$this->id]['reprewards'][] = [$fac, $val]; + } } } @@ -427,6 +447,9 @@ class CreatureListFilter extends Filter case 42: // increasesrepwith [enum] if (in_array($cr[1], $this->enums[3])) // reuse { + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1])) + $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')]; + if ($cIds = DB::World()->selectCol('SELECT creature_id FROM creature_onkill_reputation WHERE (RewOnKillRepFaction1 = ?d AND RewOnKillRepValue1 > 0) OR (RewOnKillRepFaction2 = ?d AND RewOnKillRepValue2 > 0)', $cr[1], $cr[1])) return ['id', $cIds]; else @@ -437,6 +460,9 @@ class CreatureListFilter extends Filter case 43: // decreasesrepwith [enum] if (in_array($cr[1], $this->enums[3])) // reuse { + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1])) + $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')]; + if ($cIds = DB::World()->selectCol('SELECT creature_id FROM creature_onkill_reputation WHERE (RewOnKillRepFaction1 = ?d AND RewOnKillRepValue1 < 0) OR (RewOnKillRepFaction2 = ?d AND RewOnKillRepValue2 < 0)', $cr[1], $cr[1])) return ['id', $cIds]; else diff --git a/includes/types/quest.class.php b/includes/types/quest.class.php index 1f1a852e..1a3ac60a 100644 --- a/includes/types/quest.class.php +++ b/includes/types/quest.class.php @@ -457,6 +457,9 @@ class QuestListFilter extends Filter case 1: // increasesrepwith if ($this->isSaneNumeric($cr[1]) && $cr[1] > 0) { + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1])) + $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')]; + return [ 'OR', ['AND', ['rewardFactionId1', $cr[1]], ['rewardFactionValue1', 0, '>']], @@ -470,6 +473,9 @@ class QuestListFilter extends Filter case 10: // decreasesrepwith if ($this->isSaneNumeric($cr[1]) && $cr[1] > 0) { + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE id = ?d', $cr[1])) + $this->formData['reputationCols'][] = [$cr[1], Util::localizedString($_, 'name')]; + return [ 'OR', ['AND', ['rewardFactionId1', $cr[1]], ['rewardFactionValue1', 0, '<']], diff --git a/pages/npcs.php b/pages/npcs.php index ccea9ddf..488c4ec5 100644 --- a/pages/npcs.php +++ b/pages/npcs.php @@ -57,13 +57,17 @@ class NpcsPage extends GenericPage $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : NULL; $this->filter['fi'] = $this->filterObj->getForm(); + $repCols = $this->filterObj->getForm('reputationCols'); + $lv = array( 'file' => 'creature', - 'data' => $npcs->getListviewData(), // listview content + 'data' => $npcs->getListviewData($repCols ? NPCINFO_REP : 0x0), 'params' => [] ); - if (!empty($this->filter['fi']['extraCols'])) + if ($repCols) + $lv['params']['extraCols'] = '$fi_getReputationCols('.Util::toJSON($repCols).')'; + else if (!empty($this->filter['fi']['extraCols'])) $lv['params']['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; if ($this->category) diff --git a/pages/quests.php b/pages/quests.php index d982d5a1..6827df79 100644 --- a/pages/quests.php +++ b/pages/quests.php @@ -60,7 +60,9 @@ class QuestsPage extends GenericPage 'params' => [] ); - if (!empty($this->filter['fi']['extraCols'])) + if ($_ = $this->filterObj->getForm('reputationCols')) + $lv['params']['extraCols'] = '$fi_getReputationCols('.Util::toJSON($_).')'; + else if (!empty($this->filter['fi']['extraCols'])) $lv['params']['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; // create note if search limit was exceeded From 6f59afe8e657e378ffac9a68211a515d1f9efaab Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Jul 2015 00:26:49 +0200 Subject: [PATCH 0042/1249] DB/Emotes * added emotes to DB .. why? just because! * also added to search * cross-linked achievements and emotes * data is generated via: php aowow --sql=emotes * setup requires GlobalStrings.lua (see README.md) --- README.md | Bin 9230 -> 9332 bytes includes/defines.php | 6 +- includes/shared.php | 2 +- includes/types/emote.class.php | 42 ++++++++++++ index.php | 2 + localization/lang.class.php | 2 + localization/locale_dede.php | 13 +++- localization/locale_enus.php | 11 +++ localization/locale_eses.php | 11 +++ localization/locale_frfr.php | 11 +++ localization/locale_ruru.php | 11 +++ pages/achievement.php | 7 ++ pages/emote.php | 106 +++++++++++++++++++++++++++++ pages/emotes.php | 48 +++++++++++++ pages/search.php | 28 +++++++- setup/db_structure.sql | 50 +++++++++++++- setup/tools/dbc.class.php | 6 ++ setup/tools/sqlGen.class.php | 1 + setup/tools/sqlgen/emotes.func.php | 90 ++++++++++++++++++++++++ setup/updates/1437430574_01.sql | 31 +++++++++ static/js/locale_dede.js | 1 + static/js/locale_enus.js | 3 +- static/js/locale_eses.js | 3 +- static/js/locale_frfr.js | 3 +- static/js/locale_ruru.js | 3 +- template/listviews/emote.tpl.php | 90 ++++++++++++++++++++++++ template/pages/quest.tpl.php | 2 +- 27 files changed, 569 insertions(+), 14 deletions(-) create mode 100644 includes/types/emote.class.php create mode 100644 pages/emote.php create mode 100644 pages/emotes.php create mode 100644 setup/tools/sqlgen/emotes.func.php create mode 100644 setup/updates/1437430574_01.sql create mode 100644 template/listviews/emote.tpl.php diff --git a/README.md b/README.md index 009beae1327fa1ec71f0b14f9d179bdd5428060c..198bc1791a22a080d1f0b1d839b49e473645de2f 100644 GIT binary patch delta 72 zcmeD4_~NnQoY3TLB6`Md3`Goy47m)c3=s^z3_c9{4DJj$4EYR6K+znAV1^Q)d?rI4 YLpnn-gC0<%lp%4lt(fHIM?yM+0LP^f`Tzg` delta 16 Xcmez3(dV(@oY3SIVj`P8gmnY~L4XER diff --git a/includes/defines.php b/includes/defines.php index 84d6a481..fd330894 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -24,7 +24,9 @@ define('TYPE_CLASS', 13); define('TYPE_RACE', 14); define('TYPE_SKILL', 15); define('TYPE_CURRENCY', 17); -define('TYPE_USER', 100); // internal use only +// internal types (not published to js) +define('TYPE_USER', 500); +define('TYPE_EMOTE', 501); define('CACHE_TYPE_NONE', 0); // page will not be cached define('CACHE_TYPE_PAGE', 1); @@ -711,7 +713,7 @@ define('ACHIEVEMENT_CRITERIA_TYPE_GAIN_REPUTATION', 46); // define('ACHIEVEMENT_CRITERIA_TYPE_ROLL_GREED_ON_LOOT', 51); define('ACHIEVEMENT_CRITERIA_TYPE_HK_CLASS', 52); define('ACHIEVEMENT_CRITERIA_TYPE_HK_RACE', 53); -// define('ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE', 54); +define('ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE', 54); // define('ACHIEVEMENT_CRITERIA_TYPE_HEALING_DONE', 55); // define('ACHIEVEMENT_CRITERIA_TYPE_GET_KILLING_BLOWS', 56); define('ACHIEVEMENT_CRITERIA_TYPE_EQUIP_ITEM', 57); diff --git a/includes/shared.php b/includes/shared.php index b134a807..9b5ce531 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ iterate() as $__) + { + $data[$this->id] = array( + 'id' => $this->curTpl['id'], + 'name' => $this->curTpl['cmd'], + 'preview' => $this->getField('self', true) ?: ($this->getField('noTarget', true) ?: $this->getField('target', true)) + ); + + // [nyi] sounds + } + + return $data; + } + + public function getJSGlobals($addMask = GLOBALINFO_ANY) + { + return []; + } + + public function renderTooltip() { } +} + +?> diff --git a/index.php b/index.php index 9cc47359..725b8d3c 100644 --- a/index.php +++ b/index.php @@ -29,6 +29,8 @@ switch ($pageCall) case 'currency': case 'currencies': case 'compare': // tool: item comparison + case 'emote': + case 'emotes': case 'event': case 'events': case 'faction': diff --git a/localization/lang.class.php b/localization/lang.class.php index b5c92723..beafdb37 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -29,6 +29,8 @@ class Lang private static $title; private static $zone; + private static $emote; + public static function load($loc) { if (!file_exists('localization/locale_'.$loc.'.php')) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 0e89a619..1fc14ec3 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -173,6 +173,8 @@ $lang = array( 'difficulty' => "Modus", 'dispelType' => "Bannart", 'duration' => "Dauer", + 'emote' => "Emote", + 'emotes' => "Emotes", 'object' => "Objekt", 'objects' => "Objekte", 'glyphType' => "Glyphenart", @@ -378,8 +380,17 @@ $lang = array( 'recoverUser' => ["Benutzernamenanfrage", "Folgt diesem Link um euch anzumelden.\r\n\r\n".HOST_URL."?account=signin&token=%s\r\n\r\nFalls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden."], 'resetPass' => ["Kennwortreset", "Folgt diesem Link um euer Kennwort zurückzusetzen.\r\n\r\n".HOST_URL."?account=forgotpassword&token=%s\r\n\r\nFalls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden."] ), + 'emote' => array( + 'notFound' => "Dieses Emote existiert nicht.", + 'self' => "An Euch selbst", + 'target' => "An Andere mit Ziel", + 'noTarget' => "An Andere ohne Ziel", + 'isAnimated' => "Besitzt eine Animation", + 'aliases' => "Aliasse", + 'noText' => "Dieses Emote besitzt keinen Text.", + ), 'gameObject' => array( - 'notFound' => "Dieses Objekt existiert nicht .", + 'notFound' => "Dieses Objekt existiert nicht.", 'cat' => [0 => "Anderes", 9 => "Bücher", 3 => "Behälter", -5 => "Truhen", 25 => "Fischschwärme", -3 => "Kräuter", -4 => "Erzadern", -2 => "Quest", -6 => "Werkzeuge"], 'type' => [ 9 => "Buch", 3 => "Behälter", -5 => "Truhe", 25 => "", -3 => "Kraut", -4 => "Erzvorkommen", -2 => "Quest", -6 => ""], 'unkPosition' => "Der Standort dieses Objekts ist nicht bekannt.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 3e115810..30aa4e3e 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -168,6 +168,8 @@ $lang = array( 'difficulty' => "Difficulty", 'dispelType' => "Dispel type", 'duration' => "Duration", + 'emote' => "Emote", + 'emotes' => "Emotes", 'object' => "object", 'objects' => "Objects", 'glyphType' => "Glyph type", @@ -373,6 +375,15 @@ $lang = array( 'recoverUser' => ["User Recovery", "Follow this link to log in.\r\n\r\n".HOST_URL."?account=signin&token=%s\r\n\r\nIf you did not request this mail simply ignore it."], 'resetPass' => ["Password Reset", "Follow this link to reset your password.\r\n\r\n".HOST_URL."?account=forgotpassword&token=%s\r\n\r\nIf you did not request this mail simply ignore it."] ), + 'emote' => array( + 'notFound' => "This Emote doesn't exist.", + 'self' => "To Yourself", + 'target' => "To others with a target", + 'noTarget' => "To others without a target", + 'isAnimated' => "Uses an animation", + 'aliases' => "Aliases", + 'noText' => "This Emote has no text.", + ), 'gameObject' => array( 'notFound' => "This object doesn't exist.", 'cat' => [0 => "Other", 9 => "Books", 3 => "Containers", -5 => "Chests", 25 => "Fishing Pools", -3 => "Herbs", -4 => "Mineral Veins", -2 => "Quest", -6 => "Tools"], diff --git a/localization/locale_eses.php b/localization/locale_eses.php index e158657a..a54e5f11 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -173,6 +173,8 @@ $lang = array( 'difficulty' => "Dificultad", 'dispelType' => "Tipo de disipación", 'duration' => "Duración", + 'emote' => "Emoción", + 'emotes' => "Emociones", 'object' => "entidad", 'objects' => "Entidades", 'glyphType' => "Tipo de glifo", @@ -379,6 +381,15 @@ $lang = array( 'recoverUser' => ["User Recovery", "Follow this link to log in.\r\n\r\n".HOST_URL."?account=signin&token=%s\r\n\r\nIf you did not request this mail simply ignore it."], 'resetPass' => ["Password Reset", "Follow this link to reset your password.\r\n\r\n".HOST_URL."?account=forgotpassword&token=%s\r\n\r\nIf you did not request this mail simply ignore it."] ), + 'emote' => array( + 'notFound' => "[This Emote doesn't exist.]", + 'self' => "[To Yourself]", + 'target' => "[To others with a target]", + 'noTarget' => "[To others without a target]", + 'isAnimated' => "[Uses an animation]", + 'aliases' => "[Aliases]", + 'noText' => "[This Emote has no text.]", + ), 'gameObject' => array( 'notFound' => "Este entidad no existe.", 'cat' => [0 => "Otros", 9 => "Libros", 3 => "Contenedores", -5 => "Cofres", 25 => "Bancos de peces", -3 => "Hierbas", -4 => "Venas de minerales", -2 => "Misiones", -6 => "Herramientas"], diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 3ef26007..b9594603 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -173,6 +173,8 @@ $lang = array( 'difficulty' => "Difficulté", 'dispelType' => "Type de dissipation", 'duration' => "Durée", + 'emote' => "Emote", + 'emotes' => "Emotes", 'object' => "entité", 'objects' => "Entités", 'glyphType' => "Type de glyphe", @@ -378,6 +380,15 @@ $lang = array( 'recoverUser' => ["User Recovery", "Follow this link to log in.\r\n\r\n".HOST_URL."?account=signin&token=%s\r\n\r\nIf you did not request this mail simply ignore it."], 'resetPass' => ["Password Reset", "Follow this link to reset your password.\r\n\r\n".HOST_URL."?account=forgotpassword&token=%s\r\n\r\nIf you did not request this mail simply ignore it."] ), + 'emote' => array( + 'notFound' => "[This Emote doesn't exist.]", + 'self' => "[To Yourself]", + 'target' => "[To others with a target]", + 'noTarget' => "[To others without a target]", + 'isAnimated' => "[Uses an animation]", + 'aliases' => "[Aliases]", + 'noText' => "[This Emote has no text.]", + ), 'gameObject' => array( 'notFound' => "Cette entité n'existe pas.", 'cat' => [0 => "Autre", 9 => "Livres", 3 => "Conteneurs", -5 => "Coffres", 25 => "Bancs de poissons", -3 => "Herbes", -4 => "Filons de minerai", -2 => "Quêtes", -6 => "Outils"], diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index ced46cfc..7dab6a89 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -173,6 +173,8 @@ $lang = array( 'difficulty' => "СложноÑть", 'dispelType' => "Тип раÑÑеиваниÑ", 'duration' => "ДлительноÑть", + 'emote' => "ЭмоциÑ", + 'emotes' => "Эмоции", 'object' => "объект", 'objects' => "Объекты", 'glyphType' => "Тип Ñимвола", @@ -378,6 +380,15 @@ $lang = array( 'recoverUser' => ["User Recovery", "Follow this link to log in.\r\n\r\n".HOST_URL."?account=signin&token=%s\r\n\r\nIf you did not request this mail simply ignore it."], 'resetPass' => ["Password Reset", "Follow this link to reset your password.\r\n\r\n".HOST_URL."?account=forgotpassword&token=%s\r\n\r\nIf you did not request this mail simply ignore it."] ), + 'emote' => array( + 'notFound' => "[This Emote doesn't exist.]", + 'self' => "[To Yourself]", + 'target' => "[To others with a target]", + 'noTarget' => "[To others without a target]", + 'isAnimated' => "[Uses an animation]", + 'aliases' => "[Aliases]", + 'noText' => "[This Emote has no text.]", + ), 'gameObject' => array( 'notFound' => "Такой объект не ÑущеÑтвует.", 'cat' => [0 => "Другое", 9 => "Книги", 3 => "Контейнеры", -5 => "Сундуки", 25 => "Рыболовные лунки",-3 => "Травы", -4 => "Полезные иÑкопаемые", -2 => "ЗаданиÑ", -6 => "ИнÑтрументы"], diff --git a/pages/achievement.php b/pages/achievement.php index 0fc90127..6e5a6d26 100644 --- a/pages/achievement.php +++ b/pages/achievement.php @@ -418,6 +418,13 @@ class AchievementPage extends GenericPage 'text' => $crtName, ); break; + // link to emote + case ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE: + $tmp['link'] = array( + 'href' => '?emote='.$obj, + 'text' => $crtName, + ); + break; default: // Add a gold coin icon if required $tmp['extraText'] = $displayMoney ? Util::formatMoney($qty) : $crtName; diff --git a/pages/emote.php b/pages/emote.php new file mode 100644 index 00000000..f6524beb --- /dev/null +++ b/pages/emote.php @@ -0,0 +1,106 @@ +typeId = intVal($id); + + $this->subject = new EmoteList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->notFound(Util::ucFirst(Lang::game('emote')), Lang::emote('notFound')); + + $this->name = Util::ucFirst($this->subject->getField('cmd')); + } + + protected function generatePath() { } + + protected function generateTitle() + { + array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('emote'))); + } + + protected function generateContent() + { + /***********/ + /* Infobox */ + /***********/ + + $infobox = []; + + // has Animation + if ($this->subject->getField('isAnimated')) + $infobox[] = Lang::emote('isAnimated'); + + /****************/ + /* Main Content */ + /****************/ + + $text = ''; + if ($aliasses = DB::Aowow()->selectCol('SELECT command FROM ?_emotes_aliasses WHERE id = ?d AND locales & ?d', $this->typeId, 1 << User::$localeId)) + { + $text .= '[h3]'.Lang::emote('aliases').'[/h3][ul]'; + foreach ($aliasses as $a) + $text .= '[li]/'.$a.'[/li]'; + + $text .= '[/ul][br][br]'; + } + + $texts = []; + if ($_ = $this->subject->getField('self', true)) + $texts[Lang::emote('self')] = $_; + + if ($_ = $this->subject->getField('target', true)) + $texts[Lang::emote('target')] = $_; + + if ($_ = $this->subject->getField('noTarget', true)) + $texts[Lang::emote('noTarget')] = $_; + + if (!$texts) + $text .= '[div][i class=q0]'.Lang::emote('noText').'[/i][/div]'; + else + foreach ($texts as $h => $t) + $text .= '[pad][b]'.$h.'[/b][ul][li][span class=s4]'.preg_replace('/%\d?\$?s/', '<'.Util::ucFirst(Lang::main('name')).'>', $t).'[/span][/li][/ul]'; + + $this->extraText = $text; + $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; + + /**************/ + /* Extra Tabs */ + /**************/ + + // tab: achievement + $condition = array( + ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE], + ['ac.value1', $this->typeId], + ); + $acv = new AchievementList($condition); + + $this->lvTabs[] = array( + 'file' => 'achievement', + 'data' => $acv->getListviewData(), + 'params' => [] + ); + + $this->extendGlobalData($acv->getJsGlobals()); + } +} + +?> diff --git a/pages/emotes.php b/pages/emotes.php new file mode 100644 index 00000000..d7e76d7f --- /dev/null +++ b/pages/emotes.php @@ -0,0 +1,48 @@ +name = Util::ucFirst(Lang::game('emotes')); + } + + protected function generateContent() + { + $emotes = new EmoteList(); + if (!$emotes->error) + { + + $this->lvTabs[] = array( + 'file' => 'emote', + 'data' => $emotes->getListviewData(), + 'params' => [] + ); + }; + } + + protected function generateTitle() + { + array_unshift($this->title, $this->name); + } + + protected function generatePath() { } +} + +?> diff --git a/pages/search.php b/pages/search.php index 92a28d32..8f3aa2f7 100644 --- a/pages/search.php +++ b/pages/search.php @@ -52,6 +52,7 @@ class SearchPage extends GenericPage ['_searchProficiency'], ['_searchProfession'], ['_searchCompanion'], ['_searchMount'], ['_searchCreature'], ['_searchQuest'], ['_searchAchievement'], ['_searchStatistic'], ['_searchZone'], ['_searchObject'], ['_searchFaction'], ['_searchSkill'], ['_searchPet'], ['_searchCreatureAbility'], ['_searchSpell'], + ['_searchEmote'] ); public function __construct($pageCall, $pageParam) @@ -1409,9 +1410,30 @@ class SearchPage extends GenericPage return $result; } - // private function _searchCharacter($cndBase) { } // 25 Characters $searchMask & 0x2000000 - // private function _searchGuild($cndBase) { } // 26 Guilds $searchMask & 0x4000000 - // private function _searchArenaTeam($cndBase) { } // 27 Arena Teams $searchMask & 0x8000000 + private function _searchEmote($cndBase) // 25 Emotes $searchMask & 0x2000000 + { + $result = []; + $cnd = array_merge($cndBase, [$this->createLookup(['cmd', 'self_loc'.User::$localeId, 'target_loc'.User::$localeId, 'noTarget_loc'.User::$localeId])]); + $emote = new EmoteList($cnd); + + if ($data = $emote->getListviewData()) + { + $result = array( + 'type' => TYPE_EMOTE, + 'appendix' => ' (Emote)', + 'matches' => $emote->getMatches(), + 'file' => EmoteList::$brickFile, + 'data' => $data, + 'params' => [] + ); + } + + return $result; + } + + // private function _searchCharacter($cndBase) { } // 26 Characters $searchMask & 0x4000000 + // private function _searchGuild($cndBase) { } // 27 Guilds $searchMask & 0x8000000 + // private function _searchArenaTeam($cndBase) { } // 28 Arena Teams $searchMask & 0x10000000 } ?> diff --git a/setup/db_structure.sql b/setup/db_structure.sql index abba0c58..56f712bc 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2,7 +2,7 @@ -- -- Host: localhost Database: sarjuuk_aowow -- ------------------------------------------------------ --- Server version 5.5.30-30.1 +-- Server version 5.5.30-30.1 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; @@ -518,6 +518,52 @@ CREATE TABLE `aowow_currencies` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_emotes` +-- + +DROP TABLE IF EXISTS `aowow_emotes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_emotes` ( + `id` smallint(5) unsigned NOT NULL, + `cmd` varchar(15) NOT NULL, + `isAnimated` tinyint(1) unsigned NOT NULL, + `target_loc0` varchar(65) NULL DEFAULT NULL, + `target_loc2` varchar(70) NULL DEFAULT NULL, + `target_loc3` varchar(95) NULL DEFAULT NULL, + `target_loc6` varchar(90) NULL DEFAULT NULL, + `target_loc8` varchar(70) NULL DEFAULT NULL, + `noTarget_loc0` varchar(65) NULL DEFAULT NULL, + `noTarget_loc2` varchar(110) NULL DEFAULT NULL, + `noTarget_loc3` varchar(85) NULL DEFAULT NULL, + `noTarget_loc6` varchar(75) NULL DEFAULT NULL, + `noTarget_loc8` varchar(60) NULL DEFAULT NULL, + `self_loc0` varchar(65) NULL DEFAULT NULL, + `self_loc2` varchar(115) NULL DEFAULT NULL, + `self_loc3` varchar(85) NULL DEFAULT NULL, + `self_loc6` varchar(75) NULL DEFAULT NULL, + `self_loc8` varchar(70) NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_emotes_aliasses` +-- + +DROP TABLE IF EXISTS `aowow_emotes_aliasses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_emotes_aliasses` ( + `id` smallint(6) unsigned NOT NULL, + `locales` smallint(6) unsigned NOT NULL, + `command` varchar(15) NOT NULL, + UNIQUE INDEX `id_command` (`id`, `command`), + INDEX `id` (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_errors` -- @@ -2234,7 +2280,7 @@ CREATE TABLE `aowow_zones` ( -- -- Host: localhost Database: sarjuuk_aowow -- ------------------------------------------------------ --- Server version 5.5.30-30.1 +-- Server version 5.5.30-30.1 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; diff --git a/setup/tools/dbc.class.php b/setup/tools/dbc.class.php index ba2274a1..a400f380 100644 --- a/setup/tools/dbc.class.php +++ b/setup/tools/dbc.class.php @@ -58,6 +58,9 @@ class DBC 'dungeonmap' => 'niiffffi', 'durabilitycosts' => 'niiiiiiiiixiiiiiiiiiiixiiiixix', 'durabilityquality' => 'nf', + 'emotes' => 'nxixxxx', + 'emotestext' => 'nsiixxxixixxxxxxxxx', + 'emotestextdata' => 'nsxssxxsxsxxxxxxxx', 'faction' => 'nixxxxxxxxxxxxixxxiffixsxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxx', 'factiontemplate' => 'nixiiiiiiiiiii', 'gemproperties' => 'nixxi', @@ -137,6 +140,9 @@ class DBC 'dungeonmap' => 'Id,mapId,floor,minY,maxY,minX,maxX,areaId', 'durabilitycosts' => 'Id,w0,w1,w2,w3,w4,w5,w6,w7,w8,w10,w11,w12,w13,w14,w15,w16,w17,w18,w19,w20,a1,a2,a3,a4,a6', 'durabilityquality' => 'Id,mod', + 'emotes' => 'Id,animationId', + 'emotestext' => 'Id,command,emoteId,targetId,noTargetId,selfId', + 'emotestextdata' => 'Id,text_loc0,text_loc2,text_loc3,text_loc6,text_loc8', 'faction' => 'Id,repIdx,repFlags1,parentFaction,spilloverRateIn,spilloverRateOut,spilloverMaxRank,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', 'factiontemplate' => 'Id,factionId,ourMask,friendlyMask,hostileMask,enemyFactionId1,enemyFactionId2,enemyFactionId3,enemyFactionId4,friendFactionId1,friendFactionId2,friendFactionId3,friendFactionId4', 'gemproperties' => 'Id,enchantmentId,colorMask', diff --git a/setup/tools/sqlGen.class.php b/setup/tools/sqlGen.class.php index 9bc7445e..fe9f9f39 100644 --- a/setup/tools/sqlGen.class.php +++ b/setup/tools/sqlGen.class.php @@ -45,6 +45,7 @@ class SqlGen 'races' => [null, null, null, null], 'shapeshiftforms' => [null, null, null, null], 'skillline' => [null, null, null, null], + 'emotes' => [null, null, null, null], 'achievement' => [null, null, null, ['dbc_achievement']], 'creature' => [null, null, null, ['creature_template', 'locales_creature', 'creature_classlevelstats', 'instance_encounters']], 'currencies' => [null, null, null, ['item_template', 'locales_item']], diff --git a/setup/tools/sqlgen/emotes.func.php b/setup/tools/sqlgen/emotes.func.php new file mode 100644 index 00000000..0c1b6cb2 --- /dev/null +++ b/setup/tools/sqlgen/emotes.func.php @@ -0,0 +1,90 @@ +query('TRUNCATE ?_emotes_aliasses'); + + $path = sprintf($globStrPath, Util::$localeStrings[$lId].'/'); + if (CLISetup::fileExists($path)) + { + $locPath[$lId] = $path; + continue; + } + + // locale not found, try base mpqData + $path = sprintf($globStrPath, ''); + if (CLISetup::fileExists($path)) + { + $locPath[$lId] = $path; + continue; + } + + CLISetup::log('GlobalStrings.lua not found for selected locale '.CLISetup::bold(Util::$localeStrings[$lId]), CLISetup::LOG_WARN); + $allOK = false; + } + + DB::Aowow()->query('REPLACE INTO ?_emotes SELECT + et.Id, + LOWER(et.command), + IF(e.animationId, 1, 0), + etdT.text_loc0, etdT.text_loc2, etdT.text_loc3, etdT.text_loc6, etdT.text_loc8, + etdNT.text_loc0, etdNT.text_loc2, etdNT.text_loc3, etdNT.text_loc6, etdNT.text_loc8, + etdS.text_loc0, etdS.text_loc2, etdS.text_loc3, etdS.text_loc6, etdS.text_loc8 + FROM + dbc_emotestext et + LEFT JOIN + dbc_emotes e ON e.Id = et.emoteId + LEFT JOIN + dbc_emotestextdata etdT ON etdT.Id = et.targetId + LEFT JOIN + dbc_emotestextdata etdNT ON etdNT.Id = et.noTargetId + LEFT JOIN + dbc_emotestextdata etdS ON etdS.Id = et.selfId' + ); + + // i have no idea, how the indexing in this file works. + // sometimes the \d+ after EMOTE is the emoteTextId, but not nearly often enough + $aliasses = []; + foreach ($locPath as $lId => $path) + foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) + if (preg_match('/^EMOTE(\d+)_CMD\d+\s=\s\"\/([^"]+)\";$/', $line, $m)) + $aliasses[$m[1]][] = [$lId, $m[2]]; + + + $emotes = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, cmd FROM ?_emotes'); + + foreach($emotes as $eId => $cmd) + { + foreach ($aliasses as $gsId => $data) + { + if (in_array($cmd, array_column($data, 1))) + { + foreach ($data as $d) + DB::Aowow()->query('INSERT IGNORE INTO ?_emotes_aliasses VALUES (?d, ?d, ?) ON DUPLICATE KEY UPDATE locales = locales | ?d', $eId, (1 << $d[0]), strtolower($d[1]), (1 << $d[0])); + + break; + } + } + } + + return $allOK; +} + +?> \ No newline at end of file diff --git a/setup/updates/1437430574_01.sql b/setup/updates/1437430574_01.sql new file mode 100644 index 00000000..2d609c46 --- /dev/null +++ b/setup/updates/1437430574_01.sql @@ -0,0 +1,31 @@ +DROP TABLE IF EXISTS `aowow_emotes`; +CREATE TABLE `aowow_emotes` ( + `id` SMALLINT(5) UNSIGNED NOT NULL, + `cmd` VARCHAR(15) NOT NULL, + `isAnimated` TINYINT(1) UNSIGNED NOT NULL, + `target_loc0` VARCHAR(65) NULL DEFAULT NULL, + `target_loc2` VARCHAR(70) NULL DEFAULT NULL, + `target_loc3` VARCHAR(95) NULL DEFAULT NULL, + `target_loc6` VARCHAR(90) NULL DEFAULT NULL, + `target_loc8` VARCHAR(70) NULL DEFAULT NULL, + `noTarget_loc0` VARCHAR(65) NULL DEFAULT NULL, + `noTarget_loc2` VARCHAR(110) NULL DEFAULT NULL, + `noTarget_loc3` VARCHAR(85) NULL DEFAULT NULL, + `noTarget_loc6` VARCHAR(75) NULL DEFAULT NULL, + `noTarget_loc8` VARCHAR(60) NULL DEFAULT NULL, + `self_loc0` VARCHAR(65) NULL DEFAULT NULL, + `self_loc2` VARCHAR(115) NULL DEFAULT NULL, + `self_loc3` VARCHAR(85) NULL DEFAULT NULL, + `self_loc6` VARCHAR(75) NULL DEFAULT NULL, + `self_loc8` VARCHAR(70) NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM; + +DROP TABLE IF EXISTS `aowow_emotes_aliasses`; +CREATE TABLE `aowow_emotes_aliasses` ( + `id` SMALLINT(6) UNSIGNED NOT NULL, + `locales` SMALLINT(6) UNSIGNED NOT NULL, + `command` VARCHAR(15) NOT NULL, + UNIQUE INDEX `id_command` (`id`, `command`), + INDEX `id` (`id`) +) ENGINE=MyISAM; diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 0b4065ad..4cef5e36 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -836,6 +836,7 @@ var mn_database = [ [15,"Währungen","?currencies",mn_currencies], [11,"Weltereignisse","?events",mn_holidays], [1,"Zauber","?spells",mn_spells], + [100,"Emotes","?emotes",null] ]; var mn_tools = [ [0,"Talentrechner","?talent",mn_talentCalc], diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index b2e5f697..40eb796d 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -881,7 +881,8 @@ var mn_database = [ [1,"Spells","?spells",mn_spells], [10,"Titles","?titles",mn_titles], [11,"World Events","?events",mn_holidays], - [6,"Zones","?zones",mn_zones] + [6,"Zones","?zones",mn_zones], + [100,"Emotes","?emotes",null] ]; var mn_tools = [ [0,"Talent Calculator","?talent",mn_talentCalc], diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 338c63e2..8effe0e0 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -835,7 +835,8 @@ var mn_database = [ [1,"Hechizos","?spells",mn_spells], [10,"Títulos","?titles",mn_titles], [11,"Eventos del mundo","?events",mn_holidays], - [6,"Zonas","?zones",mn_zones] + [6,"Zonas","?zones",mn_zones], + [100,"Emociones","?emotes",null] ]; var mn_tools = [ [0,"Calculadora de talentos","?talent",mn_talentCalc], diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 43533018..67cb6ddc 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -835,7 +835,8 @@ var mn_database = [ [1,"Sorts","?spells",mn_spells], [10,"Titres","?titles",mn_titles], [11,"Évènements mondiaux","?events",mn_holidays], - [6,"Zones","?zones",mn_zones] + [6,"Zones","?zones",mn_zones], + [100,"Emotes","?emotes",null] ]; var mn_tools = [ [0,"Calculateur de talents","?talent",mn_talentCalc], diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 83a5613a..04153054 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -835,7 +835,8 @@ var mn_database = [ [1,"ЗаклинаниÑ","?spells",mn_spells], [10,"ЗваниÑ","?titles",mn_titles], [11,"Игровые ÑобытиÑ","?events",mn_holidays], - [6,"МеÑтноÑти","?zones",mn_zones] + [6,"МеÑтноÑти","?zones",mn_zones], + [100,"Эмоции", "?emotes", null] ]; var mn_tools = [ [0,"РаÑчёт талантов","?talent",mn_talentCalc], diff --git a/template/listviews/emote.tpl.php b/template/listviews/emote.tpl.php new file mode 100644 index 00000000..dc35641e --- /dev/null +++ b/template/listviews/emote.tpl.php @@ -0,0 +1,90 @@ +Listview.templates.emote = { + sort: [1], + searchable: 1, + filtrable: 1, + + columns: [ + { + id: 'name', + name: LANG.name, + type: 'text', + align: 'left', + value: 'name', + compute: function(emote, td, tr) { + var wrapper = $WH.ce('div'); + + var a = $WH.ce('a'); + a.style.fontFamily = 'Verdana, sans-serif'; + a.href = this.getItemLink(emote); + $WH.ae(a, $WH.ct(emote.name)); + + $WH.ae(wrapper, a); + + $WH.ae(td, wrapper); + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.name, b.name); + }, + getVisibleText: function(emote) { + return emote.name; + } + }, + { + id: 'preview', + name: LANG.preview, + type: 'text', + align: 'left', + value: 'name', + compute: function(emote, td, tr) { + var prev = ''; + if (emote.preview) { + td.className = 's4'; + prev = emote.preview.replace(/%\d?\$?s/g, '<' + LANG.name + '>'); + $WH.ae(td, $WH.ct(prev)); + } + else { + td.className = 'q0'; + td.style.textAlign = 'right'; + td.style.Align = 'right'; + + var + sm = $WH.ce('small'), + i = $WH.ce('i'); + + sm.style.paddingRight = '8px'; + + $WH.ae(i, $WH.ct(LANG.lvnodata)); + $WH.ae(sm, i); + $WH.ae(td, sm); + } + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.preview.replace(/%\d?\$?s/g, ''), b.preview.replace(/%\d?\$?s/g, '')); + }, + getVisibleText: function(emote) { + return emote.preview.replace(/%\d?\$?s/g, ''); + } + } + ], + getItemLink: function(emote) { + return '?emote=' + emote.id; + } +} + +new Listview({ + template:'emote', + $v): + if ($v[0] == '$'): + echo $k.':'.substr($v, 1).','; + elseif ($v): + echo $k.":'".$v."',"; + endif; + endforeach; +?> + data: +}); diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index db43e72a..1af2c987 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -174,7 +174,7 @@ if ($g = $this->gains): echo "
      \n"; if (!empty($g['xp'])): - echo '
    • '.number_format($g['xp']).' '.Lang::quest('experience')."
    • \n"; + echo '
    • '.Lang::nf($g['xp']).' '.Lang::quest('experience')."
    • \n"; endif; if (!empty($g['rep'])): From 4ab558858fdee382879691bc12838e88fd880635 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Jul 2015 00:51:11 +0200 Subject: [PATCH 0043/1249] Users * do not numeric-check g_user in case of numeric user names (wich then break js string functions) --- includes/utilities.php | 4 ++-- template/bricks/head.tpl.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/utilities.php b/includes/utilities.php index 74e7a1f9..9e79a2d7 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1687,9 +1687,9 @@ class Util header('Pragma: no-cache'); } - public static function toJSON($data) + public static function toJSON($data, $forceFlags = 0) { - $flags = JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE; + $flags = $forceFlags ?: (JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE); if (CFG_DEBUG) $flags |= JSON_PRETTY_PRINT; diff --git a/template/bricks/head.tpl.php b/template/bricks/head.tpl.php index d29f2094..b8efd5bf 100644 --- a/template/bricks/head.tpl.php +++ b/template/bricks/head.tpl.php @@ -51,5 +51,5 @@ foreach ($this->js as $js): endforeach; ?> From 8fa83eab646cad8ad92ad05b7bc9a589c0b58c18 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Jul 2015 11:49:35 +0200 Subject: [PATCH 0044/1249] Misc/Fixups * made emotes random-searchable * fixed error with pageTexts on Item Detail Page --- includes/types/emote.class.php | 2 -- includes/utilities.php | 7 +++-- pages/emote.php | 6 ++-- pages/emotes.php | 4 +-- pages/item.php | 52 ++++++++++++++++++---------------- pages/utility.php | 2 +- setup/db_structure.sql | 1 + 7 files changed, 39 insertions(+), 35 deletions(-) diff --git a/includes/types/emote.class.php b/includes/types/emote.class.php index 579b71c3..09d52503 100644 --- a/includes/types/emote.class.php +++ b/includes/types/emote.class.php @@ -6,8 +6,6 @@ if (!defined('AOWOW_REVISION')) class EmoteList extends BaseType { - use ListviewHelper; - public static $type = TYPE_EMOTE; public static $brickFile = 'emote'; diff --git a/includes/utilities.php b/includes/utilities.php index 9e79a2d7..4183a0e4 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -39,13 +39,16 @@ class Util public static $typeClasses = array( null, 'CreatureList', 'GameObjectList', 'ItemList', 'ItemsetList', 'QuestList', 'SpellList', 'ZoneList', 'FactionList', 'PetList', 'AchievementList', 'TitleList', 'WorldEventList', 'CharClassList', - 'CharRaceList', 'SkillList', null, 'CurrencyList' + 'CharRaceList', 'SkillList', null, 'CurrencyList', + TYPE_EMOTE => 'EmoteList' ); public static $typeStrings = array( // zero-indexed null, 'npc', 'object', 'item', 'itemset', 'quest', 'spell', 'zone', 'faction', 'pet', 'achievement', 'title', 'event', 'class', 'race', 'skill', null, 'currency', - TYPE_USER => 'user' + TYPE_USER => 'user', + TYPE_EMOTE => 'emote' + ); public static $combatRatingToItemMod = array( // zero-indexed idx:CR; val:Mod diff --git a/pages/emote.php b/pages/emote.php index f6524beb..f66889b9 100644 --- a/pages/emote.php +++ b/pages/emote.php @@ -4,8 +4,8 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 8: Pets g_initPath() -// tabid 0: Database g_initHeader() +// menuId 100: Emotes g_initPath() +// tabid 0: Database g_initHeader() class EmotePage extends GenericPage { use DetailPage; @@ -43,7 +43,7 @@ class EmotePage extends GenericPage /* Infobox */ /***********/ - $infobox = []; + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // has Animation if ($this->subject->getField('isAnimated')) diff --git a/pages/emotes.php b/pages/emotes.php index d7e76d7f..84c897af 100644 --- a/pages/emotes.php +++ b/pages/emotes.php @@ -4,8 +4,8 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 8: Pets g_initPath() -// tabid 0: Database g_initHeader() +// menuId 100: Emotes g_initPath() +// tabid 0: Database g_initHeader() class EmotesPage extends GenericPage { use ListPage; diff --git a/pages/item.php b/pages/item.php index 744d7091..a4e4f242 100644 --- a/pages/item.php +++ b/pages/item.php @@ -328,8 +328,35 @@ class ItemPage extends genericPage $_cu = in_array($_class, [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR]) || $this->subject->getField('gemEnchantmentId'); + // pageText + $pageText = []; + if ($next = $this->subject->getField('pageTextId')) + { + while ($next) + { + if ($row = DB::World()->selectRow('SELECT *, Text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.ID = lpt.entry WHERE pt.ID = ?d', $next)) + { + $next = $row['NextPageID']; + $pageText = Util::parseHtmlText(Util::localizedString($row, 'Text')); + } + else + { + trigger_error('Referenced PageTextId #'.$next.' is not in DB', E_USER_WARNING); + break; + } + } + } + + // add conditional js & css + if ($pageText) + { + $this->addJS('Book.js'); + $this->addCSS(['path' => 'Book.css']); + } + $this->headIcons = [$this->subject->getField('iconString'), $this->subject->getField('stackable')]; $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; + $this->pageText = $pageText; $this->tooltip = $this->subject->renderTooltip(true); $this->redButtons = array( BUTTON_WOWHEAD => true, @@ -343,31 +370,6 @@ class ItemPage extends genericPage // availablility $this->disabled = false; // todo (med): get itemSources (which are not yet in DB :x) or - // pageText - if ($next = $this->subject->getField('pageTextId')) - { - while ($next) - { - if ($row = DB::World()->selectRow('SELECT *, Text as Text_loc0 FROM page_text pt LEFT JOIN locales_page_text lpt ON pt.ID = lpt.entry WHERE pt.ID = ?d', $next)) - { - $next = $row['NextPageID']; - $this->pageText[] = Util::parseHtmlText(Util::localizedString($row, 'Text')); - } - else - { - trigger_error('Referenced PageTextId #'.$next.' is not in DB', E_USER_WARNING); - break; - } - } - } - - // add conditional js & css - if ($this->pageText) - { - $this->addJS('Book.js'); - $this->addCSS(['path' => 'Book.css']); - } - // subItems $this->subject->initSubItems(); if (!empty($this->subject->subItems[$this->typeId])) diff --git a/pages/utility.php b/pages/utility.php index 5f12ef43..b01bf6b5 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -65,7 +65,7 @@ class UtilityPage extends GenericPage switch ($this->page) { case 'random': - $type = array_rand(array_keys(array_filter(Util::$typeClasses))); + $type = array_rand(array_filter(Util::$typeClasses)); $typeId = (new Util::$typeClasses[$type](null))->getRandomId(); header('Location: ?'.Util::$typeStrings[$type].'='.$typeId, true, 302); diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 56f712bc..baab5ded 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -529,6 +529,7 @@ CREATE TABLE `aowow_emotes` ( `id` smallint(5) unsigned NOT NULL, `cmd` varchar(15) NOT NULL, `isAnimated` tinyint(1) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, `target_loc0` varchar(65) NULL DEFAULT NULL, `target_loc2` varchar(70) NULL DEFAULT NULL, `target_loc3` varchar(95) NULL DEFAULT NULL, From 3d08de51e11d6e57100abb6056181aea255c1173 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Jul 2015 21:08:15 +0200 Subject: [PATCH 0045/1249] Items/Heirlooms * damage range of melee weapons is noe 20% and ranged weapons 30% * removed localized number format from item-tooltips. this broke damage recalculation User/Weightscales * fixed managing weightscales by moving ! around --- includes/ajaxHandler.class.php | 12 ++++++------ includes/types/item.class.php | 23 +++++++++++++---------- includes/utilities.php | 3 ++- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index b68d6712..ba73ef5c 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -204,8 +204,8 @@ class AjaxHandler */ private function handleCookie() { - if (User::$id && $this->params && !empty($this->get[$this->params[0]])) - if (DB::Aowow()->query('REPLACE INTO ?_account_cookies VALUES (?d, ?, ?)', User::$id, $this->params[0], $this->get[$this->params[0]])) + if (User::$id && $this->params && $this->get($this->params[0])) + if (DB::Aowow()->query('REPLACE INTO ?_account_cookies VALUES (?d, ?, ?)', User::$id, $this->params[0], $this->get($this->params[0]))) return 0; return null; @@ -552,7 +552,7 @@ class AjaxHandler break; case 'flag-reply': - if (!User::$id || $this->post('id')) + if (!User::$id || !$this->post('id')) break; DB::Aowow()->query( @@ -633,11 +633,11 @@ class AjaxHandler // should probably occur in g_user.excludegroups (dont forget to also set g_users.settings = {}) return ''; case 'weightscales': - if (!$this->post('save')) + if ($this->post('save')) { - if (!isset($this->post['id'])) + if (!$this->post('id')) { - $res = DB::Aowow()->selectRow('SELECT max(id) as max, count(id) as num FROM ?_account_weightscales WHERE userId = ?d', User::$id); + $res = DB::Aowow()->selectRow('SELECT MAX(id) AS max, count(id) AS num FROM ?_account_weightscales WHERE userId = ?d', User::$id); if ($res['num'] < 5) // more or less hard-defined in LANG.message_weightscalesaveerror $this->post['id'] = ++$res['max']; else diff --git a/includes/types/item.class.php b/includes/types/item.class.php index e8a9f2ba..93d3845b 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -593,14 +593,14 @@ class ItemList extends BaseType $dps = $speed ? ($dmgmin1 + $dmgmax1) / (2 * $speed) : 0; if ($_class == ITEM_CLASS_AMMUNITION && $dmgmin1 && $dmgmax1) - $x .= Lang::item('addsDps').' '.Lang::nf(($dmgmin1 + $dmgmax1) / 2, 1).' '.Lang::item('dps2').'
      '; + $x .= Lang::item('addsDps').' '.number_format(($dmgmin1 + $dmgmax1) / 2, 1).' '.Lang::item('dps2').'
      '; else if ($dps) { if ($_class == ITEM_CLASS_WEAPON) { $x .= ''; $x .= ''; - $x .= ''; + $x .= ''; // do not use localized format here! $x .= '
      '.sprintf($this->curTpl['dmgType1'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin1'].' - '.$this->curTpl['dmgMax1'], Lang::game('sc', $this->curTpl['dmgType1'])).''.Lang::item('speed').' '.Lang::nf($speed, 2).''.Lang::item('speed').' '.number_format($speed, 2).'
      '; } else @@ -611,7 +611,7 @@ class ItemList extends BaseType $x .= '+'.sprintf($this->curTpl['dmgType2'] ? Lang::item('damageMagic') : Lang::item('damagePhys'), $this->curTpl['dmgMin2'].' - '.$this->curTpl['dmgMax2'], Lang::game('sc', $this->curTpl['dmgType2'])).'
      '; if ($_class == ITEM_CLASS_WEAPON) - $x .= '('.Lang::nf($dps, 1).' '.Lang::item('dps').')
      '; + $x .= '('.number_format($dps, 1).' '.Lang::item('dps').')
      '; // do not use localized format here! // display FeralAttackPower if set if ($fap = $this->getFeralAP()) @@ -1329,12 +1329,12 @@ class ItemList extends BaseType if ($mask & (1 << $i)) $field = Util::$ssdMaskFields[$i]; - return $field ? DB::Aowow()->selectCell("SELECT ?# FROM ?_scalingstatvalues WHERE id = ?", $field, $this->ssd[$this->id]['maxLevel']) : 0; + return $field ? DB::Aowow()->selectCell('SELECT ?# FROM ?_scalingstatvalues WHERE id = ?d', $field, $this->ssd[$this->id]['maxLevel']) : 0; } private function initScalingStats() { - $this->ssd[$this->id] = DB::Aowow()->selectRow("SELECT * FROM ?_scalingstatdistribution WHERE id = ?", $this->curTpl['scalingStatDistribution']); + $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ?_scalingstatdistribution WHERE id = ?d', $this->curTpl['scalingStatDistribution']); if (!$this->ssd[$this->id]) return; @@ -1358,12 +1358,15 @@ class ItemList extends BaseType if ($ssvArmor = $this->getSSDMod('armor')) $this->templates[$this->id]['armor'] = $ssvArmor; - // if set dpsMod in ScalingStatValue use it for min (70% from average), max (130% from average) damage + // if set dpsMod in ScalingStatValue use it for min/max damage + // mle: 20% range / rgd: 30% range if ($extraDPS = $this->getSSDMod('dps')) // dmg_x2 not used for heirlooms { + $range = isset($this->json[$this->id]['rgddps']) ? 0.3 : 0.2; $average = $extraDPS * $this->curTpl['delay'] / 1000; - $this->templates[$this->id]['dmgMin1'] = Lang::nf(0.7 * $average); - $this->templates[$this->id]['dmgMax1'] = Lang::nf(1.3 * $average); + + $this->templates[$this->id]['dmgMin1'] = floor((1 - $range) * $average); + $this->templates[$this->id]['dmgMax1'] = floor((1 + $range) * $average); } // apply Spell Power from ScalingStatValue if set @@ -1519,8 +1522,8 @@ class ItemList extends BaseType $json['dmgtype1'] = $this->curTpl['dmgType1']; $json['dmgmin1'] = $this->curTpl['dmgMin1'] + $this->curTpl['dmgMin2']; $json['dmgmax1'] = $this->curTpl['dmgMax1'] + $this->curTpl['dmgMax2']; - $json['speed'] = Lang::nf($this->curTpl['delay'] / 1000, 2); - $json['dps'] = !floatVal($json['speed']) ? 0 : Lang::nf(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1); + $json['speed'] = number_format($this->curTpl['delay'] / 1000, 2); + $json['dps'] = !floatVal($json['speed']) ? 0 : number_format(($json['dmgmin1'] + $json['dmgmax1']) / (2 * $json['speed']), 1); if (in_array($json['subclass'], [2, 3, 18, 19])) { diff --git a/includes/utilities.php b/includes/utilities.php index 4183a0e4..cca4908d 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1067,7 +1067,8 @@ class Util else $c = 2 / 52; - $result = Lang::nf($val / Util::$gtCombatRatings[$type] / $c, 2); + // do not use localized number format here! + $result = number_format($val / Util::$gtCombatRatings[$type] / $c, 2); } if (!in_array($type, array(ITEM_MOD_DEFENSE_SKILL_RATING, ITEM_MOD_EXPERTISE_RATING))) From 2afcceaefbc2a1e68c44680c1328e454a4511057 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 21 Jul 2015 23:21:10 +0200 Subject: [PATCH 0046/1249] Heirlooms * fixed expected structure of ScalingStatValues (js-scaling heirlooms should be working again) * weapons are now properly assigned ranged- or melee-dps need to rerun: php aowow --build=itemScaling --- includes/shared.php | 2 +- setup/tools/filegen/itemScaling.func.php | 19 +++++++++++++------ static/js/basic.js | 6 +++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 9b5ce531..1c6e0a46 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ select('SELECT *, Id AS ARRAY_KEY FROM dbc_scalingstatvalues'); - foreach ($data as &$row) - { - $row = array_values($row); - array_splice($row, 0, 1); - } + /* so the javascript expects a slightly different structure, than the dbc provides .. f*** it + e.g. + dbc - 80: 97 97 56 41 210 395 878 570 120 156 86 112 108 220 343 131 73 140 280 527 1171 2093 + expected - 80: 97 97 56 131 41 210 395 878 1570 120 156 86 112 108 220 343 0 0 73 140 280 527 1171 2093 + */ + $fields = Util::$ssdMaskFields; + array_walk($fields, function(&$v, $k) { + $v = $v ?: '0 AS idx'.$k; // NULL => 0 (plus some index so we can have 2x 0) + }); + + $data = DB::Aowow()->select('SELECT Id AS ARRAY_KEY, '.implode(', ', $fields).' FROM dbc_scalingstatvalues'); + foreach ($data as &$d) + $d = array_values($d); // strip indizes return CFG_DEBUG ? debugify($data) : Util::toJSON($data); } diff --git a/static/js/basic.js b/static/js/basic.js index 392177f1..32c864d6 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -1266,7 +1266,7 @@ $WH.g_setJsonItemLevel = function (json, level) { scaleMask = 0x04001F, armorMask = 0xF801E0, damageMask = 0x007E00, - spelPwrMask = 0x008000, + splPwrMask = 0x008000, meleeMask = 0x001400; for (var i = 0; i < 24; ++i) { @@ -1281,7 +1281,7 @@ $WH.g_setJsonItemLevel = function (json, level) { else if (mask & damageMask && damageColumn < 0) { damageColumn = i; } - else if (mask & spelPwrMask && splPwrColumn < 0) { + else if (mask & splPwrMask && splPwrColumn < 0) { splPwrColumn = i; } } @@ -1306,7 +1306,7 @@ $WH.g_setJsonItemLevel = function (json, level) { if (damageColumn >= 0) { var damageRange = (json.scaflags & meleeMask ? 0.2 : 0.3), - damageType = (json.mledps ? "mle": "rgd"); + damageType = (json.scaflags & meleeMask ? "mle": "rgd"); json.dps = json[damageType + "dps"] = $WH.g_convertScalingFactor(level, damageColumn); json.dmgmin = json[damageType + "dmgmin"] = Math.floor(json.dps * json.speed * (1 - damageRange)); From d771103428628841a146ba67478e3c899b6dda32 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 23 Jul 2015 19:55:37 +0200 Subject: [PATCH 0047/1249] added forgotten file should have been committed in 8fa83eab646cad8ad92ad05b7bc9a589c0b58c18 --- setup/updates/1437472069_01.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup/updates/1437472069_01.sql diff --git a/setup/updates/1437472069_01.sql b/setup/updates/1437472069_01.sql new file mode 100644 index 00000000..80aba39e --- /dev/null +++ b/setup/updates/1437472069_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_emotes` + ADD COLUMN `cuFlags` INT UNSIGNED NOT NULL AFTER `isAnimated`; From 7b30c49785ec4eb1a53c6a9c992192ce0c5adcaf Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 23 Jul 2015 22:43:41 +0200 Subject: [PATCH 0048/1249] Setup/Emotes fixed field count after 8fa83eab646cad8ad92ad05b7bc9a589c0b58c18 --- setup/tools/sqlgen/emotes.func.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup/tools/sqlgen/emotes.func.php b/setup/tools/sqlgen/emotes.func.php index 0c1b6cb2..f1a3f928 100644 --- a/setup/tools/sqlgen/emotes.func.php +++ b/setup/tools/sqlgen/emotes.func.php @@ -40,10 +40,11 @@ function emotes(/*array $ids = [] */) $allOK = false; } - DB::Aowow()->query('REPLACE INTO ?_emotes SELECT + $_= DB::Aowow()->query('REPLACE INTO ?_emotes SELECT et.Id, LOWER(et.command), IF(e.animationId, 1, 0), + 0, -- cuFlags etdT.text_loc0, etdT.text_loc2, etdT.text_loc3, etdT.text_loc6, etdT.text_loc8, etdNT.text_loc0, etdNT.text_loc2, etdNT.text_loc3, etdNT.text_loc6, etdNT.text_loc8, etdS.text_loc0, etdS.text_loc2, etdS.text_loc3, etdS.text_loc6, etdS.text_loc8 @@ -59,6 +60,9 @@ function emotes(/*array $ids = [] */) dbc_emotestextdata etdS ON etdS.Id = et.selfId' ); + if (!$_) + $allOK = false; + // i have no idea, how the indexing in this file works. // sometimes the \d+ after EMOTE is the emoteTextId, but not nearly often enough $aliasses = []; From f6e15c35fc6899dd283d942fd95abcda50661a50 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 24 Jul 2015 22:41:00 +0200 Subject: [PATCH 0049/1249] Tooltips * added word events to tooltips * added option to pass achieved criteria to achievement tooltips via rel-parameter --- includes/markup.class.php | 2 +- includes/types/achievement.class.php | 8 +- includes/types/worldevent.class.php | 52 +++++-- pages/currency.php | 3 +- pages/event.php | 168 +++++++++++++++------- setup/tools/filegen/templates/power.js.in | 49 +++++-- 6 files changed, 202 insertions(+), 80 deletions(-) diff --git a/includes/markup.class.php b/includes/markup.class.php index 7d4df093..1400b9eb 100644 --- a/includes/markup.class.php +++ b/includes/markup.class.php @@ -21,7 +21,7 @@ class Markup public function parseGlobalsFromText(&$jsg = []) { - if (preg_match_all('/(?text, $matches, PREG_SET_ORDER)) + if (preg_match_all('/(?text, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { diff --git a/includes/types/achievement.class.php b/includes/types/achievement.class.php index 24e49d37..c8803b3d 100644 --- a/includes/types/achievement.class.php +++ b/includes/types/achievement.class.php @@ -206,10 +206,12 @@ class AchievementList extends BaseType break; } + $criteria .= '- '.Util::jsEscape($crtName); + if ($crt['completionFlags'] & ACHIEVEMENT_CRITERIA_FLAG_MONEY_COUNTER) - $criteria .= '- '.Util::jsEscape($crtName).' '.Lang::nf($crt['value2' ] / 10000).'
      '; - else - $criteria .= '- '.Util::jsEscape($crtName).'
      '; + $criteria .= ' '.Lang::nf($crt['value2' ] / 10000).''; + + $criteria .= '
      '; if (++$i == round(count($rows)/2)) $criteria .= '
      '; diff --git a/includes/types/worldevent.class.php b/includes/types/worldevent.class.php index 6021cca6..ac98c68d 100644 --- a/includes/types/worldevent.class.php +++ b/includes/types/worldevent.class.php @@ -9,7 +9,7 @@ class WorldEventList extends BaseType public static $type = TYPE_WORLDEVENT; public static $brickFile = 'event'; - protected $queryBase = 'SELECT *, e.id as id, e.id AS ARRAY_KEY FROM ?_events e'; + protected $queryBase = 'SELECT e.*, h.*, e.description AS nameINT, e.id AS id, e.id AS ARRAY_KEY FROM ?_events e'; protected $queryOpts = array( 'e' => [['h']], 'h' => ['j' => ['?_holidays h ON e.holidayId = h.id', true], 'o' => '-e.id ASC'] @@ -45,14 +45,13 @@ class WorldEventList extends BaseType { $this->curTpl['name'] = $this->getField('name', true); $replace[$this->id] = $this->curTpl; - unset($this->curTpl['description']); } else // set a name if holiday is missing { // template - $this->curTpl['name_loc0'] = $this->curTpl['description']; + $this->curTpl['name_loc0'] = $this->curTpl['nameINT']; $this->curTpl['iconString'] = 'trade_engineering'; - $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('description', true); + $this->curTpl['name'] = '(SERVERSIDE) '.$this->getField('nameINT', true); $replace[$this->id] = $this->curTpl; } } @@ -66,10 +65,21 @@ class WorldEventList extends BaseType public static function getName($id) { - if ($id > 0) - $row = DB::Aowow()->SelectRow('SELECT * FROM ?_holidays WHERE Id = ?d', intVal($id)); - else - $row = DB::Aowow()->SelectRow('SELECT description as name FROM ?_events WHERE Id = ?d', intVal(-$id)); + $row = DB::Aowow()->SelectRow(' + SELECT + IFNULL(h.name_loc0, e.description) AS name_loc0, + h.name_loc2, + h.name_loc3, + h.name_loc6, + h.name_loc8 + FROM + ?_events e + LEFT JOIN + ?_holidays h ON e.holidayId = h.id + WHERE + e.id = ?d', + $id + ); return Util::localizedString($row, 'name'); } @@ -155,7 +165,31 @@ class WorldEventList extends BaseType return $data; } - public function renderTooltip() { } + public function renderTooltip() + { + if (!$this->curTpl) + return null; + + $x = '
      '; + + // head v that extra % is nesecary because we are using sprintf later on + $x .= '
      '.Util::jsEscape($this->getField('name', true)).''.Lang::event('category', $this->getField('category')).'
      '; + + // use string-placeholder for dates + // start + $x .= Lang::event('start').Lang::main('colon').'%s
      '; + // end + $x .= Lang::event('end').Lang::main('colon').'%s'; + + $x .= '
      '; + + // desc + if ($this->getField('holidayId')) + if ($_ = $this->getField('description', true)) + $x .= '
      '.Util::jsEscape($_).'
      '; + + return $x; + } } ?> diff --git a/pages/currency.php b/pages/currency.php index fcc6706d..1b23f449 100644 --- a/pages/currency.php +++ b/pages/currency.php @@ -230,7 +230,7 @@ class CurrencyPage extends GenericPage } } - protected function generateTooltip($asError = false) + protected function generateTooltip($asError = false) { if ($asError) return '$WowheadPower.registerCurrency('.$this->typeId.', '.User::$localeId.', {});'; @@ -268,7 +268,6 @@ class CurrencyPage extends GenericPage echo $this->generateTooltip(true); exit(); } - } ?> diff --git a/pages/event.php b/pages/event.php index e8f68340..a904abfc 100644 --- a/pages/event.php +++ b/pages/event.php @@ -4,8 +4,8 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 11: Object g_initPath() -// tabId 0: Database g_initHeader() +// menuId 11: Worldevent g_initPath() +// tabId 0: Database g_initHeader() class EventPage extends GenericPage { use DetailPage; @@ -24,16 +24,25 @@ class EventPage extends GenericPage { parent::__construct($pageCall, $id); + // temp locale + if ($this->mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) + Util::powerUseLocale($_GET['domain']); + $this->typeId = intVal($id); $this->subject = new WorldEventList(array(['id', $this->typeId])); if ($this->subject->error) $this->notFound(Lang::game('event'), Lang::event('notFound')); - $this->hId = $this->subject->getField('holidayId'); - $this->eId = $this->typeId; - - $this->name = $this->subject->getField('name', true); + $this->hId = $this->subject->getField('holidayId'); + $this->eId = $this->typeId; + $this->name = $this->subject->getField('name', true); + $this->dates = array( + 'firstDate' => $this->subject->getField('startTime'), + 'lastDate' => $this->subject->getField('endTime'), + 'length' => $this->subject->getField('length'), + 'rec' => $this->subject->getField('occurence') + ); } protected function generatePath() @@ -78,17 +87,14 @@ class EventPage extends GenericPage /* Main Content */ /****************/ + if ($this->hId) + $this->extraText = Util::jsEscape($this->subject->getField('description', true)); + $this->headIcons = [$this->subject->getField('iconString')]; $this->redButtons = array( BUTTON_WOWHEAD => $this->hId > 0, BUTTON_LINKS => true ); - $this->dates = array( - 'firstDate' => $this->subject->getField('startTime'), - 'lastDate' => $this->subject->getField('endTime'), - 'length' => $this->subject->getField('length'), - 'rec' => $this->subject->getField('occurence') - ); /**************/ /* Extra Tabs */ @@ -259,53 +265,107 @@ class EventPage extends GenericPage protected function postCache() { - if ($this->hId) - Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/event='.$this->hId; - - /********************/ - /* finalize infobox */ - /********************/ - // update dates to now() $updated = WorldEventList::updateDates($this->dates); - // start - if ($updated['start']) - array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['start'])); - - // end - if ($updated['end']) - array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['end'])); - - // occurence - if ($updated['rec'] > 0) - array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($updated['rec'] * 1000)); - - // in progress - if ($updated['start'] < time() && $updated['end'] > time()) - array_push($this->infobox, '[span class=q2]'.Lang::event('inProgress').'[/span]'); - - $this->infobox = '[ul][li]'.implode('[/li][li]', $this->infobox).'[/li][/ul]'; - - /***************************/ - /* finalize related events */ - /***************************/ - - foreach ($this->lvTabs as &$view) + if ($this->mode == CACHE_TYPE_TOOLTIP) { - if ($view['file'] != WorldEventList::$brickFile) - continue; - - foreach ($view['data'] as &$data) - { - $updated = WorldEventList::updateDates($data['_date']); - unset($data['_date']); - $data['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false; - $data['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false; - $data['rec'] = $updated['rec']; - } - + return array( + date(Lang::main('dateFmtLong'), $updated['start']), + date(Lang::main('dateFmtLong'), $updated['end']) + ); } + else + { + if ($this->hId) + Util::$wowheadLink = 'http://'.Util::$subDomains[User::$localeId].'.wowhead.com/event='.$this->hId; + + /********************/ + /* finalize infobox */ + /********************/ + + // start + if ($updated['start']) + array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['start'])); + + // end + if ($updated['end']) + array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $updated['end'])); + + // occurence + if ($updated['rec'] > 0) + array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($updated['rec'] * 1000)); + + // in progress + if ($updated['start'] < time() && $updated['end'] > time()) + array_push($this->infobox, '[span class=q2]'.Lang::event('inProgress').'[/span]'); + + $this->infobox = '[ul][li]'.implode('[/li][li]', $this->infobox).'[/li][/ul]'; + + /***************************/ + /* finalize related events */ + /***************************/ + + foreach ($this->lvTabs as &$view) + { + if ($view['file'] != WorldEventList::$brickFile) + continue; + + foreach ($view['data'] as &$data) + { + $updated = WorldEventList::updateDates($data['_date']); + unset($data['_date']); + $data['startDate'] = $updated['start'] ? date(Util::$dateFormatInternal, $updated['start']) : false; + $data['endDate'] = $updated['end'] ? date(Util::$dateFormatInternal, $updated['end']) : false; + $data['rec'] = $updated['rec']; + } + + } + } + } + + protected function generateTooltip($asError = false) + { + if ($asError) + return '$WowheadPower.registerHoliday('.$this->typeId.', '.User::$localeId.', {});'; + + $x = '$WowheadPower.registerHoliday('.$this->typeId.', '.User::$localeId.", {\n"; + $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; + + if ($this->subject->getField('iconString') != 'trade_engineering') + $x .= "\ticon: '".urlencode($this->subject->getField('iconString'))."',\n"; + + $x .= "\ttooltip_".User::$localeString.": '".$this->subject->renderTooltip()."'\n"; + $x .= "});"; + + return $x; + } + + public function display($override = '') + { + if ($this->mode != CACHE_TYPE_TOOLTIP) + return parent::display($override); + + if (!$this->loadCache($tt)) + { + $tt = $this->generateTooltip(); + $this->saveCache($tt); + } + + list($start, $end) = $this->postCache(); + + header('Content-type: application/x-javascript; charset=utf-8'); + die(sprintf($tt, $start, $end)); + } + + public function notFound() + { + if ($this->mode != CACHE_TYPE_TOOLTIP) + return parent::notFound(Lang::game('event'), Lang::event('notFound')); + + header('Content-type: application/x-javascript; charset=utf-8'); + echo $this->generateTooltip(true); + exit(); } } diff --git a/setup/tools/filegen/templates/power.js.in b/setup/tools/filegen/templates/power.js.in index 34367b6b..1316162b 100644 --- a/setup/tools/filegen/templates/power.js.in +++ b/setup/tools/filegen/templates/power.js.in @@ -33,6 +33,7 @@ if (typeof $WowheadPower == "undefined") { quests = {}, spells = {}, achievements = {}, + holidays = {}, itemsets = {}, currencies = {}, profiles = {}, @@ -57,6 +58,7 @@ if (typeof $WowheadPower == "undefined") { TYPE_QUEST = 5, TYPE_SPELL = 6, TYPE_ACHIEVEMENT = 10, + TYPE_HOLIDAY = 12, TYPE_CURRENCY = 17, TYPE_PROFILE = 100, @@ -76,6 +78,7 @@ if (typeof $WowheadPower == "undefined") { 5: [quests, "quest", "Quest" ], 6: [spells, "spell", "Spell" ], 10: [achievements, "achievement", "Achievement"], + 12: [holidays, "event", "Holiday" ], 17: [currencies, "currency", "Currency" ], 100: [profiles, "profile", "Profile" ] }, @@ -215,11 +218,26 @@ if (typeof $WowheadPower == "undefined") { return -2323; } - if (!t.href.length && !t.rel) { + var rel = t.rel; + try { + if (t.dataset && t.dataset.hasOwnProperty("wowhead")) { + rel = t.dataset.wowhead; + } + else if (t.getAttribute && t.getAttribute("data-wowhead")) { + rel = t.getAttribute("data-wowhead"); + } + } + catch(e) { void(0); } + + if (!t.href.length && !rel) { return; } - if (t.rel && t.rel.indexOf("np") != -1 && t.rel.indexOf("np") != t.rel.indexOf("npc=")) { + if (rel && /^np\b/.test(rel)) { + return; + } + + if (t.getAttribute("data-disable-wowhead-tooltip") == "true") { return; } @@ -239,7 +257,7 @@ if (typeof $WowheadPower == "undefined") { else if (k == "rand" || k == "ench" || k == "lvl" || k == "c") { params[k] = parseInt(v); } - else if (k == "gems" || k == "pcs" || k == "know") { + else if (k == "gems" || k == "pcs" || k == "know" || k == "cri") { params[k] = v.split(":"); } else if (k == "who" || k == "domain") { @@ -261,8 +279,8 @@ if (typeof $WowheadPower == "undefined") { i2 = 3; if (t.href.indexOf("http://") == 0 || t.href.indexOf("https://") == 0) { i0 = 1; - // url = t.href.match(/^https?:\/\/(.+?)?\.?wowhead\.com(?:\:\d+)?\/\??(item|quest|spell|achievement|npc|object|itemset|currency)=(-?[0-9]+)/); - url = t.href.match(/^https?:\/\/(.*)\/?\??(item|quest|spell|achievement|npc|object|itemset|currency)=(-?[0-9]+)/); + // url = t.href.match(/^https?:\/\/(.+?)?\.?wowhead\.com(?:\:\d+)?\/\??(item|quest|spell|achievement|event|npc|object|itemset|currency)=(-?[0-9]+)/); + url = t.href.match(/^https?:\/\/(.*)\/?\??(item|quest|spell|achievement|event|npc|object|itemset|currency)=(-?[0-9]+)/); if (url == null) { // url = t.href.match(/^http:\/\/(.+?)?\.?wowhead\.com\/\?(profile)=([^&#]+)/) url = t.href.match(/^https?:\/\/(.*)\/?\??(profile)=([^&#]+)/); @@ -271,7 +289,7 @@ if (typeof $WowheadPower == "undefined") { showLogo = 0; } else { - url = t.href.match(/()\?(item|quest|spell|achievement|npc|object|itemset|currency)=(-?[0-9]+)/); + url = t.href.match(/()\?(item|quest|spell|achievement|event|npc|object|itemset|currency)=(-?[0-9]+)/); if (url == null) { url = t.href.match(/()\?(profile)=([^&#]+)/); } @@ -280,20 +298,20 @@ if (typeof $WowheadPower == "undefined") { } } - if (url == null && t.rel && (opt.applyto & 2)) { + if (url == null && rel && (opt.applyto & 2)) { i0 = 0; i1 = 1; i2 = 2; - url = t.rel.match(/(item|quest|spell|achievement|npc|object|itemset|currency).?(-?[0-9]+)/); + url = rel.match(/(item|quest|spell|achievement|event|npc|object|itemset|currency).?(-?[0-9]+)/); // if (url == null) { // sarjuuk: also matches 'profiler' and 'profiles' which screws with the language-menu workaround - // url = t.rel.match(/(profile).?([^&#]+)/); + // url = rel.match(/(profile).?([^&#]+)/); // } showLogo = 1; } t.href.replace(/([a-zA-Z]+)=?([a-zA-Z0-9:-]*)/g, p); - if (t.rel) { - t.rel.replace(/([a-zA-Z]+)=?([a-zA-Z0-9:-]*)/g, p); + if (rel) { + rel.replace(/([a-zA-Z]+)=?([a-zA-Z0-9:-]*)/g, p); } if (params.gems && params.gems.length > 0) { @@ -606,6 +624,11 @@ if (typeof $WowheadPower == "undefined") { html = html.replace("

      ", ' From 7ac0601f889282ba3745a96927a07d8bf0d6c022 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 3 Aug 2015 20:51:13 +0200 Subject: [PATCH 0051/1249] Enchantments * added tab: used by socketbonus to detail page * format ppm as float --- pages/enchantment.php | 30 ++++++++++++++++++++++++++---- template/pages/enchantment.tpl.php | 2 +- template/pages/spell.tpl.php | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pages/enchantment.php b/pages/enchantment.php index 2e374ffb..f082a3b8 100644 --- a/pages/enchantment.php +++ b/pages/enchantment.php @@ -179,13 +179,29 @@ class EnchantmentPage extends GenericPage 'data' => $gemList->getListviewData(), 'params' => array( 'name' => '$LANG.tab_usedby + \' \' + LANG.gems', - 'id' => 'used-by-gems', + 'id' => 'used-by-gem', ) ); $this->extendGlobalData($gemList->getJsGlobals()); } + // used by socket bonus + $socketsList = new ItemList(array(['socketBonus', $this->typeId])); + if (!$socketsList->error) + { + $this->lvTabs[] = array( + 'file' => 'item', + 'data' => $socketsList->getListviewData(), + 'params' => array( + 'name' => '$LANG.tab_usedby + \' \' + \''.Lang::item('socketBonus').'\'', + 'id' => 'used-by-socketbonus', + ) + ); + + $this->extendGlobalData($socketsList->getJsGlobals()); + } + // used by spell // used by useItem $cnd = array( @@ -216,7 +232,10 @@ class EnchantmentPage extends GenericPage $this->lvTabs[] = array( 'file' => 'item', 'data' => $ubItems->getListviewData(), - 'params' => [] + 'params' => array( + 'name' => '$LANG.tab_usedby + \' \' + LANG.types[3][0]', + 'id' => 'used-by-item', + ) ); $this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF)); @@ -249,7 +268,10 @@ class EnchantmentPage extends GenericPage $this->lvTabs[] = array( 'file' => 'spell', 'data' => $spellData, - 'params' => [] + 'params' => array( + 'name' => '$LANG.tab_usedby + \' \' + LANG.types[6][0]', + 'id' => 'used-by-spell', + ) ); } @@ -285,7 +307,7 @@ class EnchantmentPage extends GenericPage 'data' => $data, 'params' => array( 'id' => 'used-by-rand', - 'name' => Lang::item('_rndEnchants'), + 'name' => '$LANG.tab_usedby + \' \' + \''.Lang::item('_rndEnchants').'\'', 'extraCols' => '$[Listview.extraCols.percent]' ) ); diff --git a/template/pages/enchantment.tpl.php b/template/pages/enchantment.tpl.php index b453f315..5e60ba58 100644 --- a/template/pages/enchantment.tpl.php +++ b/template/pages/enchantment.tpl.php @@ -57,7 +57,7 @@ foreach ($this->effects as $i => $e): echo '
      '; if ($e['proc'] < 0): - echo sprintf(Lang::spell('ppm'), -$e['proc']); + echo sprintf(Lang::spell('ppm'), Lang::nf(-$e['proc'], 1)); elseif ($e['proc'] < 100.0): echo Lang::spell('procChance').Lang::main('colon').$e['proc'].'%'; endif; diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index ae6724b8..6a7b73eb 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -181,7 +181,7 @@ foreach ($this->effects as $i => $e): echo '
      '; if ($e['procData'][0] < 0): - echo sprintf(Lang::spell('ppm'), -$e['procData'][0]); + echo sprintf(Lang::spell('ppm'), Lang::nf(-$e['procData'][0], 1)); elseif ($e['procData'][0] < 100.0): echo Lang::spell('procChance').Lang::main('colon').$e['procData'][0].'%'; endif; From a5f13325c0856f535cf6a7596b247874acab3875 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 3 Aug 2015 22:30:11 +0200 Subject: [PATCH 0052/1249] Enchantments * forgot notFound message * added Enchantments Tab to Spell Detail Page --- includes/types/item.class.php | 3 ++- localization/locale_dede.php | 1 + localization/locale_enus.php | 1 + localization/locale_eses.php | 1 + localization/locale_frfr.php | 1 + localization/locale_ruru.php | 1 + pages/enchantment.php | 7 ++++--- pages/spell.php | 19 +++++++++++++++++++ 8 files changed, 30 insertions(+), 4 deletions(-) diff --git a/includes/types/item.class.php b/includes/types/item.class.php index 50d079c6..c52f2e43 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -569,7 +569,8 @@ class ItemList extends BaseType $x .= '

      ' + $WH.sprintf(_LANG.achievementcomplete, currentParams.who, currentParams.when.getMonth() + 1, currentParams.when.getDate(), currentParams.when.getFullYear()) + "

      "); html = html.replace(/class="q0"/g, 'class="r3"'); } + if ((currentType == TYPE_ACHIEVEMENT) && currentParams.cri) { + for (var i = 0; i < currentParams.cri.length; i++) { + html = html.replace(new RegExp("'.Util::localizedString($enchText, 'text').'
      '; + $x .= ''.Util::localizedString($enchText, 'name').'
      '; else { unset($enhance['e']); @@ -749,7 +749,7 @@ class ItemList extends BaseType $col = $pop ? 1 : 0; $hasMatch &= $pop ? (($gems[$pop]['colorMask'] & (1 << $colorId)) ? 1 : 0) : 0; $icon = $pop ? sprintf(Util::$bgImagePath['tiny'], STATIC_URL, strtolower($gems[$pop]['iconString'])) : null; - $text = $pop ? Util::localizedString($gems[$pop], 'text') : Lang::item('socket', $colorId); + $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', $colorId); if ($interactive) $x .= ''.$text.'
      '; @@ -763,7 +763,7 @@ class ItemList extends BaseType $pop = array_pop($enhance['g']); $col = $pop ? 1 : 0; $icon = $pop ? sprintf(Util::$bgImagePath['tiny'], STATIC_URL, strtolower($gems[$pop]['iconString'])) : null; - $text = $pop ? Util::localizedString($gems[$pop], 'text') : Lang::item('socket', -1); + $text = $pop ? Util::localizedString($gems[$pop], 'name') : Lang::item('socket', -1); if ($interactive) $x .= ''.$text.'
      '; @@ -773,10 +773,10 @@ class ItemList extends BaseType else // prismatic socket placeholder $x .= ''; - if ($this->curTpl['socketBonus']) + if ($_ = $this->curTpl['socketBonus']) { - $sbonus = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $this->curTpl['socketBonus']); - $x .= ''.Lang::item('socketBonus').Lang::main('colon').Util::localizedString($sbonus, 'text').'
      '; + $sbonus = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $_); + $x .= ''.Lang::item('socketBonus').Lang::main('colon').''.Util::localizedString($sbonus, 'name').'
      '; } // durability @@ -1182,17 +1182,20 @@ class ItemList extends BaseType if ($enchantments) { - $parsed = Util::parseItemEnchantment(array_keys($enchantments)); + $eStats = DB::Aowow()->select('SELECT *, typeId AS ARRAY_KEY FROM ?_item_stats WHERE `type` = ?d AND typeId IN (?a)', TYPE_ENCHANTMENT, array_keys($enchantments)); // and merge enchantments back - foreach ($parsed as $eId => $stats) + foreach ($enchantments as $eId => $items) { - foreach ($enchantments[$eId] as $item) + if (empty($eStats[$eId])) + continue; + + foreach ($items as $item) { if ($item > 0) // apply socketBonus - $this->json[$item]['socketbonusstat'] = $stats; + $this->json[$item]['socketbonusstat'] = array_filter($eStats[$eId]); else /* if ($item < 0) */ // apply gemEnchantment - Util::arraySumByKey($this->json[-$item][$mod], $stats); + Util::arraySumByKey($this->json[-$item][$mod], array_filter($eStats[$eId])); } } } @@ -1437,11 +1440,12 @@ class ItemList extends BaseType $enchIds[] = $enchId; } - foreach (Util::parseItemEnchantment($enchIds, false, $misc) as $eId => $stats) + $enchants = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); + foreach ($enchants->iterate() as $eId => $_) { $this->rndEnchIds[$eId] = array( - 'text' => $misc[$eId]['name'], - 'stats' => $stats + 'text' => $enchants->getField('name', true), + 'stats' => $enchants->getStatGain() ); } @@ -1456,19 +1460,19 @@ class ItemList extends BaseType $qty = intVal($data['allocationPct'.$i] * $this->generateEnchSuffixFactor()); $stats = array_fill_keys(array_keys($this->rndEnchIds[$enchId]['stats']), $qty); - $jsonText[] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']); + $jsonText[$enchId] = str_replace('$i', $qty, $this->rndEnchIds[$enchId]['text']); Util::arraySumByKey($jsonEquip, $stats); } else // RandomProperty: static Enchantment; enchId > 0 { - $jsonText[] = $this->rndEnchIds[$enchId]['text']; + $jsonText[$enchId] = $this->rndEnchIds[$enchId]['text']; Util::arraySumByKey($jsonEquip, $this->rndEnchIds[$enchId]['stats']); } } $this->subItems[$mstItem][$subId] = array( 'name' => Util::localizedString($data, 'name'), - 'enchantment' => implode(', ', $jsonText), + 'enchantment' => $jsonText, 'jsonequip' => $jsonEquip, 'chance' => $data['chance'] // hmm, only needed for item detail page... ); diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php index 665b2c45..a172c597 100644 --- a/includes/types/spell.class.php +++ b/includes/types/spell.class.php @@ -156,8 +156,16 @@ class SpellList extends BaseType // Enchant Item Permanent (53) / Temporary (54) if (in_array($this->curTpl['effect'.$i.'Id'], [53, 54])) { - if ($mv && ($_ = Util::parseItemEnchantment($mv, true))) - Util::arraySumByKey($stats, $_[$mv]); + if ($mv && ($json = DB::Aowow()->selectRow('SELECT * FROM ?_item_stats WHERE `type` = ?d AND `typeId` = ?d', TYPE_ENCHANTMENT, $mv))) + { + $mods = []; + foreach ($json as $str => $val) + if ($val && ($idx = array_search($str, Util::$itemMods))) + $mods[$idx] = $val; + + if ($mods) + Util::arraySumByKey($stats, $mods); + } continue; } @@ -165,7 +173,6 @@ class SpellList extends BaseType switch ($au) { case 29: // ModStat MiscVal:type - { if ($mv < 0) // all stats { for ($j = 0; $j < 5; $j++) @@ -175,16 +182,12 @@ class SpellList extends BaseType Util::arraySumByKey($stats, [(ITEM_MOD_AGILITY + $mv) => $pts]); break; - } case 34: // Increase Health case 230: case 250: - { Util::arraySumByKey($stats, [ITEM_MOD_HEALTH => $pts]); break; - } case 13: // damage splpwr + physical (dmg & any) - { // + weapon damage if ($mv == (1 << SPELL_SCHOOL_NORMAL)) { @@ -226,14 +229,10 @@ class SpellList extends BaseType } break; - } case 135: // healing splpwr (healing & any) .. not as a mask.. - { Util::arraySumByKey($stats, [ITEM_MOD_SPELL_HEALING_DONE => $pts]); break; - } case 35: // ModPower - MiscVal:type see defined Powers only energy/mana in use - { if ($mv == POWER_HEALTH) Util::arraySumByKey($stats, [ITEM_MOD_HEALTH => $pts]); if ($mv == POWER_ENERGY) @@ -244,7 +243,6 @@ class SpellList extends BaseType Util::arraySumByKey($stats, [ITEM_MOD_RUNIC_POWER => $pts]); break; - } case 189: // CombatRating MiscVal:ratingMask case 220: if ($mod = Util::itemModByRatingMask($mv)) @@ -310,6 +308,10 @@ class SpellList extends BaseType case 240: // ModExpertise Util::arraySumByKey($stats, [ITEM_MOD_EXPERTISE_RATING => $pts]); break; + case 123: // Mod Target Resistance + if ($mv == 0x7C && $pts < 0) + Util::arraySumByKey($stats, [ITEM_MOD_SPELL_PENETRATION => -$pts]); + break; } } @@ -1411,7 +1413,7 @@ class SpellList extends BaseType if (isset($var[1]) && $var[0] != $var[1] && !isset($var[4])) { - $_ = is_numeric($var[0]) ? abs($var[0]) : $var[0]; + $_ = is_numeric($var[1]) ? abs($var[1]) : $var[1]; $resolved .= Lang::game('valueDelim'); $resolved .= isset($var[3]) ? sprintf($var[3], $_) : $_; } diff --git a/includes/utilities.php b/includes/utilities.php index cca4908d..79aaee37 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -40,15 +40,16 @@ class Util null, 'CreatureList', 'GameObjectList', 'ItemList', 'ItemsetList', 'QuestList', 'SpellList', 'ZoneList', 'FactionList', 'PetList', 'AchievementList', 'TitleList', 'WorldEventList', 'CharClassList', 'CharRaceList', 'SkillList', null, 'CurrencyList', - TYPE_EMOTE => 'EmoteList' + TYPE_EMOTE => 'EmoteList', + TYPE_ENCHANTMENT => 'EnchantmentList' ); public static $typeStrings = array( // zero-indexed null, 'npc', 'object', 'item', 'itemset', 'quest', 'spell', 'zone', 'faction', 'pet', 'achievement', 'title', 'event', 'class', 'race', 'skill', null, 'currency', - TYPE_USER => 'user', - TYPE_EMOTE => 'emote' - + TYPE_USER => 'user', + TYPE_EMOTE => 'emote', + TYPE_ENCHANTMENT => 'enchantment' ); public static $combatRatingToItemMod = array( // zero-indexed idx:CR; val:Mod @@ -1096,131 +1097,6 @@ class Util } } - // EnchantmentTypes - // 0 => TYPE_NONE dnd stuff; (ignore) - // 1 => TYPE_COMBAT_SPELL proc spell from ObjectX (amountX == procChance?; ignore) - // 2 => TYPE_DAMAGE +AmountX damage - // 3 => TYPE_EQUIP_SPELL Spells from ObjectX (amountX == procChance?) - // 4 => TYPE_RESISTANCE +AmountX resistance for ObjectX School - // 5 => TYPE_STAT +AmountX for Statistic by type of ObjectX - // 6 => TYPE_TOTEM Rockbiter AmountX as Damage (ignore) - // 7 => TYPE_USE_SPELL Engineering gadgets - // 8 => TYPE_PRISMATIC_SOCKET Extra Sockets AmountX as socketCount (ignore) - public static function parseItemEnchantment($ench, $raw = false, &$misc = null) - { - if (!$ench) - return []; - - if (is_numeric($ench)) - $ench = [$ench]; - - if (!is_array($ench)) - return []; - - $enchants = DB::Aowow()->select('SELECT *, Id AS ARRAY_KEY FROM ?_itemenchantment WHERE id IN (?a)', $ench); - if (!$enchants) - return []; - - $result = []; - foreach ($enchants as $eId => $e) - { - $misc[$eId] = array( - 'name' => self::localizedString($e, 'text'), - 'text' => array( - 'text_loc0' => $e['text_loc0'], - 'text_loc2' => $e['text_loc2'], - 'text_loc3' => $e['text_loc3'], - 'text_loc6' => $e['text_loc6'], - 'text_loc8' => $e['text_loc8'] - ) - ); - - if ($e['skillLine'] > 0) - $misc[$eId]['reqskill'] = $e['skillLine']; - - if ($e['skillLevel'] > 0) - $misc[$eId]['reqskillrank'] = $e['skillLevel']; - - if ($e['requiredLevel'] > 0) - $misc[$eId]['reqlevel'] = $e['requiredLevel']; - - // parse stats - $jsonStats = []; - for ($h = 1; $h <= 3; $h++) - { - $obj = (int)$e['object'.$h]; - $val = (int)$e['amount'.$h]; - - switch ($e['type'.$h]) - { - case 2: - $obj = ITEM_MOD_WEAPON_DMG; - break; - case 3: - case 7: - $spl = new SpellList(array(['s.id', $obj])); - if (!$spl->error) - Util::arraySumByKey($jsonStats, $spl->getStatGain()[$obj]); - - $obj = null; - break; - case 4: - switch ($obj) - { - case 0: // Physical - $obj = ITEM_MOD_ARMOR; - break; - case 1: // Holy - $obj = ITEM_MOD_HOLY_RESISTANCE; - break; - case 2: // Fire - $obj = ITEM_MOD_FIRE_RESISTANCE; - break; - case 3: // Nature - $obj = ITEM_MOD_NATURE_RESISTANCE; - break; - case 4: // Frost - $obj = ITEM_MOD_FROST_RESISTANCE; - break; - case 5: // Shadow - $obj = ITEM_MOD_SHADOW_RESISTANCE; - break; - case 6: // Arcane - $obj = ITEM_MOD_ARCANE_RESISTANCE; - break; - default: - $obj = null; - } - break; - case 5: - break; - default: // skip assignment below - $obj = null; - } - - if ($obj) - { - if (!isset($jsonStats[$obj])) - $jsonStats[$obj] = 0; - - $jsonStats[$obj] += $val; - } - } - - if ($raw) - $result[$eId] = $jsonStats; - else - { - $result[$eId] = []; - foreach ($jsonStats as $k => $v) // check if we use these mods - if ($str = Util::$itemMods[$k]) - $result[$eId][$str] = $v; - } - } - - return $result; - } - // default ucFirst doesn't convert UTF-8 chars public static function ucFirst($str) { diff --git a/index.php b/index.php index 725b8d3c..b42e35bf 100644 --- a/index.php +++ b/index.php @@ -31,6 +31,8 @@ switch ($pageCall) case 'compare': // tool: item comparison case 'emote': case 'emotes': + case 'enchantment': + case 'enchantments': case 'event': case 'events': case 'faction': diff --git a/localization/lang.class.php b/localization/lang.class.php index beafdb37..a5f5254a 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -30,6 +30,7 @@ class Lang private static $zone; private static $emote; + private static $enchantment; public static function load($loc) { diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 1fc14ec3..12dbde58 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -175,6 +175,8 @@ $lang = array( 'duration' => "Dauer", 'emote' => "Emote", 'emotes' => "Emotes", + 'enchantment' => "Verzauberung", + 'enchantments' => "Verzauberungen", 'object' => "Objekt", 'objects' => "Objekte", 'glyphType' => "Glyphenart", @@ -389,6 +391,14 @@ $lang = array( 'aliases' => "Aliasse", 'noText' => "Dieses Emote besitzt keinen Text.", ), + 'enchantment' => array( + 'details' => "Details", + 'activation' => "Aktivierung", + 'types' => array( + 1 => "Zauber (Auslösung)", 3 => "Zauber (Anlegen)", 7 => "Zauber (Benutzen)", 8 => "Prismatischer Sockel", + 5 => "Statistik", 2 => "Waffenschaden", 6 => "DPS", 4 => "Verteidigung" + ) + ), 'gameObject' => array( 'notFound' => "Dieses Objekt existiert nicht.", 'cat' => [0 => "Anderes", 9 => "Bücher", 3 => "Behälter", -5 => "Truhen", 25 => "Fischschwärme", -3 => "Kräuter", -4 => "Erzadern", -2 => "Quest", -6 => "Werkzeuge"], diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 30aa4e3e..f52e83ff 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -168,8 +168,10 @@ $lang = array( 'difficulty' => "Difficulty", 'dispelType' => "Dispel type", 'duration' => "Duration", - 'emote' => "Emote", + 'emote' => "emote", 'emotes' => "Emotes", + 'enchantment' => "enchantment", + 'enchantments' => "Enchantments", 'object' => "object", 'objects' => "Objects", 'glyphType' => "Glyph type", @@ -384,6 +386,14 @@ $lang = array( 'aliases' => "Aliases", 'noText' => "This Emote has no text.", ), + 'enchantment' => array( + 'details' => "Details", + 'activation' => "Activation", + 'types' => array( + 1 => "Proc Spell", 3 => "Equip Spell", 7 => "Use Spell", 8 => "Prismatic Socket", + 5 => "Statistics", 2 => "Weapon Damage", 6 => "DPS", 4 => "Defense" + ) + ), 'gameObject' => array( 'notFound' => "This object doesn't exist.", 'cat' => [0 => "Other", 9 => "Books", 3 => "Containers", -5 => "Chests", 25 => "Fishing Pools", -3 => "Herbs", -4 => "Mineral Veins", -2 => "Quest", -6 => "Tools"], diff --git a/localization/locale_eses.php b/localization/locale_eses.php index a54e5f11..cc4ce2b3 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -173,8 +173,10 @@ $lang = array( 'difficulty' => "Dificultad", 'dispelType' => "Tipo de disipación", 'duration' => "Duración", - 'emote' => "Emoción", + 'emote' => "emoción", 'emotes' => "Emociones", + 'enchantment' => "encantamiento", + 'enchantments' => "Encantamientos", 'object' => "entidad", 'objects' => "Entidades", 'glyphType' => "Tipo de glifo", @@ -390,6 +392,14 @@ $lang = array( 'aliases' => "[Aliases]", 'noText' => "[This Emote has no text.]", ), + 'enchantment' => array( + 'details' => "Detalles", + 'activation' => "Activación", + 'types' => array( + 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "Ranura prismática", + 5 => "Atributos", 2 => "Daño de arma", 6 => "DPS", 4 => "Defensa" + ) + ), 'gameObject' => array( 'notFound' => "Este entidad no existe.", 'cat' => [0 => "Otros", 9 => "Libros", 3 => "Contenedores", -5 => "Cofres", 25 => "Bancos de peces", -3 => "Hierbas", -4 => "Venas de minerales", -2 => "Misiones", -6 => "Herramientas"], diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index b9594603..406685f8 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -173,8 +173,10 @@ $lang = array( 'difficulty' => "Difficulté", 'dispelType' => "Type de dissipation", 'duration' => "Durée", - 'emote' => "Emote", + 'emote' => "emote", 'emotes' => "Emotes", + 'enchantment' => "enchantement", + 'enchantments' => "Enchantements", 'object' => "entité", 'objects' => "Entités", 'glyphType' => "Type de glyphe", @@ -389,6 +391,14 @@ $lang = array( 'aliases' => "[Aliases]", 'noText' => "[This Emote has no text.]", ), + 'enchantment' => array( + 'details' => "En détail", + 'activation' => "Activation", + 'types' => array( + 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "Châsse prismatique", + 5 => "Statistiques", 2 => "Dégâts d'arme", 6 => "DPS", 4 => "Défense" + ) + ), 'gameObject' => array( 'notFound' => "Cette entité n'existe pas.", 'cat' => [0 => "Autre", 9 => "Livres", 3 => "Conteneurs", -5 => "Coffres", 25 => "Bancs de poissons", -3 => "Herbes", -4 => "Filons de minerai", -2 => "Quêtes", -6 => "Outils"], diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 7dab6a89..5441bbd8 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -175,6 +175,8 @@ $lang = array( 'duration' => "ДлительноÑть", 'emote' => "ЭмоциÑ", 'emotes' => "Эмоции", + 'enchantment' => "улучшение", + 'enchantments' => "УлучшениÑ", 'object' => "объект", 'objects' => "Объекты", 'glyphType' => "Тип Ñимвола", @@ -389,6 +391,14 @@ $lang = array( 'aliases' => "[Aliases]", 'noText' => "[This Emote has no text.]", ), + 'enchantment' => array( + 'details' => "ПодробноÑти", + 'activation' => "Ðктивации", + 'types' => array( + 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "БеÑцветное гнездо", + 5 => "ХарактериÑтики", 2 => "Урон оружиÑ", 6 => "УВС", 4 => "Защита" + ) + ), 'gameObject' => array( 'notFound' => "Такой объект не ÑущеÑтвует.", 'cat' => [0 => "Другое", 9 => "Книги", 3 => "Контейнеры", -5 => "Сундуки", 25 => "Рыболовные лунки",-3 => "Травы", -4 => "Полезные иÑкопаемые", -2 => "ЗаданиÑ", -6 => "ИнÑтрументы"], diff --git a/pages/emote.php b/pages/emote.php index f66889b9..9d78a2f0 100644 --- a/pages/emote.php +++ b/pages/emote.php @@ -10,7 +10,7 @@ class EmotePage extends GenericPage { use DetailPage; - protected $type = TYPE_PET; + protected $type = TYPE_EMOTE; protected $typeId = 0; protected $tpl = 'detail-page-generic'; protected $path = [0, 100]; @@ -80,7 +80,7 @@ class EmotePage extends GenericPage $text .= '[pad][b]'.$h.'[/b][ul][li][span class=s4]'.preg_replace('/%\d?\$?s/', '<'.Util::ucFirst(Lang::main('name')).'>', $t).'[/span][/li][/ul]'; $this->extraText = $text; - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; + $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; /**************/ /* Extra Tabs */ diff --git a/pages/enchantment.php b/pages/enchantment.php new file mode 100644 index 00000000..2e374ffb --- /dev/null +++ b/pages/enchantment.php @@ -0,0 +1,311 @@ +typeId = intVal($id); + + $this->subject = new EnchantmentList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->notFound(Util::ucFirst(Lang::game('enchantment')), Lang::enchantment('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->name = Util::ucFirst($this->subject->getField('name', true)); + } + + private function getDistinctType() + { + $type = 0; + for ($i = 1; $i < 4; $i++) + { + if ($_ = $this->subject->getField('type'.$i)) + { + if ($type) // already set + return 0; + else + $type = $_; + } + } + + return $type; + } + + protected function generateContent() + { + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // reqLevel + if ($_ = $this->subject->getField('requiredLevel')) + $infobox[] = sprintf(Lang::game('reqLevel'), $_); + + // reqskill + if ($_ = $this->subject->getField('skillLine')) + { + $this->extendGlobalIds(TYPE_SKILL, $_); + + $foo = sprintf(Lang::game('requires'), ' [skill='.$_.']'); + if ($_ = $this->subject->getField('skillLevel')) + $foo .= ' ('.$_.')'; + + $infobox[] = $foo; + } + + + /****************/ + /* Main Content */ + /****************/ + + + $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; + $this->effects = []; + // 3 effects + for ($i = 1; $i < 4; $i++) + { + $_ty = $this->subject->getField('type'.$i); + $_qty = $this->subject->getField('amount'.$i); + $_obj = $this->subject->getField('object'.$i); + + switch ($_ty) + { + case 1: + case 3: + case 7: + $sArr = $this->subject->getField('spells')[$i]; + $spl = $this->subject->getRelSpell($sArr[0]); + $this->effects[$i]['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Type: '.$_ty, Lang::item('trigger', $sArr[1])) : Lang::item('trigger', $sArr[1]); + $this->effects[$i]['proc'] = $sArr[3]; + $this->effects[$i]['icon'] = array( + 'name' => !$spl ? Util::ucFirst(Lang::game('spell')).' #'.$sArr[0] : Util::localizedString($spl, 'name'), + 'id' => $sArr[0], + 'count' => $sArr[2] + ); + break; + case 5: + if ($_obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] .. + $_obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere + + $this->effects[$i]['tip'] = [$_obj, Util::$itemMods[$_obj]]; + // DO NOT BREAK! + case 2: + case 6: + case 8: + case 4: + $this->effects[$i]['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Type: '.$_ty, Lang::enchantment('types', $_ty)) : Lang::enchantment('types', $_ty); + $this->effects[$i]['value'] = $_qty; + if ($_ty == 4) + $this->effects[$i]['name'] .= Lang::main('colon').'('.(User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Object: '.$_obj, Lang::getMagicSchools(1 << $_obj)) : Lang::getMagicSchools(1 << $_obj)).')'; + } + } + + // activation conditions + if ($_ = $this->subject->getField('conditionId')) + { + $x = ''; + + if ($gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE id = ?d', $_)) + { + for ($i = 1; $i < 6; $i++) + { + if (!$gemCnd['color'.$i]) + continue; + + $fiColors = function ($idx) + { + $foo = ''; + switch ($idx) + { + case 2: $foo = '0:3:5'; break; // red + case 3: $foo = '2:4:5'; break; // yellow + case 4: $foo = '1:3:4'; break; // blue + } + + return $foo; + }; + + $bLink = $gemCnd['color'.$i] ? ''.Lang::item('gemColors', $gemCnd['color'.$i] - 1).'' : ''; + $cLink = $gemCnd['cmpColor'.$i] ? ''.Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1).'' : ''; + + switch ($gemCnd['comparator'.$i]) + { + case 2: // requires less than ( || ) gems + case 5: // requires at least than ( || ) gems + $sp = (int)$gemCnd['value'.$i] > 1; + $x .= ''.Lang::achievement('reqNumCrt').' '.sprintf(Lang::item('gemConditions', $gemCnd['comparator'.$i], $sp), $gemCnd['value'.$i], $bLink).'
      '; + break; + case 3: // requires more than ( || ) gems + $link = ''.Lang::item('gemColors', $gemCnd['cmpColor'.$i] - 1).''; + $x .= ''.Lang::achievement('reqNumCrt').' '.sprintf(Lang::item('gemConditions', 3), $bLink, $cLink).'
      '; + break; + } + } + } + + $this->activateCondition = $x; + } + + /**************/ + /* Extra Tabs */ + /**************/ + + // used by gem + $gemList = new ItemList(array(['gemEnchantmentId', $this->typeId])); + if (!$gemList->error) + { + $this->lvTabs[] = array( + 'file' => 'item', + 'data' => $gemList->getListviewData(), + 'params' => array( + 'name' => '$LANG.tab_usedby + \' \' + LANG.gems', + 'id' => 'used-by-gems', + ) + ); + + $this->extendGlobalData($gemList->getJsGlobals()); + } + + // used by spell + // used by useItem + $cnd = array( + 'OR', + ['AND', ['effect1Id', [53, 54, 156, 92]], ['effect1MiscValue', $this->typeId]], + ['AND', ['effect2Id', [53, 54, 156, 92]], ['effect2MiscValue', $this->typeId]], + ['AND', ['effect3Id', [53, 54, 156, 92]], ['effect3MiscValue', $this->typeId]], + ); + $spellList = new SpellList($cnd); + if (!$spellList->error) + { + $spellData = $spellList->getListviewData(); + $this->extendGlobalData($spellList->getJsGlobals()); + + $spellIds = $spellList->getFoundIDs(); + $conditions = array( + 'OR', // [use, useUndelayed] + ['AND', ['spellTrigger1', [0, 5]], ['spellId1', $spellIds]], + ['AND', ['spellTrigger2', [0, 5]], ['spellId2', $spellIds]], + ['AND', ['spellTrigger3', [0, 5]], ['spellId3', $spellIds]], + ['AND', ['spellTrigger4', [0, 5]], ['spellId4', $spellIds]], + ['AND', ['spellTrigger5', [0, 5]], ['spellId5', $spellIds]] + ); + + $ubItems = new ItemList($conditions); + if (!$ubItems->error) + { + $this->lvTabs[] = array( + 'file' => 'item', + 'data' => $ubItems->getListviewData(), + 'params' => [] + ); + + $this->extendGlobalData($ubItems->getJSGlobals(GLOBALINFO_SELF)); + } + + // remove found spells if they are used by an item + if (!$ubItems->error) + { + foreach ($spellList->iterate() as $sId => $__) + { + // if Perm. Enchantment has a createItem its a Scroll of Enchantment (display both) + for ($i = 1; $i < 4; $i++) + if ($spellList->getField('effect'.$i.'Id') == 53 && $spellList->getField('effect'.$i.'CreateItemId')) + continue 2; + + foreach ($ubItems->iterate() as $__) + { + for ($i = 1; $i < 6; $i++) + { + if ($ubItems->getField('spellId'.$i) == $sId) + { + unset($spellData[$sId]); + break 2; + } + } + } + } + } + + $this->lvTabs[] = array( + 'file' => 'spell', + 'data' => $spellData, + 'params' => [] + ); + } + + // used by randomAttrItem + $ire = DB::Aowow()->select( + 'SELECT *, ABS(id) AS ARRAY_KEY FROM ?_itemrandomenchant WHERE enchantId1 = ?d OR enchantId2 = ?d OR enchantId3 = ?d OR enchantId4 = ?d OR enchantId5 = ?d', + $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId + ); + if ($ire) + { + if ($iet = DB::World()->select('SELECT entry AS ARRAY_KEY, ench, chance FROM item_enchantment_template WHERE ench IN (?a)', array_keys($ire))) + { + $randIds = []; // transform back to signed format + foreach ($iet as $tplId => $data) + $randIds[$ire[$data['ench']]['id'] > 0 ? $tplId : -$tplId] = $ire[$data['ench']]['id']; + + $randItems = new ItemList(array(CFG_SQL_LIMIT_NONE, ['randomEnchant', array_keys($randIds)])); + if (!$randItems->error) + { + $data = $randItems->getListviewData(); + foreach ($randItems->iterate() as $iId => $__) + { + $re = $randItems->getField('randomEnchant'); + + $data[$iId]['percent'] = $iet[abs($re)]['chance']; + $data[$iId]['count'] = 1; // expected by js or the pct-col becomes unsortable + $data[$iId]['rel'] = 'rand='.$ire[$iet[abs($re)]['ench']]['id']; + $data[$iId]['name'] .= ' '.Util::localizedString($ire[$iet[abs($re)]['ench']], 'name'); + } + + $this->lvTabs[] = array( + 'file' => 'item', + 'data' => $data, + 'params' => array( + 'id' => 'used-by-rand', + 'name' => Lang::item('_rndEnchants'), + 'extraCols' => '$[Listview.extraCols.percent]' + ) + ); + + $this->extendGlobalData($randItems->getJSGlobals(GLOBALINFO_SELF)); + } + } + } + } + + protected function generateTitle() + { + array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('enchantment'))); + } + + protected function generatePath() + { + if ($_ = $this->getDistinctType()) + $this->path[] = $_; + } +} + +?> diff --git a/pages/enchantments.php b/pages/enchantments.php new file mode 100644 index 00000000..961c5cc3 --- /dev/null +++ b/pages/enchantments.php @@ -0,0 +1,106 @@ +filterObj = new EnchantmentListFilter(); + $this->getCategoryFromUrl($pageParam);; + + parent::__construct($pageCall, $pageParam); + + $this->name = Util::ucFirst(Lang::game('enchantments')); + $this->subCat = $pageParam !== null ? '='.$pageParam : ''; + } + + protected function generateContent() + { + $tab = array( + 'file' => 'enchantment', + 'data' => [], + 'params' => [] + ); + + $conditions = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filterObj->getConditions()) + $conditions[] = $_; + + $ench = new EnchantmentList($conditions); + + $tab['data'] = $ench->getListviewData(); + $this->extendGlobalData($ench->getJSGlobals()); + + // recreate form selection + $this->filter = array_merge($this->filterObj->getForm('form'), $this->filter); + $this->filter['query'] = isset($_GET['filter']) ? $_GET['filter'] : NULL; + $this->filter['fi'] = $this->filterObj->getForm(); + + $xCols = $this->filterObj->getForm('extraCols', true); + foreach (Util::$itemFilter as $fiId => $str) + if (array_column($tab['data'], $str)) + $xCols[] = $fiId; + + if (array_column($tab['data'], 'dmg')) + $xCols[] = 34; + + if ($xCols) + $this->filter['fi']['extraCols'] = "fi_extraCols = ".Util::toJSON(array_values(array_unique($xCols))).";"; + + if (!empty($this->filter['fi']['extraCols'])) + $tab['params']['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + if ($ench->getMatches() > CFG_SQL_LIMIT_DEFAULT) + { + $tab['params']['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_enchantmentsfound', $ench->getMatches(), CFG_SQL_LIMIT_DEFAULT); + $tab['params']['_truncated'] = 1; + } + + if (array_filter(array_column($tab['data'], 'spells'))) + $tab['params']['visibleCols'] = '$[\'trigger\']'; + + if (!$ench->hasSetFields(['skillLine'])) + $tab['params']['hiddenCols'] = '$[\'skill\']'; + + if ($this->filterObj->error) + $tab['params']['_errors'] = '$1'; + + $this->lvTabs[] = $tab; + } + + protected function generateTitle() + { + $form = $this->filterObj->getForm('form'); + if (!empty($form['ty']) && intVal($form['ty']) && $form['ty'] > 0 && $form['ty'] < 9) + array_unshift($this->title, Lang::enchantment('types', $form['ty'])); + + array_unshift($this->title, $this->name); + } + + protected function generatePath() + { + $form = $this->filterObj->getForm('form'); + if (isset($form['ty']) && !is_array($form['ty'])) + $this->path[] = $form['ty']; + } +} + +?> diff --git a/pages/genericPage.class.php b/pages/genericPage.class.php index add97605..8a599e1c 100644 --- a/pages/genericPage.class.php +++ b/pages/genericPage.class.php @@ -620,6 +620,8 @@ class GenericPage case TYPE_CURRENCY: $jsg[TYPE_CURRENCY] = ['g_gatheredcurrencies', [], []]; break; // well, this is awkward case TYPE_USER: $jsg[TYPE_USER] = ['g_users', [], []]; break; + case TYPE_EMOTE: $jsg[TYPE_EMOTE] = ['g_emotes', [], []]; break; + case TYPE_ENCHANTMENT: $jsg[TYPE_ENCHANTMENT] = ['g_enchantments', [], []]; break; } } @@ -658,6 +660,8 @@ class GenericPage case TYPE_CURRENCY: $obj = new CurrencyList($cnd); break; // "um, eh":, he ums and ehs. case TYPE_USER: $obj = new UserList($cnd); break; + case TYPE_EMOTE: $obj = new EmoteList($cnd); break; + case TYPE_ENCHANTMENT: $obj = new EnchantmentList($cnd); break; default: continue; } diff --git a/pages/search.php b/pages/search.php index 8f3aa2f7..d1ca8de8 100644 --- a/pages/search.php +++ b/pages/search.php @@ -52,7 +52,7 @@ class SearchPage extends GenericPage ['_searchProficiency'], ['_searchProfession'], ['_searchCompanion'], ['_searchMount'], ['_searchCreature'], ['_searchQuest'], ['_searchAchievement'], ['_searchStatistic'], ['_searchZone'], ['_searchObject'], ['_searchFaction'], ['_searchSkill'], ['_searchPet'], ['_searchCreatureAbility'], ['_searchSpell'], - ['_searchEmote'] + ['_searchEmote'], ['_searchEnchantment'] ); public function __construct($pageCall, $pageParam) @@ -1431,9 +1431,44 @@ class SearchPage extends GenericPage return $result; } - // private function _searchCharacter($cndBase) { } // 26 Characters $searchMask & 0x4000000 - // private function _searchGuild($cndBase) { } // 27 Guilds $searchMask & 0x8000000 - // private function _searchArenaTeam($cndBase) { } // 28 Arena Teams $searchMask & 0x10000000 + private function _searchEnchantment($cndBase) // 26 Enchantments $searchMask & 0x4000000 + { + $result = []; + $cnd = array_merge($cndBase, [$this->createLookup(['name_loc'.User::$localeId])]); + $enchantment = new EnchantmentList($cnd); + + if ($data = $enchantment->getListviewData()) + { + $this->extendGlobalData($enchantment->getJSGlobals()); + + $result = array( + 'type' => TYPE_ENCHANTMENT, + 'appendix' => ' (Enchantment)', + 'matches' => $enchantment->getMatches(), + 'file' => EnchantmentList::$brickFile, + 'data' => $data, + 'params' => [] + ); + + if (array_filter(array_column($result['data'], 'spells'))) + $result['params']['visibleCols'] = '$[\'trigger\']'; + + if (!$enchantment->hasSetFields(['skillLine'])) + $result['params']['hiddenCols'] = '$[\'skill\']'; + + if ($enchantment->getMatches() > $this->maxResults) + { + $result['params']['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_enchantmentsfound', $enchantment->getMatches(), $this->maxResults); + $result['params']['_truncated'] = 1; + } + } + + return $result; + } + + // private function _searchCharacter($cndBase) { } // 27 Characters $searchMask & 0x8000000 + // private function _searchGuild($cndBase) { } // 28 Guilds $searchMask & 0x10000000 + // private function _searchArenaTeam($cndBase) { } // 29 Arena Teams $searchMask & 0x20000000 } ?> diff --git a/pages/spell.php b/pages/spell.php index 3c4fe5cc..ff2098e7 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -180,7 +180,7 @@ class SpellPage extends GenericPage { $this->extendGlobalData($rSkill->getJSGlobals()); - $bar = sprintf(Lang::game('requires'), '[skill='.$rSkill->id.']'); + $bar = sprintf(Lang::game('requires'), ' [skill='.$rSkill->id.']'); if ($_ = $this->subject->getField('learnedAt')) $bar .= ' ('.$_.')'; @@ -1585,7 +1585,7 @@ class SpellPage extends GenericPage } // Effect Name - $foo['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'EffectId: '.$effId, Util::$spellEffectStrings[$effId]) : Util::$spellEffectStrings[$effId]; + $foo['name'] = (User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'EffectId: '.$effId, Util::$spellEffectStrings[$effId]) : Util::$spellEffectStrings[$effId]).Lang::main('colon'); if ($this->subject->getField('effect'.$i.'RadiusMax') > 0) $foo['radius'] = $this->subject->getField('effect'.$i.'RadiusMax'); @@ -1631,9 +1631,9 @@ class SpellPage extends GenericPage break; case 16: // QuestComplete if ($_ = QuestList::getName($effMV)) - $foo['name'] .= Lang::main('colon').'('.$_.')'; + $foo['name'] .= '('.$_.')'; else - $foo['name'] .= Lang::main('colon').Util::ucFirst(Lang::game('quest')).' #'.$effMV;; + $foo['name'] .= Util::ucFirst(Lang::game('quest')).' #'.$effMV;; break; case 28: // Summon case 90: // Kill Credit @@ -1645,7 +1645,7 @@ class SpellPage extends GenericPage $redButtons[BUTTON_VIEW3D] = ['type' => TYPE_NPC, 'displayId' => $summon['displayId']]; } - $foo['name'] .= Lang::main('colon').$_; + $foo['name'] .= $_; break; case 33: // Open Lock $_ = Lang::spell('lockType', $effMV); @@ -1658,9 +1658,10 @@ class SpellPage extends GenericPage break; case 53: // Enchant Item Perm case 54: // Enchant Item Temp + case 92: // Enchant Held Item case 156: // Enchant Item Prismatic if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE id = ?d', $effMV)) - $foo['name'] .= ' '.Util::localizedString($_, 'text').' ('.$effMV.')'; + $foo['name'] .= ' ('.Util::localizedString($_, 'name').')'; else $foo['name'] .= ' #'.$effMV; break; @@ -1697,15 +1698,15 @@ class SpellPage extends GenericPage $redButtons[BUTTON_VIEW3D] = ['type' => TYPE_OBJECT, 'displayId' => $summon['displayId']]; } - $foo['name'] .= Lang::main('colon').$_; + $foo['name'] .= $_; break; case 74: // Apply Glyph if ($_ = DB::Aowow()->selectCell('SELECT spellId FROM ?_glyphproperties WHERE id = ?d', $effMV)) { if ($n = SpellList::getName($_)) - $foo['name'] .= Lang::main('colon').'('.$n.')'; + $foo['name'] .= '('.$n.')'; else - $foo['name'] .= Lang::main('colon').Util::ucFirst(Lang::game('spell')).' #'.$effMV; + $foo['name'] .= Util::ucFirst(Lang::game('spell')).' #'.$effMV; } else $foo['name'] .= ' #'.$effMV;; @@ -1737,9 +1738,9 @@ class SpellPage extends GenericPage break; case 118: // Require Skill if ($_ = SkillList::getName($effMV)) - $foo['name'] .= Lang::main('colon').'('.$_.')'; + $foo['name'] .= '('.$_.')'; else - $foo['name'] .= Lang::main('colon').Util::ucFirst(Lang::game('skill')).' #'.$effMV;; + $foo['name'] .= Util::ucFirst(Lang::game('skill')).' #'.$effMV;; break; case 146: // Activate Rune $_ = Lang::spell('powerRunes', $effMV); @@ -1768,7 +1769,7 @@ class SpellPage extends GenericPage { if ($effAura > 0 && isset(Util::$spellAuraStrings[$effAura])) { - $foo['name'] .= User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'AuraId: '.$effAura, Lang::main('colon').Util::$spellAuraStrings[$effAura]) : Lang::main('colon').Util::$spellAuraStrings[$effAura]; + $foo['name'] .= User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'AuraId: '.$effAura, Util::$spellAuraStrings[$effAura]) : Util::$spellAuraStrings[$effAura]; $bar = $effMV; switch ($effAura) @@ -1888,6 +1889,7 @@ class SpellPage extends GenericPage case 22: // Mod Resistance case 39: // School Immunity case 40: // Damage Immunity + case 50: // Mod Critical Healing Amount case 57: // Mod Spell Crit Chance case 69: // School Absorb case 71: // Mod Spell Crit Chance School @@ -1949,6 +1951,9 @@ class SpellPage extends GenericPage break; case 168: // Mod Damage Done Versus case 59: // Mod Damage Done Versus Creature + case 102: // Mod Melee Attack Power Versus + case 131: // Mod Ranged Attack Power Versus + case 180: // Mod Spell Damage Versus $_ = []; foreach (Lang::game('ct') as $k => $str) if ($effMV & (1 << $k - 1)) diff --git a/setup/db_structure.sql b/setup/db_structure.sql index baab5ded..cc74e85f 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -262,8 +262,8 @@ DROP TABLE IF EXISTS `aowow_articles`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_articles` ( - `type` tinyint(4) NOT NULL, - `typeId` int(11) NOT NULL, + `type` smallint(5) NOT NULL, + `typeId` mediumint(9) NOT NULL, `locale` tinyint(4) NOT NULL, `article` text COMMENT 'Markdown formated', `quickInfo` text COMMENT 'Markdown formated', @@ -330,8 +330,8 @@ DROP TABLE IF EXISTS `aowow_comments`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_comments` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Comment ID', - `type` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Type of Page', - `typeId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'ID Of Page', + `type` smallint(5) unsigned NOT NULL COMMENT 'Type of Page', + `typeId` mediumint(9) NOT NULL COMMENT 'ID Of Page', `userId` int(10) unsigned NOT NULL COMMENT 'User ID', `roles` smallint(5) unsigned NOT NULL, `body` text NOT NULL COMMENT 'Comment text', @@ -718,7 +718,8 @@ DROP TABLE IF EXISTS `aowow_item_stats`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_item_stats` ( - `id` mediumint(8) unsigned NOT NULL, + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(9) unsigned NOT NULL, `nsockets` tinyint(3) unsigned NOT NULL, `dmgmin1` smallint(5) unsigned NOT NULL, `dmgmax1` smallint(5) unsigned NOT NULL, @@ -798,8 +799,7 @@ CREATE TABLE `aowow_item_stats` ( `shasplpwr` smallint(6) NOT NULL, `natsplpwr` smallint(6) NOT NULL, `arcsplpwr` smallint(6) NOT NULL, - PRIMARY KEY (`id`), - KEY `item` (`id`) + PRIMARY KEY (`typeId`, `type`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -812,6 +812,10 @@ DROP TABLE IF EXISTS `aowow_itemenchantment`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_itemenchantment` ( `id` smallint(5) unsigned NOT NULL, + `charges` tinyint(4) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `procChance` tinyint(3) unsigned NOT NULL, + `ppmRate` float NOT NULL, `type1` tinyint(4) unsigned NOT NULL, `type2` tinyint(4) unsigned NOT NULL, `type3` tinyint(4) unsigned NOT NULL, @@ -821,12 +825,11 @@ CREATE TABLE `aowow_itemenchantment` ( `object1` mediumint(9) unsigned NOT NULL, `object2` mediumint(9) unsigned NOT NULL, `object3` smallint(6) unsigned NOT NULL, - `text_loc0` varchar(65) NOT NULL, - `text_loc2` varchar(91) NOT NULL, - `text_loc3` varchar(84) NOT NULL, - `text_loc6` varchar(89) NOT NULL, - `text_loc8` varchar(96) NOT NULL, - `gemReference` mediumint(8) unsigned NOT NULL, + `name_loc0` varchar(65) NOT NULL, + `name_loc2` varchar(91) NOT NULL, + `name_loc3` varchar(84) NOT NULL, + `name_loc6` varchar(89) NOT NULL, + `name_loc8` varchar(96) NOT NULL, `conditionId` tinyint(3) unsigned NOT NULL, `skillLine` smallint(5) unsigned NOT NULL, `skillLevel` smallint(5) unsigned NOT NULL, @@ -1688,7 +1691,7 @@ DROP TABLE IF EXISTS `aowow_screenshots`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_screenshots` ( `id` int(16) unsigned NOT NULL AUTO_INCREMENT, - `type` tinyint(4) unsigned NOT NULL, + `type` smallint(5) unsigned NOT NULL, `typeId` mediumint(9) NOT NULL, `uploader` int(16) unsigned NOT NULL, `date` int(32) unsigned NOT NULL, @@ -2214,8 +2217,8 @@ DROP TABLE IF EXISTS `aowow_videos`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_videos` ( `id` int(16) NOT NULL, - `type` int(8) NOT NULL, - `typeId` int(16) NOT NULL, + `type` smallint(5) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, `uploader` int(16) NOT NULL, `date` int(32) NOT NULL, `videoId` varchar(12) NOT NULL, diff --git a/setup/tools/dbc.class.php b/setup/tools/dbc.class.php index a400f380..365f55fe 100644 --- a/setup/tools/dbc.class.php +++ b/setup/tools/dbc.class.php @@ -104,7 +104,7 @@ class DBC 'spellduration' => 'nixx', 'spellfocusobject' => 'nsxssxxsxsxxxxxxxx', 'spellicon' => 'ns', - 'spellitemenchantment' => 'nxiiiiiixxxiiisxssxxsxsxxxxxxxxxxiiiii', + 'spellitemenchantment' => 'niiiiiiixxxiiisxssxxsxsxxxxxxxxxxxiiii', 'spellitemenchantmentcondition' => 'nbbbbbxxxxxbbbbbbbbbbiiiiiXXXXX', 'spellradius' => 'nfxf', 'spellrange' => 'nffffisxssxxsxsxxxxxxxxxxxxxxxxxxxxxxxxx', @@ -186,7 +186,7 @@ class DBC 'spelldifficulty' => 'normal10,normal25,heroic10,heroic25', 'spellfocusobject' => 'Id,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', 'spellicon' => 'Id,iconPath', - 'spellitemenchantment' => 'Id,type1,type2,type3,amount1,amount2,amount3,object1,object2,object3,text_loc0,text_loc2,text_loc3,text_loc6,text_loc8,gemReference,conditionId,skillLine,skillLevel,requiredLevel', + 'spellitemenchantment' => 'Id,charges,type1,type2,type3,amount1,amount2,amount3,object1,object2,object3,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8,conditionId,skillLine,skillLevel,requiredLevel', 'spellitemenchantmentcondition' => 'Id,color1,color2,color3,color4,color5,comparator1,comparator2,comparator3,comparator4,comparator5,cmpColor1,cmpColor2,cmpColor3,cmpColor4,cmpColor5,value1,value2,value3,value4,value5', 'spellradius' => 'Id,radiusMin,radiusMax', 'spellrange' => 'Id,rangeMinHostile,rangeMinFriend,rangeMaxHostile,rangeMaxFriend,rangeType,name_loc0,name_loc2,name_loc3,name_loc6,name_loc8', diff --git a/setup/tools/fileGen.class.php b/setup/tools/fileGen.class.php index d04c47b1..103870a4 100644 --- a/setup/tools/fileGen.class.php +++ b/setup/tools/fileGen.class.php @@ -40,9 +40,9 @@ class FileGen 'pets' => [['spawns', 'creature'], null], 'talentIcons' => [null, null], 'glyphs' => [['items', 'spell'], null], - 'itemsets' => [['itemset'], null], - 'enchants' => [['items'], null], - 'gems' => [['items'], null], + 'itemsets' => [['itemset', 'spell'], null], + 'enchants' => [['items', 'spell', 'itemenchantment'], null], + 'gems' => [['items', 'spell', 'itemenchantment'], null], 'profiler' => [['quests', 'quests_startend', 'spell', 'currencies', 'achievement', 'titles'], null] ); @@ -82,6 +82,8 @@ class FileGen self::$subScripts = array_merge(array_keys(self::$tplFiles), array_keys(self::$datasets)); if ($doScripts) self::$subScripts = array_intersect($doScripts, self::$subScripts); + else if ($doScripts === null) + self::$subScripts = []; if (!CLISetup::$localeIds /* todo: && this script has localized text */) { @@ -127,7 +129,7 @@ class FileGen $doScripts[] = $name; } - $doScripts = array_unique($doScripts); + $doScripts = $doScripts ? array_unique($doScripts) : null; } else if (!empty($_['build'])) $doScripts = explode(',', $_['build']); diff --git a/setup/tools/filegen/enchants.func.php b/setup/tools/filegen/enchants.func.php index 040b5e8f..c1e271a5 100644 --- a/setup/tools/filegen/enchants.func.php +++ b/setup/tools/filegen/enchants.func.php @@ -11,8 +11,6 @@ if (!CLI) // this script requires the following dbc-files to be parsed and available // Spells, SkillLineAbility, SpellItemEnchantment - // todo (high): restructure to work more efficiently (outer loop: spells, inner loop: locales) - /* Examples 15: { name:'Leichtes Rüstungsset', @@ -72,8 +70,13 @@ if (!CLI) foreach ($enchantSpells->iterate() as $__) $enchIds[] = $enchantSpells->getField('effect1MiscValue'); - $enchMisc = []; - $enchJSON = Util::parseItemEnchantment($enchIds, false, $enchMisc); + $enchantments = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); + if ($enchantments->error) + { + CLISetup::log('Required table ?_itemenchantment seems to be empty! Leaving enchants()...', CLISetup::LOG_ERROR); + CLISetup::log(); + return false; + } foreach (CLISetup::$localeIds as $lId) { @@ -85,6 +88,13 @@ if (!CLI) $enchantsOut = []; foreach ($enchantSpells->iterate() as $__) { + $eId = $enchantSpells->getField('effect1MiscValue'); + if (!$enchantments->getEntry($eId)) + { + CLISetup::log(' * could not find enchantment #'.$eId.' referenced by spell #'.$enchantSpells->id, CLISetup::LOG_WARN); + continue; + } + // slots have to be recalculated $slot = 0; if ($enchantSpells->getField('equippedItemClass') == 4) // armor @@ -111,7 +121,6 @@ if (!CLI) } } - $eId = $enchantSpells->getField('effect1MiscValue'); // defaults $ench = array( @@ -121,20 +130,20 @@ if (!CLI) 'source' => [], // <0: item; >0:spell 'skill' => -1, // modified if skill 'slots' => [], // determined per spell but set per item - 'enchantment' => Util::localizedString($enchMisc[$eId]['text'], 'text'), - 'jsonequip' => @$enchJSON[$eId] ?: [], + 'enchantment' => $enchantments->getField('name', true), + 'jsonequip' => $enchantments->getStatGain(), 'temp' => 0, // always 0 'classes' => 0, // modified by item ); - if (isset($enchMisc[$eId]['reqskill'])) - $ench['jsonequip']['reqskill'] = $enchMisc[$eId]['reqskill']; + if ($_ = $enchantments->getField('skillLine')) + $ench['jsonequip']['reqskill'] = $_; - if (isset($enchMisc[$eId]['reqskillrank'])) - $ench['jsonequip']['reqskill'] = $enchMisc[$eId]['reqskillrank']; + if ($_ = $enchantments->getField('skillLevel')) + $ench['jsonequip']['reqskillrank'] = $_; - if (isset($enchMisc[$eId]['requiredLevel'])) - $ench['jsonequip']['requiredLevel'] = $enchMisc[$eId]['requiredLevel']; + if (($_ = $enchantments->getField('requiredLevel')) && $_ > 1) + $ench['jsonequip']['reqlevel'] = $_; // check if the spell has an entry in skill_line_ability -> Source:Profession if ($skills = $enchantSpells->getField('skillLines')) diff --git a/setup/tools/filegen/gems.func.php b/setup/tools/filegen/gems.func.php index 68f6723e..be8e87ad 100644 --- a/setup/tools/filegen/gems.func.php +++ b/setup/tools/filegen/gems.func.php @@ -52,8 +52,13 @@ if (!CLI) foreach ($gems as $pop) $enchIds[] = $pop['enchId']; - $enchMisc = []; - $enchJSON = Util::parseItemEnchantment($enchIds, false, $enchMisc); + $enchantments = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); + if ($enchantments->error) + { + CLISetup::log('Required table ?_itemenchantment seems to be empty! Leaving gems()...', CLISetup::LOG_ERROR); + CLISetup::log(); + return false; + } foreach (CLISetup::$localeIds as $lId) { @@ -65,12 +70,18 @@ if (!CLI) $gemsOut = []; foreach ($gems as $pop) { + if (!$enchantments->getEntry($pop['enchId'])) + { + CLISetup::log(' * could not find enchantment #'.$pop['enchId'].' referenced by item #'.$gem['itemId'], CLISetup::LOG_WARN); + continue; + } + $gemsOut[$pop['itemId']] = array( 'name' => Util::localizedString($pop, 'name'), 'quality' => $pop['quality'], 'icon' => strToLower($pop['icon']), - 'enchantment' => Util::localizedString(@$enchMisc[$pop['enchId']]['text'] ?: [], 'text'), - 'jsonequip' => @$enchJSON[$pop['enchId']] ?: [], + 'enchantment' => $enchantments->getField('name', true), + 'jsonequip' => $enchantments->getStatGain(), 'colors' => $pop['colors'], 'expansion' => $pop['expansion'] ); diff --git a/setup/tools/sqlGen.class.php b/setup/tools/sqlGen.class.php index fe9f9f39..d11e855e 100644 --- a/setup/tools/sqlGen.class.php +++ b/setup/tools/sqlGen.class.php @@ -23,7 +23,6 @@ class SqlGen 'achievementcategory' => ['achievement_category', false, null, null], 'achievementcriteria' => ['achievement_criteria', false, null, null], 'glyphproperties' => ['glyphproperties', true, null, null], - 'itemenchantment' => ['spellitemenchantment', false, null, null], 'itemenchantmentcondition' => ['spellitemenchantmentcondition', false, null, null], 'itemextendedcost' => ['itemextendedcost', false, null, null], 'itemlimitcategory' => ['itemlimitcategory', false, null, null], @@ -46,6 +45,7 @@ class SqlGen 'shapeshiftforms' => [null, null, null, null], 'skillline' => [null, null, null, null], 'emotes' => [null, null, null, null], + 'itemenchantment' => [null, null, null, ['spell_enchant_proc_data']], 'achievement' => [null, null, null, ['dbc_achievement']], 'creature' => [null, null, null, ['creature_template', 'locales_creature', 'creature_classlevelstats', 'instance_encounters']], 'currencies' => [null, null, null, ['item_template', 'locales_item']], @@ -62,8 +62,8 @@ class SqlGen 'spawns' /* + waypoints */ => [null, null, null, ['creature', 'creature_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'script_waypoint', 'waypoints', 'waypoint_data']], 'zones' => [null, null, null, ['access_requirement']], 'itemset' => [null, null, ['spell'], ['item_template', 'game_event']], - 'item_stats' => [null, null, ['items', 'spell'], null], - 'source' => [null, null, ['spell', 'achievements'], ['npc_vendor', 'game_event_npc_vendor', 'creature', 'quest_template', 'playercreateinfo_item', 'npc_trainer', 'skill_discovery_template', 'playercreateinfo_spell', 'achievement_reward']] + 'item_stats' /* + ench */ => [null, null, ['items', 'spell'], null], + 'source' => [null, null, ['spell', 'achievement'], ['npc_vendor', 'game_event_npc_vendor', 'creature', 'quest_template', 'playercreateinfo_item', 'npc_trainer', 'skill_discovery_template', 'playercreateinfo_spell', 'achievement_reward']] ); public static $cliOpts = []; @@ -92,6 +92,8 @@ class SqlGen self::$subScripts = array_keys(self::$tables); if ($doScripts) self::$subScripts = array_intersect($doScripts, self::$subScripts); + else if ($doScripts === null) + self::$subScripts = []; if (!CLISetup::$localeIds /* && this script has localized text */) { @@ -124,7 +126,7 @@ class SqlGen if (!empty($info[2]) && array_intersect($doTbls, $info[2])) $doTbls[] = $name; - $doTbls = array_unique($doTbls); + $doTbls = $doTbls ? array_unique($doTbls) : null; } else if (!empty($_['sql'])) $doTbls = explode(',', $_['sql']); diff --git a/setup/tools/sqlgen/item_stats.func.php b/setup/tools/sqlgen/item_stats.func.php index 268d3c02..3aebe1ff 100644 --- a/setup/tools/sqlgen/item_stats.func.php +++ b/setup/tools/sqlgen/item_stats.func.php @@ -10,17 +10,18 @@ if (!CLI) /* deps: * ?_items finalized * ?_spell finalized + * dbc_spellitemenchantment */ $customData = array( ); -$reqDBC = []; +$reqDBC = ['spellitemenchantment']; class ItemStatSetup extends ItemList { private $statCols = []; - public function __construct($start, $limit, array $ids) + public function __construct($start, $limit, array $ids, array $enchStats) { $this->statCols = DB::Aowow()->selectCol('SELECT `COLUMN_NAME` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_NAME` LIKE "%item_stats"'); $this->queryOpts['i']['o'] = 'i.id ASC'; @@ -36,6 +37,8 @@ class ItemStatSetup extends ItemList $conditions[] = ['id', $ids]; parent::__construct($conditions); + + $this->enchParsed = $enchStats; } public function writeStatsTable() @@ -87,27 +90,25 @@ class ItemStatSetup extends ItemList } // execute: convert enchantments to stats - if ($enchantments) + // and merge enchantments back + foreach ($enchantments as $eId => $items) { - $parsed = Util::parseItemEnchantment(array_keys($enchantments)); + if (empty($this->enchParsed[$eId])) + continue; - // and merge enchantments back - foreach ($parsed as $eId => $stats) + foreach ($items as $item) { - foreach ($enchantments[$eId] as $item) - { - if ($item > 0) // apply socketBonus - $this->json[$item]['socketbonusstat'] = $stats; - else /* if ($item < 0) */ // apply gemEnchantment - Util::arraySumByKey($this->json[-$item], $stats); - } + if ($item > 0) // apply socketBonus + $this->json[$item]['socketbonusstat'] = $this->enchParsed[$eId]; + else /* if ($item < 0) */ // apply gemEnchantment + Util::arraySumByKey($this->json[-$item], $this->enchParsed[$eId]); } } // collect data and write to DB foreach ($this->iterate() as $__) { - $updateFields = ['id' => $this->id]; + $updateFields = ['type' => TYPE_ITEM, 'typeId' => $this->id]; foreach (@$this->json[$this->id] as $k => $v) { @@ -128,8 +129,7 @@ class ItemStatSetup extends ItemList } } - if (count($updateFields) > 1) - DB::Aowow()->query('REPLACE INTO ?_item_stats (?#) VALUES (?a)', array_keys($updateFields), array_values($updateFields), $this->id); + DB::Aowow()->query('REPLACE INTO ?_item_stats (?#) VALUES (?a)', array_keys($updateFields), array_values($updateFields)); } } } @@ -137,9 +137,15 @@ class ItemStatSetup extends ItemList function item_stats(array $ids = []) { $offset = 0; + + CLISetup::log(' - applying stats for enchantments'); + $enchStats = enchantment_stats(); + CLISetup::log(' '.count($enchStats).' enchantments parsed'); + CLISetup::log(' - applying stats for items'); + while (true) { - $items = new ItemStatSetup($offset, SqlGen::$stepSize, $ids); + $items = new ItemStatSetup($offset, SqlGen::$stepSize, $ids, $enchStats); if ($items->error) break; @@ -157,4 +163,112 @@ function item_stats(array $ids = []) return true; } +function enchantment_stats() +{ + $statCols = DB::Aowow()->selectCol('SELECT `COLUMN_NAME` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_NAME` LIKE "%item_stats"'); + $enchants = DB::Aowow()->select('SELECT *, Id AS ARRAY_KEY FROM dbc_spellitemenchantment'); + $spells = []; + $spellStats = []; + + foreach ($enchants as $eId => $e) + { + for ($i = 1; $i <=3; $i++) + { + // trigger: onEquip + valid SpellId + if ($e['object'.$i] > 0 && $e['type'.$i] == 3) + $spells[] = $e['object'.$i]; + } + } + + if ($spells) + $spellStats = (new SpellList(array(['id', $spells], CFG_SQL_LIMIT_NONE)))->getStatGain(); + + $result = []; + foreach ($enchants as $eId => $e) + { + // parse stats + $result[$eId] = []; + for ($h = 1; $h <= 3; $h++) + { + $obj = (int)$e['object'.$h]; + $val = (int)$e['amount'.$h]; + + switch ($e['type'.$h]) + { + case 6: // TYPE_TOTEM +AmountX as DPS (Rockbiter) + $result[$eId]['dps'] = $val; // we do not use dps as itemMod, so apply it directly + $obj = null; + break; + case 2: // TYPE_DAMAGE +AmountX damage + $obj = ITEM_MOD_WEAPON_DMG; + break; + // case 1: // TYPE_COMBAT_SPELL proc spell from ObjectX (amountX == procChance) + // case 7: // TYPE_USE_SPELL Engineering gadgets + case 3: // TYPE_EQUIP_SPELL Spells from ObjectX (use of amountX?) + if (!empty($spellStats[$obj])) + foreach ($spellStats[$obj] as $mod => $val) + if ($str = Util::$itemMods[$mod]) + Util::arraySumByKey($result[$eId], [$str => $val]); + + $obj = null; + break; + case 4: // TYPE_RESISTANCE +AmountX resistance for ObjectX School + switch ($obj) + { + case 0: // Physical + $obj = ITEM_MOD_ARMOR; + break; + case 1: // Holy + $obj = ITEM_MOD_HOLY_RESISTANCE; + break; + case 2: // Fire + $obj = ITEM_MOD_FIRE_RESISTANCE; + break; + case 3: // Nature + $obj = ITEM_MOD_NATURE_RESISTANCE; + break; + case 4: // Frost + $obj = ITEM_MOD_FROST_RESISTANCE; + break; + case 5: // Shadow + $obj = ITEM_MOD_SHADOW_RESISTANCE; + break; + case 6: // Arcane + $obj = ITEM_MOD_ARCANE_RESISTANCE; + break; + default: + $obj = null; + } + break; + case 5: // TYPE_STAT +AmountX for Statistic by type of ObjectX + if ($obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] .. + $obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere + + break; // stats are directly assigned below + case 8: // TYPE_PRISMATIC_SOCKET Extra Sockets AmountX as socketCount (ignore) + $result[$eId]['nsockets'] = $val; // there is no itemmod for sockets, so apply it directly + default: // TYPE_NONE dnd stuff; skip assignment below + $obj = null; + } + + if ($obj !== null) + if ($str = Util::$itemMods[$obj]) // check if we use these mods + Util::arraySumByKey($result[$eId], [$str => $val]); + } + + $updateCols = ['type' => TYPE_ENCHANTMENT, 'typeId' => $eId]; + foreach ($result[$eId] as $k => $v) + { + if (!in_array($k, $statCols) || !$v || $k == 'id') + continue; + + $updateCols[$k] = number_format($v, 2, '.', ''); + } + + DB::Aowow()->query('REPLACE INTO ?_item_stats (?#) VALUES (?a)', array_keys($updateCols), array_values($updateCols)); + } + + return $result; +} + ?> diff --git a/setup/tools/sqlgen/itemenchantment.func.php b/setup/tools/sqlgen/itemenchantment.func.php new file mode 100644 index 00000000..4c4c1f36 --- /dev/null +++ b/setup/tools/sqlgen/itemenchantment.func.php @@ -0,0 +1,37 @@ +query($baseQuery); + + $cuProcs = DB::World()->select('SELECT entry AS ARRAY_KEY, customChance AS procChance, PPMChance AS ppmRate FROM spell_enchant_proc_data'); + foreach ($cuProcs as $id => $vals) + DB::Aowow()->query('UPDATE ?_itemenchantment SET ?a WHERE id = ?d', $vals, $id); + + // hide strange stuff + DB::Aowow()->query('UPDATE ?_itemenchantment SET cuFlags = ?d WHERE type1 = 0 AND type2 = 0 AND type3 = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->query('UPDATE ?_itemenchantment SET cuFlags = ?d WHERE name_loc0 LIKE "%test%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); + + return true; +} + +?> diff --git a/setup/updates/1438620486_01.sql b/setup/updates/1438620486_01.sql new file mode 100644 index 00000000..6e3cc6da --- /dev/null +++ b/setup/updates/1438620486_01.sql @@ -0,0 +1,66 @@ +-- structure changed hard +DROP TABLE IF EXISTS `dbc_spellitemenchantment`; +DROP TABLE IF EXISTS `aowow_itemenchantment`; +CREATE TABLE `aowow_itemenchantment` ( + `id` smallint(5) unsigned NOT NULL, + `charges` tinyint(4) unsigned NOT NULL, + `cuFlags` int(10) unsigned NOT NULL, + `procChance` tinyint(3) unsigned NOT NULL, + `ppmRate` float NOT NULL, + `type1` tinyint(4) unsigned NOT NULL, + `type2` tinyint(4) unsigned NOT NULL, + `type3` tinyint(4) unsigned NOT NULL, + `amount1` smallint(5) NOT NULL, + `amount2` smallint(5) NOT NULL, + `amount3` smallint(5) NOT NULL, + `object1` mediumint(9) unsigned NOT NULL, + `object2` mediumint(9) unsigned NOT NULL, + `object3` smallint(5) unsigned NOT NULL, + `name_loc0` varchar(65) NOT NULL, + `name_loc2` varchar(91) NOT NULL, + `name_loc3` varchar(84) NOT NULL, + `name_loc6` varchar(89) NOT NULL, + `name_loc8` varchar(96) NOT NULL, + `conditionId` tinyint(3) unsigned NOT NULL, + `skillLine` smallint(5) unsigned NOT NULL, + `skillLevel` smallint(5) unsigned NOT NULL, + `requiredLevel` tinyint(3) unsigned NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +ALTER TABLE `aowow_item_stats` + ALTER `id` DROP DEFAULT; +ALTER TABLE `aowow_item_stats` + ADD COLUMN `type` smallint(5) unsigned NOT NULL FIRST, + CHANGE COLUMN `id` `typeId` mediumint(9) unsigned NOT NULL AFTER `type`, + DROP INDEX `item`, + DROP PRIMARY KEY, + ADD PRIMARY KEY (`typeId`, `type`) + +ALTER TABLE `aowow_articles` + ALTER `type` DROP DEFAULT, + ALTER `typeId` DROP DEFAULT; +ALTER TABLE `aowow_articles` + CHANGE COLUMN `type` `type` smallint(5) NOT NULL FIRST, + CHANGE COLUMN `typeId` `typeId` mediumint(9) NOT NULL AFTER `type`; + +ALTER TABLE `aowow_comments` + ALTER `type` DROP DEFAULT, + ALTER `typeId` DROP DEFAULT; +ALTER TABLE `aowow_comments` + CHANGE COLUMN `type` `type` smallint(5) unsigned NOT NULL COMMENT 'Type of Page' AFTER `id`, + CHANGE COLUMN `typeId` `typeId` mediumint(9) NOT NULL COMMENT 'ID Of Page' AFTER `type`; + +ALTER TABLE `aowow_screenshots` + ALTER `type` DROP DEFAULT; + ALTER `typeId` DROP DEFAULT; +ALTER TABLE `aowow_screenshots` + CHANGE COLUMN `type` `type` smallint(5) unsigned NOT NULL AFTER `id`, + CHANGE COLUMN `typeId` `typeId` mediumint(9) NOT NULL AFTER `type`; + +ALTER TABLE `aowow_videos` + ALTER `type` DROP DEFAULT, + ALTER `typeId` DROP DEFAULT; +ALTER TABLE `aowow_videos` + CHANGE COLUMN `type` `type` smallint(5) unsigned NOT NULL AFTER `id`, + CHANGE COLUMN `typeId` `typeId` mediumint(9) NOT NULL AFTER `type`; diff --git a/static/css/aowow.css b/static/css/aowow.css index accbaf6d..464b2940 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -38,6 +38,7 @@ a { color: #FFD100; cursor: pointer; outline: none; + text-decoration: none; } a:hover { diff --git a/static/js/Markup.js b/static/js/Markup.js index e76f59fb..89d03411 100644 --- a/static/js/Markup.js +++ b/static/js/Markup.js @@ -570,6 +570,86 @@ var Markup = { return str; } }, + emote: + { + empty: true, + allowInReplies: true, + attr: + { + unnamed: { req: true, valid: /^[0-9]+$/ }, + domain: { req: false, valid: /^(beta|mop|ptr|www|de|es|fr|ru|pt)$/ }, + site: { req: false, valid: /^(beta|mop|ptr|www|de|es|fr|ru|pt)$/ } + }, + validate: function(attr) + { + if((attr.domain || attr.site) && Markup.dbpage) + return false; + return true; + }, + toHtml: function(attr) + { + var id = attr.unnamed; + var domainInfo = Markup._getDatabaseDomainInfo(attr); + var url = domainInfo[0]; + var nameCol = domainInfo[1]; + + if(g_emotes[id] && g_emotes[id][nameCol]) + { + return '' + Markup._safeHtml(g_emotes[id][nameCol]) + ''; + } + return '(' + LANG.types[501][0] + ' #' + id + ')'; + }, + toText: function(attr) + { + var id = attr.unnamed; + var domainInfo = Markup._getDatabaseDomainInfo(attr); + var nameCol = domainInfo[1]; + + if(g_emotes[id] && g_emotes[id][nameCol]) + return Markup._safeHtml(g_emotes[id][nameCol]); + return LANG.types[501][0] + ' #' + id; + } + }, + enchantment: + { + empty: true, + allowInReplies: true, + attr: + { + unnamed: { req: true, valid: /^[0-9]+$/ }, + domain: { req: false, valid: /^(beta|mop|ptr|www|de|es|fr|ru|pt)$/ }, + site: { req: false, valid: /^(beta|mop|ptr|www|de|es|fr|ru|pt)$/ } + }, + validate: function(attr) + { + if((attr.domain || attr.site) && Markup.dbpage) + return false; + return true; + }, + toHtml: function(attr) + { + var id = attr.unnamed; + var domainInfo = Markup._getDatabaseDomainInfo(attr); + var url = domainInfo[0]; + var nameCol = domainInfo[1]; + + if(g_enchantments[id] && g_enchantments[id][nameCol]) + { + return '' + Markup._safeHtml(g_enchantments[id][nameCol]) + ''; + } + return '(' + LANG.types[502][0] + ' #' + id + ')'; + }, + toText: function(attr) + { + var id = attr.unnamed; + var domainInfo = Markup._getDatabaseDomainInfo(attr); + var nameCol = domainInfo[1]; + + if(g_enchantments[id] && g_enchantments[id][nameCol]) + return Markup._safeHtml(g_enchantments[id][nameCol]); + return LANG.types[502][0] + ' #' + id; + } + }, event: { empty: true, diff --git a/static/js/filters.js b/static/js/filters.js index b0444f96..b4f2cafe 100644 --- a/static/js/filters.js +++ b/static/js/filters.js @@ -401,8 +401,83 @@ var fi_filters = { { id: 18, name: 'teamname5v5', type: 'str' }, { id: 19, name: 'teamrtng5v5', type: 'num' }, { id: 20, name: 'teamcontrib5v5', type: 'num' } - ] + ], + // custom + enchantments: [ + { id: 1, name: 'sepgeneral' }, + { id: 2, name: 'id', type: 'num', before: 'name' }, + { id: 3, name: 'requiresprof', type: 'profession' }, + { id: 4, name: 'reqskillrank', type: 'num' }, + { id: 5, name: 'hascondition', type: 'yn' }, + + { id: 19, name: 'sepbasestats' }, + { id: 21, name: 'agi', type: 'num' }, + { id: 23, name: 'int', type: 'num' }, + { id: 22, name: 'sta', type: 'num' }, + { id: 24, name: 'spi', type: 'num' }, + { id: 20, name: 'str', type: 'num' }, + { id: 115, name: 'health', type: 'num' }, + { id: 116, name: 'mana', type: 'num' }, + { id: 60, name: 'healthrgn', type: 'num' }, + { id: 61, name: 'manargn', type: 'num' }, + + { id: 120, name: 'sepdefensivestats' }, + { id: 41, name: 'armor', type: 'num' }, + { id: 44, name: 'blockrtng', type: 'num' }, + { id: 43, name: 'block', type: 'num' }, + { id: 42, name: 'defrtng', type: 'num' }, + { id: 45, name: 'dodgertng', type: 'num' }, + { id: 46, name: 'parryrtng', type: 'num' }, + { id: 79, name: 'resirtng', type: 'num' }, + + { id: 31, name: 'sepoffensivestats' }, + { id: 32, name: 'dps', type: 'num' }, + { id: 34, name: 'dmg', type: 'num' }, + { id: 77, name: 'atkpwr', type: 'num' }, + { id: 97, name: 'feratkpwr', type: 'num', indent: 1 }, + { id: 114, name: 'armorpenrtng', type: 'num' }, + { id: 96, name: 'critstrkrtng', type: 'num' }, + { id: 117, name: 'exprtng', type: 'num' }, + { id: 103, name: 'hastertng', type: 'num' }, + { id: 119, name: 'hitrtng', type: 'num' }, + { id: 94, name: 'splpen', type: 'num' }, + { id: 123, name: 'splpwr', type: 'num' }, + { id: 52, name: 'arcsplpwr', type: 'num', indent: 1 }, + { id: 53, name: 'firsplpwr', type: 'num', indent: 1 }, + { id: 54, name: 'frosplpwr', type: 'num', indent: 1 }, + { id: 55, name: 'holsplpwr', type: 'num', indent: 1 }, + { id: 56, name: 'natsplpwr', type: 'num', indent: 1 }, + { id: 57, name: 'shasplpwr', type: 'num', indent: 1 }, + + { id: 121, name: 'sepresistances' }, + { id: 25, name: 'arcres', type: 'num' }, + { id: 26, name: 'firres', type: 'num' }, + { id: 28, name: 'frores', type: 'num' }, + { id: 30, name: 'holres', type: 'num' }, + { id: 27, name: 'natres', type: 'num' }, + { id: 29, name: 'shares', type: 'num' }, + + { id: 47, name: 'sepindividualstats' }, + { id: 37, name: 'mleatkpwr', type: 'num' }, + { id: 84, name: 'mlecritstrkrtng', type: 'num' }, + { id: 78, name: 'mlehastertng', type: 'num' }, + { id: 95, name: 'mlehitrtng', type: 'num' }, + { id: 38, name: 'rgdatkpwr', type: 'num' }, + { id: 40, name: 'rgdcritstrkrtng', type: 'num' }, + { id: 101, name: 'rgdhastertng', type: 'num' }, + { id: 39, name: 'rgdhitrtng', type: 'num' }, + { id: 49, name: 'splcritstrkrtng', type: 'num' }, + { id: 102, name: 'splhastertng', type: 'num' }, + { id: 48, name: 'splhitrtng', type: 'num' }, + { id: 51, name: 'spldmg', type: 'num' }, + { id: 50, name: 'splheal', type: 'num' }, + + { id: 9999,name: 'sepcommunity' }, + { id: 10, name: 'hascomments', type: 'yn' }, + { id: 11, name: 'hasscreenshots', type: 'yn' }, + { id: 12, name: 'hasvideos', type: 'yn' } + ] }; function fi_toggle() { diff --git a/static/js/global.js b/static/js/global.js index 729cde2c..c27d386b 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -2256,7 +2256,7 @@ function ss_appendSticky() { th = sections[sections.length - (lv_videos && lv_videos.length ? 2 : 1)]; } - $WH.ae(a, $WH.ct(th.innerText + ' (' + lv_screenshots.length + ')')); + $WH.ae(a, $WH.ct(th.textContent + ' (' + lv_screenshots.length + ')')); a.href = '#screenshots' a.title = $WH.sprintf(LANG.infobox_showall, lv_screenshots.length); a.onclick = function() { @@ -2876,12 +2876,13 @@ function vi_appendSticky() { var th = $WH.ge('infobox-videos'); var a = $WH.ce('a'); + if (!th) { var sections = $('th', _.parentNode); th = sections[sections.length - (lv_videos && lv_videos.length ? 2 : 1)]; } - $WH.ae(a, $WH.ct(th.innerText + ' (' + lv_videos.length + ')')); + $WH.ae(a, $WH.ct(th.textContent + ' (' + lv_videos.length + ')')); a.href = '#videos' a.title = $WH.sprintf(LANG.infobox_showall, lv_videos.length); a.onclick = function() { @@ -21102,25 +21103,29 @@ var g_classes = {}, g_races = {}, g_skills = {}, - g_gatheredcurrencies = {}; + g_gatheredcurrencies = {}, + g_enchantments = {}, + g_emotes = {}; var g_types = { - 1: 'npc', - 2: 'object', - 3: 'item', - 4: 'itemset', - 5: 'quest', - 6: 'spell', - 7: 'zone', - 8: 'faction', - 9: 'pet', - 10: 'achievement', - 11: 'title', - 12: 'event', - 13: 'class', - 14: 'race', - 15: 'skill', - 17: 'currency' + 1: 'npc', + 2: 'object', + 3: 'item', + 4: 'itemset', + 5: 'quest', + 6: 'spell', + 7: 'zone', + 8: 'faction', + 9: 'pet', + 10: 'achievement', + 11: 'title', + 12: 'event', + 13: 'class', + 14: 'race', + 15: 'skill', + 17: 'currency', + 501: 'emote', + 502: 'enchantment' }; // Items diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 4cef5e36..4a41bbdc 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -770,6 +770,17 @@ var mn_currencies = [ [2,"Spieler gegen Spieler","?currencies=2"], [1,"Verschiedenes","?currencies=1"] ]; +var mn_enchantments = [ + [1,"Zauber (Auslösung)","?enchantments&filter=ty=1"], + [3,"Zauber (Anlegen)","?enchantments&filter=ty=3"], + [7,"Zauber (Benutzen)","?enchantments&filter=ty=7"], + [8,"Prismatischer Sockel","?enchantments&filter=ty=8"], + [5,"Statistik","?enchantments&filter=ty=5"], + [2,"Waffenschaden","?enchantments&filter=ty=2"], + [6,"DPS","?enchantments&filter=ty=6"], + [4,"Verteidigung","?enchantments&filter=ty=4"] +]; + var mn_talentCalc = [ [6,"Todesritter","?talent#j",,{className:"c6",tinyIcon:"class_deathknight"}], [11,"Druide","?talent#0",,{className:"c11",tinyIcon:"class_druid"}], @@ -836,7 +847,8 @@ var mn_database = [ [15,"Währungen","?currencies",mn_currencies], [11,"Weltereignisse","?events",mn_holidays], [1,"Zauber","?spells",mn_spells], - [100,"Emotes","?emotes",null] + [100,"Emotes","?emotes",null], + [101,"Verzauberungen","?enchantments",mn_enchantments] ]; var mn_tools = [ [0,"Talentrechner","?talent",mn_talentCalc], @@ -2604,6 +2616,7 @@ var LANG = { lvnote_arenateamsfound: "Insgesamt $1 Arena-Teams", lvnote_arenateamsfound2: "Insgesamt $1 Arena-Teams, $2 passende", lvnote_currenciesfound: "$1 Währungen gefunden ($2 angezeigt)", + lvnote_enchantmentsfound: "$1 Verzauberungen gefunden ($2 angezeigt)", lvnote_createafilter: 'Filter erstellen', lvnote_filterresults: 'Diese Ergebnisse filtern', @@ -3175,23 +3188,25 @@ var LANG = { myaccount_purgesuccess: "Bekanntmachungsdaten wurden erfolgreich gelöscht!", types: { - 1: ["NPC", "NPC" , "NPCs", "NPCs"], - 2: ["Objekt", "Objekt", "Objekte", "Objekte"], - 3: ["Gegenstand", "Gegenstand", "Gegenstände", "Gegenstände"], - 4: ["Ausrüstungsset", "Ausrüstungsset", "Ausrüstungssets", "Ausrüstungssets"], - 5: ["Quest", "Quest", "Quests", "Quests"], - 6: ["Zauber", "Zauber", "Zauber", "Zauber"], - 7: ["Zone", "Zone", "Gebiete", "Gebiete"], - 8: ["Fraktion", "Fraktion", "Fraktionen", "Fraktionen"], - 9: ["Begleiter", "Begleiter", "Begleiter", "Begleiter"], - 10: ["Erfolg", "Erfolg", "Erfolge", "Erfolge"], - 11: ["Titel", "Titel", "Titel", "Titel"], - 12: ["Weltereignis", "Weltereignis", "Weltereignisse", "Weltereignisse"], - 13: ["Klasse", "Klasse", "Klassen", "Klassen"], - 14: ["Volk", "Volk", "Völker", "Völker"], - 15: ["Fertigkeit", "Fertigkeit", "Fertigkeiten", "Fertigkeiten"], - 16: ["Statistik", "Statistik", "Statistiken", "Statistiken"], - 17: ["Währung", "Währung", "Währungen", "Währungen"] + 1: ["NPC", "NPC" , "NPCs", "NPCs"], + 2: ["Objekt", "Objekt", "Objekte", "Objekte"], + 3: ["Gegenstand", "Gegenstand", "Gegenstände", "Gegenstände"], + 4: ["Ausrüstungsset", "Ausrüstungsset", "Ausrüstungssets", "Ausrüstungssets"], + 5: ["Quest", "Quest", "Quests", "Quests"], + 6: ["Zauber", "Zauber", "Zauber", "Zauber"], + 7: ["Zone", "Zone", "Gebiete", "Gebiete"], + 8: ["Fraktion", "Fraktion", "Fraktionen", "Fraktionen"], + 9: ["Begleiter", "Begleiter", "Begleiter", "Begleiter"], + 10: ["Erfolg", "Erfolg", "Erfolge", "Erfolge"], + 11: ["Titel", "Titel", "Titel", "Titel"], + 12: ["Weltereignis", "Weltereignis", "Weltereignisse", "Weltereignisse"], + 13: ["Klasse", "Klasse", "Klassen", "Klassen"], + 14: ["Volk", "Volk", "Völker", "Völker"], + 15: ["Fertigkeit", "Fertigkeit", "Fertigkeiten", "Fertigkeiten"], + 16: ["Statistik", "Statistik", "Statistiken", "Statistiken"], + 17: ["Währung", "Währung", "Währungen", "Währungen"], + 501: ["Emote", "Emote", "Emotes", "Emotes"], + 502: ["Verzauberung", "Verzauberung", "Verzauberungen", "Verzauberungen"] }, timeunitssg: ["Jahr", "Monat", "Woche", "Tag", "Stunde", "Minute", "Sekunde"], @@ -3865,6 +3880,18 @@ var LANG = { teamcontrib5v5: "5v5 Arena-Teambeteiligung" }, + // custom + fienchantments: { + id: "ID", + hascondition: "Benötigt Edelsteinkombination", + requiresprof: "Benötigt einen Beruf", + + sepcommunity: "Community", + hascomments: "Has comments", + hasscreenshots: "Has screenshots", + hasvideos: "Has videos" + }, + pr_notice: 'Zum ersten Mal hier? – Seid nicht schüchtern! Schaut ruhig mal auf unserer Hilfeseite (zurzeit noch unübersetzt) nach!   Schließen', pr_datasource: 'Daten in dieser Registerkarte wurden das letzte Mal $2 von $1 aktualisiert.', pr_purgedata: "Klickt, um alle Abschlussdaten in der aktuellen Registerkarte zu löschen.
      Nur der Benuzter, der die Daten hochgeladen hat, darf sie löschen.", diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 40eb796d..980b0cbe 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -816,6 +816,16 @@ var mn_currencies = [ [1,"Miscellaneous","?currencies=1"], [2,"Player vs. Player","?currencies=2"] ]; +var mn_enchantments = [ + [1,"Proc Spell","?enchantments&filter=ty=1"], + [3,"Equip Spell","?enchantments&filter=ty=3"], + [7,"Use Spell","?enchantments&filter=ty=7"], + [8,"Prismatic Socket","?enchantments&filter=ty=8"], + [5,"Statistics","?enchantments&filter=ty=5"], + [2,"Weapon Damage","?enchantments&filter=ty=2"], + [6,"DPS","?enchantments&filter=ty=6"], + [4,"Defense","?enchantments&filter=ty=4"] +]; var mn_talentCalc = [ [6,"Death Knight","?talent#j",,{className:"c6",tinyIcon:"class_deathknight"}], [11,"Druid","?talent#0",,{className:"c11",tinyIcon:"class_druid"}], @@ -882,7 +892,8 @@ var mn_database = [ [10,"Titles","?titles",mn_titles], [11,"World Events","?events",mn_holidays], [6,"Zones","?zones",mn_zones], - [100,"Emotes","?emotes",null] + [100,"Emotes","?emotes",null], + [101,"Enchantments","?enchantments",mn_enchantments] ]; var mn_tools = [ [0,"Talent Calculator","?talent",mn_talentCalc], @@ -2652,6 +2663,7 @@ var LANG = { lvnote_arenateamsfound: "$1 total arena teams", lvnote_arenateamsfound2: "$1 total arena teams, $2 matching", lvnote_currenciesfound: "$1 currencies found ($2 displayed)", + lvnote_enchantmentsfound: "$1 enchantments found ($2 displayed)", lvnote_createafilter: 'Create a filter', lvnote_filterresults: 'Filter these results', @@ -3223,23 +3235,25 @@ var LANG = { myaccount_purgesuccess: "Announcement data has been successfully purged!", types: { - 1: ["NPC", "NPC" , "NPCs", "NPCs"], - 2: ["Object", "object", "Objects", "objects"], - 3: ["Item", "item", "Items", "items"], - 4: ["Item Set", "item set", "Item Sets", "item sets"], - 5: ["Quest", "quest", "Quests", "quests"], - 6: ["Spell", "spell", "Spells", "spells"], - 7: ["Zone", "zone", "Zones", "zones"], - 8: ["Faction", "faction", "Factions", "factions"], - 9: ["Pet", "pet", "Pets", "pets"], - 10: ["Achievement", "achievement", "Achievements", "achievements"], - 11: ["Title", "title", "Titles", "titles"], - 12: ["World Event", "world event", "World Events", "world events"], - 13: ["Class", "class", "Classes", "classes"], - 14: ["Race", "race", "Races", "races"], - 15: ["Skill", "skill", "Skills", "skills"], - 16: ["Statistic", "statistic", "Statistics", "statistics"], - 17: ["Currency", "currency", "Currencies", "currencies"] + 1: ["NPC", "NPC" , "NPCs", "NPCs"], + 2: ["Object", "object", "Objects", "objects"], + 3: ["Item", "item", "Items", "items"], + 4: ["Item Set", "item set", "Item Sets", "item sets"], + 5: ["Quest", "quest", "Quests", "quests"], + 6: ["Spell", "spell", "Spells", "spells"], + 7: ["Zone", "zone", "Zones", "zones"], + 8: ["Faction", "faction", "Factions", "factions"], + 9: ["Pet", "pet", "Pets", "pets"], + 10: ["Achievement", "achievement", "Achievements", "achievements"], + 11: ["Title", "title", "Titles", "titles"], + 12: ["World Event", "world event", "World Events", "world events"], + 13: ["Class", "class", "Classes", "classes"], + 14: ["Race", "race", "Races", "races"], + 15: ["Skill", "skill", "Skills", "skills"], + 16: ["Statistic", "statistic", "Statistics", "statistics"], + 17: ["Currency", "currency", "Currencies", "currencies"], + 501: ["Emote", "emote", "Emotes", "emotes"], + 502: ["Enchantment", "enchantment", "Enchantments", "enchantments"] }, timeunitssg: ["year", "month", "week", "day", "hour", "minute", "second"], @@ -3912,6 +3926,18 @@ var LANG = { teamcontrib5v5: "5v5 arena team contribution" }, + // custom + fienchantments: { + id: "ID", + hascondition: "Requires a combination of gems", + requiresprof: "Requires a profession", + + sepcommunity: "Community", + hascomments: "Has comments", + hasscreenshots: "Has screenshots", + hasvideos: "Has videos" + }, + pr_notice: 'First time? – Don\'t be shy! Just check out our Help page!   close', pr_datasource: 'Data in this tab was last updated $2 by $1.', pr_purgedata: "Click to delete all completion data in the current tab.
      Only the user who uploaded the data may purge it.", diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 8effe0e0..17cb6a11 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -770,6 +770,16 @@ var mn_currencies = [ [1,"Miscelánea","?currencies=1"], [2,"Jugador contra Jugador","?currencies=2"] ]; +var mn_enchantments = [ + [1,"[Proc Spell]","?enchantments&filter=ty=1"], + [3,"[Equip Spell]","?enchantments&filter=ty=3"], + [7,"[Use Spell]","?enchantments&filter=ty=7"], + [8,"Ranura prismática","?enchantments&filter=ty=8"], + [5,"Atributos","?enchantments&filter=ty=5"], + [2,"Daño de arma","?enchantments&filter=ty=2"], + [6,"DPS","?enchantments&filter=ty=6"], + [4,"Defensa","?enchantments&filter=ty=4"] +]; var mn_talentCalc = [ [6,"Caballero de la muerte","?talent#j",,{className:"c6",tinyIcon:"class_deathknight"}], [11,"Druida","?talent#0",,{className:"c11",tinyIcon:"class_druid"}], @@ -836,7 +846,8 @@ var mn_database = [ [10,"Títulos","?titles",mn_titles], [11,"Eventos del mundo","?events",mn_holidays], [6,"Zonas","?zones",mn_zones], - [100,"Emociones","?emotes",null] + [100,"Emociones","?emotes",null], + [101,"Encantamientos","?enchantments",mn_enchantments] ]; var mn_tools = [ [0,"Calculadora de talentos","?talent",mn_talentCalc], @@ -2607,6 +2618,7 @@ var LANG = { lvnote_arenateamsfound: "$1 equipos de arena en total", lvnote_arenateamsfound2: "$1 equipos de arena en total, $2 coincidente(s)", lvnote_currenciesfound: "$1 monedas encontradas ($2 mostradas)", + lvnote_enchantmentsfound: "$1 encantamientos encontrados (mostrados $2)", lvnote_createafilter: 'Crea un filtro', lvnote_filterresults: 'Filtrar estos resultados', @@ -3178,23 +3190,25 @@ var LANG = { myaccount_purgesuccess: "¡Se han purgado los datos de los anuncios correctamente!", types: { - 1: ["PNJ", "PNJ" , "PNJs", "PNJs"], - 2: ["Entidad", "entidad", "Entidades", "entidades"], - 3: ["Objeto", "objeto", "Objetos", "objetos"], - 4: ["Conjunto de objetos", "conjunto de objetos", "Conjuntos de objetos", "conjuntos de objetos"], - 5: ["Misión", "misión", "Misiones", "misiones"], - 6: ["Hechizo", "hechizo", "Hechizos", "hechizos"], - 7: ["Zona", "zona", "Zonas", "zonas"], - 8: ["Facción", "facción", "Facciones", "facciones"], - 9: ["Mascota", "mascota", "Mascotas", "mascotas"], - 10: ["Logro", "logro", "Logros", "logros"], - 11: ["Título", "título", "Títulos", "títulos"], - 12: ["Suceso mundial", "evento del mundo", "Eventos del mundo", "eventos del mundo"], - 13: ["Clase", "Clase", "Clases", "Clases"], - 14: ["Raza", "raza", "Razas", "razas"], - 15: ["Habilidad", "habilidad", "Habilidades", "habilidades"], - 16: ["Atributo", "atributo", "Atributos", "atributos"], - 17: ["Monedas", "monedas", "Monedas", "monedas"] + 1: ["PNJ", "PNJ" , "PNJs", "PNJs"], + 2: ["Entidad", "entidad", "Entidades", "entidades"], + 3: ["Objeto", "objeto", "Objetos", "objetos"], + 4: ["Conjunto de objetos", "conjunto de objetos", "Conjuntos de objetos", "conjuntos de objetos"], + 5: ["Misión", "misión", "Misiones", "misiones"], + 6: ["Hechizo", "hechizo", "Hechizos", "hechizos"], + 7: ["Zona", "zona", "Zonas", "zonas"], + 8: ["Facción", "facción", "Facciones", "facciones"], + 9: ["Mascota", "mascota", "Mascotas", "mascotas"], + 10: ["Logro", "logro", "Logros", "logros"], + 11: ["Título", "título", "Títulos", "títulos"], + 12: ["Suceso mundial", "evento del mundo", "Eventos del mundo", "eventos del mundo"], + 13: ["Clase", "Clase", "Clases", "Clases"], + 14: ["Raza", "raza", "Razas", "razas"], + 15: ["Habilidad", "habilidad", "Habilidades", "habilidades"], + 16: ["Atributo", "atributo", "Atributos", "atributos"], + 17: ["Monedas", "monedas", "Monedas", "monedas"], + 501: ["Emoción", "emoción", "Emociones", "emociones"], + 502: ["Encantamiento", "encantamiento", "Encantamientos", "encantamientos"] }, timeunitssg: ["año", "mes", "semana", "día", "hora", "minuto", "segundo"], @@ -3870,6 +3884,18 @@ var LANG = { teamcontrib5v5: "Contribución de equipo de arena 5v5" }, + // custom + fienchantments: { + id: "ID", + hascondition: "[Requires a combination of gems]", + requiresprof: "Requiere una profesión", + + sepcommunity: "Comunidad", + hascomments: "Tiene comentarios", + hasscreenshots: "Tiene capturas de pantalla", + hasvideos: "Tiene vídeos", + }, + pr_notice: '¿La primera vez? – ¡No temas! ¡Visita nuestra página de ayuda!   cerrar', pr_datasource: 'Los datos de esta pestaña se actualizarón por última vez el $2 por $1.', pr_purgedata: "Haz click para eliminar todos los datos recogidos en la pestaña actual.
      Solo aquel que ha subido los datos puede hacerlo.", diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 67cb6ddc..92aa75fc 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -770,6 +770,16 @@ var mn_currencies = [ [1,"Divers","?currencies=1"], [2,"JcJ","?currencies=2"] ]; +var mn_enchantments = [ + [1,"[Proc Spell]","?enchantments&filter=ty=1"], + [3,"[Equip Spell]","?enchantments&filter=ty=3"], + [7,"[Use Spell]","?enchantments&filter=ty=7"], + [8,"Châsse prismatique","?enchantments&filter=ty=8"], + [5,"Statistiques","?enchantments&filter=ty=5"], + [2,"Dégâts d'arme","?enchantments&filter=ty=2"], + [6,"DPS","?enchantments&filter=ty=6"], + [4,"Défense","?enchantments&filter=ty=4"] +]; var mn_talentCalc = [ [6,"Chevalier de la mort","?talent#j",,{className:"c6",tinyIcon:"class_deathknight"}], [11,"Druide","?talent#0",,{className:"c11",tinyIcon:"class_druid"}], @@ -836,7 +846,8 @@ var mn_database = [ [10,"Titres","?titles",mn_titles], [11,"Évènements mondiaux","?events",mn_holidays], [6,"Zones","?zones",mn_zones], - [100,"Emotes","?emotes",null] + [100,"Emotes","?emotes",null], + [101,"Enchantements","?enchantments",mn_enchantments] ]; var mn_tools = [ [0,"Calculateur de talents","?talent",mn_talentCalc], @@ -2595,6 +2606,7 @@ var LANG = { lvnote_arenateamsfound: "Total de $1 équipes d'aréna", lvnote_arenateamsfound2: "Total de $1 équipes d'aréna, $2 qui coïncides", lvnote_currenciesfound: "$1 monnaies trouvées ($2 affichées)", + lvnote_enchantmentsfound: "$1 enchantements trouvés ($2 affichés)", lvnote_createafilter: 'Créer un filtre', lvnote_filterresults: 'Filtrer ces résultats', @@ -3166,23 +3178,25 @@ var LANG = { myaccount_purgesuccess: "Les données d'annonce ont été purgées correctement!", types: { - 1: ["PNJ", "PNJ" , "PNJs", "PNJs"], - 2: ["Entité", "entité", "Entités", "entités"], - 3: ["Objet", "objet", "Objets", "objets"], - 4: ["Ensemble d'objets", "ensemble d'objets", "Ensembles d'objets", "ensembles d'objets"], - 5: ["Quête", "quête", "Quêtes", "quêtes"], - 6: ["Sort", "sort", "Sorts", "sorts"], - 7: ["Zone", "zone", "Zones", "zones"], - 8: ["Faction", "faction", "Factions", "factions"], - 9: ["Familier", "familier", "Familiers", "familiers"], - 10: ["Haut fait", "haut fait", "Hauts faits", "hauts faits"], - 11: ["Titre", "titre", "Titres", "titres"], - 12: ["Événement mondial", "évènement mondial", "Évènements mondiaux", "évènements mondiaux"], - 13: ["Classe", "classe", "Classes", "classes"], - 14: ["Race", "race", "Races", "races"], - 15: ["Compétence", "compétence", "Compétences", "compétences"], - 16: ["Statistique", "statistique", "Statistiques", "statistiques"], - 17: ["Monnaies", "monnaie", "Monnaies", "monnaies"] + 1: ["PNJ", "PNJ" , "PNJs", "PNJs"], + 2: ["Entité", "entité", "Entités", "entités"], + 3: ["Objet", "objet", "Objets", "objets"], + 4: ["Ensemble d'objets", "ensemble d'objets", "Ensembles d'objets", "ensembles d'objets"], + 5: ["Quête", "quête", "Quêtes", "quêtes"], + 6: ["Sort", "sort", "Sorts", "sorts"], + 7: ["Zone", "zone", "Zones", "zones"], + 8: ["Faction", "faction", "Factions", "factions"], + 9: ["Familier", "familier", "Familiers", "familiers"], + 10: ["Haut fait", "haut fait", "Hauts faits", "hauts faits"], + 11: ["Titre", "titre", "Titres", "titres"], + 12: ["Événement mondial", "évènement mondial", "Évènements mondiaux", "évènements mondiaux"], + 13: ["Classe", "classe", "Classes", "classes"], + 14: ["Race", "race", "Races", "races"], + 15: ["Compétence", "compétence", "Compétences", "compétences"], + 16: ["Statistique", "statistique", "Statistiques", "statistiques"], + 17: ["Monnaies", "monnaie", "Monnaies", "monnaies"], + 501: ["Emote", "emote", "Emotes", "emotes"], + 502: ["Enchantement", "enchantement", "Enchantements", "enchantements"] }, timeunitssg: ["année", "mois", "semaine", "jour", "heure", "minute", "seconde"], @@ -3857,6 +3871,18 @@ var LANG = { teamcontrib5v5: "Contribution d'un équipe d'aréna 5v5" }, + // custom + fienchantments: { + id: "ID", + hascondition: "[Requires a combination of gems]", + requiresprof: "Requiert un métier", + + sepcommunity: "Communauté", + hascomments: "A des commentaires", + hasscreenshots: "A des captures d'écrans", + hasvideos: "A des vidéos", + }, + pr_notice: 'Première fois? Ne soyez pas gêné! Visitez notre page d\'aide!   close', pr_datasource: 'Les données dans cette table ont été updatées $2 par $1.', pr_purgedata: "Cliquer pour supprimer toutes les données d'accomplissement dans l'onglet présent.
      Seul l'utilisateur qui a uploadé les données peut les effacer.", diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 04153054..3b8876f8 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -770,6 +770,16 @@ var mn_currencies = [ [1,"Разное","?currencies=22"], [2,"PvP","?currencies=2"] ]; +var mn_enchantments = [ + [1,"[Proc Spell]","?enchantments&filter=ty=1"], + [3,"[Equip Spell]","?enchantments&filter=ty=3"], + [7,"[Use Spell]","?enchantments&filter=ty=7"], + [8,"БеÑцветное гнездо","?enchantments&filter=ty=8"], + [5,"ХарактериÑтики","?enchantments&filter=ty=5"], + [2,"Урон оружиÑ","?enchantments&filter=ty=2"], + [6,"УВС","?enchantments&filter=ty=6"], + [4,"Защита","?enchantments&filter=ty=4"] +]; var mn_talentCalc = [ [6,"Рыцарь Ñмерти","?talent#j",,{className:"c6",tinyIcon:"class_deathknight"}], [11,"Друид","?talent#0",,{className:"c11",tinyIcon:"class_druid"}], @@ -836,7 +846,8 @@ var mn_database = [ [10,"ЗваниÑ","?titles",mn_titles], [11,"Игровые ÑобытиÑ","?events",mn_holidays], [6,"МеÑтноÑти","?zones",mn_zones], - [100,"Эмоции", "?emotes", null] + [100,"Эмоции","?emotes",null], + [101,"УлучшениÑ","?enchantments",mn_enchantments] ]; var mn_tools = [ [0,"РаÑчёт талантов","?talent",mn_talentCalc], @@ -2595,6 +2606,7 @@ var LANG = { lvnote_arenateamsfound: "Команд арены: $1", lvnote_arenateamsfound2: "Команд арены: $1, подходÑщих: $2", lvnote_currenciesfound: "Ðайдено валюты: $1 (показано: $2)", + lvnote_enchantmentsfound: "Ðайдено улучшениÑ: $1 (показано: $2)", lvnote_createafilter: 'Применить фильтр', lvnote_filterresults: 'Отфильтровать результаты', @@ -3166,23 +3178,25 @@ var LANG = { myaccount_purgesuccess: "Закрытые объÑÐ²Ð»ÐµÐ½Ð¸Ñ ÑƒÑпешно Ñброшены!", types: { - 1: ["ÐИП", "ÐИП" , "ÐИП", "ÐИП"], - 2: ["Объект", "объект", "Объекты", "объекты"], - 3: ["Предмет", "предмет", "Предметы", "предметы"], - 4: ["Комплект", "комплект", "Комплекты", "комплекты"], - 5: ["Задание", "задание", "ЗаданиÑ", "задание"], - 6: ["Заклинание", "заклинание", "ЗаклинаниÑ", "заклинаниÑ"], - 7: ["Ð˜Ð³Ñ€Ð¾Ð²Ð°Ñ Ð·Ð¾Ð½Ð°", "Ð˜Ð³Ñ€Ð¾Ð²Ð°Ñ Ð·Ð¾Ð½Ð°", "МеÑтноÑти", "меÑтноÑти"], - 8: ["ФракциÑ", "фракциÑ", "Фракции", "фракции"], - 9: ["Питомец", "питомец", "Питомцы", "питомцы"], - 10: ["ДоÑтижение", "доÑтижение", "ДоÑтижениÑ", "доÑтижениÑ"], - 11: ["Звание", "звание", "ЗваниÑ", "званиÑ"], - 12: ["Событие", "игровое Ñобытие", "Игровые ÑобытиÑ", "игровые ÑобытиÑ"], - 13: ["КлаÑÑ", "клаÑÑ", "КлаÑÑÑ‹", "клаÑÑÑ‹"], - 14: ["РаÑа", "раÑа", "РаÑÑ‹", "раÑÑ‹"], - 15: ["Уровень навыка", "навык", "УмениÑ", "навыки"], - 16: ["СтатиÑтика", "характериÑтика", "ХарактериÑтики", "характериÑтики"], - 17: ["Валюта", "валюта", "Валюта", "валюта"] + 1: ["ÐИП", "ÐИП" , "ÐИП", "ÐИП"], + 2: ["Объект", "объект", "Объекты", "объекты"], + 3: ["Предмет", "предмет", "Предметы", "предметы"], + 4: ["Комплект", "комплект", "Комплекты", "комплекты"], + 5: ["Задание", "задание", "ЗаданиÑ", "задание"], + 6: ["Заклинание", "заклинание", "ЗаклинаниÑ", "заклинаниÑ"], + 7: ["Ð˜Ð³Ñ€Ð¾Ð²Ð°Ñ Ð·Ð¾Ð½Ð°", "Ð˜Ð³Ñ€Ð¾Ð²Ð°Ñ Ð·Ð¾Ð½Ð°", "МеÑтноÑти", "меÑтноÑти"], + 8: ["ФракциÑ", "фракциÑ", "Фракции", "фракции"], + 9: ["Питомец", "питомец", "Питомцы", "питомцы"], + 10: ["ДоÑтижение", "доÑтижение", "ДоÑтижениÑ", "доÑтижениÑ"], + 11: ["Звание", "звание", "ЗваниÑ", "званиÑ"], + 12: ["Событие", "игровое Ñобытие", "Игровые ÑобытиÑ", "игровые ÑобытиÑ"], + 13: ["КлаÑÑ", "клаÑÑ", "КлаÑÑÑ‹", "клаÑÑÑ‹"], + 14: ["РаÑа", "раÑа", "РаÑÑ‹", "раÑÑ‹"], + 15: ["Уровень навыка", "навык", "УмениÑ", "навыки"], + 16: ["СтатиÑтика", "характериÑтика", "ХарактериÑтики", "характериÑтики"], + 17: ["Валюта", "валюта", "Валюта", "валюта"], + 501: ["ЭмоциÑ", "ÑмоциÑ", "Эмоции", "Ñмоции"], + 502: ["Улучшение", "улучшение", "УлучшениÑ", "улучшениÑ"] }, timeunitssg: ["год", "меÑÑц", "неделÑ", "день", "чаÑ", "минута", "Ñекунда"], @@ -3858,6 +3872,18 @@ var LANG = { teamcontrib5v5: "Очки команды арены 5Ñ…5" }, + // custom + fienchantments: { + id: "Ðомер", + hascondition: "[Requires a combination of gems]", + requiresprof: "ТребуетÑÑ Ð¿Ñ€Ð¾Ñ„ÐµÑÑиÑ", + + sepcommunity: "СообщеÑтво", + hascomments: "ЕÑть комментарии", + hasscreenshots: "ЕÑть изображениÑ", + hasvideos: "ЕÑть видео", + }, + pr_notice: 'Первый раз? – Ðе ÑтеÑнÑйтеÑÑŒ! ВзглÑните на Ñтраницу помощи!   закрыть', pr_datasource: 'Данные в Ñтой вкладке были поÑледний раз обновлены пользователем $1 $2.', pr_purgedata: "Ðажмите, чтобы удалить вÑе Ñобранные данные в текущей вкладке.
      Только тот, кто загрузил данные, может их удалить.", diff --git a/template/listviews/enchantment.tpl.php b/template/listviews/enchantment.tpl.php new file mode 100644 index 00000000..2527e3f8 --- /dev/null +++ b/template/listviews/enchantment.tpl.php @@ -0,0 +1,158 @@ +Listview.templates.enchantment = { + sort: [1], + searchable: 1, + filtrable: 1, + + columns: [ + { + id: 'name', + name: LANG.name, + type: 'text', + align: 'left', + value: 'name', + compute: function(enchantment, td, tr) { + var + wrapper = $WH.ce('div'); + + var a = $WH.ce('a'); + a.style.fontFamily = 'Verdana, sans-serif'; + a.href = this.getItemLink(enchantment); + + if (!enchantment.name) { + var i = $WH.ce('i'); + i.className = 'q0'; + + $WH.ae(i, $WH.ct('<' + LANG.pr_header_noname + '>')); + $WH.ae(a, i); + } + else + $WH.ae(a, $WH.ct(enchantment.name)); + + $WH.ae(wrapper, a); + + $WH.ae(td, wrapper); + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(a.name, b.name); + }, + getVisibleText: function(enchantment) { + return enchantment.name || LANG.pr_header_noname; + } + }, + { + id: 'trigger', + name: LANG.types[6][2], + type: 'text', + align: 'left', + value: 'name', + width: '10%', + hidden: 1, + compute: function(enchantment, td, tr) { + if (enchantment.spells) { + var d = $WH.ce('div'); + d.style.width = (26 * Object.keys(enchantment.spells).length) + 'px'; + d.style.margin = '0 auto'; + + $.each(enchantment.spells, function (spellId, charges) { + var icon = g_spells.createIcon(spellId, 0, charges); + icon.style.cssFloat = icon.style.styleFloat = 'left'; + $WH.ae(d, icon); + }); + + $WH.ae(td, d); + } + }, + getVisibleText: function(enchantment) { + if (!enchantment.spells) + return null; // no spell + + var spellId = $(enchantment.spells).first(); + if (g_spells[spellId]) + return g_spells[spellId]['name_' + Locale.getName()]; + + return ''; // unk spell + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(this.getVisibleText(a), this.getVisibleText(b)); + } + }, + { + id: 'skill', + name: LANG.skill, + type: 'text', + width: '16%', + getValue: function(enchantment) { + return enchantment.reqskillrank || 0; + }, + compute: function(enchantment, td, tr) { + if (enchantment.reqskill != null) { + var div = $WH.ce('div'); + div.className = 'small'; + + // DK is the only class in use + if (enchantment.reqskill == 776) { + var a = $WH.ce('a'); + a.className = 'q0'; + a.href = '?class=6'; + + $WH.ae(a, $WH.ct(g_chr_classes[6])); + $WH.ae(div, a); + $WH.ae(div, $WH.ce('br')); + } + + var a = $WH.ce('a'); + a.className = 'q1'; + a.href = '?skill=' + enchantment.reqskill; + + $WH.ae(a, $WH.ct(g_spell_skills[enchantment.reqskill])); + $WH.ae(div, a); + + if (enchantment.reqskillrank > 0) { + var sp = $WH.ce('span'); + + sp.className = 'q0'; + $WH.ae(sp, $WH.ct(' (' + enchantment.reqskillrank + ')')); + $WH.ae(div, sp); + } + + $WH.ae(td, div); + } + }, + getVisibleText: function(enchantment) { + var buff = ''; + if (g_spell_skills[enchantment.reqskill]) { + buff += g_spell_skills[enchantment.reqskill]; + + if (enchantment.reqskillrank > 0) { + buff += ' ' + enchantment.reqskillrank; + } + } + return buff; + }, + sortFunc: function(a, b, col) { + return $WH.strcmp(g_spell_skills[a.reqskill], g_spell_skills[b.reqskill]) || $WH.strcmp(a.reqskillrank, b.reqskillrank); + } + }, + ], + getItemLink: function(enchantment) { + return '?enchantment=' + enchantment.id; + } +} + +new Listview({ + template:'enchantment', + $v): + if ($v[0] == '$'): + echo $k.':'.substr($v, 1).','; + elseif ($v): + echo $k.":'".$v."',"; + endif; + endforeach; +?> + data: +}); diff --git a/template/pages/enchantment.tpl.php b/template/pages/enchantment.tpl.php new file mode 100644 index 00000000..b453f315 --- /dev/null +++ b/template/pages/enchantment.tpl.php @@ -0,0 +1,118 @@ +brick('header'); ?> + +
      +
      +
      + +brick('announcement'); + + $this->brick('pageTemplate'); + + $this->brick('infobox'); +?> + +
      + +brick('redButtons'); ?> + +

      name; ?>

      + +
      + +brick('article'); ?> + + +

      + + + + + + + +activateCondition)): +?> + + + + +effects as $i => $e): +?> + + + + + +
      activateCondition; ?>
      +)' : '').''; + + if (isset($e['value'])): + echo '
      '.Lang::spell('_value').Lang::main('colon').$e['value']; + endif; + + if (!empty($e['proc'])): + echo '
      '; + + if ($e['proc'] < 0): + echo sprintf(Lang::spell('ppm'), -$e['proc']); + elseif ($e['proc'] < 100.0): + echo Lang::spell('procChance').Lang::main('colon').$e['proc'].'%'; + endif; + endif; + + echo "
      \n"; + + if (!empty($e['tip'])): +?> + + + + + +'.(strpos($e['icon']['name'], '#') ? $e['icon']['name'] : sprintf('%s', $e['icon']['id'], $e['icon']['name']))."\n"; +?> + + +
      + + +
      + +

      +
      + +brick('lvTabs', ['relTabs' => true]); + +$this->brick('contribute'); +?> +
      +
      +
      + +brick('footer'); ?> diff --git a/template/pages/enchantments.tpl.php b/template/pages/enchantments.tpl.php new file mode 100644 index 00000000..735adb3d --- /dev/null +++ b/template/pages/enchantments.tpl.php @@ -0,0 +1,76 @@ +brick('header'); +$f = $this->filter; // shorthand +?> + +
      +
      +
      + +brick('announcement'); + +$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 1]]); +?> + +
      +
      +
      +
      + +
      + +
      + + + + + +
      + + +
       />
      +
      + +
      +
      + +
      + /> /> +
      + +
      + +
      + + +
      + +
      +
      +
      + + + +brick('lvTabs'); ?> + +
      +
      +
      + +brick('footer'); ?> diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index 9b710a68..1df5b188 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -43,8 +43,13 @@ if (!empty($this->subItems)): subItems['data'] as $k => $i): if ($k < (count($this->subItems['data']) / 2)): + $eText = []; + foreach ($i['enchantment'] as $eId => $txt): + $eText[] = ''.$txt.''; + endforeach; + echo '
    • ...'.$i['name'].''; - echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.$i['enchantment'].'
    • '; + echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).''; endif; endforeach; ?> @@ -58,8 +63,13 @@ if (!empty($this->subItems)): subItems['data'] as $k => $i): if ($k >= (count($this->subItems['data']) / 2)): + $eText = []; + foreach ($i['enchantment'] as $eId => $txt): + $eText[] = ''.$txt.''; + endforeach; + echo '
    • ...'.$i['name'].''; - echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.$i['enchantment'].'
    • '; + echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).''; endif; endforeach; ?> diff --git a/template/pages/spell.tpl.php b/template/pages/spell.tpl.php index fd58e553..ae6724b8 100644 --- a/template/pages/spell.tpl.php +++ b/template/pages/spell.tpl.php @@ -215,7 +215,7 @@ foreach ($this->effects as $i => $e):
      '; // Class - $x .= ''; + if ($_slot) + $x .= ''; // Subclass if ($_class == ITEM_CLASS_ARMOR && $_subClass > 0) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 12dbde58..23d5d8ce 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -394,6 +394,7 @@ $lang = array( 'enchantment' => array( 'details' => "Details", 'activation' => "Aktivierung", + 'notFound' => "Diese Verzauberung existiert nicht.", 'types' => array( 1 => "Zauber (Auslösung)", 3 => "Zauber (Anlegen)", 7 => "Zauber (Benutzen)", 8 => "Prismatischer Sockel", 5 => "Statistik", 2 => "Waffenschaden", 6 => "DPS", 4 => "Verteidigung" diff --git a/localization/locale_enus.php b/localization/locale_enus.php index f52e83ff..6fb4f55f 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -389,6 +389,7 @@ $lang = array( 'enchantment' => array( 'details' => "Details", 'activation' => "Activation", + 'notFound' => "This enchantment doesn't exist.", 'types' => array( 1 => "Proc Spell", 3 => "Equip Spell", 7 => "Use Spell", 8 => "Prismatic Socket", 5 => "Statistics", 2 => "Weapon Damage", 6 => "DPS", 4 => "Defense" diff --git a/localization/locale_eses.php b/localization/locale_eses.php index cc4ce2b3..c8a3b2c4 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -395,6 +395,7 @@ $lang = array( 'enchantment' => array( 'details' => "Detalles", 'activation' => "Activación", + 'notFound' => "Este encantamiento no existe.", 'types' => array( 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "Ranura prismática", 5 => "Atributos", 2 => "Daño de arma", 6 => "DPS", 4 => "Defensa" diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 406685f8..7953a2f2 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -394,6 +394,7 @@ $lang = array( 'enchantment' => array( 'details' => "En détail", 'activation' => "Activation", + 'notFound' => "Cet enchantement n'existe pas.", 'types' => array( 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "Châsse prismatique", 5 => "Statistiques", 2 => "Dégâts d'arme", 6 => "DPS", 4 => "Défense" diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 5441bbd8..b251b6e4 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -394,6 +394,7 @@ $lang = array( 'enchantment' => array( 'details' => "ПодробноÑти", 'activation' => "Ðктивации", + 'notFound' => "Такой улучшение не ÑущеÑтвует.", 'types' => array( 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "БеÑцветное гнездо", 5 => "ХарактериÑтики", 2 => "Урон оружиÑ", 6 => "УВС", 4 => "Защита" diff --git a/pages/enchantment.php b/pages/enchantment.php index f082a3b8..a964b57a 100644 --- a/pages/enchantment.php +++ b/pages/enchantment.php @@ -95,9 +95,10 @@ class EnchantmentPage extends GenericPage case 7: $sArr = $this->subject->getField('spells')[$i]; $spl = $this->subject->getRelSpell($sArr[0]); - $this->effects[$i]['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Type: '.$_ty, Lang::item('trigger', $sArr[1])) : Lang::item('trigger', $sArr[1]); - $this->effects[$i]['proc'] = $sArr[3]; - $this->effects[$i]['icon'] = array( + $this->effects[$i]['name'] = User::isInGroup(U_GROUP_EMPLOYEE) ? sprintf(Util::$dfnString, 'Type: '.$_ty, Lang::item('trigger', $sArr[1])) : Lang::item('trigger', $sArr[1]); + $this->effects[$i]['proc'] = $sArr[3]; + $this->effects[$i]['value'] = $_qty ?: null; + $this->effects[$i]['icon'] = array( 'name' => !$spl ? Util::ucFirst(Lang::game('spell')).' #'.$sArr[0] : Util::localizedString($spl, 'name'), 'id' => $sArr[0], 'count' => $sArr[2] diff --git a/pages/spell.php b/pages/spell.php index ff2098e7..fb539289 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -1124,6 +1124,25 @@ class SpellPage extends GenericPage $this->extendGlobalData($tbItem->getJSGlobals(GLOBALINFO_SELF)); } + // tab: enchantments + $conditions = array( + 'OR', + ['AND', ['type1', [1, 3, 7]], ['object1', $this->typeId]], + ['AND', ['type2', [1, 3, 7]], ['object2', $this->typeId]], + ['AND', ['type3', [1, 3, 7]], ['object3', $this->typeId]] + ); + $enchList = new EnchantmentList($conditions); + if (!$enchList->error) + { + $this->lvTabs[] = array( + 'file' => 'enchantment', + 'data' => $enchList->getListviewData(), + 'params' => [] + ); + + $this->extendGlobalData($enchList->getJSGlobals()); + } + // find associated NPC, Item and merge results // taughtbypets (unused..?) // taughtbyquest (usually the spell casted as quest reward teaches something; exclude those seplls from taughtBySpell) From 1d5436bbd24e2df6380ca62082f0317725d5b36c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 3 Aug 2015 23:16:11 +0200 Subject: [PATCH 0053/1249] don't ask.. --- setup/updates/1438620486_01.sql | 4 +++- template/pages/enchantment.tpl.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setup/updates/1438620486_01.sql b/setup/updates/1438620486_01.sql index 6e3cc6da..2c56ff11 100644 --- a/setup/updates/1438620486_01.sql +++ b/setup/updates/1438620486_01.sql @@ -35,7 +35,9 @@ ALTER TABLE `aowow_item_stats` CHANGE COLUMN `id` `typeId` mediumint(9) unsigned NOT NULL AFTER `type`, DROP INDEX `item`, DROP PRIMARY KEY, - ADD PRIMARY KEY (`typeId`, `type`) + ADD PRIMARY KEY (`typeId`, `type`); + +UPDATE `aowow_item_stats` SET `type` = 3; ALTER TABLE `aowow_articles` ALTER `type` DROP DEFAULT, diff --git a/template/pages/enchantment.tpl.php b/template/pages/enchantment.tpl.php index 5e60ba58..33dcf50b 100644 --- a/template/pages/enchantment.tpl.php +++ b/template/pages/enchantment.tpl.php @@ -38,7 +38,7 @@ if (!empty($this->activateCondition)): -effects as $i => $e): From f35be8fa36db720b1aacf3f18c4f7583ff431c5e Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 00:34:08 +0200 Subject: [PATCH 0054/1249] Util/MostComments * fixed wrong indexing on counting comments per page --- pages/utility.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/utility.php b/pages/utility.php index b01bf6b5..b8c70f36 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -214,7 +214,7 @@ class UtilityPage extends GenericPage continue; $comments = DB::Aowow()->selectCol(' - SELECT `typeId` AS ARRAY_KEY, count(1) AS ncomments FROM ?_comments + SELECT `typeId` AS ARRAY_KEY, count(1) FROM ?_comments WHERE `replyTo` = 0 AND (`flags` & ?d) = 0 AND `type`= ?d AND `date` > (UNIX_TIMESTAMP() - ?d) GROUP BY `type`, `typeId` LIMIT 100', @@ -238,14 +238,14 @@ class UtilityPage extends GenericPage 'title' => [true, [], htmlentities(Util::$typeStrings[$type] == 'item' ? substr($d['name'], 1) : $d['name'])], 'type' => [false, [], Util::$typeStrings[$type]], 'link' => [false, [], HOST_URL.'/?'.Util::$typeStrings[$type].'='.$d['id']], - 'ncomments' => [false, [], $comments[$typeId]['ncomments']] + 'ncomments' => [false, [], $comments[$typeId]] ); } } else { foreach ($data as $typeId => &$d) - $d['ncomments'] = $comments[$typeId]['ncomments']; + $d['ncomments'] = $comments[$typeId]; $this->extendGlobalData($typeClass->getJSGlobals(GLOBALINFO_ANY)); $this->lvTabs[] = array( From b1a6911edef413404ce1031ae9f1782c0d575c26 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 02:14:12 +0200 Subject: [PATCH 0055/1249] Optimization * minor improvements to the use of Util::checkNumeric * gargantuan improvements to ItemList handling random enchantments (sec -> ms) --- includes/types/basetype.class.php | 11 +++----- includes/types/enchantment.class.php | 3 ++- includes/types/item.class.php | 38 ++++++++++++---------------- includes/utilities.php | 5 +--- 4 files changed, 22 insertions(+), 35 deletions(-) diff --git a/includes/types/basetype.class.php b/includes/types/basetype.class.php index 22c4e0b5..23f9bc1b 100644 --- a/includes/types/basetype.class.php +++ b/includes/types/basetype.class.php @@ -312,14 +312,9 @@ abstract class BaseType return Util::localizedString($this->curTpl, $field, $silent); $value = $this->curTpl[$field]; - if (Util::checkNumeric($value)) - { - $intVal = intVal($value); - $floatVal = floatVal($value); - return $intVal == $floatVal ? $intVal : $floatVal; - } - else - return $value; + Util::checkNumeric($value); + + return $value; } public function getRandomId() diff --git a/includes/types/enchantment.class.php b/includes/types/enchantment.class.php index 125889df..a23b728f 100644 --- a/includes/types/enchantment.class.php +++ b/includes/types/enchantment.class.php @@ -53,7 +53,8 @@ class EnchantmentList extends BaseType } // floats are fetched as string from db :< - Util::checkNumeric($curTpl); + $curTpl['dmg'] = floatVal($curTpl['dmg']); + $curTpl['dps'] = floatVal($curTpl['dps']); // remove zero-stats foreach (Util::$itemMods as $str) diff --git a/includes/types/item.class.php b/includes/types/item.class.php index c52f2e43..92463c7f 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -1410,6 +1410,22 @@ class ItemList extends BaseType return; $randEnchants = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_itemrandomenchant WHERE id IN (?a)', $randIds); + $enchIds = array_unique(array_merge( + array_column($randEnchants, 'enchantId1'), + array_column($randEnchants, 'enchantId2'), + array_column($randEnchants, 'enchantId3'), + array_column($randEnchants, 'enchantId4'), + array_column($randEnchants, 'enchantId5') + )); + + $enchants = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); + foreach ($enchants->iterate() as $eId => $_) + { + $this->rndEnchIds[$eId] = array( + 'text' => $enchants->getField('name', true), + 'stats' => $enchants->getStatGain() + ); + } foreach ($this->iterate() as $mstItem => $__) { @@ -1427,28 +1443,6 @@ class ItemList extends BaseType $data = array_merge($randEnchants[$subId], $data); $jsonEquip = []; $jsonText = []; - $enchIds = []; - - for ($i = 1; $i < 6; $i++) - { - $enchId = $data['enchantId'.$i]; - if ($enchId <= 0) - continue; - - if (isset($this->rndEnchIds[$enchId])) - continue; - - $enchIds[] = $enchId; - } - - $enchants = new EnchantmentList(array(['id', $enchIds], CFG_SQL_LIMIT_NONE)); - foreach ($enchants->iterate() as $eId => $_) - { - $this->rndEnchIds[$eId] = array( - 'text' => $enchants->getField('name', true), - 'stats' => $enchants->getStatGain() - ); - } for ($i = 1; $i < 6; $i++) { diff --git a/includes/utilities.php b/includes/utilities.php index 79aaee37..0fb55b03 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1123,10 +1123,7 @@ class Util if (is_numeric($data)) { - $_int = intVal($data); - $_float = floatVal($data); - - $data = ($_int == $_float) ? $_int : $_float; + $data += 0; return true; } else if (preg_match('/^\d*,\d+$/', $data)) From e2beb82f2917b9b0bd317df90fc4e8b66f57e518 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 14:02:51 +0200 Subject: [PATCH 0056/1249] Items * display randomEnchantment-Id as title on suffix-text on detail page Compare * fixed stats display in subitem-picker --- pages/compare.php | 3 +++ pages/item.php | 3 +++ static/css/aowow.css | 4 ++++ template/pages/item.tpl.php | 4 ++-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pages/compare.php b/pages/compare.php index de8f1ea9..0c047bb6 100644 --- a/pages/compare.php +++ b/pages/compare.php @@ -74,6 +74,9 @@ class ComparePage extends GenericPage if (empty($data[$itemId])) continue; + foreach ($data[$itemId]['subitems'] as &$si) + $si['enchantment'] = implode(', ', $si['enchantment']); + $this->cmpItems[] = [ $itemId, $iList->getField('name', true), diff --git a/pages/item.php b/pages/item.php index a4e4f242..fd71076f 100644 --- a/pages/item.php +++ b/pages/item.php @@ -377,10 +377,12 @@ class ItemPage extends genericPage uaSort($this->subject->subItems[$this->typeId], function($a, $b) { return strcmp($a['name'], $b['name']); }); $this->subItems = array( 'data' => array_values($this->subject->subItems[$this->typeId]), + 'randIds' => array_keys($this->subject->subItems[$this->typeId]), 'quality' => $this->subject->getField('quality') ); // merge identical stats and names for normal users (e.g. spellPower of a specific school became generel spellPower with 3.0) + if (!User::isInGroup(U_GROUP_EMPLOYEE)) { for ($i = 1; $i < count($this->subItems['data']); $i++) @@ -391,6 +393,7 @@ class ItemPage extends genericPage { $prev['chance'] += $cur['chance']; array_splice($this->subItems['data'], $i , 1); + array_splice($this->subItems['randIds'], $i , 1); $i = 1; } } diff --git a/static/css/aowow.css b/static/css/aowow.css index 464b2940..73801da6 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -171,6 +171,10 @@ h5 a.icontiny span { text-decoration:none !important; } width: 47%; } +.random-enchantments span{ + cursor: help; +} + h1.h1-icon { padding-top: 5px !important; } diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index 1df5b188..a92b8567 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -48,7 +48,7 @@ if (!empty($this->subItems)): $eText[] = ''.$txt.''; endforeach; - echo '
    • ...'.$i['name'].''; + echo '
    • ...'.$i['name'].''; echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).'
    • '; endif; endforeach; @@ -68,7 +68,7 @@ if (!empty($this->subItems)): $eText[] = ''.$txt.''; endforeach; - echo '
    • ...'.$i['name'].''; + echo '
    • ...'.$i['name'].''; echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).'
    • '; endif; endforeach; From e28c00ccda5ae5a392a0a3d973ffa8b4a8938bec Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 14:44:57 +0200 Subject: [PATCH 0057/1249] Misc: * also load item-scaling data for spell-tooltips * fixed an event reference in the markup-guide (holidayIds became eventIds some time ago) * removed underline from anchor elements * also increment version --- includes/shared.php | 2 +- setup/db_structure.sql | 2 +- setup/tools/filegen/templates/power.js.in | 3 ++- static/css/aowow.css | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 1c6e0a46..04bd9d2e 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(-13,6,0,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(-13,4,0,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(-13,1,0,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(-10,0,0,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-16,0,0,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(-8,0,0,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(-99,0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(-3,0,0,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(-7,0,0,'[small]this page for example[/small]',NULL),(-1,0,0,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(-99,0,3,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(-99,0,6,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(-99,0,0,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(-13,7,0,'Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: www (default), en, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=341] -> [event=341]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL); +INSERT INTO `aowow_articles` VALUES (13,4,0,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]',NULL),(14,1,0,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.',NULL),(13,1,0,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]',NULL),(13,2,0,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]',NULL),(14,2,0,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.',NULL),(13,3,0,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]',NULL),(13,5,0,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]',NULL),(13,6,0,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]',NULL),(13,7,0,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]',NULL),(13,8,0,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]',NULL),(13,9,0,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]',NULL),(13,11,0,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]',NULL),(14,3,0,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.',NULL),(14,4,0,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.',NULL),(14,5,0,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.',NULL),(14,6,0,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.',NULL),(14,7,0,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.',NULL),(14,8,0,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.',NULL),(14,10,0,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.',NULL),(14,11,0,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.',NULL),(8,21,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=301], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.',NULL),(8,47,0,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.',NULL),(8,54,0,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.',NULL),(8,59,0,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]',NULL),(8,68,0,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.',NULL),(8,69,0,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.',NULL),(8,70,0,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.',NULL),(8,72,0,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].',NULL),(8,76,0,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.',NULL),(8,81,0,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.',NULL),(8,87,0,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].',NULL),(8,92,0,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.',NULL),(8,93,0,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.',NULL),(8,270,0,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.',NULL),(8,349,0,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].',NULL),(8,369,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.',NULL),(8,470,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.',NULL),(8,471,0,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.',NULL),(8,509,0,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].',NULL),(8,510,0,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].',NULL),(8,529,0,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.',NULL),(8,530,0,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].',NULL),(8,576,0,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]',NULL),(-13,0,0,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,5,0,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,3,0,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,2,0,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(-13,6,0,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(-13,4,0,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(-13,1,0,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(-10,0,0,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-16,0,0,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(-8,0,0,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(-99,0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(-3,0,0,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(-7,0,0,'[small]this page for example[/small]',NULL),(-1,0,0,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(-99,0,3,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(-99,0,6,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(-99,0,0,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(-13,7,0,'Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: www (default), en, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL); /*!40000 ALTER TABLE `aowow_articles` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/tools/filegen/templates/power.js.in b/setup/tools/filegen/templates/power.js.in index 1316162b..56438dd6 100644 --- a/setup/tools/filegen/templates/power.js.in +++ b/setup/tools/filegen/templates/power.js.in @@ -83,7 +83,8 @@ if (typeof $WowheadPower == "undefined") { 100: [profiles, "profile", "Profile" ] }, SCALES = { - 3: { url: "?data=item-scaling" } + 3: { url: "?data=item-scaling" }, + 6: { url: "?data=item-scaling" } }, LOCALES = { 0: "enus", diff --git a/static/css/aowow.css b/static/css/aowow.css index 73801da6..77a9f79b 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -931,7 +931,6 @@ a.dialog-right { .icontinyr { padding-right: 18px !important; background: right center no-repeat; } .icontinyl { padding-left: 18px !important; background: left center no-repeat; } a.icontiny { text-decoration: none; } -a.icontiny span /* span */ { text-decoration:underline; } span.icontiny, a.tinyspecial { padding-left:18px !important; background:left center no-repeat; } /* From ef686c9c575b8634ddb23d569b71a8e47d4864da Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 16:04:55 +0200 Subject: [PATCH 0058/1249] Achievements: * display criterium-Ids for everyone on detail page * unified display with subitems on item detail page --- static/css/aowow.css | 4 ---- template/pages/achievement.tpl.php | 8 ++------ template/pages/item.tpl.php | 4 ++-- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/static/css/aowow.css b/static/css/aowow.css index 77a9f79b..391a59ef 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -171,10 +171,6 @@ h5 a.icontiny span { text-decoration:none !important; } width: 47%; } -.random-enchantments span{ - cursor: help; -} - h1.h1-icon { padding-top: 5px !important; } diff --git a/template/pages/achievement.tpl.php b/template/pages/achievement.tpl.php index 6c037e67..e7de7bc3 100644 --- a/template/pages/achievement.tpl.php +++ b/template/pages/achievement.tpl.php @@ -37,7 +37,7 @@ foreach ($this->criteria['data'] as $i => $cr): echo '
      •  
      '; endif; - echo '
      '; + echo ''; // every odd number of elements if ($i + 1 == round(count($this->criteria['data']) / 2)): diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index a92b8567..8717f9a6 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -48,7 +48,7 @@ if (!empty($this->subItems)): $eText[] = ''.$txt.''; endforeach; - echo '
    • ...'.$i['name'].''; + echo '
    • ...'.$i['name'].''; echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).'
    • '; endif; endforeach; @@ -68,7 +68,7 @@ if (!empty($this->subItems)): $eText[] = ''.$txt.''; endforeach; - echo '
    • ...'.$i['name'].''; + echo '
    • ...'.$i['name'].''; echo ' '.sprintf(Lang::item('_chance'), $i['chance']).'
      '.implode(', ', $eText).'
    • '; endif; endforeach; From 320ad252d1f3db2b28e6a8c3dd21410273fc19a7 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 19:02:23 +0200 Subject: [PATCH 0059/1249] Tooltips/Items * skipping an error, when handling heirloom-armor with scaled level on external tooltips the tooltip will now be displayed but without the armor-class changing at level 40 * prevent assigning arbitrary randomEnchantments to items (e.g. Warglaive of Azzinoth of the Whale) --- includes/shared.php | 2 +- includes/types/item.class.php | 34 ++++++++++++++++++++++++++++------ pages/item.php | 2 +- static/js/basic.js | 3 ++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 04bd9d2e..8278509f 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ getRandEnchantForItem($enhance['r'])) + $res .= ' '.Util::localizedString($this->enhanceR, 'name'); + + return $res; + } + public function renderTooltip($interactive = false, $subOf = 0, $enhance = []) { if ($this->error) @@ -455,20 +466,20 @@ class ItemList extends BaseType if (!empty($enhance['r'])) { - if ($rndEnch = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomenchant WHERE Id = ?d', $enhance['r'])) + if ($this->getRandEnchantForItem($enhance['r'])) { - $_name .= ' '.Util::localizedString($rndEnch, 'name'); + $_name .= ' '.Util::localizedString($this->enhanceR, 'name'); $randEnchant = ''; for ($i = 1; $i < 6; $i++) { - if ($rndEnch['enchantId'.$i] <= 0) + if ($this->enhanceR['enchantId'.$i] <= 0) continue; - $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $rndEnch['enchantId'.$i]); - if ($rndEnch['allocationPct'.$i] > 0) + $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE Id = ?d', $this->enhanceR['enchantId'.$i]); + if ($this->enhanceR['allocationPct'.$i] > 0) { - $amount = intVal($rndEnch['allocationPct'.$i] * $this->generateEnchSuffixFactor()); + $amount = intVal($this->enhanceR['allocationPct'.$i] * $this->generateEnchSuffixFactor()); $randEnchant .= ''.str_replace('$i', $amount, Util::localizedString($enchant, 'name')).'
      '; } else @@ -1080,6 +1091,17 @@ class ItemList extends BaseType return $x; } + private function getRandEnchantForItem($randId) + { + // is it available for this item? .. does it even exist?! + if (empty($this->enhanceR)) + if (DB::World()->selectCell('SELECT 1 FROM item_enchantment_template WHERE entry = ?d AND ench = ?d', abs($this->getField('randomEnchant')), abs($randId))) + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomenchant WHERE Id = ?d', $randId)) + $this->enhanceR = $_; + + return !empty($this->enhanceR); + } + // from Trinity public function generateEnchSuffixFactor() { diff --git a/pages/item.php b/pages/item.php index fd71076f..4c1f142a 100644 --- a/pages/item.php +++ b/pages/item.php @@ -983,7 +983,7 @@ class ItemPage extends genericPage return '$WowheadPower.registerItem(\''.$itemString.'\', '.User::$localeId.', {})'; $x = '$WowheadPower.registerItem(\''.$itemString.'\', '.User::$localeId.", {\n"; - $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true))."',\n"; + $x .= "\tname_".User::$localeString.": '".Util::jsEscape($this->subject->getField('name', true, $this->enhancedTT))."',\n"; $x .= "\tquality: ".$this->subject->getField('quality').",\n"; $x .= "\ticon: '".urlencode($this->subject->getField('iconString'))."',\n"; $x .= "\ttooltip_".User::$localeString.": '".Util::jsEscape($this->subject->renderTooltip(false, 0, $this->enhancedTT))."'\n"; diff --git a/static/js/basic.js b/static/js/basic.js index 32c864d6..aff4535c 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -1378,7 +1378,8 @@ $WH.g_setTooltipLevel = function(tooltip, level) { } // Always keep the base armor type - return prefix + g_itemset_types[_]; + return $WH.isset('g_itemset_types') ? prefix + g_itemset_types[_] : _all; // sarjuuk: LANG is not available if the tooltip is included externaly + // return prefix + g_itemset_types[_]; }); // Update min-max damage From 7ce72bf623ff9b77c04e96024251889dbf4b0c8c Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 21:16:27 +0200 Subject: [PATCH 0060/1249] Articles * convert all holidayIds to eventIds (by TC standard assignment) --- setup/db_structure.sql | 2 +- setup/updates/1438715648_01.sql | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 setup/updates/1438715648_01.sql diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 8622423f..5a130b1e 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2313,7 +2313,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_articles` WRITE; /*!40000 ALTER TABLE `aowow_articles` DISABLE KEYS */; -INSERT INTO `aowow_articles` VALUES (13,4,0,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]',NULL),(14,1,0,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.',NULL),(13,1,0,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]',NULL),(13,2,0,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]',NULL),(14,2,0,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.',NULL),(13,3,0,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]',NULL),(13,5,0,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]',NULL),(13,6,0,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]',NULL),(13,7,0,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]',NULL),(13,8,0,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]',NULL),(13,9,0,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]',NULL),(13,11,0,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]',NULL),(14,3,0,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.',NULL),(14,4,0,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.',NULL),(14,5,0,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.',NULL),(14,6,0,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.',NULL),(14,7,0,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.',NULL),(14,8,0,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.',NULL),(14,10,0,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.',NULL),(14,11,0,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.',NULL),(8,21,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=301], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.',NULL),(8,47,0,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.',NULL),(8,54,0,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.',NULL),(8,59,0,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]',NULL),(8,68,0,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.',NULL),(8,69,0,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.',NULL),(8,70,0,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.',NULL),(8,72,0,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].',NULL),(8,76,0,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.',NULL),(8,81,0,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.',NULL),(8,87,0,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].',NULL),(8,92,0,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.',NULL),(8,93,0,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.',NULL),(8,270,0,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.',NULL),(8,349,0,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].',NULL),(8,369,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.',NULL),(8,470,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.',NULL),(8,471,0,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.',NULL),(8,509,0,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].',NULL),(8,510,0,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].',NULL),(8,529,0,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.',NULL),(8,530,0,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].',NULL),(8,576,0,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]',NULL),(-13,0,0,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,5,0,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,3,0,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,2,0,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(-13,6,0,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(-13,4,0,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(-13,1,0,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(-10,0,0,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-16,0,0,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(-8,0,0,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(-99,0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(-3,0,0,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(-7,0,0,'[small]this page for example[/small]',NULL),(-1,0,0,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(-99,0,3,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(-99,0,6,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(-99,0,0,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(-13,7,0,'Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: www (default), en, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL); +INSERT INTO `aowow_articles` VALUES (13,4,0,'[b][color=c4]Rogues[/color][/b] are a leather-clad melee class capable of dealing large amounts of damage to their enemies with very fast attacks. They are masters of stealth and assassination, passing by enemies unseen and striking from the shadows, then escaping from combat in the blink of an eye.\r\n\r\nThey are capable of using poisons to cripple their opponents, massively weakening them in battle. Rogues have a powerful arsenal of skills, many of which are strengthened by their ability to stealth and to incapacitate their victims.\r\n[ul]\r\n[li]Rogues can use a wide variety of melee weapons, such as daggers, fist weapons, one-handed maces, one-handed swords and one-handed axes.[/li]\r\n[li]By coating their weapons with [url=items=0.-3&filter=na=poison;ub=4]poison[/url] rogues can severely cripple or weaken their enemies.[/li]\r\n[li]When using [spell=1784] rogues will be unseen except by the most perceptive enemies.[/li]\r\n[/ul]',NULL),(14,1,0,'[b]Overview:[/b] The [b]humans[/b] are the most populous and the youngest race in Azeroth. The humans have become the [i]de facto[/i] leaders of the Alliance, with their youthful ambitions and resilience.\n\n[b]Capital City:[/b] The human seat of power is in the rebuilt city of [zone=1519].\n\n[b]Starting Zone:[/b] Humans begin questing in [zone=12].\n\n[b]Mounts:[/b] [npc=384] sells armoried ponies in Stormwind, and [npc=33307] at the Argent Tournament has a few distinct models.',NULL),(13,1,0,'[b][color=c1]Warriors[/color][/b] are a very powerful class, with the ability to tank or deal significant melee damage. The warrior\'s Protection tree contains many talents to improve their survivability and generate threat versus monsters. Protection warriors are one of the main tanking classes of the game.\n\nThey also have two damage-oriented talent trees - [icon name=ability_rogue_eviscerate][url=spells=7.1.26]Arms[/url][/icon] and [icon name=ability_warrior_innerrage][url=spells=7.1.256]Fury[/url][/icon], the latter of which includes the talent [spell=46917], which allows the warrior to wield two two-handed weapons at the same time! They are capable of strong melee AoE damage with spells such as [spell=845], [spell=1680], [spell=46924]. A warrior fights while in a specific [i]stance[/i], which grants him bonuses and access to different sets of abilities. He will use [spell=71] for tanking, and [spell=2457] or [spell=2458] for melee DPS.\n\n[ul]\n[li]All warriors can buff their raid or group by using a [i]shout[/i], [spell=6673] or [spell=469], and Fury warriors can provide the passive buff [spell=29801] which significantly increases the melee and ranged critical strike chance of his allies.[/li]\n[li]Warriors start out with only [spell=2457] at first, but learn [spell=71] at level 10 and [spell=2458] at level 30.[/li]\n[li]Warriors have numerous useful methods of getting to their target in a hurry! All warriors can use [spell=100] or [spell=20252] to reach an enemy and Protection warriors have [spell=3411], which allows them to intercept a friendly target and protect them from an attack.[/li]\n[/ul]',NULL),(13,2,0,'[b][color=c2]Paladins[/color][/b] bolster their allies with holy auras and blessing to protect their friends from harm and enhance their powers. Wearing heavy armor, they can withstand terrible blows in the thickest battles while healing their wounded allies and resurrecting the slain. In combat, they can wield massive two-handed weapons, stun their foes, destroy undead and demons, and judge their enemies with holy vengeance. Paladins are a defensive class, primarily designed to outlast their opponents.\n\nThe paladin is a mix of a melee fighter and a secondary spell caster. The paladin has a great deal of group utility due to the paladin\'s healing, blessings, and other abilities. Paladins can have one active aura per paladin on each party member and use specific blessings for specific players. Paladins are pretty hard to kill, thanks to their assortment of defensive abilities. They also make excellent tanks using their [spell=25780] ability.\n\n[ul]\n[li]Can effectively heal, tank, and deal damage in melee.[/li]\n[li]Has a wide selection of [url=spells=7.2&filter=na=blessing]Blessings[/url], [url=spells=7.2&filter=na=aura]Auras[/url], and other buffs.[/li]\n[li]Is the only class with access to a true invulnerability spell: [spell=642][/li]\n[/ul]',NULL),(14,2,0,'[b]Overview:[/b] The [b]orcs[/b] were originally a race of noble savages, residing on the world of Draenor. Unfortunately, The Burning Legion made use of them in an attempt to conquer Azeroth—they were infected with the daemonic blood of Mannoroth the Destructor, driven mad, and turned upon both the Draenei and the denizens of Azeroth. After losing the Second War, they were cut off from the corrupting influence of Mannoroth, and began to return to their shamanistic roots. Now, under the leadership of their new Warchief, the orcs are carving out a home for themselves in Azeroth.\n\n[b]Capital City:[/b] The orcs now reside in the city of [zone=1637], named after the deceased Orgrim Doomhammer, former Warchief of the Horde.\n\n[b]Starting Zone:[/b] Orcs begin questing in [zone=14].\n\n[b]Mounts:[/b] [npc=3362] in Orgrimmar sells a variety of wolves; [npc=33553] sells a few distinctive mounts at the Argent Tournament.',NULL),(13,3,0,'[b][color=c3]Hunters[/color][/b] are a very unique class in World of Warcraft. They are the sole non-magical ranged damage-dealers, fighting with bows and guns. Hunters have a number of different kinds of shots and stings, which can be used to debuff an enemy, and are capable of laying traps to deal damage or otherwise slow/incapacitate their enemy.\n\nA hunter will also tame his very own [url=pets]pet[/url] to aid them in combat. While they are not the only class which can use pet minions, the hunter\'s pet is unique in that each species has a particular type of talent tree, which the hunter can use to distribute points into various skills and passive abilities.\n\nIn addition, each species has a unique special ability. Hunters can seek out the most desirable pets based on their appearances or abilities, and if they spec deep enough into the [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon] tree they gain access to special, \"exotic\" beasts such as [pet=46] or [pet=39]!\n\n[ul]\n[li]Hunters have access to 23 (32 if [icon name=ability_hunter_beasttaming][url=spells=7.3.50]Beast Mastery[/url][/icon]) different [url=pets]species of pets[/url], featuring over 150 different appearances![/li]\n[li]Hunters have a number of survival-oriented skills which they can use to escape or avoid potential danger, such as [spell=5384] and [spell=781].[/li]\n[li][icon name=ability_hunter_swiftstrike][url=spells=7.3.51]Survival[/url][/icon] hunters can spec down the tree into [spell=53292], which allows them to provide the [spell=57669] buff to their party and raid members.[/li]\n[/ul]',NULL),(13,5,0,'[b][color=c5]Priests[/color][/b] are commonly considered one of the standard healing classes in World of Warcraft, as they have two talent specs that can be used to heal quite effectively.\n\nTheir [icon name=spell_holy_holybolt][url=spells=7.5.56]Holy[/url][/icon] tree includes talents which strongly boost the healing done to their allies, including spells that can be used to heal multiple players at once, such as [spell=48089]. The [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] tree, while still capable of significant raw healing output, focuses primarily on damage absorption and mitigation through use of [spell=48066] and procced shielding effects. Priests are also capable of very powerful ranged damage with their unique [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] abilities, and upon entering [spell=15473] will see a significant increase in their shadow damage while losing the ability to cast any Holy spells.\n\n[ul]\n[li]While the [icon name=spell_holy_wordfortitude][url=spells=7.5.613]Discipline[/url][/icon] talent tree is commonly used for healing, it also contains some powerful talents that can boost the priest\'s Holy damage, though [icon name=spell_shadow_shadowwordpain][url=spells=7.5.78]Shadow[/url][/icon] spells and abilities should be used primarily for DPS.[/li]\n[li]Priests provide of the most appreciated buffs in the game - [spell=48161], which grants an indispensable stamina buff to everyone in the raid. They can also buff both [spell=48073] and [spell=48169]![/li]\n[li]Shadow priests are an excellent utility class for any raid, providing the much-loved [spell=57669] buff to boost mana regeneration and can even heal their own party with [spell=15286]![/li]\n[/ul]',NULL),(13,6,0,'Introduced in the Wrath of the Lich King expansion, [b][color=c6]Death Knights[/color][/b] are World of Warcraft\'s first hero class. Death knights start at level 55 in a special, instanced zone unreachable by any other class: Acherus, the Ebon Hold, located in [zone=4298]. Here they will earn their talent points as quest rewards and even get a special summoned mount, the [spell=48778]!\n\nDeath knights have multiple very strong damage dealing options, as each of their talent trees can be specced to perform exceptionally well with a variety of melee abilities, spells and damage-over-time dealing diseases. They are also very capable tank classes, with both their Blood and Frost trees providing unique options - [icon name=spell_deathknight_bloodboil][url=spells=7.6.770]Blood[/url][/icon] dealing more with self-healing abilities and [icon name=spell_frost_frostnova][url=spells=7.6.771]Frost[/url][/icon] providing significant damage mitigation and strong AoE damage.\n\nDeath knights fight with a special buff active called a [i]presence[/i] (similar to a warrior\'s stances) which provides special bonuses to their roles. Death knights utilize a unique power system, with most spells costing either Runes, which are replenished throughout battle, or Runic Power, which can be generated by various abilities.\n\n[ul]\n[li][icon name=spell_deathknight_armyofthedead][url=spells=7.6.772]Unholy[/url][/icon] death knights can spec into [spell=52143], which makes their summoned Ghoul minion a permanent pet to aid in battle![/li]\n[li]The death knight class has its own special weapon enchanting ability called [spell=53428], which replaces the need for conventional weapon enchants.[/li]\n[li]Death knights are a very unique damage-dealing class in that their damage is dealt by both melee abilities [i]and[/i] spells![/li]\n[/ul]',NULL),(13,7,0,'[b][color=c7]Shamans[/color][/b] master elemental and nature magics and bring the most potential buffs to any group in the form of totems. A shaman can summon one totem of each element - earth, fire, air, and water - which appears at the shaman\'s feet and provides a buff to anyone in the shaman\'s party or raid within range of it. Some shaman totems, notably the fire ones, also do damage to opponents. The trick to playing any type of shaman is knowing which totems to cast under which circumstances to maximize the group\'s damage output and survivability.\n\nShamans are primarily spellcasters, although an [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shaman likes to get close and personal and do damage within melee range. An enhancement shaman learns to [spell=30798] weapons and can use [spell=51533] to summon a pair of Spirit Wolves to aid in battle. Despite being primarily melee, [icon name=spell_nature_lightningshield][url=spells=7.7.373]Enhancement[/url][/icon] shamans can still gain some benefit from spellpower and can cast instant [spell=403] or heals with [spell=51530]. \n\n[icon name=spell_nature_lightning][url=spells=7.7.375]Elemental[/url][/icon] shamans stand back and cast fire and lightning spells to deal great amounts of damage. They can push back enemies with [spell=51490] and root all enemies in an area with[spell=51486]. They also bring [icon name=spell_fire_totemofwrath][url=spell=57722]Totem of Wrath[/url][/icon] and [spell=51470] as amazing spellcaster raid buffs. A shaman that choses [icon name=spell_nature_magicimmunity][url=spells=7.7.374]Restoration[/url][/icon] gains improved healing spells and can be a great raid or tank healer. Resto shamans are known for their powerful [spell=1064] ability and for providing a [spell=16190] to help their party\'s mana restoration. They also gain a powerful [spell=974], can use [spell=51886] to remove curses, and have an instant-cast direct heal plus heal over time effect called [spell=61295].\n\n[ul]\n[li]There are over twenty different totems a shaman can learn![/li]\n[li]Shamans can cast [spell=2825] (or [spell=32182]) to boost the entire group\'s damage and healing. This buff is unique and oft sought after for a raid group.[/li]\n[li]A shaman can turn into a [spell=2645] at level 16 and can even make it instant cast with [spell=16287]. This spell can be used in combat, but not indoors.[/li]\n[li]Shamans can only have one elemental shield - [spell=324] or [spell=52127] - on at a time. [spell=974], if the shaman knows it, can be cast on another player.[/li]\n[/ul]',NULL),(13,8,0,'[b][color=c8]Mages[/color][/b] wield the elements of fire, frost, and arcane to destroy or neutralize their enemies. They are a robed class that excels at dealing massive damage from afar, casting elemental bolts at a single target, or raining destruction down upon their enemies in a wide area of effect. Mages can also augment their allies\' spell-casting powers, summon food or drink to restore their friends, and even travel across the world in an instant by opening arcane portals to distant lands.\n\nWhen seeking someone to introduce monsters to a world of pain, the Mage is a good choice. With their elemental and arcane attacks, it\'s a safe bet something they can do won\'t be resisted by your chosen enemy. Damage is the name of the Mage game, and they do it well. Their arsenal includes some powerful buffs, debuffs, stuns, and snares, enabling them to dictate the terms of any fight.\n\n[ul]\n[li]Can [spell=42956] to restore their allies\' health and mana.[/li]\n[li]Are the only class that can create portals to transport other players. They cannot, however, summon players [i]from[/i] a distant location - that\'s a [icon name=class_warlock][color=c9]Warlock\'s[/color][/icon] job![/li]\n[li]Mages who use [item=50045] can have a permanent water elemental pet![/li]\n[/ul]',NULL),(13,9,0,'[b][color=c9]Warlocks[/color][/b] are masters of the demonic arts. Clothed in demonic styled cloth, they excel in using curses, firing bolts of fire or shadow, and summoning demons to help them in combat. Warlocks, while being excellent spell casters, also excel in supporting fellow allies by summoning other players or using ritual magics to conjure stones imbued with the power to heal.\r\n\r\nA warlock has very powerful abilities that, if used correctly, make them a very formidable opponent. Using their curses in combination with direct damage spells, Warlocks wreak havoc and destruction.\r\n\r\n[ul]\r\n[li]Can use a [spell=698] to summon another player to the portals location.[/li]\r\n[li]Are able to conjure [icon name=inv_stone_04][url=item=5509]Healthstones[/url][/icon] that have the ability to heal the user.[/li]\r\n[li]Can use curses on enemies to [url=spell=47865]weaken[/url] them or [url=spell=47864]damage[/url] them.[/li]\r\n[/ul]',NULL),(13,11,0,'[b][color=c11]Druids[/color][/b] are World of Warcraft\'s \"jack of all trades\" class -- that is, capable of performing in a variety of different roles and as such have one of the most varied playstyles. A druid can act as a healer, melee DPS, ranged DPS or a tank, utilizing a variety of [i]shapeshifting[/i] forms. As a druid levels up, he is able to learn new, powerful forms which he can cast to change into different creatures to suit their roles.\n\nAt lower levels, a druid will heal or ranged DPS in his caster form, but at later levels players who spec into the specialized trees will gain access to two special shapeshift forms for each different role.\n\nHealing druids will learn [spell=33891], which reduces the mana cost of their healing spells and grants a passive healing aura to their allies. Their ranged damage-dealing counterparts will learn [spell=24858], increasing their armor and granting a spell critical aura to their allies. There are also two feral form druid forms -- the mighty [spell=5487] (and at later level, [spell=9634]), a tanking-oriented form which provides additional armor and health and grants access to an arsenal of threat-building and damage mitigation abilities, and the rogue-like [spell=768] which is capable of significant melee DPS.\n\n[ul]\n[li]Druids learn their different forms through questing or training. Some shapeshifts are only learned via talents.[/li]\n[li]There are some shapeshifts that all druids can learn. [spell=5487] is obtained at level 10, [spell=1066] and [spell=783] at level 16, [spell=768] at level 20 and [spell=9634] at level 40.[/li]\n[li]Druids even have their own flying travel form! [spell=33943] can be trained at level 60, and [spell=40120] at level 71 provided the player has already trained [spell=34091].[/li]\n[li]Some druid shapeshifts are obtained via talents only - [spell=24858] can be obtained at level 40 when a player specs deep into the [icon name=spell_nature_starfall][url=spells=7.11.574]Balance[/url][/icon] tree, and [spell=33891] at level 50 after speccing deep into [icon name=spell_nature_healingtouch][url=spells=7.11.573]Restoration[/url][/icon].[/li]\n[li]Druids have their own, class-specific teleport ability that allows them to travel to and from [zone=493], which is handy when needing to train![/li]\n[li]Because feral druids do not actually swing weapons while in shapeshift forms, they instead gain a special statistic from any melee weapon they equip called \"feral attack power.\" This stat is a conversion of a weapon\'s DPS (damage per second) into an attack power-granting statistic which affects the cat or bear\'s damage output.[/li]\n[/ul]',NULL),(14,3,0,'[b]Overview:[/b] The [b]dwarves[/b] are a hardy race, hailing from Khaz Modan in the Eastern Kingdoms. Rumor has it they are descended from the Titans. There are three main clans of dwarves vying for power in Ironforge: the Bronzebeards, Wildhammers, and Dark Irons.\n\n[b]Capital City:[/b] The dwarves make their home in their ancestral seat of [zone=1537].\n\n[b]Starting Zone:[/b] Dwarves begin in [zone=1].\n\n[b]Mounts:[/b] [npc=1261] by the Amberstill Ranch sells rams, as well as [npc=33310] at the Argent Tournament.',NULL),(14,4,0,'[b]Overview:[/b] The [b]night elves[/b] are an ancient and mysterious race. They lived in Kalimdor for thousands of years, undisturbed until the world tree was sacrificed to halt the advance of the Burning Legion prior to the events of World of Warcraft.\n\n[b]Capital City:[/b] The night elf capital city is [zone=1657], situated in the branches of the world tree itself.\n\n[b]Starting Zone:[/b] Night Elves begin in [zone=141], learning about the recent political changes in Darnassus.\n\n[b]Mounts:[/b] [npc=4730] in Darnassus sells a variety of nightsabers, as well as [npc=33653] at the Argent Tournament.',NULL),(14,5,0,'[b]Overview:[/b] When the [b]undead[/b] scourge initially swept across Azeroth, they converted a number of members of the Alliance to the undead. When the combined forces of the orcs, elves, trolls, dwarves and humans began to fight back, though, [npc=36597]\'s hold on his forces began to weaken. A small faction of humans, known as the Forsaken, broke free of the Lich King\'s control.\n\nNow, free of the bonds of servitude as well as the troublesome emotions and connections of their human lives, the Forsaken have found a new home—with the Horde.\n\n[b]Capital City:[/b] The Forsaken reside in the [zone=1497], underneath the ruins of the former human city of Lordaeron.\n\n[b]Starting Zone:[/b] [zone=85] is the starting zone for Forsaken players--they are raised as second-generation Forsaken by val\'kyr and experience Sylvanas\' menacing new agenda firsthand.\n\n[b]Mounts:[/b] [npc=4731] in Tirisfal Glades sells numerous undead horses; [npc=33555] at the Argent Tournament sells a few distinct models.',NULL),(14,6,0,'[b]Overview:[/b] The [b]tauren[/b], a race with deep shamanistic roots, are longtime residents of Kalimdor. They have a deep and abiding love of nature, and the vast majority of them worship a deity known as the Earth Mother. \n\n[b]Capital City:[/b] The tauren reside in [zone=1638].\n\n[b]Starting Zone:[/b] Tauren begin questing in [zone=215].\n\n[b]Mounts:[/b] [npc=3685] sells numerous kodo mounts; [npc=33556] at the Argent Tournament sells a few distinctive models.',NULL),(14,7,0,'[b]Overview:[/b] The [b]gnomes[/b] are a quirky race, obsessed with gadgets and technology. They originally come from the city of [zone=721], which was destroyed by [npc=7937] in an attempt to save it from an invading army of troggs.\n\n[b]Capital City:[/b] The gnomes now make their home in [zone=1537]; they have made efforts to retake their beloved former city with [achievement=4786].\n\n[b]Starting Zone:[/b] Gnomes begin in [zone=1], but they have a very different quest sequence from Dwarves, covering Gnomeregan.\n\n[b]Mounts:[/b] [npc=7955] in Dun Morogh sells numerous mechanostriders, as well as [npc=33650] at the Argent Tournament.',NULL),(14,8,0,'[b]Overview:[/b] While there are many different tribes of [b]trolls[/b] scattered across Azeroth, only the [url=?faction=530]Darkspear Tribe[/url] has ever sworn allegiance to the Horde. The trolls originally lived in the Broken Isles, but were overrun by naga and murlocs and driven from their home. The orcs, led by [npc=4949], saved the Darkspear tribe from certain destruction and offered them amnesty among the Horde. In return, the Darkspear tribe swore fealty to the orcish warchief.\n\n[b]Capital City:[/b] The Darkspear Trolls live now in the Horde capital of [zone=1637].\n\n[b]Starting Zone:[/b] Trolls begin questing in [b]Echo Isles[/b].\n\n[b]Mounts:[/b] [npc=7952] in Sen\'jin Village sells numerous raptors; [npc=33554] at the Argent Tournament sells a few distinctive models.',NULL),(14,10,0,'[b]Overview:[/b] The [b]blood elves[/b] are a proud, haughty race, joining the Horde in Burning Crusade. They represent a faction of former high elves, split off from the rest of elven society; they are also survivors of Arthas\' assault on Silvermoon. Blood elves are fully dependent on magic, having revelled in its power for so long that they suffer horrible withdrawal if it were to be taken away.\n\n[b]Capital City:[/b] The blood elves have rebuilt [zone=3487].\n\n[b]Starting Zone:[/b] [zone=3430] is the starting zone for Blood Elves.\n\n[b]Mounts:[/b] [npc=16264] in Eversong Woods sells numerous hawkstriders; [npc=33557] at the Argent Tournament sells a few unique models.',NULL),(14,11,0,'[b]Overview:[/b] The [b]Draenei[/b] are followers of the Naaru and worshipers of the Holy Light. They originally hail from the distant world of Argus, fleeing after Sargeras tried to corrupt them. They then settled on the Orcish homeworld of Draenor, where after a period of peace, they were brutally murdered during Guldan\'s corruption of the Orcs. Finally they settled in Azeroth, to seek aid in their battle against the Burning Legion. Draenei were introduced in the Burning Crusade expansion.\n\n[b]Capital City:[/b] The Draenei have the seat of their power in the ruins of their once-great ship, [zone=3557].\n\n[b]Starting Zone:[/b] [zone=3524] and [zone=3525] cover the attempts of the Draenei to settle on their new island and deal with the inherent corruption present.\n\n[b]Mounts:[/b] [npc=17584] sells a variety of Elekks, as well as [npc=33657] at the Argent Tournament.',NULL),(8,21,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[b]Booty Bay[/b]\n[faction=577]\n[faction=369]\n[faction=470]\n[/minibox]\n\n\n[b]Booty Bay[/b] is a large pirate town nestled into the cliffs surrounding a beautiful blue lagoon on the southern tip of [zone=33]. The city is entered by traversing through the bleached-white jaws of a giant shark.\n\nRun by the Blackwater Raiders who are closely associated with the Steamwheedle Cartel, the port offers facilities to any traveller passing through, regardless of their faction. Combined with the world renowned Salty Sailor Tavern, [event=15], numerous profession trainers, and vendors that sell everything from pets to diamond rings, it is one of the most popular locations in Azeroth.\n\n[npc=2496], ruler of this city, is hiring all the help he can get against the pesky [faction=87] and other threats of the city. He resides, together with the leader of the Blackwater Raiders, [npc=2487], at the top of the inn of Booty Bay.\n\nDue to the boat route from Booty Bay to Ratchet, players of all level ranges (mostly Horde, if lower level) can be expected to be found going about their business, although frequent visitors will more than likely fit in the 35 - 45 range. The quests available from the locals reflect this range nicely.\n\nThe water there occasionally has floating wreckages and schools of fish. The schools that are found most often are [item=6359], [item=6358], and [item=13422]. Fishing in the floating wreckages will also give you very high chances of fishing out chests and items, making Booty Bay an ideal place for fishing.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Booty Bay are located in The Cape of Stranglethorn. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Booty Bay, you can do the repeatable quest [quest=9259] to get back to Neutral.',NULL),(8,47,0,'[b]Ironforge[/b] is the faction associated with the capital city of the dwarves, [zone=1537]. [npc=2784] rules his kingdom of Khaz Modan from his throne room within the city, and the [npc=7937], leader of the gnomes, has temporarily had to settle down in Tinker Town after the recent fall of the gnome city [zone=133].\n\n[h3]History[/h3]\nIronforge is the ancient home of the dwarves. A marvel to the dwarves\' skill at shaping rock and stone, Ironforge was constructed in the very heart of the mountains, an expansive underground city home to explorers, miners, and warriors. Massive doors of rock protect the city in times of war, and lava from the mountain itself is redirected and distributed for heat, energy and smithing purposes. Before the Dark Iron Clan was banished from the city, eventually leading to the War of the Three Hammers, Ironforge was the commercial and social center of all the dwarven clans. It is now home to the Bronzebeard Clan. Many dwarven strongholds fell during the Second War between the Horde and the Alliance of Lordaeron, but the mighty city of Ironforge, nestled in the wintry peaks of [zone=1] and protected by its great gates, was never breached by the invading Horde.\n\nRelatively recently, Ironforge also became home to the Gnomeregan refugees. After the Third War, the gnomish city of Gnomeregan became overrun by troggs. Since then, a number of gnomes have settled in Ironforge, converting an area of that city to their liking, an area now known as Tinker Town.\n\nIronforge is one of most populated cities in the world, coming after the human city of [zone=1519], and housing 20,000 people.\n\nWhile the Alliance has been weakened by recent events, the dwarves of Ironforge, led by King Magni Bronzebeard, are forging a new future in the world.[h3]Reputation[/h3]\n[npc=14723] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-dwarf players are able to ride [url=?items=15.5&filter=na=Ram;cr=93:92;crs=2:1;crv=0:0]rams[/url].\n\nSurrounding zones [zone=1], [zone=38] and [zone=11] contain the most quests for gaining reputation with Ironforge.',NULL),(8,54,0,'[b]Gnomeregan Exiles[/b] is the faction of gnomes who fled from their home, [zone=133] in [zone=1]. It was destroyed by the [url=?npcs=7&filter=na=Trogg]Trogg[/url] after a toxic invasion. Now a member of the Alliance, most are located in the Tinkertown section of the neighboring city [zone=1537], including leader [npc=7937].\n\n[h3]History[/h3]\nIt has been speculated that gnomes were formed as robots by the Titans, due to their inquisitive nature and technical skills.\n\nGnomes were an underground race of tinkers, residing in Gnomeregan until the troggs destroyed it. In this war, over 80% of the gnomish population was lost.\n\n[h3]Reputation[/h3]\n[npc=14724] has the repeatable cloth reputation quests. As a reward for being exalted with Ironforge, non-gnome dwarf players are able to ride [url=?items=15.5&filter=na=Mechanostrider;cr=93:92;crs=2:1;crv=0:0]mechanostriders[/url].\nSurrounding zone [zone=1] contain the most quests for gaining reputation with the Gnomeregan Exiles.',NULL),(8,59,0,'The [b]Thorium Brotherhood[/b] are an elite group of craftsmen who can reveal a number of epic recipes if you gain enough faction reputation with them. All players start off at Neutral reputation with them.\n\n[h3]History[/h3]\n\nThe [zone=51] is home to a group of exceptionally stout dwarves who have split from the Dark Iron Clan. On the cliffs overlooking the region called the Cauldron, in the far north of the Searing Gorge, the dwarves of the Thorium Brotherhood have established a base of operations, Thorium Point. From here, they keep a close eye on the Dark Iron dwarves\' activities in the Searing Gorge and beyond. Adventurers seeking out Thorium Point will find that the dwarves of the Thorium Brotherhood hold great rewards for those who aid them in their never ending struggle against their former brethren.\n\nThe Thorium Brotherhood comprises many exceptionally talented craftsmen, and the blacksmiths of the Brotherhood are rumored to be among the finest Azeroth has ever seen. They possess the knowledge required to make the arms and armaments of [npc=11502], the Fire Lord, but lack the manpower to obtain the materials required for the crafting. It is rumored that one member of the Thorium Brotherhood has been empowered to trade the dwarves\' fabled recipes and plans with those who can prove their loyalty to the Brotherhood. Of course, proving one\'s loyalty at some point may include venturing to the heart of the [zone=2717], the domain of Ragnaros, the Fire Lord himself, to supply the dwarves with the rare raw materials found there. A daunting task, no doubt, but gaining access to the Thorium Brotherhood\'s secrets should prove to be a reward well worth the effort.\n\n[h3]Reputation[/h3]\n\n[b]Neutral to Friendly[/b]\n\n[ul]\n[li]Turn in [item=18944], [item=3857] and either [item=4234], [item=3575], or [item=3356] to [npc=14624].[/li][/ul]\n[b]Friendly to Honored[/b]\n\n[ul]\n[li]Turn in [item=18945] to Master Smith Burninante.[/li][/ul]\n[b]Honored to Exalted[/b]\n\n[ul]\n[li]Turn in [item=11370] to [npc=12944].[/li]\n[li]Turn in [item=17012] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17010] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=17011] to Lokhtos Darkbargainer.[/li]\n[li]Turn in [item=11382] to Lokhtos Darkbargainer.[/li][/ul]',NULL),(8,68,0,'[b]Undercity[/b] is the faction for the capital city of the Forsaken Undead, [zone=1497], ruled by Sylvanas Windrunner. It is located in [zone=85], at the northern edge of the Eastern Kingdoms. The city proper is located under the ruins of the historical City of Lordaeron. To enter it, you will walk through the ruined outer defenses of Lordaeron and the abandoned throneroom, until you reach one of three elevators guarded by two abominations.\n\n[h3]History[/h3]\nThe Undercity was originally simply a system of sewers, crypts, and catacombs beneath the Capital City of Lordaeron. After the city was destroyed by the Scourge, Arthas had the underground warren expanded and rebuilt. He originally intended for the Undercity to be his seat of power, from which he would rule the Plaguelands. However, shortly after the Third War ended, Arthas was forced to return to Northrend and save the Lich King. In his absence, [npc=10181] and her rebel Undead captured the ruins of the city. Soon after, she discovered the massive underground fortress, and decided to establish it as the main base of operations for the Undead Forsaken.\n\n[h3]Reputation[/h3]\n[npc=14729] has the Undercity repeatable cloth quests used by non-Undead Horde players to obtain the right to ride [url=?items=15.5&filter=na=Skeletal;cr=93:92;crs=2:1;crv=0:0]skeletal horses[/url] at exalted.\n\nSurrounding zones [zone=267], [zone=130], and Tirisfal Glades have the most quests to earn reputation with Undercity.',NULL),(8,69,0,'[b]Darnassus[/b] is the faction associated with [zone=1657], the capital city of the Night Elves. The high priestess, [npc=7999], resides in the Temple of the Moon, surrounded by other sisters of Elune. In the Cenarion Enclave, the [npc=3516] leads the [faction=609], often in direct opposition to his fellow druids in [zone=493] and Tyrande herself.\n\n[h3]History[/h3]\nIn the aftermath of the Third War, the night elves had to adjust to their mortal existence. Such an adjustment was far from easy, and there were many night elves who could not adjust to the prospects of aging, disease and frailty. Seeking to regain their immortality, a number of wayward druids conspired to plant a special tree that would reestablish a link between their spirits and the eternal world.\n\nWith [npc=15362] missing, Fandral Staghelm - the leader of those who wished to plant the new World Tree - became the new Arch-Druid. In no time at all, he and his fellow druids had forged ahead and planted the great tree, [zone=141], off the stormy coasts of northern Kalimdor. Under their care, the tree sprouted up above the clouds. Among the twilight boughs of the colossal tree, the wondrous city of Darnassus took root. However, the tree was not consecrated with nature\'s blessing and soon fell prey to the corruption of the Burning Legion. Now the wildlife and even the limbs of Teldrassil are tainted by a growing darkness.\n\n[h3]Reputation[/h3]\n[npc=14725] has the Darnassus repeatable [quest=7800] used by non-night elven Alliance players to obtain the right to ride [url=?items=15.5&filter=na=Reins+-Winterspring;ra=4;cr=93:92;crs=2:1;crv=0:0]night sabers[/url].[pad]Players who are at or close to level 44 looking to gain the favor of Darnassus should find and complete the quests of [zone=357]. The quests therein are associated with Darnassus and could prove to substantially increase your reputation should they all be completed.',NULL),(8,70,0,'The [b]Syndicate[/b] is a mostly Human criminal organization that operates primarily in the [zone=45] and the [zone=36], although a few small encampments are scattered in the [zone=267]. Their membership numbers around 3,000 persons.\n\nThey have three leaders: [npc=2423] (who took over from his father Aiden Perenolde), descendent of the original Lord of Alterac, who directs the Syndicate\'s actions in the Alterac Mountains from Strahnbrad; [npc=2597] directs Syndicate actions in Arathi Highlands from the main keep in the semi-abandoned fortress of Stromgarde; and Lady Beve Perenolde, daughter of Aiden Perenolde.\n\n[h3]History[/h3]\n\nDuring the Second War the Kingdom of Alterac, led by Lord Perenolde, was discovered to be in league with the Orcish Horde. Perenolde believed that a Horde victory was inevitable, and thus offered aid to the Horde by stirring up rebellions, attacking Alliance bases, and giving them supplies. When this treachery was discovered, the Alliance marched on Alterac and destroyed it. Perenolde and any nobles who went along with his plans were stripped of their titles and land. Many of the nobility managed to escape, however, and began plotting their revenge. Using their still sizable fortunes, the nobility hired a band of thieves and assassins, forming an organization known as the Syndicate.\n\nAt first the Syndicate\'s goal was just to spread chaos and disorder, striking from hidden bases in the Alterac Mountains. With the end of the Third War and the resultant chaos however, the leaders of the Syndicate saw their chance to return Alterac to its former power. They have now gained control of several outposts in the surrounding area including the sacked fortress of Durnholde Keep and a portion of the city of Stromgarde.\n\nThey are enemies of both the Alliance, whom they consider their mortal enemies, and the Horde, whom they consider mere brutes good for nothing but slave labor. As a result, the Syndicate is now hunted by both factions, with the [npc=10181], in particular, placing a bounty on their heads - guaranteeing that all captured Syndicate members will be summarily executed. In addition, [npc=4949] ordered a number of his agents, including [npc=2229], [npc=2239], [npc=2238] and their leader [npc=2316] to launch an investigation into the nature of the Syndicate and its activities, as well as to recover [item=3498], which belonged to a dear friend of his, [npc=18887] - a necklace now worn by Elysa, the mistress of Lord Aliden.\n\n[h3]Reputation[/h3]\n\nThe Syndicate as a faction in World of Warcraft is very odd in comparison to most factions in that the killing of the factions members will not lower your standing with the faction. For most players who are not a rogue, the only way for the Syndicate to appear on their Reputation Menu is to complete the quest [quest=8249], which is available to non-rogues. However, the quest requires [item=16885] ... which only rogues can obtain by pick-pocketing NPCs above level fifty, and those can only be traded to you - making it difficult to arrange such a transaction.\n\nCurrently there is only one known option to increase a player’s reputation with the Syndicate, and that is by killing members of the [faction=349] faction. There are no known rewards for increasing Syndicate reputation, and Ravenholdt-affiliated NPCs only give 1 Syndicate Reputation points, with the exception of [npc=13085], who gives 5 (although the corresponding loss of reputation with Ravenholdt is also five times as great). With all players starting at 32000/36000 hated with the faction, it would require killing 10,000 Ravenholdt NPCs to reach Neutral status with the faction; unfortunately, neutral status is the highest you can reach with the Syndicate, and if not to deter players further, none of the Ravenholdt NPCs drop loot.\n\n[b]WARNING[/b]: If you do decide to kill Ravenholdt NPCs, know that there is currently no way to restore your standings with Ravenholdt, if you do go below Neutral. The reason for the problem is that none of the quests that give Ravenholdt Reputation points will be available because none of the members from Ravenholdt will speak to you. This would mean its a permanent change and you will never be able to interact with any of the NPC loyal to Ravenholdt ever again. Also note that players start at 0/3000 reputation with Ravenholdt, and killing even one of their NPCs at this reputation level will forever prevent you from raising your reputation with them again.',NULL),(8,72,0,'[b]Stormwind[/b] is the faction associated with [zone=1519], the capital of the humans. It is located in the northwestern part of [zone=12]. The child king, [npc=1747], resides in Stormwind Keep, surrounded by his body guards and advisors, [npc=1748] (the regent), and [npc=1749]. The city is named for the occasional sudden squalls created by a ley line pattern in the mountains around the glorious city.\n\n[h3]History[/h3]\nDuring the First War, the Kingdom of Azeroth, including its capital, Stormwind Keep, was utterly destroyed by the Horde and its survivors fled to Lordaeron. After the orcs were defeated at the Dark Portal at the end of the Second War, it was decided that the city would be rebuilt, even surpassing its former grandeur. The nobles of Stormwind assembled a team of the most skilled and ingenious stonemasons and architects they could find. Under their direction, Stormwind was rebuilt in an amazingly short period of time. Now, at the end of the Third War, in the renamed Kingdom of Stormwind, it stands as one of the last bastions of human power left in the world. \n\nWith the fall of the northern kingdoms, Stormwind is by far the most populated city in the world. Boasting a population of two-hundred thousand people (predominantly human), it serves in many ways as the cultural and trade center of the Alliance, even with remote access to the sea. The humans living in the city are generally carefree and artistic, favoring light and colorful clothes, cuisine and art. It is home to the Academy of Arcane Sciences, the only wizarding school in Eastern Kingdoms, as well as SI:7, a rogue intelligence organization.\n\nHowever, the people of Stormwind find it difficult to accept Theramore\'s role as the home of the new Alliance, convinced not only that Stormwind should be the legitimate heir of Lordaeron\'s role in the past, but also that Theramore is doing little against the worsening situation within the Eastern Kingdoms.\n\n[h3]Reputation[/h3]\n[npc=14722] has the repeatable cloth quests to achieve a higher reputation with Stormwind. In return for exalted reputation, non-human players are able to ride horses.\n\nMost quests associated with Stormwind come from the surrounding areas of Elwynn Forest, [zone=40], and [zone=44].',NULL),(8,76,0,'[b]Orgrimmar[/b] is the faction for the capital city [zone=1637] of the orcs and trolls of the [faction=530]. Found at the northern edge of [zone=14], the imposing city is home to the orcish Warchief, [npc=4949].\n\n[h3]History[/h3]\nThrall led the orcs to the continent of Kalimdor, where they founded a new homeland with the help of their tauren brethren. Naming their new land Durotar after Thrall\'s murdered father, the orcs settled down to rebuild their once-glorious society. The demonic curse on their kind ended, the Horde changed from a warlike juggernaut into more of a loose coalition, dedicated to survival and prosperity rather than conquest. Aided by the noble tauren and the cunning trolls of the Darkspear tribe, Thrall and his orcs looked forward to a new era of peace in their own land. \n\nFrom there, they began the creation of the great warrior city, Orgrimmar. Named after the former Warchief, Orgrim Doomhammer, the new city was constructed in a short amount of time, with the aid of goblins, tauren, trolls, and the Mok\'Nathal Rexxar. Despite having some problems with the centaur, harpies, enraged thunder lizards, kobolds, evil orcish warlocks, quilboars, and unfortunately, the Alliance, Orgrimmar prospered in the end and became home to the orcs and Darkspear Trolls.\n\nToday, Orgrimmar lies at the base of a mountain between Durotar and [zone=16]. A warrior city indeed, it is home to countless amounts of orcs, trolls, tauren, and an increasing amount of Forsaken are now joining the city, as well as the Blood Elves who have recently been accepted into the Horde.\n\n[h3]Reputation[/h3]\n[npc=14726] has the Orgrimmar repeatable cloth quests used by non-orcish Horde players to obtain the right to ride [url=?items=15.5&filter=na=Wolf;cr=93:92;crs=2:1;crv=0:0]wolves[/url] at exalted.\n\nSurrounding areas Durotar and [zone=17] have the most quests for gaining reputation with Orgrimmar.',NULL),(8,81,0,'[b]Thunder Bluff[/b] is the faction of the Tauren capital city [zone=1638] located in the northern part of the region of [zone=215]. The whole of the city is built on bluffs several hundred feet above the surrounding landscape, and is accessible by elevators on the southwestern and northeastern sides.\n\n[h3]History[/h3]\nThe great city of Thunder Bluff lies atop a series of mesas that overlook the verdant grasslands of Mulgore. The once nomadic Tauren recently built the city as a center for trade caravans, traveling craftsmen and artisans of every kind. It was established by the mighty chief [npc=3057] after the Tauren, with help from the orcs, drove away the centaurs that originally inhabited Mulgore. Long bridges of rope and wood span the chasms between the mesas, topped with tents, longhouses, colorfully painted totems, and spirit lodges. The Tauren chief watches over the bustling city, ensuring that the united Tauren tribes live in peace and security.\n\n[h3]Reputation[/h3]\n[npc=14728] has the Thunder Bluff repeatable cloth quests used by non-tauren Horde players to obtain the right to ride [url=?items=15.5&filter=na=Kodo;cr=93:92;crs=2:1;crv=0:0]kodos[/url] at exalted.\n\nSurrounding zones Mulgore and [zone=17] have the most quests for gaining reputation with Thunder Bluff.',NULL),(8,87,0,'During the events leading up to and following the Third War, several criminal organizations appeared in Azeroth. The [b]Bloodsail Buccaneers[/b] appear to be one of these organizations, originating from the Bloodsail Hold on Plunder Isle and is where their ruler, Duke Falrevere holds court. They now plot to plunder and cripple the Steamwheedle Cartel controlled port town of [faction=21], currently under the protection of the Blackwater Raiders. It is likely the Bloodsail Buccaneers have come to take advantage of the town’s current loss of its fleet off the coast of the [zone=45], in which two of its ships were destroyed, and the remaining ship forced to find shelter in a cove, where its crew now fights to survive skirmishes with the Daggerspine Naga.\n\nIn preparation of the attack the Bloodsail Buccaneers have taken position in key locations near the town. Currently they have three ships anchored along the coastline south of Booty Bay, clear of the town’s defensive cannons, with camps also being built along the same coast in preparation of the attack. In addition, a scouting party has landed just west of the entrance to the town, reporting all activities, along with a compound being constructed along the road leading towards the town, likely to stop any re-enforcements from coming to help.\n\nBoth the Bloodsail Buccaneers and Blackwater Raiders seek to achieve their goals without having their forces engaged in battle, to this end each side now seek the aid of adventurers sympathetic to their cause.\n\n[h3]Reputation[/h3]\nThere is only one way to increase your reputation with the Bloodsail Buccaneers and that’s to unleash your wrath on any citizen of Booty Bay who can be found through out the Eastern Kingdoms. Below is a list of every citizen of Booty Bay and their reputation value. The amount gained with the Bloodsail Buccaneers is shown for a level 60 non-human. The amount lost for killing a citizen cannot be shown as it depends on your current level with Booty Bay and the importance of the person you kill. In addition to this what ever you lose with Booty Bay you will lose half of that in the other three goblin towns so if you lose 25 points in Booty Bay you will lose 12.5 points in [faction=470].\n\n[ul]\n[li][npc=4624]: 25 rep gained[/li]\n[li][npc=15088]: 25 rep gained[/li]\n[li][npc=2496]: 5 rep gained[/li]\n[li][npc=2636]: 5 rep gained[/li]\n[li][url=?npcs&filter=cr=3;crs=21;crv=0]Many more NPCs[/url]![/li]\n[/ul]\n\nThe fastest way to increase you reputation with the Bloodsail Buccaneers is to kill Booty Bay Bruisers. At first it may seem a simple task as the guards don\'t appear as threatening as the other monsters a player faces within the game. However, the guards are highly equipped to neutralize players of any class, to prevent people from attacking each other while in the town. What gives the Booty Bay Bruiser the advantage is several factors, one of them being their ability to use nets to lock you in place, preventing you from escaping. Another is the fact that they spawn every time you attack a citizen of the city or if you’re under Unfriendly status with Booty Bay the Bruisers can spawn if you enter a building, because of this players can soon find them selves swarmed by Bruisers.\n\nYet, theses are just the minor problems, in comparison to the Bruiser’s strongest ability, once it pulls out its gun its unlikely you will live, if you do not escape fast enough. Each time a guard shoots you, the attack throws you back, much like an Ogre hammer attack; the difference here is that the Bruiser can shoot in quick succession causing chain throw backs. A player can literally be thrown from one side of the town to the other, preventing you from attacking. More often you will find your self being forced into a corner, unable to move and unable to attack with each spell being interrupted by the Bruiser’s attack. Because the Bruisers do not put their guns away once they are out, the best course of action is to run away. \n\nThrough trial and error most people have discovered a safe place to kill Booty Bay Bruisers. If you follow the tunnel leading into the town, the path to your left that leads to the Blacksmith house is the ideal place to kill the guards. Only two guards patrol this path and normally don’t pass each other that closely, allowing both to be dispatched separately. Once they are gone, one can simply enter the first build on the path to cause a guard to spawn if they are below Unfriendly, if not they can simply attack one of the two NPC in the build, both of which are not high in level. Doing this a player should be able to kill 2 to 4 Bruisers before the two patrolling Bruisers re-spawn. On average a player doing this can kill about 30 to 40 Booty Bay Bruisers gaining about 800 reputation points with the pirates. The Bruisers here don’t appear to pull out their guns, but if you find your self in a bad situation, you can jump over the railing running along the path to the waters below, to escape.\n\n[h3]Rewards[/h3]\nBecoming friendly with the Bloodsail Buccaneers will grant you access to the following items:\n\n[ul]\n[li][item=12185] - Summons a [npc=11236][/li]\n[li][item=22742][/li]\n[li][item=22743][/li]\n[li][item=22745][/li]\n[/ul]\n\nYou will need Honored with the Bloodsail Buccaneers for [achievement=2336].',NULL),(8,92,0,'[b]Gelkis[/b] are a tribe of centaur who have made their home in the southmost parts of [zone=405]. They are mortal enemies of the [faction=93], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Gelkis was [npc=13741], second of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5602] and the clan representative [npc=5397]. \n\nThe Gelkis hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Second Khan Gelk, the Magram situated themselves in the southernmost regions of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nWhen the Gelkis tribe spoke out against Khan Magra of the Magram\'s notion that strength was essential and the tribe’s survival depended on their fighting spirit, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nAs such the Gelkis are more civilized - or as close as centaur can come to civilized - than their brethren, with an organised social structure and a firm grasp of the Common tongue. While the Magram only respect strength, the Gelkis respect nature and their birthmother Theradras, calling upon her protection and the power of earth to maintain their existence. Though the Magram view this as weak it would seem to be an erroneous view, as Earth Elementals can be sighted in Gelkis Village, putting an end to unwelcome intruders alongside their centaur masters.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Gelkis in order to start their quests. Reputation for the Gelkis can be gained by killing [url=?npcs=7&filter=na=Magram]Magram monsters[/url]. When killing Magram monsters, you gain 20 reputation with Gelkis and lose 100 with the Magram tribe.',NULL),(8,93,0,'[b]Magram[/b] are a tribe of centaur who have made their home in the southeastern parts of [zone=405]. They are mortal enemies of the [faction=92], a brother tribe also located in southern Desolace. The founding leader, or Khan, of the Magram was [npc=13740], third of the alleged offspring of Zaetar and Theradras. They are presently lead by [npc=5601] and the clan representative [npc=5398]. \n\nThe Magram hold no alliance with their brother tribes, but have been known to act both hostile and passive towards members of the Alliance and Horde.\n\n[h3]History[/h3]\nOriginally lead by the Third Khan Magra, the Magram situated themselves against the mountain ranges of Desolace when the centaur divided into five tribes and have remained there ever since. \n\nBefore the death of Magra, he installed the idea that strength was essential and the tribe’s survival depended on their fighting spirit. When their brother tribe of Gelkis centaur spoke out against this notion, arguing that Theradras always watches over the centaur and will keep the tribes safe and alive, an eternal feud between the two tribes was born. \n\nThe life-long pursuit of strength has carried on through the Khans of Magram to this day, turning them violent and determined. To solidify their title as the strongest the tribe still fights fiercely to weaken or destroy their brother clans, viewing the Kolkar as weak, the Gelkis as nothing more than a nuisance, and the Maraudine as a formidable enemy. \n\nIt can be assumed that the Magram’s culture has developed into revolving around strength worship above all else. When compared to the Gelkis, the Magram hold very primitive forms of speech and social structure. For example, their grasp of common is limited and the position of Khan would likely be sought through a death match of sorts.\n\n[h3]Reputation[/h3]\nOne of the two factions situated in Desolace, you are required to have a certain amount of reputation with the Magram in order to start their quests. Reputation for the Magram can be gained by killing [url=?npcs=7&filter=na=Gelkis]Gelkis monsters[/url]. When killing Gelkis monsters, you gain 20 reputation with Magram and lose 100 with the Gelkis tribe.',NULL),(8,270,0,'[b]Zandalar Tribe[/b] trolls have come to Yojamba Isle in [zone=33] in the effort to recruit help against the resurrected Blood God and his Atal\'ai Priests in [zone=19] and in the [zone=1417].\n\n[h3]History[/h3]\nThe Zandalarians were the earliest known trolls, the first tribe from which all tribes originated. Over time two distinct troll empires emerged - the Amani and the Gurubashi. They existed for thousands of years until the coming of the Night Elves, who warred with them and eventually drove both empires into exile. \n\nFollowing the Great Sundering, the defeated Gurubashi grew ever more desperate to eke out a living. Searching for a means to survive, they enlisted the aid of the savage [npc=14834], also known as the Soulflayer. Hakkar grew into a merciless oppressor who demanded daily sacrifices from his devotees, and so in time the Gurubashi turned on their dark master. The strongest tribes (including the Zandalar) banded together to defeat Hakkar and his loyal troll priests, the Atal\'ai. The united tribes narrowly defeated the Blood God and cast out the Atal\'ai... despite their victory, however, the Gurubashi Empire soon fell. \n\nIn recent years the exiled Atal\'ai priests have discovered that Hakkar\'s physical form can only be summoned within the ancient and once-deserted capital of the Gurubashi Empire, Zul\'Gurub. Unfortunately, the priests have met with success in their quest to call forth Hakkar—reports confirm the presence of the dreaded Soulflayer in the heart of the ruins. \n\nAnd so the Zandalar tribe has arrived on the shores of Azeroth to battle Hakkar once again. But the Blood God has grown increasingly powerful, bending several tribes to his will and even commanding the avatars of the Primal Gods— Bat, Panther, Tiger, Spider and Snake. With the tribes splintered, the Zandalarians have been forced to recruit champions from Azeroth\'s varied and disparate races to battle, and hopefully once again defeat, the Soulflayer.\n\n[h3]Reputation[/h3]\nReputation with the Zandalar Tribe is gained from killing trash and bosses in Zul\'Gurub as well as repeatable and special quests which require instance-dropped items to complete. Each full run of Zul\'Gurub gives approximately 2,500-3,000 reputation.\n\nBefore the Burning Crusade, the main reason for gaining reputation with the tribe were the [url=?items=0.6&filter=na=Zandalar]shoulder[/url], [url=?items=0.6&filter=minrl=60;maxrl=60;cr=18:107;crs=4:0;crv=0:to+a+leg+or+head+slot+item]head and leg[/url] slot item enchants. As well, there were popular alchemy and enchanting recipes that many end-game guilds sought after. All rewarded items from the item set within Zul\'Gurub required a set level of reputation.',NULL),(8,349,0,'[b]Ravenholdt[/b] is a guild of thieves and assassins that welcomes only those of extraordinary prowess into its fold. They are diametrically opposed to the [faction=70], and are a rogue-only faction as all quests are rogue-only quests. The exception is the quest [quest=8249], which is available to non-rogues, but they would require the help of a rogue to get the items for the quest. [b]Ravenholdt Manor[/b], the faction\'s headquarters, is located in [zone=36], but to get there you have to come from the northeast corner of [zone=267].\n\n[h3]Reputation[/h3]\nAll Syndicate [url=?search=Syndicate#npcs]humanoids[/url] give 1-5 reputation points per kill depending on your current level. As well, there are a few quests that increase your reputation, but your primary method to raise your reputation is from the repeatable quests for turning in pickpocketed items.\n\nYou start off at 0/3000 Neutral with Ravenholdt, meaning if you kill any Ravenholdt NPCs before raising your reputation by at least 5, you will become Unfriendly and be unable to raise your reputation any higher ever again. To raise your reputation from Neutral to Friendly, the repeatable quest [quest=6701] is available. You will have to turn in 11-12 [item=17124] and once you are Friendly, this quest is no longer an option. From Neutral to Friendly you can also deliver five [item=16885] for Junkboxes Needed.\n\nTo raise your reputation beyond Friendly, the only choice is the repeatable quest Junkboxes Needed. There is no known faction reward for obtaining Friendly, Honored, Revered or Exalted, except that the guards speak to you with more respect. However, Exalted is required to obtain the Feat of Strength [achievement=2336].',NULL),(8,369,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[b]Gadgetzan[/b]\n[faction=470]\n[/minibox]\n\n[b]Gadgetzan[/b] is the faction of the city Gadgetzan, which is home to goblinhood\'s finest engineers, alchemists and merchants and is the only spot of civilization in the entire desert. Rising out of the northern [zone=440] desert like an oasis, Gadgetzan is the headquarters of the Steamwheedle Cartel, the largest of the Goblin Cartels. The Goblins believe in profit above loyalty, thus Gadgetzan is considered neutral territory in the Horde/Alliance conflict.\n\n[h3]History[/h3]\nAlthough the goblins\' neutrality is almost universally acknowledged, there are still those who seek to sow chaos and anarchy. For Gadgetzan, this comes in the form of the Wastewander bandits, a gang of miscreants who have occupied the Waterspring Field and Noonshade Ruins of northeast Tanaris. Few goblins care about ancient ruins (unless they have treasure) – for all they care, the bandits can have the old blocks of stone. \n\nHowever, the Waterspring Field is vital to the goblins\' survival in the desert, providing them with the liquid gold of the desert. Water towers out in the field were constructed under the blazing heat of the desert sun by the backbreaking work of their slaves, and by Az, the goblins aren\'t going to give up their hard earned towers that easily. However, the Bruisers need to stay in town to keep the gnomes\' collective Napoleonic-complex from getting out of hand and to stop the seemingly endless dueling among the various visitors from disrupting business. Therefore, it falls to brave mercenaries from all corners of the world to help the goblins in their time of utmost need.\n\n[h3]Reputation[/h3]\nKilling the [url=?npcs=7&filter=na=Southsea]Southsea[/url] and [url=?npcs=7&filter=na=Wastewander]Wastewander[/url] monsters will increase your reputation with the Steamwheedle Cartel. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you. Having an exalted reputation means that the guards will never attack you even if you initiate attacks on the opposite faction.\n\nMost of the quests associated with the Gadgetzan faction are located in Tanaris.\n\nIf you are Hated with Gadgetzan, you can do the repeatable quest [quest=9268] to obtain Neutral.',NULL),(8,470,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Ratchet[/b]\n[/minibox]\n\n[b]Ratchet[/b], the faction of the city Rachet on Kalimdor’s central east coast in [zone=17], is run by goblins and shows it. Its streets sprawl in every direction, and the architecture shows no consistency or common vision. It is a city of entertainment and trade, where anything that anyone would ever want to buy — and plenty of things that no one ever wants to buy — is on sale.\n\nRatchet is currently run by a corporate group known as the Steamwheedle Cartel a splinter group from the Venture Company, who first built the port town for trading with [zone=1637]. It is initially a neutral faction to both Horde and Alliance. A ferry conveniently connects Ratchet to Booty Bay.\n\n[h3]History[/h3]\nBuilt from equal parts of industry and decadence, the goblin port city of Ratchet sprawls along nearly a mile of of coastline where the eastern Barrens poke between [zone=14] and the [zone=15] to the sea. Ratchet is the pride of the goblins, a trade city where you can find almost anything your heart desires - and if something is not in stock, you can bet the goblins can order it. Ratchet also had regular ferries that traversed the safe though roundabout route to the island stronghold of Theramore to the south.\n\nRatchet is a city where creatures who were once the butt of jokes now reign supreme. Its streets wander without rhyme or reason through neighborhoods dedicated to one activity: commerce. Ramshackle warehouses stand next to stately stone homes. Fine shops press cheek to jowl with rude huts. Wares of every type imaginable - and some beyond the imagination - are on display in markets and in exclusive boutiques.\n\nGoblins welcome anyone with gold or items of value and a willingness to trade them for their wares and services. Merchants throng the marketplaces each day, selling everything from silks to slaves, and even at night the stores lining the twisting streets and alleys remain open for business. Those with the money can listen to skilled musicians while drinking fine ales and eating food prepared by expert chefs. For those with earthier tastes, the streets along the wharf teem with whorehouses, taprooms, and casinos.\n\nRatchet is the largest port on Kalimdor, with as many ships bringing cargo in as there are ships heading out for other sites around Kalimdor. In addition to legitimate trade vessels, pirate craft receive amnesty while in the port of Ratchet as long as they can pay the stiff docking fees. This situation makes many merchant captains furious, but they cannot hope to stay in business if they boycott Ratchet. Moreover, the Lawkeepers and hired mercenaries prowling the waterfront are eager to deal with anyone looking to cause trouble.\n\n[h3]Reputation[/h3]\nMost of the quests to raise reputation with Ratchet and the Steamwheedle Cartel are located in the Barrens. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.\n\nIf you are Hated with Rachet, you can do the repeatable quest [quest=9267] to get back to Neutral.',NULL),(8,471,0,'The Wildhammers are a clan of dwarves currently centered in the [zone=47] and [zone=3520]. The faction has been removed in patch 2.0.1.\n\n[h3]History[/h3]\n\nJust prior to the [object=175739], the Wildhammer Clan, ruled by Thane Khardros Wildhammer, inhabited the foothills and crags around the base of Ironforge. The Wildhammer Clan was unsuccessful in wresting control of [zone=1537] from the Bronzebeard and Dark Iron clans. Khardros and his Wildhammer warriors traveled north through the barrier gates of Dun Algaz, and founded their own kingdom within the distant peak of Grim Batol. There, the Wildhammers thrived and rebuilt their stores of treasure.\n\n[npc=9019] and his Dark Irons vowed revenge against Ironforge. Thaurissan and his sorceress wife, Modgud, launched a two-pronged assault against both Ironforge and Grim Batol. As Modgud confronted the enemy warriors, she used her powers to strike fear into their hearts. Shadows moved at her command, and dark things crawled up from the depths of the earth to stalk the Wildhammers in their own halls. Eventually Modgud broke through the gates and laid siege to the fortress itself. The Wildhammers fought desperately, Khardros himself wading through the roiling masses to slay the sorceress queen. With their queen lost, the Dark Irons fled before the fury of the Wildhammers.\n\nOnce the immediate Dark Iron threat was eliminated, the Wildhammers returned home to Grim Batol. However, the death of the Modgud had left an evil stain on the mountain fortress, and the Wildhammers found it uninhabitable. Khardros took his people north towards the lands of Lordaeron. Settling within the mountainous region of the Aerie Peaks and The Hinterlands, and lush forests of Northeron, the Wildhammers crafted the city of Aerie Peak, where the Wildhammers grew closer to nature and even bonded with the mighty gryphons of the area. Over time they started calling their land the Hinterlands. \n\n[b]Modern Wildhammers[/b]\nThe Wildhammer Clan currently makes its home at Aerie Peak in the Hinterlands. The most immediate threat to their security comes from the east in the form of the Witherbark Trolls and Vilebranch Trolls. They are most famous for riding into battle atop Gryphons, while wielding powerful Stormhammers.\nWildhammer dwarves have a number of clans, each ruled by a Thane. The strongest Thane rules Aerie Peak.',NULL),(8,509,0,'[b]The League of Arathor[/b] was originally established by the survivors of the Kingdom of Stromgarde to reclaim the [zone=45] from the hands of the Forsaken Defilers in Hammerfall. Today it is an organization in support of the Alliance, based out of the [zone=3358] in Refuge Pointe. They have taken it upon themselves to help supply the Alliance forces where needed, and their members include all manner of Alliance races - even though they are still predominantly Stromgardian humans.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=48] once exalted with League of Arathor and the other two battleground factions, [faction=890] and [faction=730].',NULL),(8,510,0,'[b]The Defilers[/b] seek to foil the [faction=509] in the [zone=3358] battleground. Today it is an organization in support of the Horde, based out of Hammerfall in [zone=45]. They have taken it upon themselves to help supply the Horde forces where needed, and their members include all manner of Horde races - even though they are still predominantly orcs.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Arathi Basin battleground. When you fight in Arathi Basin you earn 10 reputation per 160 resources. On Arathi Basin holiday weekends the required resources is reduced to 150.\n\nYou are granted the player title [title=47] once exalted with the Defilers and the other two battleground factions, [faction=889] and [faction=729].',NULL),(8,529,0,'The [b]Argent Dawn[/b] is an organization focused on protecting Azeroth from the threats that seek to destroy it, such as the Burning Legion and the Scourge. Strongholds of the Argent Dawn can be found in the [zone=139] and [zone=28]. It also maintains a presence in [zone=1657] and in the [zone=85], among other less notable areas. Reputation with the Argent Dawn can be used to purchase various profession recipes, misc. consumables, and to mitigate the cost of attunement to [zone=3456]. With the expansion of the Burning Crusade, Argent Dawn reputation has decreased in value.\n\nArgent is Latin for silver, which could explain why the [item=22999] has an icon of a silver sun rising.[h3]History[/h3]After the death of the [npc=16062], the corruption of the Scarlet Crusade became apparent to some of its members, who subsequently left the ranks of the [url=?search=scarlet+crusade#M0z]Scarlet Crusade[/url] and established the Argent Dawn to protect Azeroth from the threat of the Scourge without the blind zealotry present in the Scarlet Crusade.\n\nWhile they share the same goals as the Crusade, the Argent Dawn has opened its ranks to not only other Alliance races besides Humans, but also members of the Horde and even some of the Forsaken. They caution discretion and introspection, and put a lot of emphasis on researching the Scourge and how to combat them.\n\nWith time the Argent Dawn has grown diversified, and like its progenitor — the Scourge — has split again, with an offshoot called the [url=?search=brotherhood+of+the+light]Brotherhood of the Light[/url], a compromise between the Argent Dawn\'s more scholarly approach and the Scarlet Crusade\'s fanaticism.\n\n[h3]Reputation[/h3]\n[b]Scourgestones[/b]\nWhile wearing a trinket granting the Argent Dawn Commission effect, characters can loot [url=?items=12&filter=na=scourgestone]scourgestones[/url] from undead monsters they\'ve killed, and subsequently turn them in in exchange for [item=12844]. These turn-ins require various numbers of [item=12843], [item=12841], and [item=12840]. It should be noted that the token items received from the turn-ins should be saved until after Revered status is reached, as the quest turn-ins will no longer grant reputation after this point.[pad][b]Cauldrons[/b]\nAnother way to gain reputation with the Argent Dawn is through repeatable \"Cauldron\" quests. The Cauldrons are a source of \"undeathness,\" that contribute to the Scourge\'s numbers.[pad][b]Instances[/b]\nLike most factions, the player can run instances to increase his reputation. These instances are [zone=2017] and [zone=2057]. Naturally, these instances also include quests that will raise Argent Dawn reputation, as well as include Scourgestone drops.',NULL),(8,530,0,'[b]Darkspear Trolls[/b], the tribe of exiled trolls that has joined forces with [npc=4949] and the Horde. They now call [zone=1637] their home, which they share with their orc allies. [npc=10540] is their current leader.\n\n[h3]History[/h3]\nAs tribal rivalries erupted throughout the former Gurubashi Empire, the Darkspear Tribe found themselves driven from their homeland in [zone=33]. Having settled in what are believed today to be the Broken Isles, the tribe soon found themselves entangled in a conflict with a band of murlocs. Their fate seemed sealed until the orcish Warchief Thrall and his band of newly freed orcs took shelter on their island home. Controlled by a Sea Witch, a group of rampaging murlocs captured the Darkspears\' leader Sen\'jin, along with Thrall and several other orcs and trolls. Thrall managed to free himself and others, but was ultimately unable to save the trolls\' leader. Although Sen\'jin was sacrificed to the Sea Witch, he was able to reveal a vision he had in which Thrall would lead the Darkspear from the island. \n\nAfter returning to the island, Thrall and his followers managed to fend off further attacks by the Sea Witch and her murloc minions, and set sail for Kalimdor once again. Under the new leadership of [npc=10540], the Darkspear swore allegiance to Thrall\'s Horde and followed him to Kalimdor. Now considered enemies by all other trolls except the Revantusk and the Zandalari, the Darkspear are held in contempt to this day. Yet, the Darkspear have not forgotten being driven from their ancestral homes and this animosity is eagerly returned, especially towards the other jungle trolls. Having reached the orc\'s new homeland, [zone=14], the trolls carved out another home for themselves - this time among the Echo Isles on the eastern shores of the new orc kingdom. \n\nHowever, with the coming of Kul Tiras and its navy, the Darkspear were forced to retreat inland under the onslaught of the misguided commander [npc=177201]. The trolls, fighting alongside their horde brethren, defeated the enemy and reclaimed their new homeland. Shortly thereafter, a witch doctor by the name of [npc=3205] began using dark magic to take the minds of his fellow Darkspear. As his army of mindless followers grew, Vol\'jin ordered the free trolls to evacuate, and Zalazane took control of the Echo Isles. The Darkspear have since settled on the nearby shore, naming their new village after their old leader, Sen\'jin. From Sen\'jin Village they, along with their allies, send forces to battle Zalazane and his enslaved army.\n\n[h3]Reputation[/h3]\n[npc=14727] has the repeatable cloth reputation quests. As a reward for being exalted with the Darkspear Trolls, non-troll Horde players are able to ride [url=?items=15.5&filter=na=Raptor;cr=93:92;crs=2:1;crv=0:0]raptors[/url].\n\nSurrounding zone Durotar contain the most quests for gaining reputation with the Darkspear Trolls. As well, higher level players with the Burning Crusade also have a good amount of quests in [zone=3521].',NULL),(8,576,0,'As the last uncorrupted furbolg tribe (at least in their view), the [b]Timbermaw[/b] seek to preserve their spiritual ways and end the suffering of their brethren.\n\nThe Timbermaw Furbolgs inhabit two areas: [zone=16] and [zone=361]. They are presumed to be the only furbolg tribe to escape demonic corruption, though this may not be true due to the existence of [npc=3897], an uncorrupted furbolg of unknown tribe, and the Stillpine tribe on [zone=3524] in Burning Crusade. However, many other races kill furbolg blindly now, without bothering to see if they are friend or foe. For this reason, the Timbermaw furbolg trust very few.\n\nAdventurers who seek out Timbermaw Hold in northern Felwood and prove themselves as friends of the Timbermaw will learn that the furbolgs value their friends above all else. Though they possess no fine jewels or any worldly riches, the Timbermaw\'s shamanistic tradition is still strong. They know much about the art of crafting armors from animal hides, and they are more than happy to share their healing/resurrection knowledge with friends of their tribe. Besides, any reputation above Unfriendly will also grant you untroubled access to [zone=493] and [zone=618] through their tunnels.\n\n[h3]Reputation[/h3]\nReputation with the Timbermaw Hold faction is mainly gained through quests and killing in Felwood. The members of the Deadwood Tribe, another Furbolg tribe in Felwood, are the Timbermaws\' main enemies.\n\n[ul]\n[li]Killing one [url=?npcs&filter=na=Winterfall]Winterfall[/url] or [url=?npcs&filter=na=Deadwood]Deadwood[/url] Furbolg gives 10 reputation points. Gains stop at revered; Deadwoods give 2 reputation point at honored.[/li]\n[li]Killing either one of the Deadwood Bosses [npc=9464] or [npc=9462], is worth 60 reputation. There is no reputation limit.[/li]\n[li]Killing the elite Winterfall Furbolg, [npc=10738], located in a cave east of [faction=577], awards 50 reputation. There is no reputation limit, and his respawn rate is 6 to 8 minutes.[/li]\n[li]Killing the named rare mob [npc=14342] is worth 50 reputation. He is a rare spawn at Deadwood Village in Felwood and there is no reputation limit for this mob.[/li]\n[li]Killing the named rare mob [npc=10199] is worth 50 reputation. He is a rare spawn at Winterfall Village in Winterspring. Killing him will grant reputation up until Revered.[/li]\n[li]After completing [quest=8460], turning in 5 [item=21377] yields 150 reputation.[/li]\n[li]After completing [quest=8464], you will be able to turn in [item=21383] collected from furbolgs in Winterspring. Turning in 5 beads at [npc=11556] yields 150 reputation.[/li]\n[/ul]',NULL),(-13,0,0,'[menu tab=2 path=2,13,0]One of many useful features is the user-submitted comment system. This system allows users to submit their own comments to augment the data provided here. As a rule, we promote the submission of informative comments, but we also like to see the occasional joke. Moderators and users alike will apply positive and negative ratings to comments in an effort to promote the useful ones and purge unnecessary information.\r\n\r\nWith that in mind, below is a guide that can be used to determine how your comment will likely be received by the community. \r\n\r\n[pad]\r\n\r\n[tabs name=comments]\r\n\r\n[tab name=\"Before you post\"]\r\n\r\n[ul]\r\n[li][b]Read existing comments[/b] – Sometimes, the information you have may already have been posted by another user. In this case, if the information is useful, the existing comment should be given a positive rank. Posting information that was already added in a previous comment will likely result in a negative rating.[pad][/li]\r\n[li][b]Verify your facts[/b] – Make sure that what you have to post is true. A friend might tell you that a mob is immune to Frost Nova, but unless you verify that yourself, you could be posting a potentially misleading comment.[pad][/li]\r\n[li][b]Temporary usability[/b] – If you want to correct invalid or missing information on a page, keep in mind that your comment may go from a positive ranking to a negative ranking when the correction occurs. For example, informing the community that a spell is cast by Illidan Stormrage before that data has been collected will be useful at first, but once Aowow learns to parse that information and adds it to the \'Abilities\' tab, your comment becomes redundant. If you do not want to worry about the comment or do not want one of your comments to be rated negatively, consider informing us in the [url=/?forums&board=1.]Site Feedback[/url] forum. The moderation staff will be happy to add a comment to correct invalid or missing information on the page for you. Alternatively, you can delete your comment later when it becomes redundant.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Comment ratings\"]\r\n\r\n[h3][color=q2]Positive (+1)[/color][/h3]\r\n[ul]\r\n[li][b]Corrections on drop percentages[/b] – There are many instances where drop percentages will be inaccurate. For example, quest items do not drop for people who do not have the quest, so their drop percentages will be low. Also, mobs that periodically do not drop loot when they die won\'t count against the drop percentages, so these mobs may appear to have higher drop rates for some items.[pad][/li]\r\n[li][b]Strategies[/b] – If you have a strategy that can assist other users in completing a quest or defeating a mob, by all means, share![pad][/li]\r\n[li][b]Quest coordinates[/b] – Providing coordinates for the location of quest items or mobs is always useful. When possible, you should provide links to quest targets as well.[pad][/li]\r\n[li][b]Theorycrafting[/b] – We encourage users to post any information they have regarding complex calculations they may have performed to, for example, prove one item has a higher DPS than another given certain abilities.[pad][/li]\r\n[li][b]Just for laughs[/b] – If your comment is one that would be universally funny (i.e. not an inside joke), post away. We like to laugh as much as anyone else. Of course, whether your joke is funny or not is subject to our other users. :)[/li]\r\n[/ul]\r\n\r\n[h3][color=q10]Negative (-1)[/color][/h3]\r\n[ul]\r\n[li][b]Redundant information[/b] – For instance, a comment that says \"Dropped by Ragnaros\" does not add anything to the page as that information can be viewed in the \"Dropped By\" tab of the page in question.[pad][/li]\r\n[li][b]Soloed by:[/b] Unless your comment contains a detailed explanation of how you defeated a mob, these comments do not add anything to the page. Simply stating your level, class, and that you soloed the mob by using a few skills is not enough to be useful.[pad][/li]\r\n[li][b]Dropped in X kills[/b] – Telling users that you were lucky enough to get the crusader enchant in one drop is not considered useful information.[pad][/li]\r\n[li][b]NPC/Object coordinates[/b] – The coordinates for NPC or mobs are already supplied in convenient maps within the interface.[pad][/li]\r\n[li][b]Best X before level Y[/b] – Simply posting that an item is the best twink weapon or the best dagger for a rogue is not helpful unless you can back up that claim with facts.[pad][/li]\r\n[li][b]HUNTAR WEPPON[/b] – While it would be acceptable to explain why you feel a certain class with a certain spec would gain the most benefit from an item, simply stating that you feel the weapon should always go to a hunter in a raid will result in negative moderation.[pad][/li]\r\n[li][b]Confirmed![/b] – Adding a comment that simply indicates that you have confirmed a comment left by someone else clutters the comments. The best way to confirm a comment as correct is to give it a positive ranking. A comment with a high ranking will indicate to users that many people think it is useful data.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Deletion]\r\n\r\nAny comment that does not abide by the same [forumrules] will be deleted by a moderator.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,5,0,'[menu tab=2 path=2,13,5]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=compare]\r\n\r\n[tab name=\"General usage\"]\r\n\r\n[h3]Basic Controls[/h3]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/icons/save.gif border=0] [b]Save[/b] – Saves the comparison so that you may continue browsing the site without losing it. When you click on the [b]Compare[/b] button found throughout the site you will be given the option to add to your saved comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/refresh.gif border=0] [b]Autosaving[/b] – Indicates that you are viewing your saved comparison, and that any changes you make will automatically be saved. To avoid modifying your saved comparison, you may click on Link to this comparison before making any changes.[/li]\r\n[li][img src=STATIC_URL/images/icons/link.gif border=0] [b]Link to this comparison[/b] – Provides a link to a new page with the current item comparison already there! Useful for showing friends your item comparisons.[/li]\r\n[li][img src=STATIC_URL/images/icons/delete.gif border=0] [b]Clear[/b] – Removes all items, groups, and weights from the comparison tool, giving you a clean slate to work with. [b]This will [u]delete[/u] your saved comparison if used while autosaving.[/b][/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Weight scale[/b] – Allows you to add one or more weight scales to the item comparison using your own weights or one of our predefined presets. Each weight scale can have its own name. A saved comparison also contains the weight information, allowing you to store custom weight scales for future use.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item[/b] – Opens a live search that displays item suggestions as you type the name of an item. Clicking on a suggestion will add that item to your comparison.[/li]\r\n[li][img src=STATIC_URL/images/icons/add.gif border=0] [b]Item set[/b] – Opens a live search that displays item set suggestions as you type the name of an item set. Clicking on a suggestion will add all of the items in that set to your comparison.[/li]\r\n[/ul]\r\n\r\n[h3]Adding Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/addingitems.gif]\r\n[small]Some of the ways to add items to a comparison.[/small][/div]The comparison tool is fully integrated with our site and designed to be as convenient as possible to work with. There are many ways to add items to a comparison depending on what part of the site you are on: \r\n[ul][li]Using the [url=/?compare]item comparison tool[/url] itself, you may add items or item sets using the links in the top right corner as described above.[/li]\r\n[li]Viewing an [url=/?item=35137]item[/url] or [url=/?itemset=-17]item set[/url] page, you may click on the red [b]Compare[/b] button near the Quick Facts box.[/li]\r\n[li]Viewing [url=/?items=4.2&filter=sl=8]search results[/url] or [url=/?npc=34077#sells]any page with a list of items[/url], checkboxes are displayed next to items which can be equipped. You may select one or more items and click the [b]Compare[/b] button at the top of the list.[/li][/ul]\r\n\r\n[i]Note: If you have a comparison saved, and you add items to your comparison from elsewhere on the site, you will be given the option to add them to your saved comparison or create a new one. If you don\'t have a saved comparison, a new comparison will automatically be created and saved with the selected items.[/i]\r\n\r\n[h3]Managing Your Items[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/item-comparison/newgroup.gif]\r\n[small]Creating a new group by dragging an item.[/small][/div]\r\n[ul][li][b]Creating a new group[/b] – [u]Drag an item into the empty column[/u] on the right to create a new group containing that item.[/li]\r\n[li][b]Moving[/b] – To move an item or group, click on the item (or the group\'s control bar) and [u]drag it to the desired position[/u].[/li]\r\n[li][b]Copying[/b] – [u]Holding shift while dragging[/u] an item or group will make a copy of it when it is dropped.[/li]\r\n[li][b]Deleting[/b] – Items and groups can be deleted by [u]dragging them out of the row[/u]. Groups may also be deleted by clicking the X on the right side of the group\'s control bar.[/li]\r\n[li][b]Deleting all but one group[/b] – [u]Holding shift while deleting a group[/u] (see above) will cause all other groups to be deleted instead of that one.[/li]\r\n[li][b]Splitting a group[/b] – Groups of 2 or more items can be split by [u]clicking on [b]Split[/b] in the menu dropdown[/u] on the group\'s control bar. This will create a new group for each item in the current group.[/li]\r\n[li][b]Exporting a group[/b] – [u]Clicking on [b]Export[/b] in the menu dropdown[/u] of the group\'s control bar will take you to a new comparison containing only the current group.[/li]\r\n[li][b]Item Enhancements[/b] - To add gems or enchantments to an item, [u]right-click on the item icon at the top[/u], then select the desired option from the menu. The stats will automatically update—including the set bonuses.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Advanced features\"]\r\n\r\n[h3]Level Adjustments[/h3]\r\nYou can select your desired character level from the dropdown at the top left. When you do, all the statistics that change according to your level (including combat ratings and heirloom item stats) will automatically adjust to the corresponding value for the level you\'ve entered.\r\n\r\n[h3]Gains[/h3]\r\nAt the bottom of the item comparison is a special row called \'Gains\'. The gains row calculates the minimum values of all stats that appear in any group in the item comparison. It then displays the bonuses each row has [b]above[/b] this minimum.\r\n\r\nFor example, the minimum stamina for any group in [url=/?compare=35031;35030;35029;35028;35027]this comparison[/url] is 50. The gains row displays nothing for the items which have 50 stamina, +23 sta for the item with 73 stamina, and +27 sta for the items with 77 stamina.\r\n\r\nBasically, the gains row removes the shared stats between all groups so that you can focus on what each group brings to the table.\r\n\r\n[h3]Focus Group[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/item-comparison/focus2.gif thumb=STATIC_URL/images/help/item-comparison/focus.gif float=right]Comparing arena sets of the first four PvP\r\nseasons using a focus group.[/screenshot]Setting a focus group is done by clicking on the eye icon in the group\'s control bar. Selecting a group as your focus will update the display of the item comparison to show the difference in stats between all other groups and the focus group.\r\n\r\nWhen a focus is set, the focus group is highlighted and each other group has numbers that indicate the stats gained or lost in comparison to the focus group.\r\n\r\n[b][color=q2]Positive[/color][/b] numbers indicate that group has a higher total for a given stat than the focus group, while [b][color=q10]negative[/color][/b] numbers indicate that group has a lower total for a given stat than the focus group. \r\n\r\n[h3]Stat Weighting[/h3]\r\nTo add a weight scale to your comparison, click on the [b]Add a weight scale[/b] link in the top right corner. You may select a weight scale from our predefined presets or create one of your own. Each weight scale may be given a name that will appear in the score tooltips to help differentiate the different scores. You may add as many weight scales as you like.\r\n\r\nTo remove a weight scale, click on the [b]X[/b] next to the appropriate score in any group. To toggle between normalized (default), raw, and percent score mode, click on the score in any group.\r\n\r\nUnlike the weighted item search, these weight scales [b]do not[/b] automatically select gems or include socket bonuses in the score at this time.\r\n\r\n[h3]Viewing a Group in 3D[/h3]\r\nClick on [b]View in 3D[/b] in the menu dropdown of the group\'s control bar to display a 3D model of the items and select the race and gender to display them on. Of course, items which do not have models, such as trinkets and rings, will not be displayed.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,3,0,'[menu tab=2 path=2,13,3]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[pad]\r\n\r\n[tabs name=weights]\r\n\r\n[tab name=FAQ]\r\n\r\n[h3]How do weights work?[/h3]\r\nThe weighting system allows you to give a weight value to attributes that matter to you and applies your ratings to items in your search results. Each weight value is multiplied by an item\'s stat points and then added together to get the item\'s total score. This score is used to sort the results and display the highest scoring items.\r\n\r\nIf you decide that spell damage is worth twice as much as spell crit, you could add the weights as 2 and 1, 100 and 50, or any other numbers with the same ratio.\r\n\r\nPlease note that weights only work for [url=/?items=4]Armor[/url], [url=/?items=2]Weapons[/url], [url=/?items=3]Gems[/url] and [url=/?items=0]Consumables[/url]. \r\n[h3]What is the difference between weights and equivalency?[/h3]\r\nThe equivalency of two attributes describes how much one equals the other. You may find equivalency ratings that say something like 1 agility = 1.5 strength. This is [b]not[/b] the same as weight values; in fact, it\'s the exact opposite! Equivalency describes the ratio of the stats to each other, which can be used to derive the stat weights. In this example, an appropriate set of weights might be agility 3 and strength 2; this works out to agility being [i]1.5 times as valuable[/i] as strength. \r\n[h3]Is there a way to save a template that I have created?[/h3]\r\nThere sure is! You can save your stat weighting scales by going to the \'Preset\' dropdown menu, selecting \'custom,\' and then filling in your own weights. After you\'ve modified them to your liking, you can hit \'Save\' to give them a name so they can be used for future searches as well.\r\n\r\nWeights also carry over from one item list to another if you use the database menu, so going from a [url=/?items=2&filter=wt=51:48:49;wtv=83:67:58]weighted list of weapons[/url] to the [url=/?items=4&filter=wt=51:48:49;wtv=83:67:58]cloth armor listing[/url] will also maintain your current weight scale. \r\n[h3]Is it better to match sockets and gain the socket bonus, or use the best gems?[/h3]\r\nThe weighting system answers this for you automatically. It compares the score of matching gems plus the score of the socket bonus, to the score of the best gems it could put in that item. It will automatically put in the gems that result in the highest net rating, taking socket bonuses into account. When the socket colors are matched, the socket bonus text will be listed below the gems for each item. \r\n\r\n[h3]What are the default weight presets based on?[/h3]\r\nWe\'ve done a great deal of research, tracking down equivalence points for all of the classes. We\'d like to thank all of the hard-working theorycrafters at [url=http://elitistjerks.com/f47/t21302-theorycrafting_think_tank/]Elitist Jerks[/url], [url=http://forums.tkasomething.com/showthread.php?t=9542]TKA Something[/url], [url=http://shadowpanther.net/aep.htm]Shadow Panther[/url], [url=http://druid.wikispaces.com/Healing+Gear+List]The Druid Wiki[/url], [url=http://www.emmerald.net/]Emmerald[/url], [url=http://www.lootrank.com/wow/templates.asp]Lootrank[/url], [url=http://pawnmod.trenchrats.com/index.php]Pawn Mod[/url], and [url=http://www.codeplex.com/Rawr]Rawr[/url], as well as a host of threads on the World of Warcraft forums. They provided the inspiration for the weighted search and a starting point for our preset values.\r\n\r\n[/tab]\r\n\r\n[tab name=\"Helpful tips\"]\r\n\r\n[ul]\r\n[li]You can help us [b]improve[/b] our presets! Email your suggestions to [feedback].[/li]\r\n[li]Don\'t weight stats that your character is [b]already capped on[/b] (e.g. Hit rating). Be sure to tweak the presets as needed![/li]\r\n[li]You can adjust a preset by clicking on the \'show details\' button.[/li]\r\n[li]Once you have generated a weighting you like, you can bookmark that page. Then, if you browse around on other pages using the menus at the top, your weight scale will be applied to that page as well.[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Why?]\r\n\r\n[h3]Why does it give a higher score to 2H weapons over 1H weapons, when using a 1H + OH is better?[/h3]\r\nThe scores are based off the stat weights of the item by itself. Two-handers rank higher because by themselves they do have better stats than a one-hander with nothing else in the off hand. If you add up the scores of a main hand and off hand item, the total score is what you should use to compare to that of a two-hander. We do not assume a score for your offhand item, as there is no way of knowing what you have or can obtain for that slot unless you do a weighted search for it. \r\n[h3]Why does the preset list X as more important than Y?[/h3]\r\nSome attributes come in unusual value ranges on items, which affects their equivalency to other stats. It does not mean that your should focus on or ignore that stat, but that a single point of it is worth more or less compared to other stats. Stats with high number ranges (armor, weapon damage, penetration, etc) will require smaller weight values, while stats with low number ranges (mana regeneration) will require much larger weight values.\r\n\r\nIn essence, giving mana regeneration a score of 100 and healing a score of 25 does [b]not[/b] say that mana regeneration is more important than healing, simply that each point of mana regeneration is the equivalent of 4 points of healing.\r\n[h3]Why don\'t you have a preset for PvP/Tier 6 Raiding/...? Why doesn\'t your preset give a stat value for X?[/h3]\r\nIf you would like to suggest changes to the existing presets or new presets for other specs or situations, please do so to [feedback]. \r\n[h3]Why doesn\'t the preset limit the items to X, Y, and Z?[/h3]\r\nThe weight presets are for sorting; filters are for limiting the search results. If you want to restrict the items you see, use the appropriate tool - the filter options. The only limit applied by the weight scales is that it will not display items with a score of 0 or less. You should continue to use the existing filtering system if you want to see items of a specific type, slot, source, speed, etc.\r\n[h3]Why does it suggest the gems it does for the sockets?[/h3]\r\nThe suggested gems are based on your weights. If you would like to see a different gem in the sockets, try increasing the weight of the appropriate stat. If you feel the weights in the presets need to be adjusted, please let us know at [feedback].\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-13,2,0,'[menu tab=2 path=2,13,2]\r\n\r\nWe thrive on user contributions! Quest data, database comments, forum posts - you name it, we love it! One of our favorite methods of contribution is via uploaded [b]screenshots[/b], images depicting various items, NPCs or quest details in the World of Warcraft. Users can submit screenshots to any database page which will then be reviewed by our staff and, upon approval, added to a database page! Taking and uploading screenshots is easy!\r\n\r\n[small]The information below is graciously provided by [url=http://us.blizzard.com/support/article.xml?locale=en_US&articleId=21048]Blizzard Support[/url].[/small]\r\n[h3]Taking Screenshots on Windows[/h3]\r\n[ul]\r\n[li]While in the game, press the Print Screen key on your keyboard.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a .JPG file in the Screenshots folder, in your main World of Warcraft directory.[/li]\r\n[li]You should be able to double click on the screenshot files to view the screenshots in Windows default image viewer.[/li]\r\n[/ul]\r\n\r\n[b]Extra notes for Windows Vista users[/b]\r\n[ul]\r\n[li]Due to extra security on the system the screenshots will be saved to the following folder:C:\\\\users\\\\*your user name*\\\\AppData\\\\Local\\\\VirtualStore\\\\Program Files\\\\World of Warcraft\\\\Screenshots[/li]\r\n[li]You may also have to turn on the ability to view hidden files as the AppData folder may be hidden.\r\n[ul]\r\n[li]Click the Start/Window button, select Control Panel, Appearance and Personalization, Folder Options.[/li]\r\n[li]Next click on the View tab, under the Advanced settings, click Show hidden files and folders, and click OK to finish.[/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]Taking Screenshots on Mac[/h3]\r\n[ul]\r\n[li]Players can take a screenshot in-game using the keyboard key bound to the Print Screen functionality.[/li]\r\n[li]If you have a keyboard with an F13 key, press the key to take an in-game screenshot. Players without an F13 key on the keyboard can change the default Screen Shot key in the Key Bindings menu.[/li]\r\n[li]You should see a \"Screen Captured\" message.[/li]\r\n[li]The screenshot will appear as a JPEG file in the Screenshots folder, in your main World of Warcraft folder.[/li]\r\n[/ul]\r\n\r\nRemember to turn off your in-game UI using the Alt+Z (or ⌘+V) command! Upon taking your screenshot, you can then go in and use an image editor (such as the free program [url=http://www.getpaint.net]Paint.NET[/url]) to crop your image for faster upload. You can select specific sections of a screenshot to upload (if you are featuring a particular piece of armor, for example) and save the file, then simply upload your pre-cropped image directly! If not, you can easily crop your screenshot after uploading but before submitting using our handy tool.\r\n\r\nTo submit a screenshot, simply navigate to the database entry for which you\'ve taken a screenshot and navigate to the \'Contribute\' section. Select the \'Submit a screenshot\' tab and click \'Choose file\' to locate the file on your system. Remember that only PNG and JPG file types are accepted! Once you have selected the screenshot simply click \"Submit\" and you\'re on your way! You will then be able to crop the image if necessary before your image is finally submitted for review. Upon approval (which may take up to 72 hours) your screenshot will then be featured on the database page, as well as in a \'Screenshots\' tab in your user profile!\r\n\r\n\r\n[h2]Quality Tips[/h2]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/hinterlands.jpg thumb=STATIC_URL/images/help/screenshots/hinterlands2.jpg float=right]The Hinterlands[/screenshot]A good screenshot is like a miniature piece of art. It should showcase the main object, but take into account the details around it. The same 7 elements of art design come into play here, Line, Shape, Form, Space, Texture, Light & Color. We\'ll touch on several of these and how to make use of the in game settings and mechanics to enhance your pictures.\r\n\r\nTurn your resolution and color sampling as high as your computer can handle. Turn on all the image effects and details, but turn down the weather effects to the lowest setting. In general you want all your glow and spell effects maxed to really show the environment to its fullest potential (they actually help with the lighting too!) You may find a shot that you need to play with these settings to enhance, sometimes turning down environmental detail is helpful to remove extra grasses.\r\n\r\nWorld of Warcraft actually has an internal setting for screenshot quality, and by default that quality is set to [b]3/10[/b]. You can turn this up, though, in order to take higher quality screenshots. In order to do so, type this command into your chatbox:\r\n\r\n[code]/console screenshotQuality 10[/code]\r\n\r\nMost of the time taking the pictures from 1st person view works best, so zoom all the way in so that you\'re looking through your character\'s eyes. Occasionally the object might be too big (large NPCs especially) to use this view - if this is the case get as close to them as you can without having your body in the shot and swing the camera around to get the angle that you\'re looking for.\r\n\r\nPay attention to the light - a well lit picture is 10 times better than a dark one. You may even want to do a little color correcting before uploading - increase the brightness and contrast a touch. For instance - it\'s a lot easier to take pictures in sunny Stormwind than deep in the mountains of torch lit Ironforge. Daytime pictures also turn out better than night.\r\n\r\n[h3]Featuring Armor[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/armor.jpg thumb=STATIC_URL/images/help/screenshots/armor2.jpg float=right]Dreamwalker Spaulders[/screenshot]We want to see the armor! Not Joe Schmoe in the armor. In general you want close ups of the piece itself (except for full set pictures). Don\'t be afraid to submit a 4 inch picture of one glove. Once\'s it\'s cropped and loaded and shrunk down to the thumbnail it will look great!\r\n\r\nUse your best judgment when cropping armor pics, but remember - we want to see details of the armor - not the person or a far away image. Of course, this also applies to weapons or any other piece of equipment!\r\n\r\n[h3]Featuring NPCs[/h3]\r\n\r\n[screenshot url=STATIC_URL/images/help/screenshots/npc.jpg thumb=STATIC_URL/images/help/screenshots/npc2.jpg float=right]Cairne Bloodhoof [/screenshot]Full body shots should be the norm. If you can\'t get a good full shot (e.g. they\'re standing behind a counter) get the waist up shot. There\'s no need to include the on-screen text and titles of NPCs. The website already lists those, so just get in close and take a great shot of the NPC itself.\r\n\r\nGet down on their level - you may need to \"/sit\" or even \"/sleep\" to get a good view of something low to the ground (scorpions, boots, spiders, etc.)\r\n\r\nWhen capturing moving NPCs, try to get as much a head on front shot as you can, being willing to take a few hits while you take picture of a mob attacking you can make for a great shot. If you don\'t want to get your hands dirty, sitting in place for a while and waiting for it to path in front of you is often easier and faster than running around it trying to get your shot.\r\n\r\nTalking to friendly NPCs will usually make them face you - you can then spin around and get the best background for your picture. You may also catch them in an interesting motion or gesture.',NULL),(-13,6,0,'[menu tab=2 path=2,13,6]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]!\r\n\r\n[pad]\r\n\r\n[tabs name=profiler]\r\n\r\n[tab name=\"Browsing characters\"]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/menu.gif]\r\n[small]Navigating the menu to your battlegroup and realm.[/small][/div]We maintain a database of [i]millions[/i] of [url=http://www.wowarmory.com/]Armory[/url] characters, guilds, and arena teams that have been imported by our users. You can browse through this extensive list by visiting the main [url=/?profiles]profiles[/url] page and selecting a region, battlegroup, or realm from the menus at the top.\r\n\r\nThis will give you an unfiltered look at the players and guilds in the area you selected, with the most recently updated characters displayed first. You can also enter your characters name in the box at the top to jump directly to that character.\r\n\r\n[h3]Finding My Characters[/h3]\r\n\r\n[ul]\r\n[li]Use the breadcrumb listings at the top to browse to your region, battlegroup, and realm. When you do this, a box will appear in the listing at the top of the page. Enter your character\'s name in this box to be taken directly to your character. You can use the \"Claim Character\", which is located under the Manage Character button, to save a character to your [url=/user=fewyn#characters]user page[/url] for later viewing.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Claimed characters can be made public or private as you choose—so you only show off the characters people want you to see! Basic information for the profiles will remain public, just as it is in the Armory—but any connection to your account will be hidden.[/i]\r\n\r\n[h3]Filters[/h3]\r\nBut that\'s not the only way to find a character! You can also search Profiles using our robust filter system, just the same way that you can search items, NPCs, or spells in game. Characters and guilds can be filtered by name, region, and realm to limit the number of displayed results.\r\n\r\nAdditionally, characters can be filtered by faction, level, race, and class – as well as a number of other unique and useful criteria. For example:\r\n\r\n[ul]\r\n[li][div float=right align=right][img src=STATIC_URL/images/help/profiler/filters.gif]\r\n[small]Searching for characters that match your criteria.[/small][/div]Let\'s see [url=/?profiles=us.draenor&filter=cl=8;ra=11;cr=35;crs=0;crv=450]all the Draenei mages on my server that have their tailoring maxed out[/url].[/li]\r\n[li]Hmm... I wonder if anyone is [url=/?profiles=eu&filter=na=Malgayne]using my name on European servers[/url]?[/li]\r\n[li]How do I compare to [url=/?profiles=us.draenor&filter=cl=2;minle=80;maxle=80;cr=7;crs=1;crv=50]other Retribution-specced paladins on my server[/url]?[/li]\r\n[li]How many [url=/?profiles&filter=cr=23;crs=0;crv=871]Bloodsail Admirals[/url] are there out there?[/li]\r\n[li]Who got caught wearing a [url=/?profiles&filter=cr=21;crs=0;crv=22279]Lovely Black Dress[/url]?[/li]\r\n[li]How many people on my server and faction [url=/?profiles=us.sentinels&filter=si=2;cr=23;crs=0;crv=2904]completed Heroic Ulduar[/url]?[/li]\r\n[/ul]\r\n\r\nWe\'ll be adding more filters as time goes on, so feel free to experiment – and let us know if you think of other ideas!\r\n\r\n[pad][pad][pad]\r\n\r\n[h3]Guild and Arena Team Rosters[/h3]\r\nWhen you click on a character\'s guild or arena team, you will be directed to a roster view listing all the characters that belong to it. The roster view displays additional information, including guild ranks and personal arena team ratings. You can further filter this information using the [b]Create a filter[/b] link, should you want to find characters matching specific criteria. Now its easy to find all of the crafters in your guild!\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/queue.gif float=right]Resync Queue[/h3]\r\nWhen a character resync is requested, it is added to the queue. The queue is used to make sure everyone\'s characters are updated and processed in the order they were submitted, without overloading the [url=http://us.battle.net/wow/en/]Battle.net Armory\'s API[/url] with requests. Whenever you access a character that does not exist in our database or has not been updated in more than 1 hour, it will automatically be added to the queue.\r\n\r\n[/tab]\r\n\r\n[tab name=\"General usage\"]\r\n\r\nThe profiler has a wealth of information it can display about characters and custom profiles, so it can seem daunting at first! Each of the sections are broken down in detail below.\r\n[h3]Basic Profile Information[/h3]\r\nAt the top of a profile you will see an expanded header with vital information about the profile itself. All profiles have an icon and the character\'s race, class and level; Armory characters display a link to the character\'s guild under the name, while custom profiles display a description set by the user that created it. A link to [b]Edit[/b] this information appears on the bottom line, allowing you to update a profile you created or make a new custom profile from an existing one.\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/edit.gif float=right][b]Name [/b]– Give your profile a name! Names must start with a letter, and can only contain letters, numbers, and spaces.[/li]\r\n[li][b]Level[/b] – Select a level for your profile. Profiles must be at least level 10 (55 for Death Knights) and no more than level 85.[/li]\r\n[li][b]Race[/b] – Ever wonder what you\'d look like as a tauren instead of an orc? Choose any race for your profile, and the character model with automatically be updated.[/li]\r\n[li][b]Class[/b] – You can select any class you like, regardless of racial restrictions. See what your stats would be if you were a draenei druid![/li]\r\n[li][b]Gender[/b] – Select male or female to set your character\'s gender.[/li]\r\n[li][b]Icon[/b] – Icons are automatically generated for Armory characters and in game class/race combinations, but you can change the icon to any you like.[/li]\r\n[li][b]Description[/b] – Enter a tag line or brief description for the profile so you and others know what it is about.[/li]\r\n[li][b]Visibility[/b] – Public profiles will be visible on your user page and anyone can view a public profile. Private ones will not be displayed or visible to others.[/li]\r\n[/ul]\r\n[i]Note: If you edit a character in any way, it will become a custom profile. The reputations, achievements, and raid progress information will be removed.[/i]\r\n\r\n[h3]Managing Profiles[/h3]\r\nIn the upper right are a number of useful buttons for managing profiles without having to go back to your user page. Each of the buttons have several options that can be used to manage the character\'s page you are currently on and include the following options.\r\n\r\n[ul]\r\n[li][b]Custom Profile[/b]\r\n[ul][li][b]New[/b] – This is a quick link to creating a new, blank profile from scratch. It will open in a new window so you do not lose your current profile. This option is always available.[/li]\r\n[li][b]Save[/b] – Save any changes you have made to this profile. This option is only available for logged in users on profiles they own.[/li]\r\n[li][b]Save as[/b] – This will let you save your current changes under a new name. It is extremely useful for making copies of profiles! This option is only available for logged in users.[/li][/ul][/li]\r\n[li][b]Manage Character[/b]\r\n[ul][li][b]Resync[/b] – Request that the character be updated from the armory; it will be added to the queue. This option is only available on Armory character pages.[/li]\r\n[li][b]Claim character[/b] – Adds an Armory character to your user page. This is a good thing to do with all your alts. This option is only available for logged in users on Armory character pages.[/li]\r\n[li][b]Remove[/b] - Removes the character from your user page. Use this if you no longer play the character or have long since deleted it.[/li]\r\n[li][b]Pin/Unpin[/b] - Pin one of your characters so you can perform personalized searches throughout the database for missing or completed quests, achievements, recipes and more![/li]\r\n[/ul][/li]\r\n[/ul]\r\n\r\n[h3]From the User Page[/h3]\r\n[img src=STATIC_URL/images/help/profiler/userpage.gif float=right]All of your claimed Armory characters and custom profiles are listed in one convenient place on your user page. From the [b]Characters[/b] tab you can remove one or more claimed characters. The [b]Profiles[/b] tab allows you to create a new profile, delete profiles, or change the visibility settings of profiles. Your private profiles will not be visible to anyone else.\r\n\r\n[i]Tip: When you are logged in, all of your characters and custom profiles can be accessed from the [b]My profiles[/b] menu at the top right of any page![/i][pad]\r\n[h3]Saving Your Work[/h3]\r\nAny profile can be edited, even if you don\'t own it, but you\'ll probably want to save your work when you\'re done! You must have an account with us in order to save a profile. Once you\'ve created an account, you can bookmark any number of Armory characters and save up to 10 custom profiles. Premium users will be able to create even more, so upgrade if 10 just isn\'t enough! You can use the red buttons to save a profile from its page, and manage your existing profiles and characters from your user page. \r\n\r\n[/tab]\r\n\r\n[tab name=\"Inventory and talents\"]\r\n[img src=STATIC_URL/images/help/profiler/character.jpg height=300 float=right]The main tab for a profile is the character inventory, which includes a lot of the same information you would see by looking at your character pane in game. This tab is broken up into four key sections - the character view, quick facts box, statistics, and gear summary.\r\n\r\n[h3]Character View[/h3]\r\nThe first thing you\'ll notice, of course, is your character – as rendered by our custom built modelviewer, in all it\'s three-dimensional glory. You can turn the character with your mouse, and zoom in and out using the A and Z keys, just like the modelviewer elsewhere in the site. [b]We even pull your face, hair, and skin color information from the Armory![/b]\r\n\r\nOn either side of the character are inventory icons which you can right click on for a menu of options:\r\n\r\n[i]Tip: You can remove a gem or enchant by clicking None in the picker window or by right clicking on it in the gear summary.[/i]\r\n\r\n[ul]\r\n[li][img src=STATIC_URL/images/help/profiler/itemmenu.gif float=right][b]Equip... / Replace...[/b] – Selecting this option will give you a quick search box in which you can type an item\'s name. Click on the item or hit return to equip it.\r\nUnequip – Unequips the item, of course. :)[/li]\r\n[li][b]Add / Replace enchant...[/b] – The spell icon on the left shows if the item is enchanted. This opens a customized picker window with all enchants available for the item slot.[/li]\r\n[li][b]Add / Replace gem...[/b] – The icon on the left shows the socket color or socketed gem. Like the enchants, this opens a picker window with valid gems for the socket.[/li]\r\n[li][b]Extra socket[/b] – The check mark on the left indicates if a blacksmithing socket has been added to this item. Click to toggle on or off.[/li]\r\n[li][b]Clear Enhancements[/b] - This will remove all reforges, enchantments, gems and extra sockets from an item. Useful if you want to start fresh with an item.[/li]\r\n[li][b]Display on character[/b] – The checkmark on the left indicates if the item is displayed on the model. Click to toggle on or off – it works for more than just cloaks and helms![/li]\r\n[li][b]Compare[/b] – Adds the item to the [url=/?compare]item comparison tool[/url] and opens it in a new window to compare with other items.[/li]\r\n[li][b]Find upgrades[/b] – Uses our [url=/?help=stat-weighting]weighted search[/url] to find upgrades based on your talent spec.[/li]\r\n[li][b]Who wears this?[/b] – Creates a filtered list of other Armory characters who are also wearing the item.[/li]\r\n[/ul]\r\n\r\n[i]Tip: Items that can take enchantments but have no enchantment, or which have empty sockets, will even have a little notification in the tooltip![/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quickfacts.gif float=right][h3]Quick Facts Box[/h3]\r\nOn the right hand side is a handy Quick Facts box that displays basic, defining information about a profile. This box is chock full of useful information, including talent spec, achievement points, and professions.\r\n\r\n[i]Tip: Any raid icon that\'s ringed in [color=c4]gold[/color] is a raid that the character has cleared![/i]\r\n[h3]Statistics[/h3]\r\nYou\'ll also notice that all of a profile\'s statistics are laid out beneath the character view. This is also all information you can get from the Armory (and then some), but we lay it out in a nice, convenient page so you can view it all at once – no more messing with drop down menus. You can also click on a statistic and expand it so you can see its tooltip information right there on the page—or click on the header to expand all the related statistics. Your statistics are updated as you edit any part of a profile, including race, class, level, items, enhancements, or talents – all in real time! [b]Statistic modifications from glyphs and buffs are not presently supported, but will be in the future.[/b]\r\n\r\n[i]Note: These statistics are calculated manually – they are not pulled from the Armory. Statistics calculations are still in beta and will ironed out as we go.[/i]\r\n\r\n[img src=STATIC_URL/images/help/profiler/statistics.gif float=center]\r\n\r\n[h3]Gear Summary[/h3]\r\n[div float=right align=right][img src=STATIC_URL/images/help/profiler/gearsummary.gif]\r\n[small]A warning message is displayed for missing enhancements.[/small][/div]Last on the character inventory tab, but not least, is the gear summary. This is a personalized list of all items worn by the character, with convenient column headers and in line filtering options. Use it to see where most of a character\'s items come from, what is the best and worst piece, and whether or not there are missing gems and enchants. Just in case the empty icons aren\'t clear enough, a warning appears at the top of the list if a character is missing gems, enchants, or blacksmith sockets. This [color=q10]warning[/color] is based on the professions of the character if it is an Armory profile, and otherwise shows you everything missing on custom profiles.\r\n\r\nThe gems and enchants can also be edited from within the gear summary, and have a few additional options not available in the character view. You can remove or replace an enhancement from here, and you can find upgrades using our [url=/?help=stat-weighting]weighted search[/url] – just like items!\r\n\r\n[h3]Talents[/h3]\r\nThe talents tab includes an inline version of our [url=/?talent]talent calculator[/url] with a full display of a character\'s talents. It is locked by default, but you can unlock it to begin editing talents, just as you would normally. There are two extra features in the Profiler\'s talent calculator: you can store and swap between two specs for each character, and export the current talent build to the calculator to link to your friends. When you change your talents (or swap between specs) your gear score and statistics will be updates real time!\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other tabs\"]\r\n\r\n[h3]Reputation[/h3]\r\nThe reputation tab displays the complete faction information of an Armory character, with collapsible headers for each section. Its much easier to read than the tiny faction pane in game! Of course, you can link directly to the faction\'s page to get more information about that faction. \r\n[h3][img src=STATIC_URL/images/help/profiler/achievements.gif float=right]Achievements[/h3]\r\nThe achievements tab lists an Armory character\'s progress in each of the main achievement categories, and has a filterable list of achievements including date completed. All of the normal column and list filters are available, along with some new ones! You can filter the list by earned, in progress or complete achievements – complete are displayed by default – or click on any of the category progress bars to only display achievements from that category.\r\n\r\n[/tab]\r\n\r\n[tab name=Completion_Tracker]\r\n\r\n[img src=STATIC_URL/images/help/profiler/quests.jpg float=right width=450]You can use the Profiler\'s [b]Completion Tracker[/b] feature to keep track of your quests, achievements, pets, mounts, recipes, and more!\r\n\r\n[h3]Getting Started[/h3]\r\n\r\nIn order to start tracking your completion data, all you need to do is visit your character\'s page on the profiler and resync it. This will automatically collect data about your character\'s completed achievements, companion pets, mounts, quests, recipes, reputations and titles.\r\n\r\n[h3][img src=STATIC_URL/images/help/profiler/completion.jpg float=right]Tracking Your Completion Data[/h3]\r\n\r\nOnce you\'ve got your data up on the site, it will be available in the form of five new tabs: [b]mounts[/b], [b]companions[/b], [b]recipes[/b], [b]quests[/b], and [b]titles[/b].\r\n\r\nIf you open the mounts, companions, or titles tabs, you\'ll immediately be greeted by a list of all the entries you\'ve already completed. You can cycle through the different tabs to see the ones you already have, the ones you still have yet to collect, a complete list, or a list of just the ones you\'ve \"excluded\" (more on that shortly). You can also use the \"Search within results\" box to search the list based on a keyword, just like you can with other search results in the database.\r\n\r\nThe recipe, and quest tabs, like the Achievements tab, contain more entries—so you\'ll be presented with a box like the one shown above. From there, all you have to do is click one of the progress bars to see the complete tabbed list in each category.\r\n\r\n[h3]Exclusions[/h3]\r\n\r\nWhen you\'re trying to make sure we check off every quest, achievement, or mount on our list, everyone knows that there are some that you just don\'t want to bother with. To that end, we\'ve created [b]exclusions[/b].\r\n\r\n[img src=STATIC_URL/images/help/profiler/exclusions.jpg float=right]Using exclusions, you can flag certain quests, mounts, achievements, recipes, pets, or titles that \"don\'t count\" toward your completion total. When you exclude (for example) a quest, that quest no longer appears in \"incomplete\" listings, and the total number of quests in that category is reduced by one.\r\n\r\n[b]For example:[/b] There are 632 quests in the \"Eastern Kingdoms\" category. If I were to decide that [quest=367] is for noobs and I don\'t want to count it, then all I have to do is put a check in the box next to the quest and click \"Exclude\". After I do so, the Eastern Kingdoms progress bar will only show [i]631[/i] quests total—the remaining quest will appear in the \"Excluded\" tab but won\'t be counted for anything else.\r\n\r\nIf you want to re-include a quest, just go to the \"Excluded\" tab and then use the checkboxes to restore as many as you like. You can do the same thing for achievements, titles, mounts, pets, or recipes.\r\n\r\nIf you [b]complete[/b] a quest that you have excluded, it will show in the progress bar as a [b]+1[/b]. Example: If there are 31 quests in the \"Miscellaneous\" category, and I\'ve completed 20 quests and excluded 1, the progress bar will show [b]20/30[/b]. If I have completed [i]the quest that I excluded[/i], then the progress bar will show [b]20(+1)/30[/b]. If I then go on to complete ALL the quests in that category (including the one I excluded), the progress bar will show [b]30(+1)/30[/b].\r\n\r\n[b]Exclusion Manager[/b]\r\nThe companions and mounts tabs let you manage your exclusions en masse with the Exclusion Manager. Just click the \"Manage Exclusions\" button on top of the tabs to see a list of convenient categories you might want to exclude. There\'s also a \"reset all\" button here to let you wipe all of your exclusions and start over.\r\n\r\n[b]Note:[/b] The Exclusion Manager is currently only available for companions and mounts.\r\n\r\n[i]Tip: Exclusions are tied to your account, not to a particular character. This is so even when you look at someone else\'s character, you\'re judging them by [/i]your[i] completion standards, not anyone else\'s![/i] \r\n\r\n[/tab]\r\n\r\n[tab name=Calculations]\r\n\r\nMost of the information we display is pretty straightforward. A lot of it, particularly the stats on items, is readily available in our database and on various tooltips. There are some new numbers on profile pages that you may ask, what does this number mean? How was it calculated?\r\n[h3]Base Statistics[/h3]\r\nA character\'s five base statistics are determined primarily by his or her class and level. This base amount has a modifier applied to it depending on the character\'s race. We gathered an extensive amount of data from the armory to come up with these base numbers, using untalented individuals of every race, class, and level combination. Because racial modifiers are consistent, we are able to create statistics for \"fake\" race and class combos using the data we already know. However, the Armory does not give data on characters below level 10 or Death Knights below level 55, so we have no statistic information for these profiles. To simplify things, we have set a minimum level for custom profiles based on the available statistics.\r\n[h3]Gear Score[/h3]\r\nOkay, so a lot of sites have gear scores. Most of them (ours included) are based around the [url=http://www.wowwiki.com/Item_level]item budget[/url] Blizzard uses to determine how much of each stat can be on an item. This budget is calculated using the item\'s level, quality, and slot, and we use the budget as the item\'s gear score. You can view a complete breakdown of an item\'s gear score by mousing over it in the [url=/?help=profiler#profiler-inventory-and-talents]gear summary[/url] at the bottom of the character tab. You can view a breakdown of a profile\'s total gear score by mousing over it in the Quick Facts box, also on the character tab.\r\n\r\nEach gear score is color coded based on the item levels of the gear in reference to the character level. [b][color=q0]Grey[/color][/b] for poor, [b][color=q1]White[/color][/b] for common, [b][color=q2]Green[/color][/b] for uncommon, [b][color=q3]Blue[/color][/b] for rare, [b][color=q4]Purple[/color][/b] for epic and [b][color=q5]Orange[/color][/b] for legendary. For example, a level 70 character wearing high item-level, raiding epics from [zone=3606] and [zone=3959] will have a purple-colored gearscore, as their items are considerably \"epic\" quality for their level. However, the same character at 80, if wearing this same gear, will have the gearscore colored blue as the items are of lower-than-optimal quality for their level.\r\n\r\nThe value of an empty socket was generated using the gear score of appropriate gems for the item in question, and subtracted from the item\'s score. This allows us to score unsocketed items lower than an item without sockets of the same level, quality, and slot. Items with better than expected gems will receive higher scores, and items with lower quality gems (or no gems at all) will receive lower scores.\r\n\r\nThe values of enchants are based off of the level of the enchantment. Endgame enchantments are 20 points, profession perks are 40 points, etc. The numbers go down from there.\r\n\r\nYou may notice that some profiles have different gear scores for the same item. There is an extreme difference in budget between a two-handed or one-handed weapon, which causes a discrepancy in scores between characters who should be fairly equal according to the level of their gear. To address this, the gear score of weapons has been normalized so that a character with appropriate weapon choices has the equivalent score of two two-handed weapons. Appropriate weapons are determined by your class and spec; for example, an enhancement shaman should dual wield one handed weapons, a protection warrior should have a one-hander and shield, etc. For classes which the melee weapons don\'t really matter – like hunters or spellcasters – anything they can use is considered appropriate.\r\n\r\n[i]Note: Gear score does not take into account the stats of the item. It is a measurement of quality of gear, not whether the stats on the gear are suited to the character\'s spec.[/i]\r\n\r\n[h3]Guild Scores[/h3]\r\nGuild gear scores and achievement points are derived using a weighted average of all of the known characters in that guild. Guilds with at least 25 level 80 players receive full benefit of the top 25 characters\' gear scores, while guilds with at least 10 level 80 characters receive a slight penalty, at least 1 level 80 a moderate penalty, and no level 80 characters a severe penalty. This is to prevent small guilds and bank alts from appearing to have higher scores than legitimate raiding guilds. Instead of being based on level, achievement point averages are based around 1,500 points, but the same penalties apply.\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(8,577,0,'[minibox]\n[h2]Steamwheedle Cartel[/h2]\n[faction=21]\n[faction=577]\n[faction=369]\n[b]Everlook[/b]\n[/minibox]\n\n[b]Everlook[/b], the faction of the town Everlook, is a trading post is run by the goblins of the Steamwheedle Cartel. It lies at the crossroads of [zone=618]\'s main trade routes.\n\n[h3]General Information[/h3]\nThis town is the last point of civilization before reaching Hyjal Summit. It is run by goblins as a trading post and is officially neutral to all races and factions. Even so, pilgrims allowed to venture up to the World Tree stop here, but otherwise this is the highest that merchants and explorers may venture without the night elves’ permission. Everlook would offer a commanding view of Kalimdor, if it were not at such a high altitude that clouds constantly shroud the mountain’s lower flanks.\n\nEverlook is the only major goblin outpost in northern Kalimdor, and it serves several purposes. First, it serves as the base of operations for goblin thorium and arcanite miners since Winterspring has some of the few untapped veins of those materials on the continent. Second, it serves as a center of trade between the Alliance and the Horde. While Everlook is hardly as safe as Moonglade, generally the Alliance and the Horde treat each other fairly well there. Additionally, Everlook is a frequent stop-off and resupply point for the faithful who make the pilgrimage through Winterspring to Hyjal Summit.\n\n[h3]Reputation[/h3]\nReputation for Everlook and the Steamwheedle Cartel is mostly gained from quests in Winterspring. Having a friendly or higher reputation will make the guards help you in case of initiated violence against you.',NULL),(-13,4,0,'[menu tab=2 path=2,13,4]Can\'t find the answer you were looking for? Just [url=/?aboutus#contact]contact us[/url], or post on our [url=/?forums&board=1]forums[/url]! \r\n\r\n[toc]\r\n\r\n[h2]General Usage[/h2]\r\n[ul]\r\n[li][screenshot url=STATIC_URL/images/help/talent-calculator/glyphs.jpg thumb=STATIC_URL/images/help/talent-calculator/glyphs2.jpg width=268 height=218 float=right][/screenshot][b]Selecting a class[/b] - Easily select a class\' talent tree by chosing from the class icon at the top, or from the dropdown menu. Clicking on a class\' name at the top left of the calculator will open that class\' page here on on this site, providing even more detailed information![/li] \r\n[li][b]Adding or removing talent points[/b] - To add points in a talent simply click the appropriate talent. To remove points, you can either right-click (or Shift+click) the talent.[/li]\r\n[li][b]Adding glyphs[/b] - Click on an empty glyph slot to open a picker window from which you can make your selection. To remove a glyph, simply right-click (or Shift+click) that glyph.[/li]\r\n[li][b]Linking to a build[/b] – Simply copy the auto-updating URL from your browser\'s address bar.[/li]\r\n[/ul]\r\n\r\n[h2]Tools + Options[/h2]\r\n[ul]\r\n[li][b]Reset all[/b] - Resets all talents across all trees.[/li]\r\n[li][img src=STATIC_URL/images/help/talent-calculator/options.jpg float=right][b]Reset tree[/b] - Clicking the red X at the top right corner of a talent tree will reset all talents in that particular tree. Other trees will not be reset.[/li]\r\n[li][b]Lock / Unlock[/b] - Locks or unlocks the talent build, preventing (or allowing) changes to be made. Linking to a build will automatically lock talents.[/li]\r\n[li][b]Import[/b] – Displays a pop-up text window where you can enter the URL of a talent build made with [url=http://www.wowarmory.com/talent-calc.xml]Blizzard\'s talent calculator[/url]. Be sure that you first select the \"Link to this build\" option in the Blizzard talent calculator so that the URL will be properly formatted for importing.[/li]\r\n[li][b]Print[/b] - Opens up a new, printer-friendly page with a textual representation of your chosen talents. Nice if you want to paste the talents you\'ve chosen somewhere, and would prefer it written out.[/li]\r\n[li][b]Link[/b] - Locks your chosen talents and creates a link to your build. Use this option to easily create a URL to share your build with others![/li]\r\n[/ul]\r\n\r\n[h2]Useful Tips[/h2]\r\n\r\n[ul]\r\n[li]When the calculator is locked, you can click talents and glyphs to view their corresponding spell or item page.[/li]\r\n[li]If you\'re building a third-party application, you can link to our talent calculator by using Blizzard-style URLs such as:\r\n[code]HOST_URL?talent#hunter-512002015051122431005311500053052002300100000000000000000000000000000000000000000[/code][/li]\r\n[/ul]',NULL),(-13,1,0,'[menu tab=2 path=2,13,1]\r\n\r\n[url=item=35350][img src=STATIC_URL/images/help/modelviewer/ss-viewin3d.gif float=right][/url]Aowow has a model viewer that will let you see the items and NPCs in the game in full 3D!\r\n\r\nYou can use the dropdown menus to select which character model you want to display armor pieces on, and the model viewer will remember your choice.\r\n\r\nThere are two different versions of the model viewer available, one written in Flash, and the other one written in Java. Aowow should remember which version you used last time, and will automatically open that model viewer the next time you click on the \"View in 3D\" button.\r\n\r\nIf you have any issues, please report them [url=/?forums&topic=202524]here[/url]!\r\n\r\n[i]Tip: You can close the box by clicking anywhere outside of the box.[/i]\r\n\r\n[h2]Modes[/h2]\r\n\r\n[tabs name=mode]\r\n\r\n[tab name=Flash]\r\n\r\n[url=item=34092][img src=STATIC_URL/images/help/modelviewer/ss-flash.png float=right][/url]The [b]Flash[/b] viewer is simple, quick to load, and should work on nearly all browsers. The Flash viewer is the default viewer, and all models will automatically load in the Flash Viewer unless you specify otherwise.\r\n\r\nIt requires the latest version of [url=http://www.adobe.com/go/BONRN]Flash[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag / arrow keys[/li]\r\n[li][b]Zoom[/b] – Mousewheel / A & Z keys[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]Motion blur[/li]\r\n[li]Full screen mode[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=Java]\r\n\r\n[url=/?item=35350][img src=STATIC_URL/images/help/modelviewer/ss-java.png float=right][/url]The Java viewer is slower to initialize than the Flash Viewer, but once it\'s initialized it renders in [b]much greater[/b] detail. Most browsers will only need to initialize it once, and subsequent loads will be much faster. Some browsers may ask you to accept a security certificate when you initialize the viewer.\r\n\r\nIt requires the latest version of [url=http://jdl.sun.com/webapps/getjava/BrowserRedirect?locale=en&host=www.java.com]Java[/url] to be installed on your computer.\r\n\r\n[h3]Controls[/h3]\r\n[ul]\r\n[li][b]Rotate[/b] – Click and drag[/li]\r\n[li][b]Zoom[/b] – Mousewheel[/li]\r\n[li][b]Move[/b] – Right-click and drag[/li]\r\n[/ul]\r\n\r\n[h3]Features[/h3]\r\n[ul]\r\n[li]3D acceleration[/li]\r\n[li]Animations on NPCs, character models, small pets, and mounts[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]\r\n',NULL),(-10,0,0,'[menu tab=2 path=2,10]\r\n\r\n[div float=right align=right][url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/][img src=STATIC_URL/images/help/tooltips/ss-wowcom.png][/url]\r\n[small]Tooltips in action on [url=http://wow.joystiq.com/2010/04/14/breakfast-topic-using-irl-irl/]WoW Insider[/url][/small][/div]\r\n\r\nIt\'s never been easier to add tooltips to your site.\r\n\r\n[ol]\r\n[li]Add this piece of HTML code in the section of your page:\r\n[code][/code][/li]\r\n[li]You are done![/li]\r\n[/ol]\r\n\r\nLinks found on your site will now sport a [b]tooltip[/b] and an [b]icon[/b]. The following pages are supported: achievement, profile, item, npc, object, spell, quest. Icons show up by default, you can customize the colors of your links, and easily rename them!\r\n\r\nYou can check out this [url=STATIC_URL/widgets/power/demo.html]working demo[/url], and see how easy it is!\r\n\r\n[h2]Related[/h2]\r\n\r\n[tabs name=Related]\r\n\r\n[tab name=\"Advanced usage\"]\r\n\r\nOnce you have the [/code]\r\n[/tab]\r\n\r\n[tab name=\"XML feeds\"]\r\n\r\n[h3]Items[/h3]\r\nAlso available are our item XML feeds. Every item in the database has a corresponding XML feed. You can reach those feeds either by ID or by name. For example:\r\n\r\n[ul]\r\n[li]By ID: HOST_URL?item=52021&xml[/li]\r\n[li]By name: HOST_URL?item=iceblade%20arrow&xml[/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[tab name=\"Other resources\"]\r\n\r\nInterested in using our script in your forum? Check out [url=http://wowhead.com/forums&topic=3464]this thread[/url] for information on implementing it on many popular forum systems (phpBB, vBulletin, etc.) or check out the handy guides written by Wowheads users:\r\n\r\n[ul]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37094]vBulletin[/url][/li]\r\n[li]phpBB: [url=http://wowhead.com/forums&topic=3464#p37492]2.x.x[/url] - [url=http://wowhead.com/forums&topic=3464.6#p58403]2.x.x Mod Version[/url] | [url=http://wowhead.com/forums&topic=14347&p=126922]3.0[/url] [small]by craCkpot[/small] - [url=http://wowhead.com/forums&topic=3464#p37204]3.0[/url] [small]by marcimi[/small] - [url=http://wowhead.com/forums&topic=3464.3#p42858]3.0 Mod Version[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464#p37618]Simple Machines Forum (SMF)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=4080#p40631]Invision Power Board (IPB)[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3&p=42952#p42952]WordPress Blog[/url] ([url=http://wowhead.com/forums&topic=3464.4#p43652]Plugin Version[/url])[/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.7&p=63338#p61443]PHP Nuke-Evolution[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p43232]MyBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p48648]TikiWiki[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.6#p49640]YaBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.5#p46801]Drupal[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=3464.3#p42456]PunBB[/url][/li]\r\n[li][url=http://wowhead.com/forums&topic=10938]Dojo[/url][/li]\r\n[/ul]\r\n\r\n[/tab]\r\n\r\n[/tabs]',NULL),(-16,0,0,'[menu tab=2 path=2,16]\r\n\r\nThe code below will produce an iframe that contains the Aowow logo and a search box.\r\n\r\n[code]\r\n[/code]\r\n\r\n[h3]Parameters[/h3]\r\n\r\n[ul]\r\n[li][b]aowow_searchbox_format[/b] – String that specifies how big the iframe should be. The following values can be used:\r\n[pad]\r\n[table width=100%]\r\n[tr]\r\n[td width=20% align=center valign=top]\r\n\"160x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x200\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x200.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"160x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-160x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"150x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-150x120.png]\r\n[/td]\r\n[td width=20% align=center valign=top]\r\n\"120x120\"\r\n[img src=STATIC_URL/images/help/searchbox/preview-120x120.png]\r\n[/td]\r\n[/tr]\r\n[/table]\r\n[/li]\r\n[/ul]\r\n\r\n[h3]Tips[/h3]\r\n\r\n[ul]\r\n[li]You can style the iframe (e.g. adding a border) by using the following class name in your CSS code:\r\n[code].aowow-searchbox { ... }[/code][/li]\r\n[/ul]',NULL),(-8,0,0,'[menu tab=2 path=2,8]\r\n\r\n[div float=right align=right][img src=STATIC_URL/images/help/searchplugins/ss-searchsuggestions.png]\r\n[small]Also features search suggestions![/small]\r\n[/div]\r\n\r\nSearch plugins make it easy to search the database right from your browser!\r\n\r\n[toc h3=false]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/firefox.gif border=0 margin=5 float=left][img src=STATIC_URL/images/help/searchplugins/ie.gif border=0 float=left]Firefox / Internet Explorer[/h2]\r\n\r\n[div clear=left][/div]Click on the button below to install the search plugin in your browser.\r\n\r\n[pad]\r\n\r\n[script]\r\nfunction addPlugin()\r\n{\r\n try {\r\n if(!$.browser.msie && !$.browser.mozilla) {\r\n throw(\'FAIL\');\r\n }\r\n\r\n window.external.AddSearchProvider(\'STATIC_URL/download/searchplugins/aowow.xml\');\r\n }\r\n catch(e)\r\n {\r\n alert(\'This feature is only for Firefox 2+ and Internet Explorer 7+.\');\r\n }\r\n}\r\n[/script]\r\n\r\n[html]Install pluginInstall plugin[/html]\r\n\r\n[div clear=left][/div][pad]\r\n\r\n[h2][img src=STATIC_URL/images/help/searchplugins/opera.gif border=0 float=left]Opera[/h2]\r\n\r\n[div clear=left][/div]\r\n\r\n[ul]\r\n[li]Right-click on the search box on the [url=/]homepage[/url].[/li]\r\n[li]Select \"Create Search\" in the menu.[/li]\r\n[li]Fill the form as follows:\r\n[pad]\r\n[img src=STATIC_URL/images/help/searchplugins/ss-opera.png border=0]\r\n[pad][/li]\r\n[li]Save your changes, and you\'ll be able to perform Aowow searches by typing \"wh\" followed by the search terms in the address bar (e.g. wh sword).[/li]\r\n[/ul]\r\n',NULL),(-99,0,2,'[tooltip name=AO815][b][color=q4]AO-815 Moteur Principal de Stabulation[/color][/b]\n[color=white]Lié lorsque utilisé\nUnique[/color]\n[color=q2]Utilise: Appelle le pouvoir de l\'Interwebs pour\ninvoquer l\'information demandé à Aowow.[/color]\n[color=q]\"En tout cas, c\'est ce que c\'est supposé faire...\"[/color][/tooltip]Quoi? Comment avez-vous... oubliez ça!\n\nIl semblerait que la page demandée n\'ait pas été trouvée. En tout cas, pas dans cette dimension.\n\nPeut-être que quelques réglages au [span class=tip tooltip=AO815][color=q4][u][AO-815 Moteur Principal de Stabulation][/u][/color][/span] pourraient résulter en l\'apparition soudaine de la page![pad][pad]\n\nOu vous pouvez essayer de [url=?aboutus#contact]nous contacter[/url] - la stabilité du AO-815 est discutable et vous ne voudriez pas un autre accident...\n\n[h2]Liens[/h2]\n[ul]\n[li]Retour à la [url=?]page d\'accueil[/url][/li]\n[li][url=?forums&board=1]Forum[/url] de feedback[/li]\n[/ul]',NULL),(-3,0,0,'[small]no questions have been asked yet[/small]\r\n\r\nbesides .. yes, i\'m insane.',NULL),(-7,0,0,'[small]this page for example[/small]',NULL),(-1,0,0,'[h3]This is [s]Sparta![/s] [u]Aowow[/u][/h3]\r\n\r\nA project for private servers to sensibly display the vast amount of data a private server contains.\r\n\r\nBuilt with TrinityCore in my neck, but i\'m trying to get away from that .. some time.\r\nWith it\'s own data structure it shouldn\'t be too hard to write a converter for MaNGOS, Ascent or whatever software you prefere.\r\n\r\nThe expected version is 3.3.5 (12340), everything else will get messy.',NULL),(-99,0,3,'[tooltip name=AO815][b][color=q4]AO-815 Großkonfabulierungsmaschine[/color][/b]\n[color=white]Bei Benutzung gebunden\nEinzigartig[/color]\n[color=q2]Benutzen: Ersucht die Mächte der Internetze darum,\nAowow die benötigten Informationen zukommen zu lassen.[/color]\n[color=q]\"Das sollte es im Prinzip eigentlich tun...\"[/color][/tooltip]Was? Wie hast du... vergesst es!\n\nAnscheinend konnte die von Euch angeforderte Seite nicht gefunden werden. Wenigstens nicht in dieser Dimension.\n\nVielleicht lassen einige Justierungen an der [span class=tip tooltip=AO815][color=q4][u][AO-815 Großkonfabulierungsmaschine][/u][/color][/span] die Seite plötzlich wieder auftauchen![pad][pad]\n\nOder, Ihr könnt es auch [url=?aboutus#contact]uns melden[/url] - die Stabilität des AO-815 ist umstritten, und wir möchten gern noch so ein Problem vermeiden...\n\n[h2]Links[/h2]\n[ul]\n[li]Zur [url=?]Titelseite[/url] zurückkehren[/li]\n[li][url=?forums&board=1]Forum[/url] für Rückmeldungen[/li]\n[/ul]',NULL),(-99,0,6,'[tooltip name=AO815][b][color=q4]Dispositivo de confabulación suprema AO-815[/color][/b]\n[color=white]Se liga al usar\nÚnico[/color]\n[color=q2]Uso: Clama a los poderes de Internet para\ninvocar información requerida a Aowow.[/color]\n[color=q]\"Al menos, eso es lo que se supone que hace...\"[/color][/tooltip]¿Pero qué? ¿Cómo? .... ¡olvídalo!\n\nParece que la página que buscas no pudo ser encontrada. Al menos, no en esta dimensión.\n\n¡Quizá un par de ajustes al [span class=tip tooltip=AO815][color=q4][u][Dispositivo de confabulación suprema AO-815][/u][/color][/span] puede que hagan que la página aparezca de repente![pad][pad]\n\nO, puedes intentar [url=?aboutus#contact]contactar con nosotros[/url] - la estabilidad del AO-815 es debatible y no queremos otro accidente...\n\n[h2]Enlaces[/h2]\n[ul]\n[li]Volver a la [url=?]página principal[/url].[/li]\n[li]Foro del [url=?forums&board=1]feedback[/url].[/li]\n[/ul]',NULL),(-99,0,0,'[tooltip name=AO815][b][color=q4]AO-815 Major Confabulation Engine[/color][/b]\n[color=white]Binds when used\nUnique[/color]\n[color=q2]Use: Calls on the powers of the Interwebs to\nsummon requested information to Aowow.[/color]\n[color=q]\"At least, that\'s what it\'s supposed to do...\"[/color][/tooltip]What? How did you... nevermind that!\n\nIt appears that the page you have requested cannot be found. At least, not in this dimension.\n\nPerhaps a few tweaks to the [span class=tip tooltip=AO815][color=q4][u][AO-815 Major Confabulation Engine][/u][/color][/span] may result in the page suddenly making an appearance![pad][pad]\n\nOr, you can try [url=?aboutus#contact]contacting us[/url] - the stability of the AO-815 is debatable, and we wouldn\'t want another accident...\n\n[h2]Links[/h2]\n[ul]\n[li]Return to the [url=?]homepage[/url][/li]\n[li]Feedback [url=?forums&board=1]forum[/url][/li]\n[/ul]',NULL),(-13,7,0,'Here we have quite a few nifty markup tags that users can insert into their comments and forum posts to improve the style and easily link to database entries! Many of these tags can easily inserted using the corresponding icon or dropdown menu found above the text box. We\'ve put together this quick reference for all of these handy tags for you guys so you can get on your way to making high quality posts and comments!\n\n[h2]Formatting Tags[/h2]\n[h3]Bold[/h3]\n\\[b]text[/b]\n\n[h3]Line break[/h3]\n\\[br] -> inserts a line break.\n\n[h3]Code[/h3]\n\\[code]text[/code] -> creates a block of text that ignores markup and uses a monospace font.\n\n[h3]Horizontal Rule[/h3]\n\\[hr] -> creates a horizontal rule\n\n[h3]Italics[/h3]\n\\[i]text[/i] -> [i]text[/i]\n\n[h3]Preformatted text[/h3]\n\\[pre]text[/pre] -> shows text with all whitespace preserved in a monospace font, but allows markup\n\n[h3]Strikethrough[/h3]\n\\[s]text[/s] -> [s]text[/s]\n\n[h3]Small text[/h3]\n\\[small]text[/small] -> [small]text[/small]\n\n[h3]Subscript[/h3]\n\\[sub]text[/sub] -> [sub]text[/sub]\n\n[h3]Superscript[/h3]\n\\[sup]text[/sup] -> [sup]text[/sup]\n\n[h3]Underline[/h3]\n\\[u]text[/u] -> [u]text[/u]\n\n[h2]Database Tags[/h2]\n\n\n[b]For all database tags:[/b]\nOptional attributes: site/domain (both work identically, only use one)\nValid options are: www (default), en, de, es, fr, ru.\nThe purpose of these is to link to localized versions of items with the pretty db tags.\n[b]Example:[/b] \\[achievement=3579 domain=ru] -> [achievement=3579 domain=ru] \n\n[h3]Achievements[/h3]\n\\[achievement=3579] -> [achievement=3579]\n\n[h3]Classes[/h3]\n\\[class=11] -> [class=11]\n\n[h3]Events[/h3]\n\\[event=1] -> [event=1]\n\n[h3]Factions[/h3]\n\\[faction=749] -> [faction=749]\n\n[h3]Items[/h3]\n\\[item=12345] -> [item=12345]\n\nTo hide the icon: \\[item=12345 icon=false] -> [item=12345 icon=false]\n\n[h3]Itemsets[/h3]\n\\[itemset=699] -> [itemset=699]\n\n[h3]NPCs[/h3]\n\\[npc=32906] -> [npc=32906]\n\n[h3]Objects[/h3]\n\\[object=1733] -> [object=1733]\n\n[h3]Pets[/h3]\n\\[pet=45] -> [pet=45]\n\n[h3]Quests[/h3]\n\\[quest=7981] -> [quest=7981]\n\n[h3]Races[/h3]\n\\[race=11] -> [race=11]\n\n[b]To specify the gender of the icon:[/b] \\[race=11 gender=1] -> [race=11 gender=1] - 0 is male, 1 is female\n\n[h3]Skills[/h3]\n\\[skill=171] -> [skill=171]\n\n[h3]Spells[/h3]\n\\[spell=52398] -> [spell=52398]\n\\[spell=31565 buff=true] -> [spell=31565 buff=true]\n\n[h3]Statistics[/h3]\n\\[statistic=1076] -> [statistic=1076]\n\n[h3]Zones[/h3]\n\\[zone=3959] -> [zone=3959]\n\n[h2]HTML Tags[/h2]\n\n[h3]Anchor[/h3]\n\\[anchor=text] -> creates an anchor with the name \\\"text\\\" at this point.\n\n[h3]Ordered List[/h3]\n\\[ol]\\[li]list item[/li][/ol] -> [ol][li]list item[/li][/ol]\n\n[h3]Tables[/h3]\n[b]\\[table][/b]\nBorder: \\[table border=2]\nSpacing: \\[table cellspacing=2]\nPadding: \\[table cellpadding=2]\nWidth: \\[table width=500px] - Valid units are px, em, %\n\n[b]\\[tr][/b] - No attributes\n\n[b]\\[td][/b]\nAlign: \\[td align=right] - Valid options are left, right, center, justify\nVertical align: \\[td valign=baseline] - Valid options are top, middle, bottom, baseline\nColumn span: \\[td colspan=2]\nRow span: \\[td rowspan=2]\nWidth: \\[td width=500px] - Valid units are px, em, %\n\n[h3]Unordered List[/h3]\n\\[ul]\\[li]list item[/li][/ul] -> [ul][li]list item[/li][/ul]\n\n[h3]URLs[/h3]\n\\[url=http://www.wowhead.com]Wowhead[/url] -> [url=http://www.wowhead.com]Wowhead[/url]\n\\[url]http://www.wowhead.com[/url] -> [url]http://www.wowhead.com[/url]\n\\[url=http://www.google.com rel=item=12345]Rel link[/url] -> [url=http://www.google.com rel=item=12345]Rel link[/url]',NULL),(8,589,0,'The [b]Wintersaber Trainers[/b] is an Alliance-only faction consisting of only two night elven NPCs that can both be found in [zone=618]. Currently, the only questgiver is [npc=10618], who is located at the top of Frostsaber Rock in Winterspring. Upon reaching exalted with this faction, Rivern will sell a special mount, the [item=13086].\n\nThis faction\'s mount is the only epic mount (100% riding speed) attainable in the game which only requires 75 riding skill (and thus only costs 90 Gold). The faction is noted for having no Horde counterpart and having the longest and most repetitive reputation grind of the entire game. The first quest can be attained at level 58, while the other two are attainable at level 60.\n\n[h3]Reputation[/h3]\nReputation with the Wintersaber Trainers can only be obtained through three repeatable quests. There are no faction items or mobs that reward repuation directly.\n\n[b]Neutral 0 to 1500[/b]\nOnly one repeatable quest will available at first, so until neutral 1500/3000 is reached the [quest=4970] quest should be repeated. Any Shardtooth and Chillvind mob in Winterspring will drop these. This quest should be done solo as the drop rates are low and not shared if others have the quest.\n\n[b]Neutral 1500 to Exalted[/b]\nHalfway through neutral the [quest=5201] quest will be available. This quest requires to kill 10 Winterfall mobs in the Winterfall Village, just east of Everlook. If the quest [quest=8464] has been done with the [faction=576], [item=21383] can drop from the Winterfall mobs. If a player wants both reputations, saving these until revered with Timbermaw Hold will result in a lot of \"free\" reputation.\n\nThis quest can be done in groups for increased speed. Players grinding either Wintersaber Trainers or Timbermaw Hold reputation can often be found in the Winterfall Village. Even with an epic mount, the travel to and from Winterfall Village takes up much time. There are tigers among the route who will daze you, which will result in a demount, this should be avoided (but can be hard as they\'ll catch up with you on a 60% mount). Usually this quest is repeated all the way to exalted, ignoring the third quest. \n\n[b]Honored to Exalted[/b]\nAt honored the third quest [quest=5981] is available. The quest requires the player to kill 8 Frostmaul giants. They are a lot harder than the Winterfall mobs and the travel lengths are quite longer. This quest is usually skipped, and instead Winterfall Intrusion is repeated.\n\nDue to some players grinding Timbermaw Hold reputation, in Winterfall Village among other places, this quest can indeed turn out to be a faster reputation reward than the Winterfall Intrusion one.',NULL),(8,609,0,'The [b]Cenarion Circle[/b] is an organization of druids, both tauren and night elf, named after Cenarius. Its members are dedicated to protecting nature and restoring the damage done to it by malevolent forces.\n\nThe Circle has many posts, but their main home is the town of Nighthaven in the [zone=493]. Druids learn the spell [spell=18960] at level 10, but anyone else will have to make it to [zone=361] and find a way through the Timbermaw Furbolg tunnels.\n\nThe Circle\'s other major presence is in [zone=1377], where they combat the Silithid, the Qiraji, and Twilight\'s Hammer. Valor\'s Rest and Cenarion Hold serve as their bases in the hostile land, and offer many opportunities to adventurers seeking to aid the druids.\n\n[h3]Notable Members[/h3]\n[ul][li][npc=11832], son of Cenarius[/li][li][npc=3516], leader of the night elven druids[/li][li][npc=5769], leader of the tauren druids[/li][/ul]\n\n[h3]Reputation[/h3]\nThere are several ways to gain reputation with the Cenarion Circle. Aside from the available [url=?quests&filter=cr=1;crs=609;crv=0]quests[/url], you may do the following to gain reputation:[ul][li]Raid the [zone=3429]. This is by far the fastest way to gain reputation, as a full clear can net over 2000 reputation.[/li][li]Kill twilight cultists. These stop yielding reputation when you reach the end of friendly for [npc=11880] and [npc=11881], and at the end of honored for [npc=15201].[/li][li]Turn in [item=20404]. These drop off the cultists, and yield 250 reputation for 10 texts.[/li][li]Turn in [item=20513], [item=20514], and [item=20515]. These drop off the minibosses that are summoned at the windstones using the [itemset=492].[/li][li]Perform the [quest=8507]. These are either [url=?search=logistics+task+briefing]Logistics quests[/url], [url=?search=combat+task+briefing]Combat quests[/url], or [url=?search=tactical+task+briefing]Tactical quests[/url]. The badges you earn from these quests may then be turned in for additional reputation, if you chose to forsake the rewards.[/li][li]Collect [object=181598] from the zone and turn it in to your faction NPC.[/li][/ul]',NULL),(8,729,0,'[b]Frostwolf Clan[/b], along with [npc=11946], lived along the [zone=36] practicing shamanism, and having Frost Wolves as their companions. The dwarven expedition known as the [faction=730] have started an expedition in the Frostwolf territory to excavate the valley and mine its veins, a transgression to the orcs who inhabited Alterac. This provoked a slaughter of the first expedition, and started the battle for [zone=2597].\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Stormpike Guard.\n\nYou are granted the player title [title=47] once exalted with the Frostwolf Clan and the other two battleground factions, [faction=889] and [faction=510].',NULL),(8,730,0,'[b]Stormpike Guard[/b] is the Alliance faction in the [zone=2597] battleground. They are an expedition of dwarves of the Stormpike Clan, native to the \"valleys of Alterac\" in [zone=36]. The Stormpikes\' search for relics of their past and harvesting of resources in Alterac Valley have led to open war with the the orcs of the [faction=729] dwelling in the southern part of the valley. They were also issued with a \"sovereign imperialistic imperative\" by [npc=2784] to take the valleys of Alterac for [zone=1537]. \n\nThe main Stormpike base is Dun Baldar, where their leader, [npc=11948], resides with his marshals. His second in command, [npc=11949], is found south of Dun Baldar, at Stonehearth Outpost.\n\n[h3]Reputation[/h3]\nPlayers can earn reputation in this faction by participating in the Alterac Valley battleground by doing various tasks as well as killing members of the opposite faction, the Frostwolf Clan.\n\nYou are granted the player title [title=48] once exalted with Stormpike Guard and the other two battleground factions, [faction=890] and [faction=509].',NULL),(8,749,0,'The [b]Hydraxian Waterlords[/b] are elementals that have made their home on the islands east of [zone=16]. Sworn enemies of the armies of [npc=11502]. Historically servants of the Old Gods, the four Elemental Lords served the gods with undying loyalty. The minions of Neptulon the Tidehunter were numerous and mindless. It is not yet known how [npc=13278] broke free of his lord\'s control (if indeed he has), or what is his ultimate goals are, but the Water elementals are the only elementals that do not attack the mortal races with abandonment.\n\nLocated on a remote island in the far east of Azshara, Duke Hydraxis offers some quests. The first two require killing various elementals in [zone=139] and [zone=1377]. Increased faction with the Waterlords opens up additional quests leading into the [zone=2717]. Any items obtained from the Hydraxian Waterlords, are obtained from its various quests.\n\nCompleting the questline allows players to obtain [item=17333] used to douse the runes found near most bosses in Molten Core. This is required to summon [npc=12018], the penultimate boss, and, after his defeat, to summon Ragnaros himself. Since there are seven runes, any raid needs at least seven players that bring a quintessence if they wish to finish the instance. Since most of the questline takes place within Molten Core, any raider can complete this task with little more than some traveling and an [zone=1583] run.\n\n[h3]Reputation[/h3]\nRepuation is gained through slaying the following elemental enemies of the waterlords.[ul][li][npc=11746] - 5 reputation, lasts until honored.[/li][li][npc=11744] - 5 reputation, lasts until honored.[/li][li][npc=7032] - 5 reputation, lasts until honored.[/li][li][npc=9017] - 15 reputation, lasts until revered.[/li][li][npc=14478] - 25 reputation, lasts until revered.[/li][li][npc=9816] - 50 reputation, lasts until revered.[/li][li][npc=11658], [npc=11673], [npc=12101] and [npc=11668] - 20 reputation, lasts until revered.[/li][li][npc=11659] and Lava Pack ([npc=12100], [npc=12076], [npc=11667], [npc=11666]) - 40 reputation, lasts until revered.[/li][li][npc=12118], [npc=11982], [npc=12259], [npc=12057], [npc=12056], [npc=12264], [npc=12098] - 100 reputation, lasts until exalted.[/li][li][npc=11988] - 150 reputation, lasts until the end of exalted.[/li][li][npc=11502] - 200 reputation, lasts until the end of exalted.[/li][/ul]Reaching revered status with the Hydraxian Waterlords allows players to obtain the [item=22754], which replenishes itself and thus eliminates the need to return to Hydraxis to obtain a new quintessence every week.',NULL),(8,809,0,'The [b]Shen\'dralar[/b] are the faction of the Night Elves remaining in [zone=2557]. They are a group of high practitioners of arcane magic in order of their former Queen Azshara, and her followers, the Highborne. They have been living in Eldre\'Thalas (previous name of Dire Maul) since the Great Sundering. They are few, but their knowledge and mystic power are great, referring to things players think are powerful such as [b]Arcanums[/b] and [b]Librams[/b] as mere cantrips.\n\nTheir leader, [npc=11486], was in charge and oversaw the construction of the pylons to contain the great demon [npc=11496] and syphon his demonic power. After many long years though, it began to dwindle so he started killing the remaining night elves to maintain energy. So their spirits come to adventurers and ask them to kill him. There are very few of the original inhabitants left alive.\n\n[h3]Reputation[/h3]\nReputation can be gained by turning repeatedly in the three Librams of Dire Maul ([item=18333], [item=18334], [item=18332]). Turning in the following class books also gives some reputation:[ul][li][item=18357] - Warrior[/li][li][item=18363] - Shaman[/li][li][item=18356] - Rogue[/li][li][item=18360] - Warlock[/li][li][item=18362] - Priest[/li][li][item=18358] - Mage[/li][li][item=18364] - Druid[/li][li][item=18361] - Hunter[/li][li][item=18359] - Paladin[/li][li][item=18401] - Warrior & Paladin[/li][/ul]Both class books and librams give 500 Reputation points each.',NULL),(8,889,0,'[b]Warsong Outriders[/b] is an orcish clan formerly led by [npc=18076], in which the clan was named after. The clan\'s Warsong Outriders form the Horde faction in the [zone=3277] battleground, where they are attempting to defend their logging operations in [zone=331] from the [faction=890].\n\nOne of the strongest and most violent clans, the Warsong Clan was also one of the most distinguished clans on Draenor and was able to evade Alliance expedition forces at every turn. Depicted as Grunts, they have mastered the use of swords and blades and a few of them have even attained the rank of a Blademaster.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title Conqueror once exalted with Warsong Outriders and the other two battleground factions, [faction=510] and [faction=729].',NULL),(8,890,0,'[b]Silverwing Sentinels[/b] are the Alliance faction for the [zone=3277] battleground. The night elves, who have begun a massive push to retake the forests of [zone=331] are now focusing their attention on ridding their land of the [faction=889] once and for all. And so, the Silverwing Sentinels have answered the call and sworn that they will not rest until every last orc is defeated and cast out of Warsong Gulch.\n\n[h3]Reputation[/h3]\nReputation is gained through participation in the Warsong Gulch battleground. You gain 35 reputation each time your side captures a flag. This reputation gain is increased to 45 on holiday weekends.\n\nYou are granted the player title [title=48] once exalted with Silverwing Sentinels and the other two battleground factions, [faction=730] and [faction=509].',NULL),(8,909,0,'The [b]Darkmoon Faire[/b] is a mysterious traveling carnival, which roams not only Azeroth but Outland as well. Led by the inimitable [npc=14823], a gnome of dubious heritage and unknown providence, the Faire brings fun, games, prizes, and exotic trinkets of unexpected power to [zone=215], [zone=12], or [zone=3519] each month.\n\nA variety of amusements can be had by the discerning fairegoer, but the most common attraction is the ticket redemption. A variety of merchants at the Faire collect items from around the worlds in exchange for [item=19182]. The tickets can, in turn, be saved up and turned in for prizes of varying worth and power. Several different ticket distributors are posted around the Faire, offering tickets for crafted items made by Leatherworkers, Blacksmiths, or Engineers as well as items gathered in the wild such as [item=11404] and [item=19933]. Tickets can be redeemed for many things, from flowers to hold in the off-hand to necklaces of great power.\n\nMany adventurers seek out the Darkmoon Faire to turn in the mystical [url=?items=15.0&filter=minle=1;cr=107;crs=0;crv=Combine+the+Ace]Darkmoon Cards[/url]. Darkmoon Cards come in eight suits, each of which has cards from Ace to Eight. Combining all cards in a suit produces a deck, which will start a quest to return that deck to the Darkmoon Faire. Each of the eight decks produces a different [url=?items=4.-4&filter=na=Darkmoon+Card]trinket[/url] with a different effect, some of which are quite powerful.\n\nThe Darkmoon Faire\'s usual schedule has it arriving on site on the first Friday of the month. For the weekend, the carnies will be seen setting up the midway, and the Faire will actually start early on the following Monday.',NULL),(8,910,0,'The [b]Brood of Nozdormu[/b] is a faction consisting of the Bronze Dragonflight. Their leader [npc=15192] can be found outside the [b]Caverns of Time[/b], with many of its agents flying in the sky of [zone=1377].\n\nIn order to open the gates of [b]Ahn\'Qiraj[/b], one champion must complete a long quest line for the bronze dragon Anachronos. This reputation is also relevant in the [zone=3428]; to obtain epic quest gear and rings.\n\n[h3]Reputation[/h3]\nPlayers begin at 0/36000 hated, the lowest level of reputation possible.\n\nBrood of Nozdormu reputation can be earned through killing bosses in both Ahn\'Qiraj instances, killing monsters inside the Temple of Ahn\'Qiraj, and doing quests related to the dungeons. You can also farm [item=20384], though this will take a lot longer, and requires one to have obtained the [item=20383] in [zone=2677] for the [item=21175] quest chain.\n\nKilling trash in the Temple of Ahn\'Qiraj can only get you to 2999 / 3000 Neutral, at which point reputation can only be further advanced through quests and handing in [item=21229] and [item=21230]. You may want to save all the insignias until after you are Neutral, since at that point gaining reputation becomes much more difficult.',NULL),(8,911,0,'[b]Silvermoon City[/b] is the capital of the blood elves, located in the northeastern part of the [zone=3430] within the kingdom of Quel\'Thalas. The breathtaking capital city of the blood elves may rival the dwarven capital of [zone=1537] as the world\'s oldest, still standing, capital. Recently rebuilt from the devastating blow dealt by the evil Prince Arthas, the city houses the largest population of blood elves left on Azeroth.[pad]Silvermoon today is only the eastern half of the original city; the western half was almost completely destroyed by the Scourge during the Third War. Falconwing Square, the second blood elf town, is the only part of western Silvermoon remaining in blood elf control. The Dead Scar (the path taken by Arthas Menethil and his undead army on the quest to resurrect Kel\'Thuzad, which carves through all of Eversong Woods) separates the rebuilt Silvermoon from the ruins of the western half. Interestingly, the Ruins of Silvermoon house no undead, instead they contain [url=?npcs&filter=na=wretched;maxle=8]Wretched[/url] and malfunctioning [npc=15638]. As it stands, what remains of Silvermoon City is still bigger than current Horde cities.\n\n[h3]History[/h3]\nThe city of Silvermoon was founded by the high elves after their arrival in Lordaeron thousands of years ago. The city was constructed out of white stone and living plants in the style of the ancient Kaldorei Empire. The city contained the famous Academies of Silvermoon as a center for the learning of Arcane Magic and Sunstrider Spire, a majestic palace home to the Royal family of the high elves. The Convocation of Silvermoon (also known as \"The Silvermoon Council\"), the ruling body of the high elves was also based here. Across a stretch of ocean to the north is the island that contains the Sunwell.[pad]Although Silvermoon itself was left relatively unscathed from the second war, in the third war the Death Knight Arthas led the Scourge into the city, attacking it on his quest to reach the Sunwell. The High Elven King was slain and the majority of the population killed. Scourge forces held the city for a time but abandoned it after the depleting of its resources.[pad]Though the city was attacked by the Scourge, it is not as destroyed as one might think. Though many of its plants are dead, and the occasional dead body is sprawled across the cobblestone, the city was immune to the fire and destruction. Silvermoon now resembles a ghost town, intact, but eerily abandoned. Nevertheless, treasure hunters often frequent Silvermoon to try and find some of the valuable artifacts that the elves left behind before they deserted the city, but the ghosts of Silvermoon\'s past inhabitants prevents anyone from taking anything.\n\n[h3]Reputation[/h3]\nA comprehensive list of quests that grant Silvermoon reputation can be found [url=?quests&filter=maxle=69;cr=1;crs=911;crv=0#00Mz]here[/url].[pad][npc=20612] is the quest giver for the repeatable [item=14047] quest that must be completed by non-blood elf Horde players in order to reach exalted and gain the ability to ride [url=?items=15.5&filter=na=hawkstrider]hawkstriders[/url], the mount of the blood elf race.',NULL),(8,922,0,'[b]Tranquillien[/b] is a joint blood elf and Forsaken town and separate faction in the [zone=3433].\n\n[h3]History[/h3]\nAs the Scourge made their way to the Sunwell, the elves had no choice but to retreat. The town of Tranquillien was abandoned by the retreating elves. The town is now used by the blood elves and the Forsaken as their base of operation to launch attacks aiming to take back the Ghostlands from the Scourge. However, the city is surrounded by the Scourge and even couriers have trouble getting past the enemy to reach the town. The undead forces of Deatholme are the most dangerous threat to the town.\n\n[h3]Reputation[/h3]\nUnlike most starting areas, the town of Tranquillien is its own faction. All quests you do for them will garner at least 1000 reputation apiece. [npc=16528] acts as the Tranquillien quartermaster. Vredigar can be found near the inn and will sell various [span class=q2]uncommon[/span] items, and even a [span class=q3]rare[/span] cloak when you reach exalted! If you complete all of the Tranquillien quests, you should be exalted by approximately level 20.[pad]There are a variety of quests mostly concerning reclaiming overrun villages, investigating undead and helping around. The \"end\" of the quest-revealed lore surrounding Tranquillien culminates with the quest to kill [npc=16329].',NULL),(8,930,0,'[b]Exodar[/b] is the faction associated with [zone=3557], the enchanted capital city of the draenei, built out of the largest husk of their crashed dimensional ship of the same name. It is located in the westernmost part of [zone=3524]. The Exodar faction leader is [npc=17468], who is located near the battlemasters in the Vault of Lights.\n\nThe history of the Exodar is a short one, as the draenei only recently raised it around the husk of their crashed ship, which is still smoking from the impact. The Exodar was once a naaru satellite structure around the dimensional fortress [url=?search=tempest+keep#z0z]Tempest Keep[/url]. The Exodar contains a large amount of technological wonders (due to its origins lying with the Tempest Keep) such as magically enchanted \"wires\" which transport holy energy throughout the ship to power the heating and lighting, as well as augmenting the draeneis\' already considerable powers.\n\n[h3]Reputation[/h3]\nAs with other major factions associated with the main races, Exodar reputation may be gained by doing repeatable cloth turn-in quests, killing the opposing faction in [zone=2597] (the blood elves), and doing the appropriately related quests. At honored, the player can purchase items from Exodar related vendors for 10% less, and at exalted, the player, if not a draenei, can purchase the [url=?items=15.5&filter=na=elekk;cr=93:92;crs=2:1;crv=0:0]various mounts[/url] sold by the Exodar. The cloth turn-in quests are available from [npc=20604] [small][/small].',NULL),(8,932,0,'[b]The Aldor[/b] are an ancient order of draenei priests who revere the naaru, and to this day they assist the naaru known as [faction=935] in their battle against [npc=22917] and the Burning Legion. They are found primarily in [zone=3703] and [zone=3520]. Though they have suffered much at the hands of the blood elves who later became [faction=934], they have put aside open warfare for the sake of the Sha\'tar. The Aldor\'s most holy temple lies on the Aldor Rise, overlooking the city from the west.\n\nMost players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players an initial quest to become friendly with the Aldor or the Scryers. This choice is reversible if players feel the need. Draenei players will be friendly with the Aldor and hostile with the Scryers, whereas blood elf players will be hostile to the Aldor and friendly to the Scryers.\n\n[npc=19321] and [npc=20807] are located in the Aldor bank on the northern edge of the Terrace of Light. The Shrine of Unending Light on Aldor Rise is home to [npc=20616]Asuur [small][/small] and [npc=21906] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.\n\n[i]Note: Reputation gains with Aldor correspond with a 10% greater loss of reputation with the Scryers. Most reputation gains with the Aldor will also grant 50% of the reputation gained toward your standing with the Sha\'tar.[/i]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.\n\nTurning in 10 [span class=q1][item=29425][/span] to [npc=18537] in Aldor Rise will grant 250 reputation with Aldor. There is also a repeatable quest for single mark turn-ins which yields 25 rep. These marks drop from low ranking Burning Legion members found in most zones in Outland, including the two camps north of Auchindoun in the Bone Wastes of [zone=3519]. Approximately 240 marks are required to go from friendly to honored. In addition these quests provide Sha\'tar reputation; 125 reputation per 10 or 12.5 reputation per single turn in.\n\nPlayers who also desire [faction=978] or [faction=941] reputation may prefer killing orcs at Kil\'Sorrow Fortress in southeastern [zone=3518], as they yield marks as well as 10 Kurenai or Mag\'har reputation per kill.[pad][b]Until Exalted[/b]\nOnce you reach level 68 you may also turn in [span class=q1][item=30809][/span] at the same rates as Marks of Kil\'jaeden. These drop from high-ranking followers of the Burning Legion. If you wish, you may turn in the higher level marks before honored reputation. In [zone=3522], grinding in Death\'s Door is the most compact group of mobs that drop marks.[pad][b]Fel Armaments[/b]\n[span class=q2][item=29740][/span] may be turned in at any time to [npc=18538]Ishanah [small][/small] inside the Shrine of Unending Light on the Aldor Rise. This will increase your reputation with Aldor by 350 per hand-in. In addition to reputation gains, you will receive [span class=q1][item=29735][/span], which is currency for the purchase of shoulder enchants from Inscriber Saalyn in the Aldor bank.\n\n[h3]Switching to Aldor[/h3]\nTo change your faction from the Scryers to the Aldor to access their crafting recipes (and undo all reputation progress you have made), find [npc=18597], an Aldor in Lower City. She offers a repeatable quest for 8x [span class=q1][item=25802][/span]. Once you are neutral with the Aldor, you may no longer receive this quest.',NULL),(8,933,0,'Led by [npc=19674], [b]The Consortium[/b] are ethereal smugglers, traders and thieves that have come to Outland. Their main base of operations and biggest settlement is the Stormspire, but they can be found at Midrealm Post, the Aeris Landing, within the [zone=3792] of Auchindoun and various other places.\n\nUpon reaching Friendly status, players are officially considered members of the Consortium and given a salary. The salary is a bag of gems at the beginning of every month, given by [npc=18265] at Aeris Landing. Higher reputation with the Consortium yields higher qualities and quantities of jewels each month.\n\n[h3]Reputation[/h3]\n[b]Until Friendly[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25416] at [npc=18265].[/li][li]Turn in [item=25463] at [npc=18333].[/li][/ul][b]Friendly to Honored[/b][ul][li]Run Mana-Tombs in [i]normal[/i] mode, ~1200 reputation per run.[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul][b]Honored to Exalted[/b][ul][li]Run Mana-Tombs in [i]heroic[/i] mode, ~2400 reputation per run.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=933;crv=0]quests[/url].[/li][li]Turn in [item=25433] at [npc=18265].[/li][li]Turn in [item=29209] at [npc=19880].[/li][/ul]Characters trying to simultaneously earn reputation with the [faction=941] or [faction=978] and the Consortium may want to focus on killing ogres ([url=?npcs&filter=na=boulderfist;cr=6;crs=3518;crv=0]Boulderfist[/url], [url=?npcs&filter=na=Warmaul;cr=6;crs=3518;crv=0]Warmaul[/url]) in Nagrand and saving the Obsidian Warbeads for Consortium turn-ins. The only caveat is the drop rate, which is roughly 33% for the warbeads, while it is 50% on the insignias. If you are level 70 and want a faster grind without concern for Mag\'har/Kurenai reputation, then you may want to grind insignias instead. Then again, the ogres are generally easier to grind, ranging from level 65 to 67. The choice is ultimately up to the player.',NULL),(8,934,0,'[b]The Scryers[/b] are blood elves who reside in [zone=3703] led by [npc=18530]. The group broke away from [npc=19622] and offered to assist the Naaru at Shattrath City. They are at odds with the [faction=932], and compete with them for power within Shattrath and the Naaru\'s favor.[pad]Most players will start at neutral with the Aldor. [npc=18166] in Shattrath City will give players the choice of aligning themselves with the Scryers or Aldor after completing [quest=10211]. This choice is reversible if players feel the need. Blood elf players will be friendly with the Scryers and hostile with the Aldor, whereas draenei players will be hostile to the Scryers and friendly to the Aldor.[pad]The Scryers have both a [npc=19251] trainer and a [npc=19252] trainer. Due to this, the enchanter nestled deep within [zone=1337] is rendered obsolete.[pad][npc=19331] and [npc=20808] are located in the Scryers bank on the southern edge of the Terrace of Light. The Seer\'s Library in the Scryer\'s Tier is home to [npc=20613] [small][/small] and [npc=21905] [small][/small], who exchange epic armor tokens for [url=?itemsets&filter=ta=12]Tier 4[/url] and [url=?itemsets&filter=ta=13]Tier 5[/url] gear, respectively.[pad][i]Note: Reputation gains with Scryers correspond with a 10% greater loss of reputation with the Aldor. Most reputation gains with the Scryers will also grant 50% of the reputation gained toward your standing with the [faction=935].[/i]\n\n[h3]Lore[/h3]\nAfter enduring relentless assaults, the harried Sha\'tar and Aldor guards braced for the next wave as it marched over the horizon. This time, the attack came from the armies of [npc=22917]. A large regiment of blood elves had been sent by Illidan’s ally, Prince Kael\'thas Sunstrider, to lay waste to the city. As the regiment of blood elves crossed the bridge, the Aldor’s exarches and vindicators lined up to defend the Terrace of Light. Then the unexpected happened, the blood elves laid down their weapons in front of the city\'s defenders. Their leader, a blood elf elder known as Voren’thal, stormed into the Terrace of Light and demanded to speak to the naaru [npc=18481]. As the naaru approached him, Voren’thal knelt and uttered the following words: \"I’ve seen you in a vision, naaru. My race’s only hope for survival lies with you. My followers and I are here to serve you.\"[pad]The defection of Voren’thal and his followers was the largest loss ever incurred by Kael’thas’ forces. Many of the strongest and brightest amongst Kael’thas’ scholars and magisters had been swayed by Voren’thal\'s influence. The naaru accepted the defectors who became known as the Scryers.\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nPlayers looking to gain the higher reputation ranks (revered, exalted) may wish to save non-repeatable quests until after reaching honored.[pad]Turning in 10 [span class=q1][item=29426][/span] to [npc=18531] in Scryer\'s Tier will grant 250 reputation with the Scryers. These signets can also be turned in one at a time at the same exchange rate, 25 reputation per signet. These signets drop from low ranking Firewing members found in the northeast section of Terrokar Forest. This repeatable quest becomes unavailable at honored. If no other reputation quests are done, 240 signets are required to go from friendly to honored.[pad][b]Until Exalted[/b]\nOnce you reach level 68, you may also turn in [span class=q1][item=30810][/span]. These drop from high-ranking Sunfury blood elves (found in [zone=3523], [zone=3520], and the [url=?search=tempest+keep+-eye+-kael]Tempest Keep[/url] instances). If you wish, you may turn in the higher level signets before honored reputation, however it is recommended that you save them for after you hit honored. For every 10 signets, you will gain 250 reputation. Once you hit honored it will take approximately 1,320 Sunfury signets to go from honored to exalted if no other reputation is earned.[pad][b]Arcane Tomes[/b]\n[span class=q2][item=29739][/span] may be turned in at any time to Voren\'thal the Seer inside the The Seer\'s Library on the Scryer\'s Tier. This will increase your reputation with the Scryers by 350 per hand-in. If you wish, you may turn in the Arcane Tomes before honored reputation, however it is recommended that you save them for after you hit honored. Once you hit honored it will take approximately 94 Arcane Tomes to go from honored to exalted if no other reputation is earned. In addition to reputation gains, you will receive an [span class=q1][item=29736][/span], which is currency for the purchase of shoulder enchants from Inscriber Veredis, who resides in the Scryers bank.\n\n[h3]Switching to Scryers[/h3]\nTo change your faction from Aldor to Scryers to access their crafting recipes (and undo all reputation progress you have made), find [npc=18596], a Scryers in the Lower City. She offers you a repeatable quest, [quest=10024], that requires you to find eight [span class=q1][item=25744][/span]. Once you are Neutral with the Scryers, you can no longer receive this quest. The quest gives you +250 Scryers reputation and -275 Aldor reputation (in addition, the quest also gives you +125 reputation with The Sha\'tar).',NULL),(8,935,0,'[b]The Sha\'tar[/b], or \"born of light,\" are naaru that aided [faction=932], the order of draenei priests formerly led by [npc=17468], in rebuilding [zone=3703]. The city was destroyed by the Orcs during their rampage across Draenor prior to the First War. Defeat of the Burning Legion is the Sha\'tar\'s ultimate goal; the Sha\'tar are aided in this war by the Aldor and their rivals, the blood elf faction known as [faction=934]. The Aldor and the Scryers fight for the favor of the Sha\'tar so that they may be assisted in their war by the naaru\'s powers. The entity that leads the Sha\'tar is known as [npc=18481]; he can be found upon the Terrace of Light in Shattrath City.\n\nBoth Alliance and Horde players begin as Neutral toward the Sha\'tar. Players can increase their Sha\'tar reputation through various quests, by raising their reputation with the Aldor or Scryers, or by adventuring into [url=?search=Tempest+Keep#z0z]Tempest Keep[/url].\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b]\nReputation can be gained from Scryer/Aldor signet/mark turn-ins. The following will only grant Sha\'tar reputation until you achieve Honored status: [item=29426], [item=30810], and [item=29739] for the Scryers; [item=29425], [item=30809], and [item=29740] for the Aldor. In addition, these will require more turn-ins to produce equable Sha\'tar reputation to the main faction. Note that this reputation gain does not show up in the combat log, but can be verified by looking at your reputation panel.\n\nReputation can also be gained by running Tempest Keep: [zone=3847], [zone=3846] and [zone=3849].\n\n[b]Through Exalted[/b]\nAfter exhausting the reputation rewards from Aldor/Scryer turn-ins and Mechanar runs, players may wish to complete the few Sha\'tar quests available. In addition to the quests, instance runs in Tempest Keep: Botanica, Arcatraz and Mechanar will continue to grant reputation. At this point, it is probably more worthwhile to run these instances in Heroic mode.',NULL),(8,941,0,'The [b]Mag\'har[/b] are a faction of brown-skinned orcs who remain on Outland and have separated themselves from the other remaining orc clans that fell prey to [npc=17257] and joined his army of fel orcs (that are now led by the powerful [npc=16808]). The Mag\'har are settled in the stronghold of Garadar in the beautiful land of [zone=3518], once home to the majority of the orcs along with [zone=3519] and the [zone=3522].[pad]The Mag\'har orcs have never been corrupted by Mannoroth or Magtheridon and thus remained untouched by the bloodlust. Unlike their former clanmates who live in the ruins of their once-mighty holds, the Mag\'har are made up of members of different orc clans who escaped corruption. The current leader of the Mag\'har, venerable [npc=18141], is an old and wise orc, yet she has recently fallen extremely ill. [npc=18063], son of the mighty Grom Hellscream, serves as the Mag\'har\'s military chief, aided by [npc=18106], son of the venerable chieftain of the Bleeding Hollow clan, Kilrogg Deadeye. In addition, there is an NPC within a Mag\'har camp to the west known as [npc=18229].[pad]It is not clear how the Mag\'har managed to retain their original brown skin. Orcish skin turns green when exposed to warlock magic, regardless of the individual\'s beliefs or practices; Garrosh and Jorin would certainly have been exposed, given the positions of their fathers. \n\nHorde players start out at unfriendly with the Mag\'har. Alliance players will always be treated as hostile. The Alliance counterpart to this faction are the [faction=978].\n\n[h3]Questing[/h3]\nQuests for the Mag\'har begin in [zone=3483] with [quest=9400] from [faction=947]. This quest will lead you to a small Mag\'har outpost north of Hellfire Citadel. Once in Nagrand, players will find the main Mag\'har city, Garadar. The city holds most of the remaining quests that will reward Mag\'har reputation.\n\nNote: You MUST have completed the quest chain of \"The Assassin\" up until the quest [quest=9410] (where you become Neutral) in order for you to talk to most people in Garadar.\n\n[h3]Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in 10x [item=25433], which drop from these ogres.[pad]Players seeking [faction=933] reputation may wish to save their warbeads, as Mag\'har reputation is generally easier to obtain.[pad]Players seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,942,0,'Upon the reopening of the Dark Portal to Outland, the [faction=609] dispatched an exploratory force, known as the [b]Cenarion Expedition[/b], to explore the uncharted world. Much like the Circle, it is a coalition of night elf and tauren forces. Since the opening of the Dark Portal, the Cenarion Expedition has quickly gained in size and autonomy, achieving enough power to be considered its own faction. The Expedition maintains its primary base at Cenarion Refuge in [zone=3521]; it has also made its presence known on [zone=3483], in [zone=3519], and in the [zone=3522]. Cenarion Refuge is located immediately west of Thornfang Hill.\n\nThe Refuge is located in the Zangarmarsh for the primary reason of studying the rich wildlife located there. However, the Expedition has discovered troubling goings-on in the marsh. Water levels in many parts of Zangarmarsh are decreasing, and some areas such as the Dead Mire have already suffered greatly from this strange phenomenon. It has become known that this decrease in the water levels can be attributed to pumps that have been constructed in the Marsh by the naga. Their purpose is to create a new Well of Eternity for [npc=22917]. However, the Expedition cannot afford direct confrontation with the naga so numerous in the Zangarmarsh and [url=?search=coilfang#c0z]Coilfang Reservoir[/url]. It needs the aid of those willing to assist the druids in their dangerous battle against those who seek to disturb the marsh\'s natural balance. Quite naturally, those heroic enough to fight the naga at Coilfang Reservoir will be well rewarded.\n\n[h3]Reputation[/h3]\n[b]Neutral to Honored[/b]\nKill Naga, while also running [zone=3717] whenever you can; a good instance run will net reputation faster than soloing. Alternatively, the player can begin turning in [item=24401] for a chance at an [item=24407], which can be turned in for 500 reputation. It is suggested that the player save his Uncatalogued Species until after Honored status is achieved, as the quest cannot be continued past that point, while Uncatalogued Species can be used until Exalted.\n\nIf you are an herbalist, and interested in [faction=970] reputation, you may want to grind the [url=?npcs&filter=na=Bog+Lord]Bog Lords[/url] which can be found in the NE, SE, and SW corners of Zangarmarsh. Their bodies can be \"picked\" by herbalists and often yield Unidentified Plant Parts, while every kill yields 15 reputation with Sporeggar.[pad][b]Honored to Revered[/b]\nOnce the player is Honored, running Slave Pens and the [zone=3716] (with the exception of [npc=17770] and some giants), will no longer grant reputation. You should now do any Cenarion Expedition quests in Hellfire Peninsula, Zangarmarsh, Terokkar Forest and the Blade\'s Edge Mountains. It is also the time to turn in any Uncatalogued Species you have found. Doing this should get you part of the way into Revered.\n\nAlternatively, you can finish leveling to 70 and run [zone=3715]. Each run gives just over 1500 reputation if you clear all mobs. Also within the Steamvault lies a repeatable quest, [quest=9764], which begins with [item=24367]. You will then be able to turn in [item=24368], which drop in both Steamvault and Slave Pens, receiving 250 reputation for the first turn-in and 75 reputation each thereafter. This turn-in is available all the way to Exalted.\n\nOnce you are 70 and have upgraded your gear, you can opt to run Slave Pens, Underbog, and Steamvault on Heroic Mode upon purchasing the [item=30623]. While the instances are difficult, they award significant reputation: regular mobs are worth 15 reputation, 2 for non-elites, and 150/250 for bosses. This method works until Exalted.[pad][b]Revered to Exalted[/b]\nContinue with the same strategy as above: finish any remaining quests, run Steamvault, and continue with [item=24368] turn-ins.\n\nIt is also possible to run Slave Pens, Underbog, and Steamvault on Heroic Mode. The reputation gained is not much more than running Steamvault in normal mode, whilst the time investment for heroic dungeons is much higher, possibly resulting in a lower net reputation per hour, however the loot is better and you will receive [item=29434] from the bosses which can be used to purchase high quality epic gear.',NULL),(8,946,0,'A refuge of human, elven, draenei and dwarven explorers, [b]Honor Hold[/b] is the first major town Alliance explorers will encounter while traversing Outland. Vestiges of the Sons of Lothar, veterans of the Alliance that first came into Draenor, have steadfastly held on to this Hellfire outpost. They are now joined by the armies from Stormwind and Ironforge.\n\n[h3]Reputation[/h3]\nHonor Hold reputation is gained through various means in Hellfire Peninsula. Mobs in and around Hellfire Citadel reward Honor Hold reputation, as well as quests picked up in town. Due to the lack of representatives in other areas, there is a large gap between Honored and Exalted during which you may not attain any Honor Hold reputation from questing and killing mobs in Outland once you depart Hellfire Peninsula.\n\n[b]Through friendly[/b]\nMobs in [zone=3562] and [zone=3713] will award reputation through Friendly. One option is to grind reputation via Ramparts and Blood Furnace runs until honored before doing any Honor Hold quests outside the instances, as those continue to yield reputation up to Exalted. You may also want to check out the following outdoor mobs which give reputation if you are Neutral. These mobs will not give reputation once you are Friendly with Honor Hold.[ul][li][npc=19415] [/li][li][npc=16878] [/li][li][npc=16870][/li][li][npc=16867][/li][li][npc=19414] [/li][li][npc=19413] [/li][li][npc=19411] [/li][li][npc=19422][/li][/ul]To make the best use of available resources, you may want to grind reputation with Honor Hold through Hellfire Ramparts and Blood Furnace prior to completing any Honor Hold quests. \n\n[b]PvP[/b]\nPlayers that enjoy PvP can earn Honor Hold reputation through the daily quest [quest=10106]. This quest awards 70 silver and 150 Honor Hold reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [span class=q1][item=24579][/span], which are used as currency for various types of items and gear when turned into [npc=17657] and [npc=18266] in Honor Hold as well as the [npc=18581] in Zangarmarsh.\n\n[i]Tip: You can use these marks to purchase [span class=q1][item=24520][/span] from Warrant Officer Tracy Proudwell and increase the amount of reputation (and experience) gained while running these instances.[/i]\n\n[b]Through Exalted[/b]\nFrom here on out there are only two ways to achieve Revered and Exalted status:[ul][li][zone=3714], this instance requires level 68 and the [span class=q1][item=28395][/span] (only one party member needs the key). Mobs in Shattered Halls will yield reputation through Exalted.[/li][li]After achieving Honored status you can purchase the [span class=q1][item=30622][/span] which grants access to the heroic mode of all Hellfire Citadel instances. Mobs in all Heroic mode Hellfire Citadel instances will yield slightly more reputation than those found in non-heroic Shattered Halls, and will continue to yield reputation through Exalted.[/li][/ul]',NULL),(8,947,0,'The expedition sent through the Dark Portal by Thrall has built a stronghold in Hellfire Peninsula. [b]Thrallmar[/b] serves as a base of operations for much of the Horde\'s activities in Outland.\n\n[h3]Reputation[/h3]\nReputation for Thrallmar up to Honored is relatively easy to earn. Even the easiest quests (those that take you from one quest giver to the next up the road, for example) can yield 75 reputation points, while those that require some effort to complete typically yield 250 reputation points or more. Some group quests that involve killing an elite can yield as much as 1000 reputation points.\n\nIf you do the bulk of the Thrallmar quests instead of quickly moving on to the next zone, you might expect to reach Honored after 1 or 2 levels of play. However, once you reach Honored, you hit an earnings barrier that you can only remove when you are level 68 and can start re-earning points in the [zone=3714] dungeon.\n\n[b]Neutral through Friendly[/b]\nReputation from mobs in [zone=3562] and [zone=3713] stops at 5999/6000 friendly. One option is to grind reputation via Ramparts and Blood Furnace runs to 5999/6000 before doing any Thrallmar quests outside the instances, as those continue to yield reputation up to Exalted.\n\nAlso, the level 63 mobs outside Hellfire Citadel (on the path) give you 5 reputation each.\n\n[b]Friendly through Honored[/b]\nPlayers that enjoy PvP can earn Thrallmar reputation through the daily quest [quest=10110]. This quest awards 70 silver and 150 Thrallmar reputation, but can only be completed once a day and counts towards your 25 daily quest limit. Completion of this quest also yields three [item=24581], which are used as currency for various types of items and gear when turned into [npc=18267] and the [npc=18564] in Thrallmar and near Zabra\'jin in [zone=3521] respectively.\n\nBlood Furnace and Ramparts instance runs will be your best bet for this reputation bracket. Be aware though, that they will only take you to the end of Honored. You will need to run Shattered Halls to reach Revered status.\n\n[b]Revered to Exalted[/b]\nFrom this point on, gaining reputation through Exalted requires one of two things:[ul][li]Access to Shattered Halls, one of the wings of Hellfire Citadel, which requires level 68 and either the [span class=q1][item=28395][/span] or a rogue with 350 lockpicking skill.[/li][li]Doing Heroic versions of Hellfire Citadel dungeons, which typically require you to be well geared and level 70.[/li][/ul]Both of these give reputation until you reach Exalted status. A full clear of Shattered Halls nets you about 2000 reputation points, trash mobs generally yield 6 or 12 each, with up to 150 points from bosses. Heroic trash yields 15-25 points, with bosses worth more. \n\n[i]Tip: You can purchase [span class=q1][item=24522][/span] from Battlecryer Blackeye for use during instance runs to speed up the reputation (and experience) gaining process![/i]',NULL),(8,967,0,'[b]The Violet Eye[/b] is a secret sect founded by the Kirin Tor of Dalaran to spy on the Guardian of Tirisfal, [npc=15608], in his tower of [zone=2562]. Though Medivh is dead, the Violet Eye remains in Karazhan, defending against the evil that appears to have taken hold in the absence of its master. \n\nIt is unknown whether Medivh\'s apprentice, [npc=18166], was a member of the Violet Eye, or whether he knew of their activities at the time (though he does seem to be aware of them now).\n\n[h3]Reputation[/h3]\nViolet Eye reputation is gained by killing mobs inside Karazhan and completing Karazhan related quests. Reputation from Karazhan mobs can be gained from neutral standing all the way to exalted. Each trash mob awards around 15 reputation, with the bosses award more.\n\n[npc=18253] begins a fairly long quest chain starting with [quest=9824] and [quest=9825]. This quest line rewards players with [span class=q1][item=24490][/span] and culminates with [quest=9644]. Full completion of this quest line rewards approximately 10,270 reputation.\n\n[h3]Reputation Rewards[/h3]\n[npc=18253] will offer players rings as rewards for reputation level gains in the form of quests. The first such quest is available at neutral standing and may be completed at friendly. You will receive a new and upgraded version of the ring you chose each time you break into a new reputation tier. The rings are sorted into the following 4 categories:[ul][li][quest=10731]: [item=29280], [item=29281], [item=29282] and [item=29283][/li][li][quest=10729]: [item=29284], [item=29285], [item=29286] and [item=29287][/li][li][quest=10732]: [item=29276], [item=29277], [item=29278], and [item=29279][/li][li][quest=10730]: [item=29288], [item=29289], [item=29291] and [item=29290][/li][/ul][npc=16388], a blacksmith located inside Karazhan just after [npc=15550], offers players with high enough reputation the ability to buy epic blacksmithing plans. Players who are honored or above will also be able to repair armor and weapons at this vendor.\n\n[npc=18255], who stands just outside the main gates of Karazhan, will sell an epic jewelcrafting recipe and shoulder enchant to players who have an honored or above standing with The Violet Eye.',NULL),(8,970,0,'The sporelings are a mostly peaceful race of mushroom-men native to Outland. Their home, [b]Sporeggar[/b], is located in the western bogs of [zone=3521].\n\n[h3]Reputation[/h3]\nPlayers both Alliance and Horde start out unfriendly with Sporeggar. There are many ways to increase your reputation at the beginning:[ul][li]Bringing 10 [span class=q1][item=24290][/span] to [npc=17923] to complete [quest=9739][/li][li]Bringing 6 [span class=q1][item=24291][/span] to Fahssn to complete [quest=9743] [i](both of these quests will be available only if you are below friendly)[/i][/li][li]Killing [url=?search=bog+lord+-hungry#z0z]Bog Lords[/url] [i](lasts until the end of honored)[/i][/li][li]Killing [npc=18137] and [npc=18136] [i](lasts until the end of revered)[/i][/li][li]Bringing 10 [span class=q1][item=24245][/span] to [npc=17924] in Sporeggar [i](lasts only during neutral)[/i][/li][/ul]After you hit [b]friendly[/b], a new handful of repeatable quests opens up at the same time Fahssn\'s quests and the Glowcap turnins become unavailable, these include:[ul][li]Killing 12 each of [npc=18088] and [npc=18089] for [npc=17856] to complete [quest=9726][/li][li]Bringing 10 [span class=q1][item=24449][/span] to [npc=17925] to complete [quest=9806][/li][li]Venturing into [zone=3716] to gather 5 [span class=q1][item=24246][/span] for Gzhun\'tt to complete [quest=9715][/li][/ul]These 3 quests are repeatable and will be available to the end of exalted.\n\nPlayers who are exalted with Sporeggar should speak to [npc=17877] for one final quest.',NULL),(8,978,0,'Draenei for \"redeemed.\" These Broken have escaped the grasp of their various slavers in Outland and have made their home at Telaar in southern [zone=3518]. It is there that they seek to rediscover their destiny. They also maintain a small presence at Orebor Harborage, [zone=3521]. Their quartermaster, [npc=20240], is located outside the inn in Telaar, below the flight point.\n\nAlliance players start out at unfriendly with the Kurenai. Horde players will always be treated as hostile. The Horde counterpart to this faction are [faction=941].\n\n[i]Kurenai is Japanese for \"crimson\".[/i]\n\n[h3]Gaining Reputation[/h3]\nReputation can be gained from killing [url=?npcs&filter=na=kil%27sorrow;ra=-1;rh=-1]Kil\'sorrow cult members[/url], [url=?npcs&filter=na=Murkblood;ra=-1;rh=-1;cr=6;crs=3518;crv=0]Murkblood Broken[/url], [url=?npcs&filter=na=warmaul+-marker]Warmaul[/url] and [url=?npcs&filter=na=boulderfist;minle=64;ra=-1;rh=-1]Boulderfist[/url] ogres in Nagrand. Players may also turn in [item=25433] (10), which drop from these ogres.\n\nPlayers seeking [faction=933] reputation may wish to save their warbeads, as Kurenai reputation is generally easier to obtain.\n\nPlayers seeking [faction=932] reputation may prefer killing cult members at Kil\'Sorrow Fortress, as they drop [item=29425] for Aldor reputation turn-ins.\n\n[i]Note: These monsters and quests do not have a limit, they grant reputation all the way through exalted![/i]',NULL),(8,989,0,'The [b]Keepers of Time[/b] are bronze dragons hand-picked by Nozdormu to watch over the Caverns of Time. They are led by [npc=19932] and [npc=19933], who are also acting leaders of the Bronze Dragonflight in Nozdormu\'s absence.\n\n[h3]Reputation[/h3]\nCurrently the only way to gain the favor of the enigmatic bronze dragons is through [zone=2367] and [zone=2366] instance runs. Keepers of Time reputation rewards may be found at the Keepers\' quartermaster, [npc=21643]. The Keepers will require you to be level 66 and complete the short quest [quest=10277] before allowing passage into Old Hillsbrad Foothills to fulfill [npc=17876]\'s destiny to become the Warchief of the Horde.',NULL),(8,990,0,'The [b]Scale of the Sands[/b] is a secretive subgroup of the Bronze Dragonflight, led by [npc=19935], prime mate of [npc=15185]. It is a subgroup of the Bronze Dragonflight. Their leader, Nozdormu, sent these guardian factions to [zone=3606] where they guard the World Tree from another attack by the demons of Darkwhisper Gorge and help restore the time-stream and preserve the future of the world.\n\n[h3]Reputation[/h3]\nBoth bosses and trash monsters give reputation with each kill. [npc=17968], the final boss, awards 1500 reputation while the other four bosses give 375. General trash award 12 reputation, while [npc=17907] give 60. Yielding an average of 7800 per full clear, it would take 5-6 clears to reach exalted.\n\nCurrently some of the best [span class=q4][url=?items=4.-2&filter=na=band+of+the+eternal]rings[/url][/span] for raiding are available via this reputation. In order to recieve the rings, you must complete the previously required attunement quest, [quest=10445]. Each new reputation level awards an upgraded ring.',NULL),(8,1011,0,'The [b]Lower City[/b] of [zone=3703] is the place where the refugees gather and help out in their own ways. When someone helps any of the mixture of races who fled from war, word gets around quickly. Their quartermaster, [npc=21655], is located at the market in the Lower City. The Lower City of Shattrath also contains a very useful Mana Loom or an Alchemy Lab. Many NPCs have extensive knowledge of crafting. The Battlemasters for both sides of all four [zones=6] can also be found here, as well as the World\'s End Tavern.\n\nOther important NPCs include:[ul][li]A neutral Grand Master Leatherworker, [npc=19187].[/li][li]A neutral Grand Master Skinner, [npc=19180].[/li][li]A neutral Grand Master Alchemist, [npc=19052], with an Alchemy Lab, who also gives the quest [quest=10902] (for alchemy specialization).[/li][li]Three specialist tailors who allow you to specialize and buy new epic tailoring recipes for armor sets and special bags (including the 20-slot bag).[ul][li][npc=22212] [small][/small] sells the patterns for the [itemset=553] set.[/li][li][npc=22213] [small][/small] sells the patterns for the [itemset=552] set.[/li][li][npc=22208] [small][/small] sells the patterns for the [itemset=554] set.[/li][/ul][/li][/ul]\n\n[h3]Reputation[/h3]\n[b]Until Honored[/b][ul][li]Run [zone=3790] in [i]normal[/i] mode, ~750 reputation.[/li][li]Run [zone=3791] in [i]normal[/i] mode, ~1250 reputation.[/li][li]Run [zone=3789] in [i]normal[/i] mode, ~2000 reputation.[/li][li]Turn in [item=25719] at [npc=22429].[/li][/ul][i]Note: Players aiming for faction higher than Honored should wait until honored to complete the Lower City quests.[/i]\n\n[b]Honored to Revered[/b][ul][li]Run Shadow Labyrinth in [i]normal[/i] mode, ~2000 reputation.[/li][li]Complete all available [url=?quests&filter=cr=1;crs=1011;crv=0]Lower City quests[/url].[/li][/ul][b]Revered to Exalted[/b][ul][li]Run Auchenai Crypts in [i]heroic[/i] mode, ~750 reputation.[/li][li]Run Sethekk Halls in [i]heroic[/i] mode, ~1250 reputation.[/li][li]Run Shadow Labyrinth in [i]normal[/i] or [i]heroic[/i] mode, ~2000 reputation.[/li][/ul]\n\n[h3]Trivia[/h3]\n[npc=19227], a vendor in Lower City, sells amulets which are very... interesting. He is quite the salesman, with items like [item=27940], which allows you to return to life as long as you return to the place you died. [i]Buyer beware![/i]\n\nAt exalted you can purchase a [item=31778]. Strangely, none of the NPCs in Lower City can be seen wearing one. Perhaps they cannot afford one...',NULL),(8,1012,0,'The [b]Ashtongue Deathsworn[/b] are the elite of the Broken draenei tribe known as the Ashtongue. The Ashtongue tribe is led by the elder sage [npc=21700]; the Deathsworn are [i]officially[/i] aligned with [npc=22917] [small][/small]. The Deathsworn are Akama\'s most trusted lieutenants and are privy to their leader\'s mysterious motivations.\n\nTo discover the Deathsworn as a faction, the player must begin and complete the majority of the quest line which begins with Tablets of Baa\'ri ([quest=10568] / [quest=10683]). Eventually, you will speak with Akama, whereupon you will become Neutral with the Deathsworn.',NULL),(8,1015,0,'The [b]Netherwing[/b] are a faction of dragons located in Outland. The unusual brood was spawned from the eggs of Deathwing\'s black dragonflight, and infused with raw nether-energies. Now, they seek to find their identity beyond the shadows of their father\'s destructive heritage.\n\n[h3]Reputation[/h3]\nPlayers are introduced to the Netherwing faction at 0/36000 hated reputation, and must be exalted to receive a [span class=q4][url=?items=15.-7&filter=na=Netherwing+Drake]Netherwing Drake[/url][/span]. The quest chain and reputation grind is a mostly solo endeavor involving quests that can only be completed once daily, a 5-player group quest on the way to neutral, and daily 3-player group quests after reaching revered. A flying mount is required for this reputation grind, and 300 riding skill is necessary to advance past neutral.\n\n[b]Hated to Neutral[/b]\nLevel 70 players will begin their journey to exalted reputation by picking up the quest chain offered by [npc=22113], a blood elf wandering the surface of the Netherwing Fields, in the southeast corner of [zone=3520]. The quest chain begins with the quest [quest=10804]. Completion of this quest line will provide an instant reputation boost to neutral and the choice of one of [span class=q3][url=?items&filter=qu=3;na=Netherwing+-wand]these[/url][/span] five items.\n\n[h3]Netherwing Reputation After Neutral[/h3]\nAfter completing the Kindness quest chain, Mordenai will be sure you have acquired 300 [spell=34091] skill and have you swear fealty to the Netherwing. This will grant you a Dragonmaw Fel Orc disguise when you enter Netherwing Ledge and allow you to communicate and work for the Dragonmaw stationed there. Mordenai will initially send you to [npc=23139] with a set of fake papers. Completing this quest will unlock the beginning Dragonmaw quests that you\'ll be working on to increase your Netherwing reputation. Most of these quests will have the new \"Daily\" tag added with 2.1. Daily quests differ from regular quests in that they are infinitely repeatable, but you may only complete each daily quest once per day and are restricted to ten total daily quests per day.[pad][i]Note: New quests will be unlocked with each reputation tier, and all daily quests of previous tiers will always be available, even after reaching exalted.[/i]\n\n[b][toggler id=Neutral hidden]Neutral[/toggler][/b]\n[div id=Neutral hidden]After turning in Mordenai\'s [item=32469] to Mor\'ghor to complete [quest=11013], your first group of quests will become available to start you on your way to the next tier of reputation with the Netherwing. Mor\'ghor will point you to the taskmaster to begin your grunt work, and [npc=23141] will reveal himself as a Netherwing ally in disguise and present another group of quests to you. One of which is [quest=11049]. Players will be able to turn in any [item=32506] that have a 1% chance to be found in [object=185881], [object=185877], and on almost all creatures on Netherwing Ledge. It can also be a rare find as a [object=185915] anywhere on Netherwing Ledge and in the Dragonmaw Fortress on the southeast corner of the Shadowmoon Valley mainland. This quest is not labeled as daily, and therefore can be done as many times as you can find eggs and will not hinder your daily quest limit.[pad]Other quests available from the beginning:[ul][li][i][small](Daily)[/small][/i] [quest=11018], [quest=11016], [quest=11017] - These will be available only to players who possess the respective profession to gather each item.[/li][li][i][small](Daily)[/small][/i] [quest=11015] - Simple gathering quest open to all players regardless of profession.[/li][li][i][small](Daily)[/small][/i] [quest=11020] - Yarzill will ask you to collect [item=32502] and use them to poison the peons that are working to gather resources for Dragonmaw.[/li][li][i][small](Daily)[/small][/i] [quest=11035] - You will need to fly to the northeast corner of Netherwing Ledge and position yourself on one of the floating rocks to intercept the [npc=23188] and recover 10 [item=32509].[/li][/ul][/div][pad][b][toggler id=Friendly hidden]Friendly[/toggler][/b]\n[div id=Friendly hidden]Mor\'ghor will award you with an [item=32694] to go with your new rank among the Dragonmaw.[ul][li][quest=11083] - [npc=23166] will task you with quelling the Murkblood Broken that are stationed deeper within the mines.[/li][li][quest=11081] - After finding [item=32726] in a [item=32724], you\'ll begin to reveal what\'s truly happening with the Murkblood in the mine.[/li][li][quest=11054] - [npc=23291] will have you fashion your very own [item=32680] for use in keeping the Dragonmaw peons in line and working at full efficiency.[/li][li][i][small](Daily)[/small][/i] [quest=11076] - The [npc=23149] will ask that you venture into the Netherwing mines and recover the cargo contained in mine carts randomly strewn among the interior of the mine.[/li][li][i][small](Daily)[/small][/i] [npc=23376] - One of the [npc=23376] will inform you that the creatures deeper in the mine are halting production and ask you to thin their numbers.[/li][li][i][small](Daily)[/small][/i] [quest=11055] - This humorous quest starts at Chief Overseer Mudlump after you bring him the required materials. You\'ll be able to fly around Netherwing Ledge and toss the Booterang at any [npc=23311] that can be found anywhere around the crystals of the ledge.[/li][/ul][/div][pad][b][toggler id=Honored hidden]Honored[/toggler][/b]\n[div id=Honored hidden]Mor\'ghor will award you with your new [item=32695], which is now usable anywhere as long as you\'re outside.[ul][li][quest=11063] - This six-part questline will have you in-flight following the other Dragonmaw masters of flight. They will all attempt to knock you off your mount with cleverly-placed air attacks, you must stay within vision range and on your mount until they land or you will fail and need to restart the quest. After defeating the last of the six riders, you\'ll be awarded a [item=32863], which functions exactly like a [item=25653]. The effects of the two trinkets do [b]not[/b] stack.[/li][li][quest=11089] - [npc=23427] will request a set of materials to fashion a special device to destroy his brother and hinder the Legion\'s advances from the Twilight Portal in western [zone=3518].[/li][li][i][small](Daily)[/small][/i] [quest=11086] - Mor\'ghor will send you to the Twilight Portal in Nagrand to kill 20 [url=?npcs&filter=na=deathshadow+-imp+-hound+-agent]Deathshadow Agents[/url]. Beware the overlords, they patrol most of the area and can pack quite a punch.[/li][/ul][/div][pad][b][toggler id=Revered hidden]Revered[/toggler][/b]\n[div id=Revered hidden]Mor\'ghor will award your final trinket upgrade, the [item=32864] after reaching revered.[ul][li]Kill Them All! ([quest=11094]/[quest=11099]) - Mor\'ghor will order you to begin the attack against your chosen faction\'s base of operations in Shadowmoon Valley. Obviously you\'re not going to actually allow the Dragonmaw to attack your allies, so report to the proper leader and unlock your final daily quest for Dragonmaw...[/li][li][i][small](Daily)[/small][/i] The Deadliest Trap Ever Laid ([quest=11097]/[quest=11101]) - Waves of Dragonmaw Skybreakers will attack after preparations are made. Bring allies, as this is a battle of attrition.[/li][/ul][/div][pad][b][toggler id=Exalted hidden]Exalted[/toggler][/b]\n[div id=Exalted hidden]After many days of work, finally the denouement of the Netherwing/Dragonmaw questline. Taskmaster Varkule will direct you to Mor\'ghor one last time, who will inform you that you will be promoted by [npc=22917] himself. Without spoiling the events that ensue, you will end up in Shattrath with your selection of Netherdrake epic mounts. You may choose one here for free, and if you decide on a different color later, you can speak with [npc=23489] back in the Dragonmaw Base Camp to buy another drake for 200 gold.[/div]',NULL),(8,1031,0,'The [b]Sha\'tari Skyguard[/b] are an air wing of the [faction=935] of [zone=3703], defending the capital from attackers in the hills as well as battling against the arakkoa of Terokk in the peaks of Skettis. The Skyguard has two outposts, one in the northern reaches of the Skethyl Mountains and one near [faction=1038]. Players start out at neutral standing with the Skyguard.\n\n[h3]Reputation[/h3]\n[b]Daily Quests[/b][ul][li][quest=11008] - [npc=23048] will grant you a pack of explosives to destroy the eggs that rest atop Skettis structures.[/li][li][quest=11085] - A [npc=23383] can be found atop certain structures, players will escort him out for reputation, gold, and a choice of either 2 [item=28100] or 2 [item=28101].[/li][li][quest=11065] - [npc=23335] will inform you that the Skyguard\'s bombing runs have taken a toll on their mounts and ask you to gather some more Aether Rays to supplement their scout force.[/li][li][quest=11010] - [npc=23120] asks you to destroy the ammo for the Legion\'s flak cannons so the Skyguard Scouts can continue their job.[/li][li][quest=11004] - After collecting 6 [item=32388], [npc=23042] will make a potion that will allow vision of the more powerful arakkoa, such as [npc=23066].\n[i][small]Note: World of Shadows is not a daily quest, but may be repeated as many times as necessary.[/small][/i][/li][/ul][b]Creatures[/b][ul][li][npc=21804] - 5 reputation, up to the end of Revered.[/li][li][url=?npcs&filter=na=skettis+-kaliri+-assassin;minle=70]All Skettis Arakkoa[/url] - 10 reputation, regardless of Skyguard standing.[/li][li][npc=23029] - 30 reputation, regardless of Skyguard standing.[/li][/ul]',NULL),(8,1038,0,'The [b]Ogri\'la[/b] are a faction of ogres in the [zone=3522], where their proximity to [item=32572] has allowed them to evolve past their brutish nature. They are currently fighting a war against both the Black Dragonflight and the Burning Legion, who seek the Apexis Crystals for their own purposes.\n\n[h3]Location[/h3]\nOgri\'la is situated near the western edge of Blade\'s Edge Mountains, between Forge Camp: Terror and Forge Camp: Wrath, just west of Sylvanaar. Ogri\'la is only accessible by flying mount/form. Another alternative is to have a reputation of honored or higher with [faction=1031]. But a player must have a flying mount to reach the Skyguard camp near Skettis.[pad]\n\n[h3]Reputation[/h3]\nReputation with Ogri\'la can only be gained via Quests, and there only repeatable quests are the available [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Thus, there is a cap on how much reputation a day a player can gain reputation with Ogri\'la, making it an \"ungrindable\" reputation.\n\n[b]Apexis Shards[/b]\n[item=32569] can be collected in a variety of ways. They can be looted from mobs, gathered from the environment, or they can be rewards from completed quests.[pad][b]Apexis Crystals[/b]\n[item=32572] are dropped from elite demons and dragons in Blade\'s Edge Mountains. In order to summon these mobs, 35 Apexis Shards are needed, and it is recommended that you have a 5 man group to defeat them.\n\n[b]Quests[/b]\nThere are a [url=?quests&filter=cr=1;crs=1038;crv=0]number of quests[/url] that a player can to do earn reputation with the Ogri\'la, as well as several [url=?quests&filter=da=ja;cr=1;crs=1038;crv=0]daily quests[/url]. Many of the daily quests will also grant reputation with the Sha\'tari Skyguard when they are first completed. \n\nIn order to access the main quests at Ogri\'la itself, a player must first complete the 5 group quests from [npc=22941].\n\n[h3]Depleted Items[/h3]\nA number of \"depleted\" items will sometimes drop from mobs. When combined with 50 Apexis Shards, the items [url=?search=Apexis+Crystal+Infusion]upgrade[/url], gaining stats and gem slots. Once the items are upgraded they become Bind on Equip, and can therefore be sold or traded to other players. One thing to note, however, is that although the depleted items may also have stats or effects, they cannot be equipped.',NULL); /*!40000 ALTER TABLE `aowow_articles` ENABLE KEYS */; UNLOCK TABLES; diff --git a/setup/updates/1438715648_01.sql b/setup/updates/1438715648_01.sql new file mode 100644 index 00000000..3a2d5386 --- /dev/null +++ b/setup/updates/1438715648_01.sql @@ -0,0 +1,23 @@ +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=341', 'event=1'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=141', 'event=2'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=376', 'event=3'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=374', 'event=4'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=375', 'event=5'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=327', 'event=7'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=423', 'event=8'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=181', 'event=9'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=201', 'event=10'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=321', 'event=11'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=324', 'event=12'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=301', 'event=15'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=283', 'event=18'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=284', 'event=19'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=285', 'event=20'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=353', 'event=21'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=372', 'event=24'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=404', 'event=26'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=398', 'event=50'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=409', 'event=51'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=400', 'event=53'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=420', 'event=54'); +UPDATE `aowow_articles` SET `article`= REPLACE(`article`, 'event=424', 'event=63'); From a0887f189fa7e83ec82bfac96a48dba02fc80e68 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 21:23:49 +0200 Subject: [PATCH 0061/1249] Faction * fixed display of reputation columns on listviews on detail page --- includes/shared.php | 2 +- pages/faction.php | 22 +++++++++++++++------- template/listviews/creature.tpl.php | 17 +++++++++++++++++ template/listviews/item.tpl.php | 2 +- template/listviews/quest.tpl.php | 2 +- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index 8278509f..fb67b6c6 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ subject->getField('reputationIndex') != -1) // only if you can actually gain reputation by kills { // inherit siblings/children from $spillover - $cIds = DB::World()->selectCol('SELECT DISTINCT creature_id FROM creature_onkill_reputation WHERE - (RewOnKillRepValue1 > 0 AND (RewOnKillRepFaction1 = ?d{ OR (RewOnKillRepFaction1 IN (?a) AND IsTeamAward1 <> 0)})) OR - (RewOnKillRepValue2 > 0 AND (RewOnKillRepFaction2 = ?d{ OR (RewOnKillRepFaction2 IN (?a) AND IsTeamAward2 <> 0)}))', + $cRep = DB::World()->selectCol('SELECT DISTINCT creature_id AS ARRAY_KEY, qty FROM ( + SELECT creature_id, RewOnKillRepValue1 as qty FROM creature_onkill_reputation WHERE RewOnKillRepValue1 > 0 AND (RewOnKillRepFaction1 = ?d{ OR (RewOnKillRepFaction1 IN (?a) AND IsTeamAward1 <> 0)}) UNION + SELECT creature_id, RewOnKillRepValue2 as qty FROM creature_onkill_reputation WHERE RewOnKillRepValue2 > 0 AND (RewOnKillRepFaction2 = ?d{ OR (RewOnKillRepFaction2 IN (?a) AND IsTeamAward2 <> 0)}) + ) x', $this->typeId, $spillover->getFoundIDs() ?: DBSIMPLE_SKIP, $this->typeId, $spillover->getFoundIDs() ?: DBSIMPLE_SKIP ); - if ($cIds) + if ($cRep) { - $killCreatures = new CreatureList(array(['id', $cIds])); + $killCreatures = new CreatureList(array(['id', array_keys($cRep)])); if (!$killCreatures->error) { + $data = $killCreatures->getListviewData(); + foreach ($data as $id => &$d) + $d['reputation'] = $cRep[$id]; + $tab = array( 'file' => 'creature', - 'data' => $killCreatures->getListviewData(), + 'data' => $data, 'showRep' => true, - 'params' => [] + 'params' => array( + 'extraCols' => '$_', + 'sort' => "$['-reputation', 'name']" + ) ); if ($killCreatures->getMatches() > CFG_SQL_LIMIT_DEFAULT) diff --git a/template/listviews/creature.tpl.php b/template/listviews/creature.tpl.php index 635e3b32..089033b1 100644 --- a/template/listviews/creature.tpl.php +++ b/template/listviews/creature.tpl.php @@ -1,3 +1,20 @@ + +var _ = [ + { + id: 'reputation', + after: 'location', + name: LANG.rep, + tooltip: LANG.tooltip_repgain, + width: '8%', + value: 'reputation' + } +]; + + new Listview({ template:'npc', var _ = [ { diff --git a/template/listviews/quest.tpl.php b/template/listviews/quest.tpl.php index 6b42b56f..69c1e502 100644 --- a/template/listviews/quest.tpl.php +++ b/template/listviews/quest.tpl.php @@ -1,5 +1,5 @@ var _ = [ { From bfa1f261e01ca14715921d9afff58a9a610bf44b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 4 Aug 2015 23:23:37 +0200 Subject: [PATCH 0062/1249] Tooltips/Itemset * handle enforced locale as espected --- pages/itemset.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pages/itemset.php b/pages/itemset.php index fbe472a6..52e19699 100644 --- a/pages/itemset.php +++ b/pages/itemset.php @@ -25,6 +25,10 @@ class ItemsetPage extends GenericPage { parent::__construct($pageCall, $id); + // temp locale + if ($this->mode == CACHE_TYPE_TOOLTIP && isset($_GET['domain'])) + Util::powerUseLocale($_GET['domain']); + $this->typeId = intVal($id); $this->subject = new ItemsetList(array(['id', $this->typeId])); From 38ac7b1dcb64f9c3ba2b224aa7210d4c6f392c6b Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 6 Aug 2015 12:49:38 +0200 Subject: [PATCH 0063/1249] tabs2spaces in global.js --- static/js/global.js | 492 ++++++++++++++++++++++---------------------- 1 file changed, 246 insertions(+), 246 deletions(-) diff --git a/static/js/global.js b/static/js/global.js index c27d386b..417c7504 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -18696,296 +18696,296 @@ var g_zone_areas = {}; var MapViewer = new function() { - var imgWidth, - imgHeight, - scale, - desiredScale, + var imgWidth, + imgHeight, + scale, + desiredScale, - mapper, - oldOnClick, - oldOnUpdate, - oldParent, - oldSibling, - tempParent, - placeholder, + mapper, + oldOnClick, + oldOnUpdate, + oldParent, + oldSibling, + tempParent, + placeholder, - container, - screen, - imgDiv, - aCover; + container, + screen, + imgDiv, + aCover; - function computeDimensions() - { - var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 72)); + function computeDimensions() + { + var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 72)); - desiredScale = 1; - scale = 1;//Math.min(1, availHeight / 515); - // no scaling because it doesnt work with background images + desiredScale = 1; + scale = 1;//Math.min(1, availHeight / 515); + // no scaling because it doesnt work with background images - if(desiredScale > 1) - desiredScale = 1; - if(scale > 1) - scale = 1; + if(desiredScale > 1) + desiredScale = 1; + if(scale > 1) + scale = 1; - imgWidth = Math.round(scale * 772); - imgHeight = Math.round(scale * 515); + imgWidth = Math.round(scale * 772); + imgHeight = Math.round(scale * 515); - var lbWidth = Math.max(480, imgWidth); + var lbWidth = Math.max(480, imgWidth); - Lightbox.setSize(lbWidth + 20, imgHeight + 52); - } + Lightbox.setSize(lbWidth + 20, imgHeight + 52); + } - function getPound(extra) - { - var extraBits = function(map, s) - { - s += ':' + map.zone; - if(map.level) - s += '.' + map.level; - return s; - }; - var buff = '#map'; + function getPound(extra) + { + var extraBits = function(map, s) + { + s += ':' + map.zone; + if(map.level) + s += '.' + map.level; + return s; + }; + var buff = '#map'; - if(tempParent) - buff += '=' + mapper.getLink(); - else if(Mapper.zoneDefaultLevel[mapper.zone]) - { - if(Mapper.zoneDefaultLevel[mapper.zone] != mapper.level) - buff = extraBits(mapper, buff); - } - else if(mapper.level != 0) - buff = extraBits(mapper, buff); - else if((!$WH.isset('g_mapperData') || !g_mapperData[mapper.zone]) && (!$WH.isset('g_mapper_data') || !g_mapper_data[mapper.zone])) - buff = extraBits(mapper, buff); + if(tempParent) + buff += '=' + mapper.getLink(); + else if(Mapper.zoneDefaultLevel[mapper.zone]) + { + if(Mapper.zoneDefaultLevel[mapper.zone] != mapper.level) + buff = extraBits(mapper, buff); + } + else if(mapper.level != 0) + buff = extraBits(mapper, buff); + else if((!$WH.isset('g_mapperData') || !g_mapperData[mapper.zone]) && (!$WH.isset('g_mapper_data') || !g_mapper_data[mapper.zone])) + buff = extraBits(mapper, buff); - return buff; - } + return buff; + } - function onUpdate() - { - if(oldOnUpdate) - oldOnUpdate(mapper); - location.replace(getPound(true)); - } + function onUpdate() + { + if(oldOnUpdate) + oldOnUpdate(mapper); + location.replace(getPound(true)); + } - function render(resizing) - { - if(resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) - return; + function render(resizing) + { + if(resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) + return; - container.style.visibility = 'hidden'; + container.style.visibility = 'hidden'; - computeDimensions(0); + computeDimensions(0); - if(!resizing) - { - if(!placeholder) - { - placeholder = $WH.ce('div'); - placeholder.style.height = '325px'; - placeholder.style.padding = '3px'; - placeholder.style.marginTop = '10px'; - } + if(!resizing) + { + if(!placeholder) + { + placeholder = $WH.ce('div'); + placeholder.style.height = '325px'; + placeholder.style.padding = '3px'; + placeholder.style.marginTop = '10px'; + } - mapper.parent.style.borderWidth = '0px'; - mapper.parent.style.marginTop = '0px'; - mapper.span.style.cursor = 'pointer'; - if(mapper.span.onclick) - oldOnClick = mapper.span.onclick; - mapper.span.onclick = Lightbox.hide; - mapper.span.onmouseover = function() { aCover.style.display = 'block'; }; - mapper.span.onmouseout = function() { setTimeout(function() { if(!aCover.hasMouse) aCover.style.display = 'none'; }, 10); }; - if(mapper.onMapUpdate) - oldOnUpdate = mapper.onMapUpdate; - mapper.onMapUpdate = onUpdate; + mapper.parent.style.borderWidth = '0px'; + mapper.parent.style.marginTop = '0px'; + mapper.span.style.cursor = 'pointer'; + if(mapper.span.onclick) + oldOnClick = mapper.span.onclick; + mapper.span.onclick = Lightbox.hide; + mapper.span.onmouseover = function() { aCover.style.display = 'block'; }; + mapper.span.onmouseout = function() { setTimeout(function() { if(!aCover.hasMouse) aCover.style.display = 'none'; }, 10); }; + if(mapper.onMapUpdate) + oldOnUpdate = mapper.onMapUpdate; + mapper.onMapUpdate = onUpdate; - if(!tempParent) - { - oldParent = mapper.parent.parentNode; - oldSibling = mapper.parent.nextSibling; - oldParent.insertBefore(placeholder, mapper.parent); - $WH.de(mapper.parent); - $WH.ae(mapDiv, mapper.parent); - } - else - { - $WH.de(tempParent); - $WH.ae(mapDiv, tempParent); - } + if(!tempParent) + { + oldParent = mapper.parent.parentNode; + oldSibling = mapper.parent.nextSibling; + oldParent.insertBefore(placeholder, mapper.parent); + $WH.de(mapper.parent); + $WH.ae(mapDiv, mapper.parent); + } + else + { + $WH.de(tempParent); + $WH.ae(mapDiv, tempParent); + } - if(location.hash.indexOf('#show') == -1) - location.replace(getPound(false)); - else if($WH.isset('mapShower')) - mapShower.onExpand(); - } + if(location.hash.indexOf('#show') == -1) + location.replace(getPound(false)); + else if($WH.isset('mapShower')) + mapShower.onExpand(); + } - Lightbox.reveal(); + Lightbox.reveal(); - container.style.visibility = 'visible'; - } + container.style.visibility = 'visible'; + } - function onResize() - { - render(1); - } + function onResize() + { + render(1); + } - function onHide() - { - if(oldOnClick) - mapper.span.onclick = oldOnClick; - else - mapper.span.onclick = null; - oldOnClick = null; - if(oldOnUpdate) - mapper.onMapUpdate = oldOnUpdate - else - mapper.onMapUpdate = null; - oldOnUpdate = null; - mapper.span.style.cursor = ''; + function onHide() + { + if(oldOnClick) + mapper.span.onclick = oldOnClick; + else + mapper.span.onclick = null; + oldOnClick = null; + if(oldOnUpdate) + mapper.onMapUpdate = oldOnUpdate + else + mapper.onMapUpdate = null; + oldOnUpdate = null; + mapper.span.style.cursor = ''; - mapper.span.onmouseover = null; - mapper.span.onmouseout = null; + mapper.span.onmouseover = null; + mapper.span.onmouseout = null; - if(!tempParent) - { - $WH.de(placeholder); - $WH.de(mapper.parent); - mapper.parent.style.borderWidth = ''; - mapper.parent.style.marginTop = ''; - if(oldSibling) - oldParent.insertBefore(mapper.parent, oldSibling); - else - $WH.ae(oldParent, mapper.parent); - oldParent = oldSibling = null; - } - else - { - $WH.de(tempParent); - tempParent = null; - } + if(!tempParent) + { + $WH.de(placeholder); + $WH.de(mapper.parent); + mapper.parent.style.borderWidth = ''; + mapper.parent.style.marginTop = ''; + if(oldSibling) + oldParent.insertBefore(mapper.parent, oldSibling); + else + $WH.ae(oldParent, mapper.parent); + oldParent = oldSibling = null; + } + else + { + $WH.de(tempParent); + tempParent = null; + } - mapper.toggleZoom(); + mapper.toggleZoom(); - if(location.hash.indexOf('#show') == -1) - location.replace('#.'); - else if($WH.isset('mapShower')) - mapShower.onCollapse(); - } + if(location.hash.indexOf('#show') == -1) + location.replace('#.'); + else if($WH.isset('mapShower')) + mapShower.onCollapse(); + } - function onShow(dest, first, opt) - { - mapper = opt.mapper; - container = dest; + function onShow(dest, first, opt) + { + mapper = opt.mapper; + container = dest; - if(first) - { - dest.className = 'mapviewer'; + if(first) + { + dest.className = 'mapviewer'; - screen = $WH.ce('div'); - screen.style.width = '772px'; - screen.style.height = '515px'; + screen = $WH.ce('div'); + screen.style.width = '772px'; + screen.style.height = '515px'; - screen.className = 'mapviewer-screen'; + screen.className = 'mapviewer-screen'; - aCover = $WH.ce('a'); - aCover.className = 'mapviewer-cover'; - aCover.href = 'javascript:;'; - aCover.onclick = Lightbox.hide; - aCover.onmouseover = function() { aCover.hasMouse = true; }; - aCover.onmouseout = function() { aCover.hasMouse = false; }; - var foo = $WH.ce('span'); - var b = $WH.ce('b'); - $WH.ae(b, $WH.ct(LANG.close)); - $WH.ae(foo, b); - $WH.ae(aCover, foo); - $WH.ae(screen, aCover); + aCover = $WH.ce('a'); + aCover.className = 'mapviewer-cover'; + aCover.href = 'javascript:;'; + aCover.onclick = Lightbox.hide; + aCover.onmouseover = function() { aCover.hasMouse = true; }; + aCover.onmouseout = function() { aCover.hasMouse = false; }; + var foo = $WH.ce('span'); + var b = $WH.ce('b'); + $WH.ae(b, $WH.ct(LANG.close)); + $WH.ae(foo, b); + $WH.ae(aCover, foo); + $WH.ae(screen, aCover); - mapDiv = $WH.ce('div'); - $WH.ae(screen, mapDiv); + mapDiv = $WH.ce('div'); + $WH.ae(screen, mapDiv); - $WH.ae(dest, screen); + $WH.ae(dest, screen); - var aClose = $WH.ce('a'); - // aClose.className = 'dialog-x'; - aClose.className = 'dialog-cancel'; - aClose.href = 'javascript:;'; - aClose.onclick = Lightbox.hide; - $WH.ae(aClose, $WH.ct(LANG.close)); - $WH.ae(dest, aClose); + var aClose = $WH.ce('a'); + // aClose.className = 'dialog-x'; + aClose.className = 'dialog-cancel'; + aClose.href = 'javascript:;'; + aClose.onclick = Lightbox.hide; + $WH.ae(aClose, $WH.ct(LANG.close)); + $WH.ae(dest, aClose); - var d = $WH.ce('div'); - d.className = 'clear'; - $WH.ae(dest, d); - } + var d = $WH.ce('div'); + d.className = 'clear'; + $WH.ae(dest, d); + } - onRender(); - } + onRender(); + } - function onRender() - { - render(); - } + function onRender() + { + render(); + } - this.checkPound = function() - { - if(location.hash && location.hash.indexOf('#map') == 0) - { - var parts = location.hash.split('='); - if(parts.length == 2) - { - var link = parts[1]; + this.checkPound = function() + { + if(location.hash && location.hash.indexOf('#map') == 0) + { + var parts = location.hash.split('='); + if(parts.length == 2) + { + var link = parts[1]; - if(link) - { - /*tempParent = $WH.ce('div'); - tempParent.id = 'fewuiojfdksl'; - $WH.ae(document.body, tempParent); - var map = new Mapper({ parent: tempParent.id }); - map.setLink(link, true); - map.toggleZoom();*/ - MapViewer.show({ link: link }); - } - } - else - { - parts = location.hash.split(':'); + if(link) + { + /*tempParent = $WH.ce('div'); + tempParent.id = 'fewuiojfdksl'; + $WH.ae(document.body, tempParent); + var map = new Mapper({ parent: tempParent.id }); + map.setLink(link, true); + map.toggleZoom();*/ + MapViewer.show({ link: link }); + } + } + else + { + parts = location.hash.split(':'); - var map = $WH.ge('sjdhfkljawelis'); - if(map) - map.onclick(); + var map = $WH.ge('sjdhfkljawelis'); + if(map) + map.onclick(); - if(parts.length == 2) - { - if(!map) - MapViewer.show({ link: parts[1]}); - var subparts = parts[1].split('.'); - var opts = { zone: subparts[0] }; - if(subparts.length == 2) - opts.level = parseInt(subparts[1])+1; - mapper.update(opts); - //if(Mapper.multiLevelZones[mapper.zone]) - // mapper.setMap(Mapper.multiLevelZones[mapper.zone][floor], floor, true); - } - } - } - } + if(parts.length == 2) + { + if(!map) + MapViewer.show({ link: parts[1]}); + var subparts = parts[1].split('.'); + var opts = { zone: subparts[0] }; + if(subparts.length == 2) + opts.level = parseInt(subparts[1])+1; + mapper.update(opts); + //if(Mapper.multiLevelZones[mapper.zone]) + // mapper.setMap(Mapper.multiLevelZones[mapper.zone][floor], floor, true); + } + } + } + } - this.show = function(opt) - { - if(opt.link) - { - tempParent = $WH.ce('div'); - tempParent.id = 'fewuiojfdksl'; - $WH.ae(document.body, tempParent); - var map = new Mapper({ parent: tempParent.id }); - map.setLink(opt.link, true); - map.toggleZoom(); - } - else - Lightbox.show('mapviewer', { onShow: onShow, onHide: onHide, onResize: onResize }, opt); - } + this.show = function(opt) + { + if(opt.link) + { + tempParent = $WH.ce('div'); + tempParent.id = 'fewuiojfdksl'; + $WH.ae(document.body, tempParent); + var map = new Mapper({ parent: tempParent.id }); + map.setLink(opt.link, true); + map.toggleZoom(); + } + else + Lightbox.show('mapviewer', { onShow: onShow, onHide: onHide, onResize: onResize }, opt); + } - $(document).ready(this.checkPound); + $(document).ready(this.checkPound); }; var ModelViewer = new function() { From 81e60989609f974b336769ef8ca7d6ca3a613018 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 6 Aug 2015 18:21:24 +0200 Subject: [PATCH 0064/1249] Home * added variable titles and oneliners to frontpage * added cascading foreign keys to news_overlay -> news * renamed news -> featuredbox for consistency Misc * aowow_dbversion is now part of the basic sql * also the db-dump is a fresh one and i'm shocked how often i forgot to apply updated there :< * from now on shorttags will be used for 'echo' in template (e.g. ) --- pages/home.php | 24 ++- setup/db_structure.sql | 306 ++++++++++++++++++--------- setup/setup.php | 11 +- setup/tools/clisetup/update.func.php | 17 +- setup/tools/sqlGen.class.php | 16 +- setup/updates/1438878038_01.sql | 53 +++++ static/css/home.css | 2 +- static/js/staff.js | 4 +- template/pages/home.tpl.php | 26 ++- 9 files changed, 309 insertions(+), 150 deletions(-) create mode 100644 setup/updates/1438878038_01.sql diff --git a/pages/home.php b/pages/home.php index ecaf4265..f954641a 100644 --- a/pages/home.php +++ b/pages/home.php @@ -6,11 +6,12 @@ if (!defined('AOWOW_REVISION')) class HomePage extends GenericPage { - protected $tpl = 'home'; - protected $js = ['home.js']; - protected $css = [['path' => 'home.css']]; + protected $tpl = 'home'; + protected $js = ['home.js']; + protected $css = [['path' => 'home.css']]; - protected $news = []; + protected $news = []; + protected $oneliner = ''; public function __construct() { @@ -21,8 +22,12 @@ class HomePage extends GenericPage { $this->addCSS(['string' => '.announcement { margin: auto; max-width: 1200px; padding: 0px 15px 15px 15px }']); + // load oneliner + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_home_oneliner WHERE active = 1 LIMIT 1')) + $this->oneliner = Util::jsEscape(Util::localizedString($_, 'text')); + // load news - $this->news = DB::Aowow()->selectRow('SELECT id as ARRAY_KEY, n.* FROM ?_news n WHERE active = 1 ORDER BY id DESC LIMIT 1'); + $this->news = DB::Aowow()->selectRow('SELECT id as ARRAY_KEY, n.* FROM ?_home_featuredbox n WHERE active = 1 ORDER BY id DESC LIMIT 1'); if (!$this->news) return; @@ -37,7 +42,7 @@ class HomePage extends GenericPage $this->news['bgImgUrl'] = strtr($this->news['bgImgUrl'], ['HOST_URL' => HOST_URL, 'STATIC_URL' => STATIC_URL]); // load overlay links - $this->news['overlays'] = DB::Aowow()->select('SELECT * FROM ?_news_overlay WHERE newsId = ?d', $this->news['id']); + $this->news['overlays'] = DB::Aowow()->select('SELECT * FROM ?_home_featuredbox_overlay WHERE featureId = ?d', $this->news['id']); foreach ($this->news['overlays'] as &$o) { $o['title'] = Util::localizedString($o, 'title', true); @@ -45,7 +50,12 @@ class HomePage extends GenericPage } } - protected function generateTitle() {} + protected function generateTitle() + { + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_home_titles WHERE active = 1 AND title_loc?d <> "" ORDER BY RAND() LIMIT 1', User::$localeId)) + $this->title[0] .= Lang::main('colon').Util::localizedString($_, 'title'); + } + protected function generatePath() {} } diff --git a/setup/db_structure.sql b/setup/db_structure.sql index 5a130b1e..f81c5a5c 100644 --- a/setup/db_structure.sql +++ b/setup/db_structure.sql @@ -2,7 +2,7 @@ -- -- Host: localhost Database: sarjuuk_aowow -- ------------------------------------------------------ --- Server version 5.5.30-30.1 +-- Server version 5.5.30-30.1 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; @@ -48,7 +48,7 @@ CREATE TABLE `aowow_account` ( `token` varchar(40) NOT NULL COMMENT 'creation & recovery', PRIMARY KEY (`id`), UNIQUE KEY `user` (`user`) -) ENGINE=MyISAM AUTO_INCREMENT=30 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -60,14 +60,16 @@ DROP TABLE IF EXISTS `aowow_account_banned`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_account_banned` ( `id` int(16) unsigned NOT NULL, - `userId` int(11) unsigned NOT NULL COMMENT 'affected accountId', - `staffId` int(11) unsigned NOT NULL COMMENT 'executive accountId', + `userId` int(10) unsigned NOT NULL COMMENT 'affected accountId', + `staffId` int(10) unsigned NOT NULL COMMENT 'executive accountId', `typeMask` tinyint(4) unsigned NOT NULL COMMENT 'ACC_BAN_*', `start` int(10) unsigned NOT NULL COMMENT 'unixtime', `end` int(10) unsigned NOT NULL COMMENT 'automatic unban @ unixtime', `reason` varchar(255) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + PRIMARY KEY (`id`), + KEY `FK_acc_banned` (`userId`), + CONSTRAINT `FK_acc_banned` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -98,8 +100,9 @@ CREATE TABLE `aowow_account_cookies` ( `name` varchar(127) NOT NULL, `data` text NOT NULL, PRIMARY KEY (`userId`), - UNIQUE KEY `userId_name` (`userId`,`name`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + UNIQUE KEY `userId_name` (`userId`,`name`), + CONSTRAINT `FK_acc_cookies` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -117,8 +120,9 @@ CREATE TABLE `aowow_account_reputation` ( `sourceB` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'e.g. upvoted commentId', `date` int(10) unsigned NOT NULL DEFAULT '0', UNIQUE KEY `userId_action_source` (`userId`,`action`,`sourceA`,`sourceB`), - KEY `userId` (`userId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='reputation log'; + KEY `userId` (`userId`), + CONSTRAINT `FK_acc_rep` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='reputation log'; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -129,12 +133,14 @@ DROP TABLE IF EXISTS `aowow_account_weightscales`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_account_weightscales` ( - `id` int(32) NOT NULL AUTO_INCREMENT, - `account` int(32) NOT NULL, + `id` int(32) NOT NULL, + `userId` int(10) unsigned NOT NULL, `name` varchar(32) NOT NULL, `weights` text NOT NULL, - PRIMARY KEY (`id`,`account`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + PRIMARY KEY (`id`,`userId`), + KEY `FK_acc_weights` (`userId`), + CONSTRAINT `FK_acc_weights` FOREIGN KEY (`userId`) REFERENCES `aowow_account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -155,7 +161,7 @@ CREATE TABLE `aowow_achievement` ( `points` tinyint(3) unsigned NOT NULL, `orderInGroup` tinyint(3) unsigned NOT NULL, `iconId` mediumint(8) unsigned NOT NULL, - `flags` tinyint(3) unsigned NOT NULL, + `flags` smallint(5) unsigned NOT NULL, `reqCriteriaCount` tinyint(3) unsigned NOT NULL, `refAchievement` smallint(5) unsigned NOT NULL, `itemExtra` mediumint(8) unsigned NOT NULL, @@ -251,7 +257,7 @@ CREATE TABLE `aowow_announcements` ( `text_loc6` text NOT NULL, `text_loc8` text NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; +) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -347,8 +353,8 @@ CREATE TABLE `aowow_comments` ( `responseBody` text, `responseRoles` smallint(5) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), - INDEX `type_typeId` (`type`, `typeId`) -) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; + KEY `type_typeId` (`type`,`typeId`) +) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -472,7 +478,10 @@ CREATE TABLE `aowow_creature` ( `flagsExtra` int(10) unsigned NOT NULL DEFAULT '0', `scriptName` varchar(50) NOT NULL DEFAULT '', PRIMARY KEY (`id`), - KEY `idx_name` (`name_loc0`) + KEY `idx_name` (`name_loc0`), + KEY `difficultyEntry1` (`difficultyEntry1`), + KEY `difficultyEntry2` (`difficultyEntry2`), + KEY `difficultyEntry3` (`difficultyEntry3`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -514,10 +523,30 @@ CREATE TABLE `aowow_currencies` ( `name_loc3` varchar(64) NOT NULL, `name_loc6` varchar(64) NOT NULL, `name_loc8` varchar(64) NOT NULL, + `description_loc0` varchar(256) NOT NULL, + `description_loc2` varchar(256) NOT NULL, + `description_loc3` varchar(256) NOT NULL, + `description_loc6` varchar(256) NOT NULL, + `description_loc8` varchar(256) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_dbversion` +-- + +DROP TABLE IF EXISTS `aowow_dbversion`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_dbversion` ( + `date` int(10) unsigned NOT NULL DEFAULT '0', + `part` tinyint(3) unsigned NOT NULL DEFAULT '0', + `sql` text, + `build` text +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_emotes` -- @@ -530,21 +559,21 @@ CREATE TABLE `aowow_emotes` ( `cmd` varchar(15) NOT NULL, `isAnimated` tinyint(1) unsigned NOT NULL, `cuFlags` int(10) unsigned NOT NULL, - `target_loc0` varchar(65) NULL DEFAULT NULL, - `target_loc2` varchar(70) NULL DEFAULT NULL, - `target_loc3` varchar(95) NULL DEFAULT NULL, - `target_loc6` varchar(90) NULL DEFAULT NULL, - `target_loc8` varchar(70) NULL DEFAULT NULL, - `noTarget_loc0` varchar(65) NULL DEFAULT NULL, - `noTarget_loc2` varchar(110) NULL DEFAULT NULL, - `noTarget_loc3` varchar(85) NULL DEFAULT NULL, - `noTarget_loc6` varchar(75) NULL DEFAULT NULL, - `noTarget_loc8` varchar(60) NULL DEFAULT NULL, - `self_loc0` varchar(65) NULL DEFAULT NULL, - `self_loc2` varchar(115) NULL DEFAULT NULL, - `self_loc3` varchar(85) NULL DEFAULT NULL, - `self_loc6` varchar(75) NULL DEFAULT NULL, - `self_loc8` varchar(70) NULL DEFAULT NULL, + `target_loc0` varchar(65) DEFAULT NULL, + `target_loc2` varchar(70) DEFAULT NULL, + `target_loc3` varchar(95) DEFAULT NULL, + `target_loc6` varchar(90) DEFAULT NULL, + `target_loc8` varchar(70) DEFAULT NULL, + `noTarget_loc0` varchar(65) DEFAULT NULL, + `noTarget_loc2` varchar(110) DEFAULT NULL, + `noTarget_loc3` varchar(85) DEFAULT NULL, + `noTarget_loc6` varchar(75) DEFAULT NULL, + `noTarget_loc8` varchar(60) DEFAULT NULL, + `self_loc0` varchar(65) DEFAULT NULL, + `self_loc2` varchar(115) DEFAULT NULL, + `self_loc3` varchar(85) DEFAULT NULL, + `self_loc6` varchar(75) DEFAULT NULL, + `self_loc8` varchar(70) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -560,8 +589,8 @@ CREATE TABLE `aowow_emotes_aliasses` ( `id` smallint(6) unsigned NOT NULL, `locales` smallint(6) unsigned NOT NULL, `command` varchar(15) NOT NULL, - UNIQUE INDEX `id_command` (`id`, `command`), - INDEX `id` (`id`) + UNIQUE KEY `id_command` (`id`,`command`), + KEY `id` (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -696,6 +725,99 @@ CREATE TABLE `aowow_holidays` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_home_featuredbox` +-- + +DROP TABLE IF EXISTS `aowow_home_featuredbox`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_featuredbox` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned DEFAULT NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(1) unsigned NOT NULL, + `extraWide` tinyint(3) unsigned NOT NULL DEFAULT '0', + `bgImgUrl` varchar(150) NOT NULL DEFAULT '', + `text_loc0` text NOT NULL, + `text_loc2` text NOT NULL, + `text_loc3` text NOT NULL, + `text_loc6` text NOT NULL, + `text_loc8` text NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_acc_hFBox` (`editorId`), + CONSTRAINT `FK_acc_hFBox` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_home_featuredbox_overlay` +-- + +DROP TABLE IF EXISTS `aowow_home_featuredbox_overlay`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_featuredbox_overlay` ( + `featureId` smallint(5) unsigned NOT NULL, + `left` smallint(5) unsigned NOT NULL, + `width` smallint(5) unsigned NOT NULL, + `url` varchar(150) NOT NULL, + `title_loc0` varchar(100) NOT NULL DEFAULT '', + `title_loc2` varchar(100) NOT NULL DEFAULT '', + `title_loc3` varchar(100) NOT NULL DEFAULT '', + `title_loc6` varchar(100) NOT NULL DEFAULT '', + `title_loc8` varchar(100) NOT NULL DEFAULT '', + KEY `FK_home_featurebox` (`featureId`), + CONSTRAINT `FK_home_featurebox` FOREIGN KEY (`featureId`) REFERENCES `aowow_home_featuredbox` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_home_oneliner` +-- + +DROP TABLE IF EXISTS `aowow_home_oneliner`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_oneliner` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned DEFAULT NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(1) unsigned NOT NULL, + `text_loc0` varchar(200) NOT NULL, + `text_loc2` varchar(200) NOT NULL, + `text_loc3` varchar(200) NOT NULL, + `text_loc6` varchar(200) NOT NULL, + `text_loc8` varchar(200) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_acc_hOneliner` (`editorId`), + CONSTRAINT `FK_acc_hOneliner` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_home_titles` +-- + +DROP TABLE IF EXISTS `aowow_home_titles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_home_titles` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned DEFAULT NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(1) unsigned NOT NULL, + `title_loc0` varchar(100) NOT NULL, + `title_loc2` varchar(100) NOT NULL, + `title_loc3` varchar(100) NOT NULL, + `title_loc6` varchar(100) NOT NULL, + `title_loc8` varchar(100) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_acc_hTitles` (`editorId`), + CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_icons` -- @@ -719,7 +841,7 @@ DROP TABLE IF EXISTS `aowow_item_stats`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_item_stats` ( `type` smallint(5) unsigned NOT NULL, - `typeId` mediumint(9) unsigned NOT NULL, + `typeId` mediumint(8) unsigned NOT NULL, `nsockets` tinyint(3) unsigned NOT NULL, `dmgmin1` smallint(5) unsigned NOT NULL, `dmgmax1` smallint(5) unsigned NOT NULL, @@ -799,7 +921,7 @@ CREATE TABLE `aowow_item_stats` ( `shasplpwr` smallint(6) NOT NULL, `natsplpwr` smallint(6) NOT NULL, `arcsplpwr` smallint(6) NOT NULL, - PRIMARY KEY (`typeId`, `type`) + PRIMARY KEY (`typeId`,`type`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1197,7 +1319,7 @@ CREATE TABLE `aowow_itemset` ( `quality` tinyint(4) NOT NULL, `type` smallint(6) NOT NULL COMMENT 'g_itemset_types', `contentGroup` smallint(6) NOT NULL COMMENT 'g_itemset_notes', - `eventId` smallint(5) unsigned NOT NULL, + `eventId` smallint(3) unsigned NOT NULL, `skillId` smallint(3) unsigned NOT NULL, `skillLevel` smallint(3) unsigned NOT NULL, PRIMARY KEY (`id`) @@ -1255,47 +1377,6 @@ CREATE TABLE `aowow_mailtemplate` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; --- --- Table structure for table `aowow_news` --- - -DROP TABLE IF EXISTS `aowow_news`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_news` ( - `id` int(16) unsigned NOT NULL, - `active` tinyint(3) unsigned NOT NULL DEFAULT '1', - `extraWide` tinyint(3) unsigned NOT NULL DEFAULT '0', - `bgImgUrl` varchar(150) NOT NULL DEFAULT '', - `text_loc0` text NOT NULL, - `text_loc2` text NOT NULL, - `text_loc3` text NOT NULL, - `text_loc6` text NOT NULL, - `text_loc8` text NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `aowow_news_overlay` --- - -DROP TABLE IF EXISTS `aowow_news_overlay`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `aowow_news_overlay` ( - `newsId` int(11) unsigned NOT NULL, - `left` smallint(5) unsigned NOT NULL, - `width` smallint(5) unsigned NOT NULL, - `url` varchar(150) NOT NULL, - `title_loc0` varchar(100) NOT NULL DEFAULT '', - `title_loc2` varchar(100) NOT NULL DEFAULT '', - `title_loc3` varchar(100) NOT NULL DEFAULT '', - `title_loc6` varchar(100) NOT NULL DEFAULT '', - `title_loc8` varchar(100) NOT NULL DEFAULT '' -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - -- -- Table structure for table `aowow_objects` -- @@ -1388,7 +1469,7 @@ CREATE TABLE `aowow_quests` ( `type` smallint(5) unsigned NOT NULL DEFAULT '0', `suggestedPlayers` tinyint(3) unsigned NOT NULL DEFAULT '0', `timeLimit` int(10) unsigned NOT NULL DEFAULT '0', - `eventId` smallint(5) unsigned NOT NULL, + `eventId` smallint(5) unsigned NOT NULL DEFAULT '0', `prevQuestId` mediumint(8) NOT NULL DEFAULT '0', `nextQuestId` mediumint(8) NOT NULL DEFAULT '0', `exclusiveGroup` mediumint(8) NOT NULL DEFAULT '0', @@ -1536,7 +1617,8 @@ CREATE TABLE `aowow_quests` ( `objectiveText4_loc3` text, `objectiveText4_loc6` text, `objectiveText4_loc8` text, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `nextQuestIdChain` (`nextQuestIdChain`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1609,7 +1691,7 @@ CREATE TABLE `aowow_reports` ( `email` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `userId` (`userId`) -) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; +) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1655,7 +1737,6 @@ DROP TABLE IF EXISTS `aowow_scalingstatvalues`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_scalingstatvalues` ( `id` tinyint(3) unsigned NOT NULL, - `charLevel` tinyint(3) unsigned NOT NULL, `shoulderMultiplier` tinyint(3) unsigned NOT NULL, `trinketMultiplier` tinyint(3) unsigned NOT NULL, `weaponMultiplier` tinyint(3) unsigned NOT NULL, @@ -1693,17 +1774,19 @@ CREATE TABLE `aowow_screenshots` ( `id` int(16) unsigned NOT NULL AUTO_INCREMENT, `type` smallint(5) unsigned NOT NULL, `typeId` mediumint(9) NOT NULL, - `uploader` int(16) unsigned NOT NULL, + `userIdOwner` int(10) unsigned DEFAULT NULL, `date` int(32) unsigned NOT NULL, `width` smallint(5) unsigned NOT NULL, `height` smallint(5) unsigned NOT NULL, `caption` varchar(250) DEFAULT NULL, `status` tinyint(3) unsigned NOT NULL COMMENT 'see defines.php - CC_FLAG_*', - `approvedBy` int(16) unsigned DEFAULT NULL, - `deletedBy` int(16) unsigned DEFAULT NULL, + `userIdApprove` int(10) unsigned DEFAULT NULL, + `userIdDelete` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`id`), - KEY `type` (`type`,`typeId`) -) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; + KEY `type` (`type`,`typeId`), + KEY `FK_acc_ss` (`userIdOwner`), + CONSTRAINT `FK_acc_ss` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2216,18 +2299,21 @@ DROP TABLE IF EXISTS `aowow_videos`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_videos` ( - `id` int(16) NOT NULL, + `id` int(16) NOT NULL AUTO_INCREMENT, `type` smallint(5) unsigned NOT NULL, `typeId` mediumint(9) NOT NULL, - `uploader` int(16) NOT NULL, + `userIdOwner` int(10) unsigned DEFAULT NULL, `date` int(32) NOT NULL, `videoId` varchar(12) NOT NULL, `caption` text, `status` int(8) NOT NULL, - `approvedBy` int(16) DEFAULT NULL, + `userIdApprove` int(10) unsigned DEFAULT NULL, + `userIdeDelete` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`id`), - KEY `type` (`type`,`typeId`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + KEY `type` (`type`,`typeId`), + KEY `FK_acc_vi` (`userIdOwner`), + CONSTRAINT `FK_acc_vi` FOREIGN KEY (`userIdOwner`) REFERENCES `aowow_account` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -2328,23 +2414,33 @@ INSERT INTO `aowow_config` VALUES ('sql_limit_search','500',0,129,'default: 500 UNLOCK TABLES; -- --- Dumping data for table `aowow_news` +-- Dumping data for table `aowow_dbversion` -- -LOCK TABLES `aowow_news` WRITE; -/*!40000 ALTER TABLE `aowow_news` DISABLE KEYS */; -INSERT INTO `aowow_news` VALUES (1,1,0,'','[pad]Welcome to [b][span class=q5]AoWoW[/span][/b]!','[pad]Bienvenue à [b][span class=q5]AoWoW[/span][/b]!','[pad]Willkommen bei [b][span class=q5]AoWoW[/span][/b]!','','Добро[pad] пожаловать на [b][span class=q5]AoWoW[/span][/b]!'),(2,0,1,'STATIC_URL/images/logos/newsbox-explained.png','[ul]\n[li][i]just demoing the newsbox here..[/i][/li]\n[li][b][url=http://www.example.com]..with urls[/url][/b][/li]\n[li][b]..typeLinks [item=45533][/b][/li]\n[li][b]..also, over there to the right is an overlay-trigger =>[/b][/li]\n[/ul]\n\n[ul]\n[li][tooltip name=demotip]hey, it hints you stuff![/tooltip][b][span class=tip tooltip=demotip]..hover me[/span][/b][/li]\n[/ul]','','','',''); -/*!40000 ALTER TABLE `aowow_news` ENABLE KEYS */; +LOCK TABLES `aowow_dbversion` WRITE; +/*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; +INSERT INTO `aowow_dbversion` VALUES (1438878038,0,NULL,NULL); +/*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; -- --- Dumping data for table `aowow_news_overlay` +-- Dumping data for table `aowow_home_featuredbox` -- -LOCK TABLES `aowow_news_overlay` WRITE; -/*!40000 ALTER TABLE `aowow_news_overlay` DISABLE KEYS */; -INSERT INTO `aowow_news_overlay` VALUES (2,405,100,'http://example.com','example overlay','','','',''); -/*!40000 ALTER TABLE `aowow_news_overlay` ENABLE KEYS */; +LOCK TABLES `aowow_home_featuredbox` WRITE; +/*!40000 ALTER TABLE `aowow_home_featuredbox` DISABLE KEYS */; +INSERT INTO `aowow_home_featuredbox` VALUES (1,NULL,0,1,0,'','[pad]Welcome to [b][span class=q5]AoWoW[/span][/b]!','[pad]Bienvenue à [b][span class=q5]AoWoW[/span][/b]!','[pad]Willkommen bei [b][span class=q5]AoWoW[/span][/b]!','','Добро[pad] пожаловать на [b][span class=q5]AoWoW[/span][/b]!'),(2,NULL,0,0,1,'STATIC_URL/images/logos/newsbox-explained.png','[ul]\n[li][i]just demoing the newsbox here..[/i][/li]\n[li][b][url=http://www.example.com]..with urls[/url][/b][/li]\n[li][b]..typeLinks [item=45533][/b][/li]\n[li][b]..also, over there to the right is an overlay-trigger =>[/b][/li]\n[/ul]\n\n[ul]\n[li][tooltip name=demotip]hey, it hints you stuff![/tooltip][b][span class=tip tooltip=demotip]..hover me[/span][/b][/li]\n[/ul]','','','',''); +/*!40000 ALTER TABLE `aowow_home_featuredbox` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Dumping data for table `aowow_home_featuredbox_overlay` +-- + +LOCK TABLES `aowow_home_featuredbox_overlay` WRITE; +/*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` DISABLE KEYS */; +INSERT INTO `aowow_home_featuredbox_overlay` VALUES (2,405,100,'http://example.com','example overlay','','','',''); +/*!40000 ALTER TABLE `aowow_home_featuredbox_overlay` ENABLE KEYS */; UNLOCK TABLES; -- @@ -2366,4 +2462,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2015-05-10 14:11:18 +-- Dump completed on 2015-08-06 17:51:15 diff --git a/setup/setup.php b/setup/setup.php index 973235f1..477cf97b 100644 --- a/setup/setup.php +++ b/setup/setup.php @@ -58,13 +58,8 @@ else CLISetup::init(); $cmd = array_pop(array_keys($opt)); -switch ($cmd) // we accept only one main parameter +switch ($cmd) // we accept only one main parameter { - case 'update': - require_once 'setup/tools/clisetup/update.func.php'; - update(); - - return; case 'firstrun': case 'resume': require_once 'setup/tools/clisetup/firstrun.func.php'; @@ -80,6 +75,10 @@ switch ($cmd) // we accept only one main parameter $cmd(); finish(); + case 'update': + require_once 'setup/tools/clisetup/update.func.php'; + if (update()) // return true if we do not rebuild stuff + return; case 'sync': require_once 'setup/tools/clisetup/sql.func.php'; require_once 'setup/tools/clisetup/build.func.php'; diff --git a/setup/tools/clisetup/update.func.php b/setup/tools/clisetup/update.func.php index f4ce13c9..053639fa 100644 --- a/setup/tools/clisetup/update.func.php +++ b/setup/tools/clisetup/update.func.php @@ -13,20 +13,7 @@ if (!CLI) function update() { - $createQuery = " - CREATE TABLE `aowow_dbversion` ( - `date` INT(10) UNSIGNED NOT NULL DEFAULT '0', - `part` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' - ) ENGINE=MyISAM"; - - $date = $part = 0; - if (!DB::Aowow()->selectCell('SHOW TABLES LIKE "%dbversion"')) - { - DB::Aowow()->query($createQuery); - DB::Aowow()->query('INSERT INTO ?_dbversion VALUES (0, 0)'); - } - else - list($date, $part) = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ?_dbversion')); + list($date, $part) = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ?_dbversion')); CLISetup::log('checking sql updates'); @@ -68,6 +55,8 @@ function update() } CLISetup::log($nFiles ? 'applied '.$nFiles.' update(s)' : 'db is already up to date', CLISetup::LOG_OK); + + return true; } ?> diff --git a/setup/tools/sqlGen.class.php b/setup/tools/sqlGen.class.php index d11e855e..13131121 100644 --- a/setup/tools/sqlGen.class.php +++ b/setup/tools/sqlGen.class.php @@ -8,12 +8,14 @@ if (!CLI) /* provide these with basic content - aowow_announcements 1 P - aowow_articles 1 P - aowow_config 1 P - aowow_news 1 P - aowow_news_overlay 1 E - aowow_sourcestrings 2 P + aowow_announcements + aowow_articles + aowow_config + aowow_home_featuredbox + aowow_home_featuredbox_overlay + aowow_home_oneliners + aowow_home_titles + aowow_sourcestrings */ @@ -198,4 +200,4 @@ class SqlGen } } -?> \ No newline at end of file +?> diff --git a/setup/updates/1438878038_01.sql b/setup/updates/1438878038_01.sql new file mode 100644 index 00000000..f109a37e --- /dev/null +++ b/setup/updates/1438878038_01.sql @@ -0,0 +1,53 @@ +SET FOREIGN_KEY_CHECKS=0; + +RENAME TABLE `aowow_news` TO `aowow_home_featuredbox`; +ALTER TABLE `aowow_home_featuredbox` + ALTER `id` DROP DEFAULT, + ALTER `active` DROP DEFAULT; +ALTER TABLE `aowow_home_featuredbox` + ENGINE=InnoDB, + CHANGE COLUMN `id` `id` smallint(5) unsigned NOT NULL FIRST, + ADD COLUMN `editorId` int(10) unsigned NULL AFTER `id`, + ADD COLUMN `editDate` int(10) unsigned NOT NULL AFTER `editorId`, + CHANGE COLUMN `active` `active` tinyint(1) unsigned NOT NULL AFTER `editDate`, + ADD CONSTRAINT `FK_acc_hFBox` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE ON DELETE SET NULL; + +RENAME TABLE `aowow_news_overlay` TO `aowow_home_featurebox_overlay`; +ALTER TABLE `aowow_home_featuredbox_overlay` + ALTER `newsId` DROP DEFAULT; +ALTER TABLE `aowow_home_featuredbox_overlay` + ENGINE=InnoDB, + CHANGE COLUMN `newsId` `featureId` smallint(5) unsigned NOT NULL FIRST, + ADD CONSTRAINT `FK_home_featurebox` FOREIGN KEY (`featureId`) REFERENCES `aowow_home_featuredbox` (`id`) ON UPDATE CASCADE ON DELETE CASCADE; + +CREATE TABLE `aowow_home_titles` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(1) unsigned NOT NULL, + `title_loc0` varchar(100) NOT NULL, + `title_loc2` varchar(100) NOT NULL, + `title_loc3` varchar(100) NOT NULL, + `title_loc6` varchar(100) NOT NULL, + `title_loc8` varchar(100) NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK_acc_hTitles` (`editorId`), + CONSTRAINT `FK_acc_hTitles` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE ON DELETE SET NULL +) ENGINE=InnoDB; + +CREATE TABLE `aowow_home_oneliner` ( + `id` smallint(5) unsigned NOT NULL, + `editorId` int(10) unsigned NULL, + `editDate` int(10) unsigned NOT NULL, + `active` tinyint(1) unsigned NOT NULL, + `text_loc0` varchar(200) NOT NULL, + `text_loc2` varchar(200) NOT NULL, + `text_loc3` varchar(200) NOT NULL, + `text_loc6` varchar(200) NOT NULL, + `text_loc8` varchar(200) NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK_acc_hOneliner` (`editorId`), + CONSTRAINT `FK_acc_hOneliner` FOREIGN KEY (`editorId`) REFERENCES `aowow_account` (`id`) ON UPDATE CASCADE ON DELETE SET NULL +) ENGINE=InnoDB; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/static/css/home.css b/static/css/home.css index 75b36e65..13ea397e 100644 --- a/static/css/home.css +++ b/static/css/home.css @@ -62,7 +62,7 @@ .home-oneliner { margin:0; - padding-top:53px; + padding-top:35px; color:#ccc; line-height:1.75em; font-size:12px; diff --git a/static/js/staff.js b/static/js/staff.js index 80a14bd0..7970b5c1 100644 --- a/static/js/staff.js +++ b/static/js/staff.js @@ -11,9 +11,9 @@ var mn_content = [ [, 'Homepage'], [13, 'Featured Box', '?admin=home-featuredbox', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU, breadcrumb: 'Homepage Featured Box'}], -// [14, 'Oneliners', '?admin=home-oneliners', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU, breadcrumb: 'Homepage Oneliners'}], + [14, 'Oneliners', '?admin=home-oneliners', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU, breadcrumb: 'Homepage Oneliners'}], // [15, 'Skins', '?admin=home-skins', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_SALESAGENT, breadcrumb: 'Homepage Skins'}], -// [16, 'Titles', '?admin=home-titles', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU, breadcrumb: 'Homepage Titles'}], + [16, 'Titles', '?admin=home-titles', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU, breadcrumb: 'Homepage Titles'}], // [, 'Articles'], // [8, 'List', '?admin=articles', null, {requiredAccess: U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV | U_GROUP_EDITOR | U_GROUP_LOCALIZER, breadcrumb: 'List of Articles'}], diff --git a/template/pages/home.tpl.php b/template/pages/home.tpl.php index 201b829b..9c7ad978 100644 --- a/template/pages/home.tpl.php +++ b/template/pages/home.tpl.php @@ -4,7 +4,7 @@ brick('head'); ?> - +

      Aowow

      @@ -20,10 +20,20 @@
      -news): ?> -
      +oneliner): ?> +

      -
      + +news): ?> +
      +news): +?> +
      news['overlays']): ?> @@ -53,14 +63,14 @@ brick('pageTemplate'); ?> - + From 44e6e2ed389876fe3e580ccd44847a84f4cf630a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 6 Aug 2015 20:37:50 +0200 Subject: [PATCH 0065/1249] Setup/Update * using --update now reads scripts that need to be executed from ?_dbversion * if the script fails it will be kept in ?_dbversion until it was successfully run via --update --- setup/setup.php | 21 ++++++++++++++++++--- setup/tools/clisetup/build.func.php | 13 +++++++++---- setup/tools/clisetup/sql.func.php | 10 +++++++--- setup/tools/clisetup/update.func.php | 7 +++++-- setup/tools/fileGen.class.php | 16 ++++++++++------ setup/tools/sqlGen.class.php | 16 ++++++++++------ 6 files changed, 59 insertions(+), 24 deletions(-) diff --git a/setup/setup.php b/setup/setup.php index 477cf97b..eb72fc75 100644 --- a/setup/setup.php +++ b/setup/setup.php @@ -58,6 +58,8 @@ else CLISetup::init(); $cmd = array_pop(array_keys($opt)); +$s = []; +$b = []; switch ($cmd) // we accept only one main parameter { case 'firstrun': @@ -77,13 +79,26 @@ switch ($cmd) // we accept only on finish(); case 'update': require_once 'setup/tools/clisetup/update.func.php'; - if (update()) // return true if we do not rebuild stuff + list($s, $b) = update(); // return true if we do not rebuild stuff + if (!$s && !$b) return; case 'sync': require_once 'setup/tools/clisetup/sql.func.php'; require_once 'setup/tools/clisetup/build.func.php'; - sql(); - build(); + $_s = sql($s); + $_b = build($b); + + if ($s) + { + $_ = array_diff($s, $_s); + DB::Aowow()->query('UPDATE ?_dbversion SET `sql` = ?', $_ ? implode(' ', $_) : ''); + } + + if ($b) + { + $_ = array_diff($b, $_b); + DB::Aowow()->query('UPDATE ?_dbversion SET `build` = ?', $_ ? implode(' ', $_) : ''); + } finish(); } diff --git a/setup/tools/clisetup/build.func.php b/setup/tools/clisetup/build.func.php index 4db68163..2160d590 100644 --- a/setup/tools/clisetup/build.func.php +++ b/setup/tools/clisetup/build.func.php @@ -11,12 +11,13 @@ if (!CLI) /* Create required files */ /*************************/ -function build() +function build($syncMe = null) { require_once 'setup/tools/fileGen.class.php'; - FileGen::init(); + FileGen::init($syncMe !== null ? FileGen::MODE_UPDATE : FileGen::MODE_NORMAL, $syncMe); + $done = []; if (FileGen::$subScripts) { $allOk = true; @@ -46,9 +47,10 @@ function build() $syncIds = []; // todo: fetch what exactly must be regenerated $ok = FileGen::generate($name, $syncIds); - if (!$ok) $allOk = false; + else + $done[] = $name; CLISetup::log(' - subscript \''.$file.'\' returned '.($ok ? 'sucessfully' : 'with errors'), $ok ? CLISetup::LOG_OK : CLISetup::LOG_ERROR); set_time_limit(FileGen::$defaultExecTime); // reset to default for the next script @@ -63,9 +65,10 @@ function build() $syncIds = []; // todo: fetch what exactly must be regenerated $ok = FileGen::generate($file, $syncIds); - if (!$ok) $allOk = false; + else + $done[] = $file; CLISetup::log(' - subscript \''.$file.'\' returned '.($ok ? 'sucessfully' : 'with errors'), $ok ? CLISetup::LOG_OK : CLISetup::LOG_ERROR); set_time_limit(FileGen::$defaultExecTime); // reset to default for the next script @@ -80,6 +83,8 @@ function build() } else CLISetup::log('no valid script names supplied', CLISetup::LOG_ERROR); + + return $done; } ?> diff --git a/setup/tools/clisetup/sql.func.php b/setup/tools/clisetup/sql.func.php index ad30dfcf..7fc254d6 100644 --- a/setup/tools/clisetup/sql.func.php +++ b/setup/tools/clisetup/sql.func.php @@ -11,12 +11,13 @@ if (!CLI) /* Create content from world tables / dbc files */ /************************************************/ -function sql($syncMe = []) +function sql($syncMe = null) { require_once 'setup/tools/sqlGen.class.php'; - SqlGen::init(); + SqlGen::init($syncMe !== null ? SqlGen::MODE_UPDATE : SqlGen::MODE_NORMAL, $syncMe); + $done = []; if (SqlGen::$subScripts) { $allOk = true; @@ -30,9 +31,10 @@ function sql($syncMe = []) $syncIds = []; // todo: fetch what exactly must be regenerated $ok = SqlGen::generate($tbl, $syncIds); - if (!$ok) $allOk = false; + else + $done[] = $tbl; CLISetup::log(' - subscript \''.$tbl.'\' returned '.($ok ? 'sucessfully' : 'with errors'), $ok ? CLISetup::LOG_OK : CLISetup::LOG_ERROR); set_time_limit(SqlGen::$defaultExecTime); // reset to default for the next script @@ -47,6 +49,8 @@ function sql($syncMe = []) } else CLISetup::log('no valid script names supplied', CLISetup::LOG_ERROR); + + return $done; } ?> diff --git a/setup/tools/clisetup/update.func.php b/setup/tools/clisetup/update.func.php index 053639fa..f2db5cd6 100644 --- a/setup/tools/clisetup/update.func.php +++ b/setup/tools/clisetup/update.func.php @@ -13,7 +13,7 @@ if (!CLI) function update() { - list($date, $part) = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ?_dbversion')); + list($date, $part, $sql, $build) = array_values(DB::Aowow()->selectRow('SELECT `date`, `part`, `sql`, `build` FROM ?_dbversion')); CLISetup::log('checking sql updates'); @@ -56,7 +56,10 @@ function update() CLISetup::log($nFiles ? 'applied '.$nFiles.' update(s)' : 'db is already up to date', CLISetup::LOG_OK); - return true; + $sql = trim($sql) ? explode(' ', trim($sql)) : []; + $build = trim($build) ? explode(' ', trim($build)) : []; + + return [$sql, $build]; } ?> diff --git a/setup/tools/fileGen.class.php b/setup/tools/fileGen.class.php index 103870a4..01a80814 100644 --- a/setup/tools/fileGen.class.php +++ b/setup/tools/fileGen.class.php @@ -10,6 +10,10 @@ if (!CLI) class FileGen { + const MODE_NORMAL = 0; + const MODE_FIRSTRUN = 1; + const MODE_UPDATE = 2; + public static $tplPath = 'setup/tools/filegen/templates/'; public static $cliOpts = []; @@ -65,14 +69,14 @@ class FileGen 'STATIC_URL' => STATIC_URL ); - public static function init($firstrun = false) + public static function init($mode = self::MODE_NORMAL, array $updScripts = []) { self::$defaultExecTime = ini_get('max_execution_time'); - $doScripts = []; + $doScripts = null; - if (getopt(self::$shortOpts, self::$longOpts) || $firstrun) + if (getopt(self::$shortOpts, self::$longOpts) || $mode == self::MODE_FIRSTRUN) self::handleCLIOpts($doScripts); - else + else if ($mode != self::MODE_UPDATE) { self::printCLIHelp(); exit; @@ -80,8 +84,8 @@ class FileGen // check passed subscript names; limit to real scriptNames self::$subScripts = array_merge(array_keys(self::$tplFiles), array_keys(self::$datasets)); - if ($doScripts) - self::$subScripts = array_intersect($doScripts, self::$subScripts); + if ($doScripts || $updScripts) + self::$subScripts = array_intersect($doScripts ?: $updScripts, self::$subScripts); else if ($doScripts === null) self::$subScripts = []; diff --git a/setup/tools/sqlGen.class.php b/setup/tools/sqlGen.class.php index 13131121..49e2a5d7 100644 --- a/setup/tools/sqlGen.class.php +++ b/setup/tools/sqlGen.class.php @@ -21,6 +21,10 @@ if (!CLI) class SqlGen { + const MODE_NORMAL = 0; + const MODE_FIRSTRUN = 1; + const MODE_UPDATE = 2; + private static $tables = array( // [dbcName, saveDbc, AowowDeps, TCDeps] 'achievementcategory' => ['achievement_category', false, null, null], 'achievementcriteria' => ['achievement_criteria', false, null, null], @@ -77,14 +81,14 @@ class SqlGen public static $defaultExecTime = 30; public static $stepSize = 1000; - public static function init($firstrun = false) + public static function init($mode = self::MODE_NORMAL, array $updScripts = []) { self::$defaultExecTime = ini_get('max_execution_time'); - $doScripts = []; + $doScripts = null; - if (getopt(self::$shortOpts, self::$longOpts) || $firstrun) + if (getopt(self::$shortOpts, self::$longOpts) || $mode == self::MODE_FIRSTRUN) self::handleCLIOpts($doScripts); - else + else if ($mode != self::MODE_UPDATE) { self::printCLIHelp(); exit; @@ -92,8 +96,8 @@ class SqlGen // check passed subscript names; limit to real scriptNames self::$subScripts = array_keys(self::$tables); - if ($doScripts) - self::$subScripts = array_intersect($doScripts, self::$subScripts); + if ($doScripts || $updScripts) + self::$subScripts = array_intersect($doScripts ?: $updScripts, self::$subScripts); else if ($doScripts === null) self::$subScripts = []; From 63756b23d98b34208294045f5513267e9e88253d Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 6 Aug 2015 21:07:28 +0200 Subject: [PATCH 0066/1249] Spell/Tooltips * only trim floats, when in topLevel while parsing tooltips * there is still an issue, where html-code gets returned from lower levels, screwing the eval() should probably not bulk-handle all spellVars/formulas/variables on top level, but one after another --- includes/types/spell.class.php | 22 +++++++++++----------- localization/lang.class.php | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/includes/types/spell.class.php b/includes/types/spell.class.php index a172c597..50102609 100644 --- a/includes/types/spell.class.php +++ b/includes/types/spell.class.php @@ -789,7 +789,7 @@ class SpellList extends BaseType // since this function may be called recursively, there are cases, where the already evaluated string is tried to be evaled again, throwing parse errors // todo (med): also quit, if we replaced vars with non-interactive text - if (strstr($formula, '')) + if (strstr($formula, '') || strstr($formula, ''.$value.$js, Lang::item('statType', $type)); } diff --git a/setup/tools/sqlgen/itemset.func.php b/setup/tools/sqlgen/itemset.func.php index 7fbcc104..2df19dbd 100644 --- a/setup/tools/sqlgen/itemset.func.php +++ b/setup/tools/sqlgen/itemset.func.php @@ -26,7 +26,6 @@ $reqDBC = ['itemset']; function itemset() { - $locales = [LOCALE_EN, LOCALE_FR, LOCALE_DE, LOCALE_ES, LOCALE_RU]; $setToHoliday = array ( 761 => 141, // Winterveil 762 => 372, // Brewfest @@ -285,7 +284,7 @@ function itemset() /* get name & description */ /**************************/ - foreach ($locales as $loc) + foreach (array_keys(array_filter(Util::$localeStrings)) as $loc) { User::useLocale($loc); diff --git a/template/bricks/redButtons.tpl.php b/template/bricks/redButtons.tpl.php index 6cada52b..2f15b592 100644 --- a/template/bricks/redButtons.tpl.php +++ b/template/bricks/redButtons.tpl.php @@ -2,7 +2,7 @@ // link to wowhead if (isset($this->redButtons[BUTTON_WOWHEAD])): if ($this->redButtons[BUTTON_WOWHEAD]): - echo 'WowheadWowhead'; + echo 'WowheadWowhead'; else: echo 'WowheadWowhead'; endif; From 432223264e34452543c0053403dd5669efd3a043 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Jan 2016 21:57:38 +0100 Subject: [PATCH 0111/1249] Items/Block * fixed formating of xml-export * fixed handling of block value (can have multiple sources per item (itemMods, spell, set individually) and is then displayed separately in tooltip, but summed for calculations) --- includes/types/item.class.php | 8 ++++---- pages/item.php | 26 +++++++++++++------------- setup/tools/sqlgen/item_stats.func.php | 4 ++++ setup/updates/1452718627_01.sql | 1 + 4 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 setup/updates/1452718627_01.sql diff --git a/includes/types/item.class.php b/includes/types/item.class.php index cf2b97c5..f037810b 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -23,7 +23,7 @@ class ItemList extends BaseType private $vendors = []; private $jsGlobals = []; // getExtendedCost creates some and has no access to template - protected $queryBase = 'SELECT i.*, i.id AS ARRAY_KEY, i.id AS id FROM ?_items i'; + protected $queryBase = 'SELECT i.*, i.block AS tplBlock, i.id AS ARRAY_KEY, i.id AS id FROM ?_items i'; protected $queryOpts = array( // 3 => TYPE_ITEM 'i' => [['is', 'src', 'ic'], 'o' => 'i.quality DESC, i.itemLevel DESC'], 'ic' => ['j' => ['?_icons `ic` ON `ic`.`id` = -`i`.`displayId`', true], 's' => ', ic.iconString'], @@ -642,9 +642,9 @@ class ItemList extends BaseType else if (($this->curTpl['armor'] + $this->curTpl['armorDamageModifier']) > 0) $x .= ''.sprintf(Lang::item('armor'), intVal($this->curTpl['armor'] + $this->curTpl['armorDamageModifier'])).'
      '; - // Block - if ($this->curTpl['block']) - $x .= ''.sprintf(Lang::item('block'), $this->curTpl['block']).'
      '; + // Block (note: block value from field block and from field stats or parsed from itemSpells are displayed independently) + if ($this->curTpl['tplBlock']) + $x .= ''.sprintf(Lang::item('block'), $this->curTpl['tplBlock']).'
      '; // Item is a gem (don't mix with sockets) if ($geId = $this->curTpl['gemEnchantmentId']) diff --git a/pages/item.php b/pages/item.php index 9369bb18..b6c5223c 100644 --- a/pages/item.php +++ b/pages/item.php @@ -1054,8 +1054,8 @@ class ItemPage extends genericPage $this->subject->extendJsonStats(); // json - $fields = ["classs", "displayid", "dps", "id", "level", "name", "reqlevel", "slot", "slotbak", "source", "sourcemore", "speed", "subclass"]; - $json = ''; + $fields = ['classs', 'displayid', 'dps', 'id', 'level', 'name', 'reqlevel', 'slot', 'slotbak', 'source', 'sourcemore', 'speed', 'subclass']; + $json = []; foreach ($fields as $f) { if (isset($this->subject->json[$this->subject->id][$f])) @@ -1064,33 +1064,33 @@ class ItemPage extends genericPage if ($f == 'name') $_ = (7 - $this->subject->getField('quality')).$_; - $json .= ',"'.$f.'":'.$_; + $json[$f] = $_; } } - $xml->addChild('json')->addCData(substr($json, 1)); + $xml->addChild('json')->addCData(substr(json_encode($json), 1, -1)); // jsonEquip missing: avgbuyout, cooldown, source, sourcemore - $json = ''; + $json = []; if ($_ = $this->subject->getField('sellPrice')) // sellprice - $json .= ',"sellprice":'.$_; + $json['sellprice'] = $_; if ($_ = $this->subject->getField('requiredLevel')) // reqlevel - $json .= ',"reqlevel":'.$_; + $json['reqlevel'] = $_; if ($_ = $this->subject->getField('requiredSkill')) // reqskill - $json .= ',"reqskill":'.$_; + $json['reqskill'] = $_; if ($_ = $this->subject->getField('requiredSkillRank')) // reqskillrank - $json .= ',"reqskillrank":'.$_; + $json['reqskillrank'] = $_; foreach ($this->subject->itemMods[$this->subject->id] as $mod => $qty) - $json .= ',"'.$mod.'":'.$qty; + $json[$mod] = $qty; - foreach ($_ = $this->subject->json[$this->subject->id] as $name => $qty) + foreach ($this->subject->json[$this->subject->id] as $name => $qty) if (in_array($name, Util::$itemFilter)) - $json .= ',"'.$name.'":'.$qty; + $json[$name] = $qty; - $xml->addChild('jsonEquip')->addCData(substr($json, 1)); + $xml->addChild('jsonEquip')->addCData(substr(json_encode($json), 1, -1)); // jsonUse if ($onUse = $this->subject->getOnUseStats()) diff --git a/setup/tools/sqlgen/item_stats.func.php b/setup/tools/sqlgen/item_stats.func.php index 3aebe1ff..31ad557a 100644 --- a/setup/tools/sqlgen/item_stats.func.php +++ b/setup/tools/sqlgen/item_stats.func.php @@ -49,6 +49,10 @@ class ItemStatSetup extends ItemList { $this->itemMods[$this->id] = []; + // also occurs as seperate field (gets summed in calculation but not in tooltip) + if ($_ = $this->getField('block')) + $this->itemMods[$this->id][ITEM_MOD_BLOCK_VALUE] = $_; + // convert itemMods to stats for ($h = 1; $h <= 10; $h++) { diff --git a/setup/updates/1452718627_01.sql b/setup/updates/1452718627_01.sql new file mode 100644 index 00000000..547fb1e1 --- /dev/null +++ b/setup/updates/1452718627_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(`sql`, ' item_stats'); From c3fe4b0224859fbdc8d1109e93dbefb1c401a2b5 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 16 Jan 2016 10:34:57 +0100 Subject: [PATCH 0112/1249] Setup/Update * get files/tables, which need to be regenerated after! applying sql updates * small wording change, to make exiting dbConfig feel less like an error occured --- setup/tools/clisetup/dbconfig.func.php | 2 +- setup/tools/clisetup/update.func.php | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/setup/tools/clisetup/dbconfig.func.php b/setup/tools/clisetup/dbconfig.func.php index 9227e30a..c6e4c679 100644 --- a/setup/tools/clisetup/dbconfig.func.php +++ b/setup/tools/clisetup/dbconfig.func.php @@ -146,7 +146,7 @@ function dbconfig() else { CLISetup::log(); - CLISetup::log("db setup aborted", CLISetup::LOG_INFO); + CLISetup::log("leaving db setup...", CLISetup::LOG_INFO); break 2; } } diff --git a/setup/tools/clisetup/update.func.php b/setup/tools/clisetup/update.func.php index f2db5cd6..a67e7031 100644 --- a/setup/tools/clisetup/update.func.php +++ b/setup/tools/clisetup/update.func.php @@ -13,7 +13,7 @@ if (!CLI) function update() { - list($date, $part, $sql, $build) = array_values(DB::Aowow()->selectRow('SELECT `date`, `part`, `sql`, `build` FROM ?_dbversion')); + list($date, $part) = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ?_dbversion')); CLISetup::log('checking sql updates'); @@ -56,8 +56,19 @@ function update() CLISetup::log($nFiles ? 'applied '.$nFiles.' update(s)' : 'db is already up to date', CLISetup::LOG_OK); - $sql = trim($sql) ? explode(' ', trim($sql)) : []; - $build = trim($build) ? explode(' ', trim($build)) : []; + // fetch sql/build after applying updates, as they may contain sync-prompts + list($sql, $build) = array_values(DB::Aowow()->selectRow('SELECT `sql`, `build` FROM ?_dbversion')); + + sleep(1); + + $sql = array_unique(explode(' ', trim($sql))); + $build = array_unique(explode(' ', trim($build))); + + if ($sql) + CLISetup::log('The following table(s) require syncing: '.implode(', ', $sql)); + + if ($build) + CLISetup::log('The following file(s) require syncing: '.implode(', ', $build)); return [$sql, $build]; } From eddb034a5c6611cd795fc7c93980d610018eaec6 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 23 Jan 2016 11:28:15 +0100 Subject: [PATCH 0113/1249] User/Passwords * allow for passwords longer than 15 characters --- includes/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/user.class.php b/includes/user.class.php index ac82e491..418e5dbf 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -382,7 +382,7 @@ class User { $errCode = 0; - if (strlen($pass) < 6 || strlen($pass) > 16) + if (mb_strlen($pass) < 6) $errCode = 1; // else if (preg_match('/[^\w\d!"#\$%]/', $pass)) // such things exist..? :o // $errCode = 2; From 278176a48eb45a6921bce60c5f0c4c6955cba6fa Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 23 Jan 2016 11:46:39 +0100 Subject: [PATCH 0114/1249] Setup/DBCs * directly extract integers as signed (may or may not help with a reported case of extracted nonsensical values) --- setup/tools/dbc.class.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/setup/tools/dbc.class.php b/setup/tools/dbc.class.php index 09db7886..5e997b39 100644 --- a/setup/tools/dbc.class.php +++ b/setup/tools/dbc.class.php @@ -426,13 +426,15 @@ class DBC return false; } + // l - signed long (always 32 bit, machine byte order) + // V - unsigned long (always 32 bit, little endian byte order) $unpackStr = ''; $unpackFmt = array( 'x' => 'x/x/x/x', 'X' => 'x', 's' => 'V', 'f' => 'f', - 'i' => 'V', // maybe use 'l' [signed long; 32bit; machine dependent byte order] + 'i' => 'l', // not sure if 'l' or 'V' should be used here 'u' => 'V', 'b' => 'C', 'd' => 'x4', @@ -504,18 +506,12 @@ class DBC $row[] = &$strings[$val]; continue 2; - case 'i': - if ($rec['f'.$j] & 0x80000000) // i suspect this will not work on 32bit machines - $row[] = $rec['f'.$j] - 0x100000000; - else - $row[] = $rec['f'.$j]; - break; case 'f': $row[] = round($rec['f'.$j], 8); break; case 'n': // DO NOT BREAK! $idx = $rec['f'.$j]; - default: // nothing special .. 'u' and the likes + default: // nothing special .. 'i', 'u' and the likes $row[] = $rec['f'.$j]; } } From bd2a5acf2121c3151f2ab40eeae055c79fc627fb Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sat, 23 Jan 2016 21:17:50 +0100 Subject: [PATCH 0115/1249] Template/JS * removed some readability formating, that caused an arbitrary text-node to appear in between DIVs, screwing with javascript --- template/pages/achievements.tpl.php | 3 +-- template/pages/enchantments.tpl.php | 3 +-- template/pages/items.tpl.php | 3 +-- template/pages/itemsets.tpl.php | 3 +-- template/pages/npcs.tpl.php | 3 +-- template/pages/objects.tpl.php | 3 +-- template/pages/quests.tpl.php | 3 +-- template/pages/spells.tpl.php | 3 +-- 8 files changed, 8 insertions(+), 16 deletions(-) diff --git a/template/pages/achievements.tpl.php b/template/pages/achievements.tpl.php index 8f0f5688..32fa30d5 100644 --- a/template/pages/achievements.tpl.php +++ b/template/pages/achievements.tpl.php @@ -45,8 +45,7 @@ endforeach;
      '.Lang::item('inventoryType', $_slot).''.Lang::item('inventoryType', $_slot).' activateCondition; ?>
      '; + echo ''; if (!empty($cr['link'])): echo ''.Util::htmlEscape($cr['link']['text']).''; @@ -51,11 +51,7 @@ foreach ($this->criteria['data'] as $i => $cr): echo ' '.$cr['extraText']; endif; - if (User::isInGroup(U_GROUP_STAFF)): - echo ' ['.$cr['id'].']'; - endif; - - echo '
      -
      -
      +
      diff --git a/template/pages/enchantments.tpl.php b/template/pages/enchantments.tpl.php index 735adb3d..4b016e80 100644 --- a/template/pages/enchantments.tpl.php +++ b/template/pages/enchantments.tpl.php @@ -40,8 +40,7 @@ endforeach;
      -
      -
      +
      /> /> diff --git a/template/pages/items.tpl.php b/template/pages/items.tpl.php index 311475cf..95d70be8 100644 --- a/template/pages/items.tpl.php +++ b/template/pages/items.tpl.php @@ -104,8 +104,7 @@ endforeach; -
      -
      +
      diff --git a/template/pages/itemsets.tpl.php b/template/pages/itemsets.tpl.php index 59ef0d34..57e46876 100644 --- a/template/pages/itemsets.tpl.php +++ b/template/pages/itemsets.tpl.php @@ -86,8 +86,7 @@ endforeach; -
      -
      +
      diff --git a/template/pages/npcs.tpl.php b/template/pages/npcs.tpl.php index 69d908bc..bc4d632d 100644 --- a/template/pages/npcs.tpl.php +++ b/template/pages/npcs.tpl.php @@ -78,8 +78,7 @@ endforeach; -
      -
      +
      diff --git a/template/pages/objects.tpl.php b/template/pages/objects.tpl.php index 5e17d303..ce5b2a45 100644 --- a/template/pages/objects.tpl.php +++ b/template/pages/objects.tpl.php @@ -19,8 +19,7 @@ $this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f  /> -
      -
      +
      diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index 053a8659..a4ee2f10 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -60,8 +60,7 @@ endforeach; -
      -
      +
      diff --git a/template/pages/spells.tpl.php b/template/pages/spells.tpl.php index 1360c72d..df671fd9 100644 --- a/template/pages/spells.tpl.php +++ b/template/pages/spells.tpl.php @@ -126,8 +126,7 @@ endforeach; -
      -
      +
      From 2bc85dd10922267a12c63f287d5ccfcc0e4e602e Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Tue, 26 Jan 2016 23:12:41 +0100 Subject: [PATCH 0116/1249] Acc/Passwords * do not enforce minimum password length for imported accounts --- includes/user.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/user.class.php b/includes/user.class.php index 418e5dbf..dbbed9a0 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -382,7 +382,8 @@ class User { $errCode = 0; - if (mb_strlen($pass) < 6) + // only enforce for own passwords + if (mb_strlen($pass) < 6 && CFG_ACC_AUTH_MODE == AUTH_MODE_SELF) $errCode = 1; // else if (preg_match('/[^\w\d!"#\$%]/', $pass)) // such things exist..? :o // $errCode = 2; From 5abdbe2080ee00474c426db6fffdff37139a5c48 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 31 Jan 2016 15:36:54 +0100 Subject: [PATCH 0117/1249] Misc/Fixes * fixed stats when comparing items with scaling random enchantments * fixed BOM on compare.php * fixed multiple typos and notices --- includes/types/enchantment.class.php | 66 +++++++++++++++++++++++++++- includes/types/item.class.php | 2 +- pages/compare.php | 2 +- pages/search.php | 5 +++ pages/utility.php | 6 ++- setup/updates/1439924313_01.sql | 2 +- template/bricks/series.tpl.php | 3 +- template/pages/enchantments.tpl.php | 2 +- 8 files changed, 80 insertions(+), 8 deletions(-) diff --git a/includes/types/enchantment.class.php b/includes/types/enchantment.class.php index a23b728f..5219b0b3 100644 --- a/includes/types/enchantment.class.php +++ b/includes/types/enchantment.class.php @@ -123,7 +123,7 @@ class EnchantmentList extends BaseType return $data; } - public function getStatGain() + public function getStatGain($addScalingKeys = false) { $data = []; @@ -134,6 +134,70 @@ class EnchantmentList extends BaseType if (isset($this->curTpl['dps'])) $data['dps'] = $this->curTpl['dps']; + // scaling enchantments are saved as 0 to item_stats, thus return empty + if ($addScalingKeys) + { + $spellStats = []; + if ($this->relSpells) + $spellStats = $this->relSpells->getStatGain(); + + for ($h = 1; $h <= 3; $h++) + { + $obj = (int)$this->curTpl['object'.$h]; + + switch ($this->curTpl['type'.$h]) + { + case 3: // TYPE_EQUIP_SPELL Spells from ObjectX (use of amountX?) + if (!empty($spellStats[$obj])) + foreach ($spellStats[$obj] as $mod => $_) + if ($str = Util::$itemMods[$mod]) + Util::arraySumByKey($data, [$str => 0]); + + $obj = null; + break; + case 4: // TYPE_RESISTANCE +AmountX resistance for ObjectX School + switch ($obj) + { + case 0: // Physical + $obj = ITEM_MOD_ARMOR; + break; + case 1: // Holy + $obj = ITEM_MOD_HOLY_RESISTANCE; + break; + case 2: // Fire + $obj = ITEM_MOD_FIRE_RESISTANCE; + break; + case 3: // Nature + $obj = ITEM_MOD_NATURE_RESISTANCE; + break; + case 4: // Frost + $obj = ITEM_MOD_FROST_RESISTANCE; + break; + case 5: // Shadow + $obj = ITEM_MOD_SHADOW_RESISTANCE; + break; + case 6: // Arcane + $obj = ITEM_MOD_ARCANE_RESISTANCE; + break; + default: + $obj = null; + } + break; + case 5: // TYPE_STAT +AmountX for Statistic by type of ObjectX + if ($obj < 2) // [mana, health] are on [0, 1] respectively and are expected on [1, 2] .. + $obj++; // 0 is weaponDmg .. ehh .. i messed up somewhere + + break; // stats are directly assigned below + default: // TYPE_NONE dnd stuff; skip assignment below + $obj = null; + } + + if ($obj !== null) + if ($str = Util::$itemMods[$obj]) // check if we use these mods + Util::arraySumByKey($data, [$str => 0]); + } + } + return $data; } diff --git a/includes/types/item.class.php b/includes/types/item.class.php index f037810b..f95e947e 100644 --- a/includes/types/item.class.php +++ b/includes/types/item.class.php @@ -1444,7 +1444,7 @@ class ItemList extends BaseType { $this->rndEnchIds[$eId] = array( 'text' => $enchants->getField('name', true), - 'stats' => $enchants->getStatGain() + 'stats' => $enchants->getStatGain(true) ); } diff --git a/pages/compare.php b/pages/compare.php index 92a2a573..cdd5c6fa 100644 --- a/pages/compare.php +++ b/pages/compare.php @@ -1,4 +1,4 @@ -iterate() as $itemId => $__) + if (!empty($data[$itemId]['subitems'])) + foreach ($data[$itemId]['subitems'] as &$si) + $si['enchantment'] = implode(', ', $si['enchantment']); + $result = array( 'type' => TYPE_ITEM, 'appendix' => ' (Item)', diff --git a/pages/utility.php b/pages/utility.php index db09c4ff..8106762c 100644 --- a/pages/utility.php +++ b/pages/utility.php @@ -29,7 +29,9 @@ class UtilityPage extends GenericPage $this->page = $pageCall; $this->rss = isset($_GET['rss']); - $this->name = Lang::main('utilities', array_search($pageCall, $this->validPages)); + + if ($this->page != 'random') + $this->name = Lang::main('utilities', array_search($pageCall, $this->validPages)); if ($this->page == 'most-comments') { @@ -316,7 +318,7 @@ class UtilityPage extends GenericPage array_unshift($this->title, Lang::main('mostComments', 0)); } - array_unshift($this->title, Lang::main('utilities', array_search($this->page, $this->validPages))); + array_unshift($this->title, $this->name); } protected function generatePath() diff --git a/setup/updates/1439924313_01.sql b/setup/updates/1439924313_01.sql index 1450aa97..87ac9aa3 100644 --- a/setup/updates/1439924313_01.sql +++ b/setup/updates/1439924313_01.sql @@ -1,2 +1,2 @@ -UPDATE aowow_dbversion SET `sql`= CONCAT(`sql, ' spawns'); +UPDATE aowow_dbversion SET `sql`= CONCAT(`sql`, ' spawns'); diff --git a/template/bricks/series.tpl.php b/template/bricks/series.tpl.php index 2f04a9b3..fc626bfa 100644 --- a/template/bricks/series.tpl.php +++ b/template/bricks/series.tpl.php @@ -6,7 +6,8 @@ foreach ($list as $idx => $itr): echo ' '.($idx + 1).'
      '; - $end = array_pop(array_keys($itr)); + $_ = array_keys($itr); + $end = array_pop($_); foreach ($itr as $k => $i): // itemItr switch ($i['side']): case 1: $wrap = '%s'; break; diff --git a/template/pages/enchantments.tpl.php b/template/pages/enchantments.tpl.php index 4b016e80..66dc55ed 100644 --- a/template/pages/enchantments.tpl.php +++ b/template/pages/enchantments.tpl.php @@ -10,7 +10,7 @@ $f = $this->filter; // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 1]]); +$this->brick('pageTemplate', ['fi' => empty($f['query']) ? null : ['query' => $f['query'], 'menuItem' => 101]]); ?>
      From f076a30180a1f47e256774fd595c70b0e27fc0cb Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 31 Jan 2016 15:54:33 +0100 Subject: [PATCH 0118/1249] AoWoW is now compatible with php7 (php7 is recommended but not required) --- includes/libs/DbSimple/Mysqli.php | 4 ++-- pages/achievement.php | 4 ++-- pages/currency.php | 6 +++--- pages/event.php | 6 +++--- pages/item.php | 4 ++-- pages/itemset.php | 6 +++--- pages/npc.php | 4 ++-- pages/object.php | 4 ++-- pages/profile.php | 4 ++-- pages/quest.php | 4 ++-- pages/search.php | 2 +- pages/spell.php | 4 ++-- setup/tools/clisetup/firstrun.func.php | 14 +++++++------- setup/tools/fileGen.class.php | 2 +- 14 files changed, 34 insertions(+), 34 deletions(-) diff --git a/includes/libs/DbSimple/Mysqli.php b/includes/libs/DbSimple/Mysqli.php index 2329e607..6b67dd14 100644 --- a/includes/libs/DbSimple/Mysqli.php +++ b/includes/libs/DbSimple/Mysqli.php @@ -30,7 +30,7 @@ class DbSimple_Mysqli extends DbSimple_Database * constructor(string $dsn) * Connect to MySQL server. */ - function DbSimple_Mysqli($dsn) + function __construct($dsn) { if (!is_callable("mysqli_connect")) @@ -178,7 +178,7 @@ class DbSimple_Mysqli extends DbSimple_Database protected function _performFetch($result) { $row = mysqli_fetch_assoc($result); - if (mysql_error()) return $this->_setDbError($this->_lastQuery); + if (mysqli_error($this->link)) return $this->_setDbError($this->_lastQuery); if ($row === false) return null; return $row; } diff --git a/pages/achievement.php b/pages/achievement.php index 6d870f89..26a98e33 100644 --- a/pages/achievement.php +++ b/pages/achievement.php @@ -466,10 +466,10 @@ class AchievementPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('achievement'), Lang::achievement('notFound')); + return parent::notFound($title ?: Lang::game('achievement'), $msg ?: Lang::achievement('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/currency.php b/pages/currency.php index 1b23f449..2e4510f8 100644 --- a/pages/currency.php +++ b/pages/currency.php @@ -29,7 +29,7 @@ class CurrencyPage extends GenericPage $this->subject = new CurrencyList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('currency'), Lang::currency('notFound')); + $this->notFound(); $this->name = $this->subject->getField('name', true); } @@ -259,10 +259,10 @@ class CurrencyPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('currency'), Lang::currency('notFound')); + return parent::notFound($title ?: Lang::game('currency'), $msg ?: Lang::currency('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/event.php b/pages/event.php index a904abfc..5a6b570d 100644 --- a/pages/event.php +++ b/pages/event.php @@ -32,7 +32,7 @@ class EventPage extends GenericPage $this->subject = new WorldEventList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('event'), Lang::event('notFound')); + $this->notFound(); $this->hId = $this->subject->getField('holidayId'); $this->eId = $this->typeId; @@ -358,10 +358,10 @@ class EventPage extends GenericPage die(sprintf($tt, $start, $end)); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('event'), Lang::event('notFound')); + return parent::notFound($title ?: Lang::game('event'), $msg ?: Lang::event('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/item.php b/pages/item.php index b6c5223c..1466dd4d 100644 --- a/pages/item.php +++ b/pages/item.php @@ -1182,7 +1182,7 @@ class ItemPage extends genericPage return parent::display($override); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode == CACHE_TYPE_TOOLTIP) { @@ -1197,7 +1197,7 @@ class ItemPage extends genericPage exit(); } else - return parent::notFound(Lang::game('item'), Lang::item('notFound')); + return parent::notFound($title ?: Lang::game('item'), $msg ?: Lang::item('notFound')); } } diff --git a/pages/itemset.php b/pages/itemset.php index 52e19699..ea046c61 100644 --- a/pages/itemset.php +++ b/pages/itemset.php @@ -33,7 +33,7 @@ class ItemsetPage extends GenericPage $this->subject = new ItemsetList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('itemset'), Lang::itemset('notFound')); + $this->notFound(); $this->name = $this->subject->getField('name', true); $this->extendGlobalData($this->subject->getJSGlobals()); @@ -260,10 +260,10 @@ class ItemsetPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('itemset'), Lang::itemset('notFound')); + return parent::notFound($title ?: Lang::game('itemset'), $msg ?: Lang::itemset('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/npc.php b/pages/npc.php index 17d34173..48283a4f 100644 --- a/pages/npc.php +++ b/pages/npc.php @@ -817,10 +817,10 @@ class NpcPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('npc'), Lang::npc('notFound')); + return parent::notFound($title ?: Lang::game('npc'), $msg ?: Lang::npc('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/object.php b/pages/object.php index 1a51f2fd..e93e42eb 100644 --- a/pages/object.php +++ b/pages/object.php @@ -498,10 +498,10 @@ class ObjectPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('object'), Lang::gameObject('notFound')); + return parent::notFound($title ?: Lang::game('object'), $msg ?: Lang::gameObject('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/profile.php b/pages/profile.php index c348f4ac..b702a716 100644 --- a/pages/profile.php +++ b/pages/profile.php @@ -122,10 +122,10 @@ class ProfilePage extends GenericPage die($this->generateTooltip()); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Util::ucFirst(Lang::game('profile')), '[NNF]profile or char doesn\'t exist'); + return parent::notFound($title ?: Util::ucFirst(Lang::game('profile')), $msg ?: '[NNF]profile or char doesn\'t exist'); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/quest.php b/pages/quest.php index f152cb04..844bd00a 100644 --- a/pages/quest.php +++ b/pages/quest.php @@ -696,10 +696,10 @@ class QuestPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('quest'), Lang::quest('notFound')); + return parent::notFound($title ?: Lang::game('quest'), $msg ?: Lang::quest('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/pages/search.php b/pages/search.php index 408d2fb8..1615cbf4 100644 --- a/pages/search.php +++ b/pages/search.php @@ -196,7 +196,7 @@ class SearchPage extends GenericPage $this->performSearch(); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->searchMask & SEARCH_TYPE_REGULAR) { diff --git a/pages/spell.php b/pages/spell.php index af7c182e..c638ba6c 100644 --- a/pages/spell.php +++ b/pages/spell.php @@ -1211,10 +1211,10 @@ class SpellPage extends GenericPage die($tt); } - public function notFound() + public function notFound($title = '', $msg = '') { if ($this->mode != CACHE_TYPE_TOOLTIP) - return parent::notFound(Lang::game('spell'), Lang::spell('notFound')); + return parent::notFound($title ?: Lang::game('spell'), $msg ?: Lang::spell('notFound')); header('Content-type: application/x-javascript; charset=utf-8'); echo $this->generateTooltip(true); diff --git a/setup/tools/clisetup/firstrun.func.php b/setup/tools/clisetup/firstrun.func.php index 5478219a..853cdb6f 100644 --- a/setup/tools/clisetup/firstrun.func.php +++ b/setup/tools/clisetup/firstrun.func.php @@ -110,7 +110,7 @@ function firstrun() fclose($h); }; - $testDB = function(&$error) + function testDB(&$error) { require 'config/config.php'; @@ -133,9 +133,9 @@ function firstrun() } return empty($error); - }; + } - $testSelf = function(&$error) + function testSelf(&$error) { $error = []; $test = function($url, &$rCode) @@ -171,13 +171,13 @@ function firstrun() $error[] = ' * STATIC_HOST is empty'; return empty($error); - }; + } - $testAcc = function(&$error) + function testAcc(&$error) { $error = []; return !!DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE userPerms = 1'); - }; + } function endSetup() { @@ -243,7 +243,7 @@ function firstrun() // check script result if ($step[2]) { - if (!$$step[2]($errors)) + if (!$step[2]($errors)) { CLISetup::log($step[4], CLISetup::LOG_ERROR); foreach ($errors as $e) diff --git a/setup/tools/fileGen.class.php b/setup/tools/fileGen.class.php index a10a5abd..f9042d0b 100644 --- a/setup/tools/fileGen.class.php +++ b/setup/tools/fileGen.class.php @@ -210,7 +210,7 @@ class FileGen // check for required auxiliary DBC files foreach ($reqDBC as $req) if (!CLISetup::loadDBC($req)) - continue 2; + continue; // must generate content // PH format: /*setup:*/ From 02205876377d3b44560ac1e3124b25da16fcdcc7 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Sun, 31 Jan 2016 18:58:15 +0100 Subject: [PATCH 0119/1249] Template/Home * update footer to current year * display current revision --- includes/shared.php | 2 +- template/bricks/header.tpl.php | 8 ++++---- template/bricks/headerMenu.tpl.php | 3 +-- template/pages/home.tpl.php | 7 ++++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/shared.php b/includes/shared.php index f71f7d8f..1f4c8eb1 100644 --- a/includes/shared.php +++ b/includes/shared.php @@ -1,6 +1,6 @@ brick('head'); ?> -
      brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/npcs.tpl.php b/template/pages/npcs.tpl.php index cb732ed6..1ba1cd19 100644 --- a/template/pages/npcs.tpl.php +++ b/template/pages/npcs.tpl.php @@ -1,10 +1,11 @@ - - brick('header'); -$f = $this->filterObj->values // shorthand -?> + namespace Aowow\Template; + use \Aowow\Lang; + +$this->brick('header'); +$f = $this->filter->values; // shorthand +?>
      @@ -12,67 +13,62 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [4]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); ?> - -
      +
      +
      +brick('headIcons'); + +$this->brick('redButtons'); +?> +

      h1; ?>

      +
      -
      +
      petFamPanel): ?>
      -
      +
      - + - + '; + + if (!$rows) + continue; + + $this->lvTabs->addDataTab(Profiler::urlize($catName), $catName, '
      ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
       />  /> />  />
       /> - /> /> - /> - - +
               - > - - - + + +
      @@ -84,7 +80,7 @@ endforeach;
      - /> /> + /> />
      @@ -98,7 +94,7 @@ endforeach;
      -brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From 253cbcb4d982f3f9b4b3a6d99262e3a5251ce5be Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 15:36:25 +0200 Subject: [PATCH 0965/1249] Template/Update (Part 30) * convert dbtype 'object' --- {pages => endpoints/object}/object.php | 343 ++++++++++++------------- endpoints/object/object_power.php | 50 ++++ endpoints/objects/objects.php | 109 ++++++++ localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- localization/locale_zhcn.php | 2 +- pages/objects.php | 90 ------- template/pages/object.tpl.php | 30 +-- template/pages/objects.tpl.php | 30 ++- 12 files changed, 366 insertions(+), 298 deletions(-) rename {pages => endpoints/object}/object.php (54%) create mode 100644 endpoints/object/object_power.php create mode 100644 endpoints/objects/objects.php delete mode 100644 pages/objects.php diff --git a/pages/object.php b/endpoints/object/object.php similarity index 54% rename from pages/object.php rename to endpoints/object/object.php index 762a47aa..203fa6a0 100644 --- a/pages/object.php +++ b/endpoints/object/object.php @@ -6,57 +6,62 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 5: Object g_initPath() -// tabId 0: Database g_initHeader() -class ObjectPage extends GenericPage +class ObjectBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $pageText = []; - protected $relBoss = null; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $type = Type::OBJECT; - protected $typeId = 0; - protected $tpl = 'object'; - protected $path = [0, 5]; - protected $tabId = 0; - protected $mode = CACHE_TYPE_PAGE; - protected $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; + protected string $template = 'object'; + protected string $pageName = 'object'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 5]; - protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; + protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - private $powerTpl = '$WowheadPower.registerObject(%d, %d, %s);'; + public int $type = Type::OBJECT; + public int $typeId = 0; + public ?Book $book = null; + public ?array $relBoss = null; - public function __construct($pageCall, $id) + private GameObjectList $subject; + + public function __construct(string $id) { - parent::__construct($pageCall, $id); + parent::__construct($id); - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - $this->typeId = intVal($id); + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + protected function generate() : void + { $this->subject = new GameObjectList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('object'), Lang::gameObject('notFound')); + $this->generateNotFound(Lang::game('object'), Lang::gameObject('notFound')); - $this->name = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML); - } + $this->h1 = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML); - protected function generatePath() - { - $this->path[] = $this->subject->getField('typeCat'); - } + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('typeCat'); + + + /**************/ + /* Page Title */ + /**************/ - protected function generateTitle() - { array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('object'))); - } - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); /***********/ /* Infobox */ @@ -65,48 +70,48 @@ class ObjectPage extends GenericPage $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_gameobject geg, gameobject g WHERE ge.eventEntry = geg.eventEntry AND g.guid = geg.guid AND g.id = ?d', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.`eventEntry` = geg.`eventEntry` AND g.`guid` = geg.`guid` AND g.`id` = ?d', $this->typeId)) { $this->extendGlobalIds(Type::WORLDEVENT, ...$_); $ev = []; foreach ($_ as $i => $e) $ev[] = ($i % 2 ? '[br]' : ' ') . '[event='.$e.']'; - $infobox[] = Util::ucFirst(Lang::game('eventShort')).Lang::main('colon').implode(',', $ev); + $infobox[] = Lang::game('eventShort', [implode(',', $ev)]); } // Faction - if ($_ = DB::Aowow()->selectCell('SELECT factionId FROM ?_factiontemplate WHERE id = ?d', $this->subject->getField('faction'))) + if ($_ = DB::Aowow()->selectCell('SELECT `factionId` FROM ?_factiontemplate WHERE `id` = ?d', $this->subject->getField('faction'))) { $this->extendGlobalIds(Type::FACTION, $_); $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$_.']'; } // Reaction - $_ = function ($r) + $color = fn (int $r) : string => match($r) { - if ($r == 1) return 2; // q2 green - if ($r == -1) return 10; // q10 red - return; // q yellow + 1 => 'q2', // q2 green + -1 => 'q10', // q10 red + default => 'q' // q yellow }; - $infobox[] = Lang::npc('react').Lang::main('colon').'[color=q'.$_($this->subject->getField('A')).']A[/color] [color=q'.$_($this->subject->getField('H')).']H[/color]'; + $infobox[] = Lang::npc('react', ['[color='.$color($this->subject->getField('A')).']A[/color] [color='.$color($this->subject->getField('H')).']H[/color]']); // reqSkill + difficulty switch ($this->subject->getField('typeCat')) { - case -3: // Herbalism - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 2).' ('.$this->subject->getField('reqSkill').')'); + case -3: // Herbalism + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 2).' ('.$this->subject->getField('reqSkill').')']); $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_HERBALISM, $this->subject->getField('reqSkill'))); break; - case -4: // Mining - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 3).' ('.$this->subject->getField('reqSkill').')'); + case -4: // Mining + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 3).' ('.$this->subject->getField('reqSkill').')']); $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_MINING, $this->subject->getField('reqSkill'))); break; - case -5: // Lockpicking - $infobox[] = sprintf(Lang::game('requires'), Lang::spell('lockType', 1).' ('.$this->subject->getField('reqSkill').')'); + case -5: // Lockpicking + $infobox[] = Lang::game('requires', [Lang::spell('lockType', 1).' ('.$this->subject->getField('reqSkill').')']); $infobox[] = Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_LOCKPICKING, $this->subject->getField('reqSkill'))); break; - default: // requires key .. maybe + default: // requires key .. maybe { $locks = Lang::getLocks($this->subject->getField('lockId'), $ids, true, Lang::FMT_MARKUP); $l = []; @@ -119,7 +124,7 @@ class ObjectPage extends GenericPage if ($idx > 0) $l[] = Lang::gameObject('key').Lang::main('colon').$str; else if ($idx < 0) - $l[] = sprintf(Lang::game('requires'), $str); + $l[] = Lang::game('requires', [$str]); } if ($l) @@ -138,114 +143,113 @@ class ObjectPage extends GenericPage // SpellFocus if ($_ = $this->subject->getField('spellFocusId')) - if ($sfo = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE id = ?d', $_)) + if ($sfo = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE `id` = ?d', $_)) $infobox[] = '[tooltip name=focus]'.Lang::gameObject('focusDesc').'[/tooltip][span class=tip tooltip=focus]'.Lang::gameObject('focus').Lang::main('colon').Util::localizedString($sfo, 'name').'[/span]'; // lootinfo: [min, max, restock] - if (($_ = $this->subject->getField('lootStack')) && $_[0]) + if ($this->subject->getField('lootStack')) { - $buff = Lang::spell('spellModOp', 4).Lang::main('colon').$_[0]; - if ($_[0] < $_[1]) - $buff .= Lang::game('valueDelim').$_[1]; + [$min, $max, $restock] = $this->subject->getField('lootStack'); + $buff = Lang::spell('spellModOp', 4).Lang::main('colon').$min; + if ($min < $max) + $buff .= Lang::game('valueDelim').$max; // since Veins don't have charges anymore, the timer is questionable - $infobox[] = $_[2] > 1 ? '[tooltip name=restock]'.sprintf(Lang::gameObject('restock'), Util::formatTime($_[2] * 1000)).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; + $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [Util::formatTime($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; } // meeting stone [minLevel, maxLevel, zone] - if ($this->subject->getField('type') == OBJECT_MEETINGSTONE) + if ($this->subject->getField('type') == OBJECT_MEETINGSTONE && $this->subject->getField('mStone')) { - if ($_ = $this->subject->getField('mStone')) - { - $this->extendGlobalIds(Type::ZONE, $_[2]); - $m = Lang::game('meetingStone').Lang::main('colon').'[zone='.$_[2].']'; + [$minLevel, $maxLevel, $zone] = $this->subject->getField('mStone'); - $l = $_[0]; - if ($_[0] > 1 && $_[1] > $_[0]) - $l .= Lang::game('valueDelim').min($_[1], MAX_LEVEL); + $this->extendGlobalIds(Type::ZONE, $zone); + $m = Lang::game('meetingStone').'[zone='.$zone.']'; - $infobox[] = $l ? '[tooltip name=meetingstone]'.sprintf(Lang::game('reqLevel'), $l).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; - } + $l = $minLevel; + if ($minLevel > 1 && $maxLevel > $minLevel) + $l .= Lang::game('valueDelim').min($maxLevel, MAX_LEVEL); + + $infobox[] = $l ? '[tooltip name=meetingstone]'.Lang::game('reqLevel', [$l]).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; } - // capture area [minPlayer, maxPlayer, minTime, maxTime, radius] - if ($this->subject->getField('type') == OBJECT_CAPTURE_POINT) + // capture area + if ($this->subject->getField('type') == OBJECT_CAPTURE_POINT && $this->subject->getField('capture')) { - if ($_ = $this->subject->getField('capture')) - { - $buff = Lang::gameObject('capturePoint'); + [$minPlayer, $maxPlayer, $minTime, $maxTime, $radius] = $this->subject->getField('capture'); - if ($_[2] > 1 || $_[0]) - $buff .= Lang::main('colon').'[ul]'; + $buff = Lang::gameObject('capturePoint'); - if ($_[2] > 1) - $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($_[3] > $_[2] ? Util::FormatTime($_[3] * 1000, true).' - ' : null).Util::FormatTime($_[2] * 1000, true).'[/li]'; + if ($minTime > 1 || $minPlayer || $radius) + $buff .= Lang::main('colon').'[ul]'; - if ($_[1]) - $buff .= '[li]'.Lang::main('players').Lang::main('colon').$_[0].($_[1] > $_[0] ? ' - '.$_[1] : null).'[/li]'; + if ($minTime > 1) + $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($maxTime > $minTime ? Util::FormatTime($maxTime * 1000, true).' - ' : '').Util::FormatTime($minTime * 1000, true).'[/li]'; - if ($_[4]) - $buff .= '[li]'.sprintf(Lang::spell('range'), $_[4]).'[/li]'; + if ($minPlayer) + $buff .= '[li]'.Lang::main('players').Lang::main('colon').$minPlayer.($maxPlayer > $minPlayer ? ' - '.$maxPlayer : '').'[/li]'; - if ($_[2] > 1 || $_[0]) - $buff .= '[/ul]'; - } + if ($radius) + $buff .= '[li]'.Lang::spell('range', [$radius]).'[/li]'; + + if ($minTime > 1 || $minPlayer || $radius) + $buff .= '[/ul]'; $infobox[] = $buff; } // AI if (User::isInGroup(U_GROUP_EMPLOYEE)) - { if ($_ = $this->subject->getField('ScriptOrAI')) - { - if ($_ == 'SmartGameObjectAI') - $infobox[] = 'AI'.Lang::main('colon').$_; - else - $infobox[] = 'Script'.Lang::main('colon').$_; - } - } + $infobox[] = ($_ == 'SmartGameObjectAI' ? 'AI' : 'Script').Lang::main('colon').$_; + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); /****************/ /* Main Content */ /****************/ - // pageText - if ($this->pageText = Game::getBook($this->subject->getField('pageTextId'))) + // pageText / book + if ($this->book = Game::getBook($this->subject->getField('pageTextId'))) $this->addScript( [SC_JS_FILE, 'js/Book.js'], [SC_CSS_FILE, 'css/Book.css'] ); // get spawns and path - $map = null; if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns, 'foundIn' => Lang::gameObject('foundIn')]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::gameObject('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); } // todo (low): consider pooled spawns - $relBoss = null; if ($ll = DB::Aowow()->selectRow('SELECT * FROM ?_loot_link WHERE `objectId` = ?d ORDER BY `priority` DESC LIMIT 1', $this->typeId)) { // group encounter if ($ll['encounterId']) - $relBoss = [$ll['npcId'], Lang::profiler('encounterNames', $ll['encounterId'])]; + $this->relBoss = [$ll['npcId'], Lang::profiler('encounterNames', $ll['encounterId'])]; // difficulty dummy else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_creature WHERE `difficultyEntry1` = ?d OR `difficultyEntry2` = ?d OR `difficultyEntry3` = ?d', $ll['npcId'], $ll['npcId'], $ll['npcId'])) - $relBoss = [$c['id'], Util::localizedString($c, 'name')]; + $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; // base creature else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_creature WHERE `id` = ?d', $ll['npcId'])) - $relBoss = [$c['id'], Util::localizedString($c, 'name')]; + $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; } - // smart AI + // Smart AI $sai = null; if ($this->subject->getField('ScriptOrAI') == 'SmartGameObjectAI') { @@ -263,15 +267,14 @@ class ObjectPage extends GenericPage } if ($sai->prepare()) + { $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } else trigger_error('Gameobject has AIName set in template but no SmartAI defined.'); } - $this->map = $map; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; - $this->relBoss = $relBoss; - $this->smartAI = $sai ? $sai->getMarkup() : null; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], @@ -283,12 +286,22 @@ class ObjectPage extends GenericPage /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: summoned by + $summonEffects = array( + SPELL_EFFECT_TRANS_DOOR, + SPELL_EFFECT_SUMMON_OBJECT_WILD, + SPELL_EFFECT_SUMMON_OBJECT_SLOT1, + SPELL_EFFECT_SUMMON_OBJECT_SLOT2, + SPELL_EFFECT_SUMMON_OBJECT_SLOT3, + SPELL_EFFECT_SUMMON_OBJECT_SLOT4 + ); $conditions = array( 'OR', - ['AND', ['effect1Id', [50, 76, 104, 105, 106, 107]], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', [50, 76, 104, 105, 106, 107]], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', [50, 76, 104, 105, 106, 107]], ['effect3MiscValue', $this->typeId]] + ['AND', ['effect1Id', $summonEffects], ['effect1MiscValue', $this->typeId]], + ['AND', ['effect2Id', $summonEffects], ['effect2MiscValue', $this->typeId]], + ['AND', ['effect3Id', $summonEffects], ['effect3MiscValue', $this->typeId]] ); $summons = new SpellList($conditions); @@ -296,11 +309,11 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($summons->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($summons->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $summons->getListviewData(), 'id' => 'summoned-by', 'name' => '$LANG.tab_summonedby' - )]; + ), SpellList::$brickFile)); } // tab: related spells @@ -315,13 +328,13 @@ class ObjectPage extends GenericPage foreach ($data as $relId => $d) $data[$relId]['trigger'] = array_search($relId, $_); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($data), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $data, 'id' => 'spells', 'name' => '$LANG.tab_spells', 'hiddenCols' => ['skill'], 'extraCols' => ["\$Listview.funcBox.createSimpleCol('trigger', 'Condition', '10%', 'trigger')"] - )]; + ), SpellList::$brickFile)); } } @@ -331,11 +344,11 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($acvs->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $acvs->getListviewData(), 'id' => 'criteria-of', 'name' => '$LANG.tab_criteriaof' - )]; + ), AchievementList::$brickFile)); } // tab: starts quest @@ -345,30 +358,29 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($startEnd->getJSGlobals()); $lvData = $startEnd->getListviewData(); - $_ = [[], []]; + $start = $end = []; foreach ($startEnd->iterate() as $id => $__) { - $m = $startEnd->getField('method'); - if ($m & 0x1) - $_[0][] = $lvData[$id]; - if ($m & 0x2) - $_[1][] = $lvData[$id]; + if ($startEnd->getField('method') & 0x1) + $start[] = $lvData[$id]; + if ($startEnd->getField('method') & 0x2) + $end[] = $lvData[$id]; } - if ($_[0]) - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($_[0]), + if ($start) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $start, 'name' => '$LANG.tab_starts', 'id' => 'starts' - )]; + ), QuestList::$brickFile)); - if ($_[1]) - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($_[1]), + if ($end) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $end, 'name' => '$LANG.tab_ends', 'id' => 'ends' - )]; + ), QuestList::$brickFile)); } // tab: related quests @@ -379,11 +391,11 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($relQuest->getJSGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($relQuest->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $relQuest->getListviewData(), 'name' => '$LANG.tab_quests', 'id' => 'quests' - )]; + ), QuestList::$brickFile)); } } @@ -402,24 +414,20 @@ class ObjectPage extends GenericPage foreach ($hiddenCols as $k => $str) { - if ($k == 1 && array_filter(array_column($lootResult, $str), function ($x) { return $x != SIDE_BOTH; })) + if ($k == 1 && array_filter(array_column($lootResult, $str), fn ($x) => $x != SIDE_BOTH)) unset($hiddenCols[$k]); - else if ($k != 1 && array_column($lootResult, $str)) + else if ($k != 1 && !array_filter(array_column($lootResult, $str))) unset($hiddenCols[$k]); } - $tabData = array( - 'data' => array_values($lootResult), - 'id' => 'contains', - 'name' => '$LANG.tab_contains', - 'sort' => ['-percent', 'name'], - 'extraCols' => array_unique($extraCols) - ); - - if ($hiddenCols) - $tabData['hiddenCols'] = array_values($hiddenCols); - - $this->lvTabs[] = [ItemList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lootResult, + 'id' => 'contains', + 'name' => '$LANG.tab_contains', + 'sort' => ['-percent', 'name'], + 'extraCols' => array_unique($extraCols), + 'hiddenCols' => $hiddenCols ?: null + ), ItemList::$brickFile)); } } @@ -430,7 +438,7 @@ class ObjectPage extends GenericPage if (!$focusSpells->error) { $tabData = array( - 'data' => array_values($focusSpells->getListviewData()), + 'data' => $focusSpells->getListviewData(), 'name' => Lang::gameObject('focus'), 'id' => 'focus-for' ); @@ -440,11 +448,11 @@ class ObjectPage extends GenericPage // create note if search limit was exceeded if ($focusSpells->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) { - $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); $tabData['_truncated'] = 1; } - $this->lvTabs[] = [SpellList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); } } @@ -454,25 +462,27 @@ class ObjectPage extends GenericPage { $this->extendGlobalData($trigger->getJSGlobals()); - $this->lvTabs[] = [GameObjectList::$brickFile, array( - 'data' => array_values($trigger->getListviewData()), + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $trigger->getListviewData(), 'name' => Lang::gameObject('triggeredBy'), 'id' => 'triggerd-by', 'note' => sprintf(Util::$filterResultString, '?objects=6') - )]; + ), GameObjectList::$brickFile)); } - // tab: Same model as .. whats the fucking point..? + // tab: Same model as $sameModel = new GameObjectList(array(['displayId', $this->subject->getField('displayId')], ['id', $this->typeId, '!'])); if (!$sameModel->error) { $this->extendGlobalData($sameModel->getJSGlobals()); - $this->lvTabs[] = [GameObjectList::$brickFile, array( - 'data' => array_values($sameModel->getListviewData()), + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sameModel->getListviewData(), 'name' => '$LANG.tab_samemodelas', 'id' => 'same-model-as' - )]; + ), GameObjectList::$brickFile)); } // tab: condition-for @@ -481,21 +491,10 @@ class ObjectPage extends GenericPage if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) { $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - } - - protected function generateTooltip() - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW); - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - $power->map = $this->subject->getSpawns(SPAWNINFO_SHORT); + $this->lvTabs->addDataTab(...$tab); } - return sprintf($this->powerTpl, $this->typeId, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); + parent::generate(); } } diff --git a/endpoints/object/object_power.php b/endpoints/object/object_power.php new file mode 100644 index 00000000..bfd73a60 --- /dev/null +++ b/endpoints/object/object_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $object = new GameObjectList(array(['id', $this->typeId])); + if ($object->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => Lang::unescapeUISequences($object->getField('name', true), Lang::FMT_RAW), + 'tooltip' => $object->renderTooltip(), + 'map' => $object->getSpawns(SPAWNINFO_SHORT) + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/objects/objects.php b/endpoints/objects/objects.php new file mode 100644 index 00000000..e08e5782 --- /dev/null +++ b/endpoints/objects/objects.php @@ -0,0 +1,109 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [-2, -3, -4, -5, -6, 0, 3, 6, 9, 25]; + + public bool $petFamPanel = false; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new GameObjectListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('objects')); + + $conditions = []; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->filter->evalCriteria(); + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + if ($this->category) + $conditions[] = ['typeCat', (int)$this->category[0]]; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::gameObject('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $tabData = ['data' => []]; + $objects = new GameObjectList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + if (!$objects->error) + { + $tabData['data'] = $objects->getListviewData(); + if ($objects->hasSetFields('reqSkill')) + $tabData['visibleCols'] = ['skill']; + + // create note if search limit was exceeded + if ($objects->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/localization/locale_dede.php b/localization/locale_dede.php index e37a8832..2cd40810 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Stufe", 'mechanic' => "Auswirkung", 'mechAbbr' => "Ausw.: ", - 'meetingStone' => "Versammlungsstein", + 'meetingStone' => "Versammlungsstein: ", 'requires' => "Benötigt %s", 'requires2' => "Benötigt", 'reqLevel' => "Benötigt Stufe %s", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 6670ad1b..ba489eef 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Level", 'mechanic' => "Mechanic", 'mechAbbr' => "Mech.: ", - 'meetingStone' => "Meeting Stone", + 'meetingStone' => "Meeting Stone: ", 'requires' => "Requires %s", 'requires2' => "Requires", 'reqLevel' => "Requires Level %s", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 65e22870..84353d5f 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Nivel", 'mechanic' => "Mecanica", 'mechAbbr' => "Mec.: ", - 'meetingStone' => "Roca de encuentro", + 'meetingStone' => "Roca de encuentro: ", 'requires' => "Requiere %s", 'requires2' => "Requiere", 'reqLevel' => "Necesitas ser de nivel %s", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index d0ad5a56..a1f7d19a 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Niveau", 'mechanic' => "Mécanique", 'mechAbbr' => "Mécan. : ", - 'meetingStone' => "Pierre de rencontre", + 'meetingStone' => "Pierre de rencontre : ", 'requires' => "%s requis", 'requires2' => "Requiert", 'reqLevel' => "Niveau %s requis", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 6034134e..a16699f7 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -339,7 +339,7 @@ $lang = array( 'level' => "Уровень", 'mechanic' => "Механика", 'mechAbbr' => "Механика: ", - 'meetingStone' => "Камень вÑтреч", + 'meetingStone' => "Камень вÑтреч: ", 'requires' => "Требует %s", 'requires2' => "ТребуетÑÑ:", 'reqLevel' => "ТребуетÑÑ ÑƒÑ€Ð¾Ð²ÐµÐ½ÑŒ: %s", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 876b8c90..2098281e 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -338,7 +338,7 @@ $lang = array( 'level' => "等级", 'mechanic' => "机制", 'mechAbbr' => "机制:", - 'meetingStone' => "集åˆçŸ³", + 'meetingStone' => "集åˆçŸ³ï¼š", 'requires' => "需è¦%s", 'requires2' => "需è¦", 'reqLevel' => "需è¦ç­‰çº§%s", diff --git a/pages/objects.php b/pages/objects.php deleted file mode 100644 index 5d516ea9..00000000 --- a/pages/objects.php +++ /dev/null @@ -1,90 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new GameObjectListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); - - $this->name = Util::ucFirst(Lang::game('objects')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); - - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - $conditions[] = ['typeCat', (int)$this->category[0]]; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = ['data' => []]; - $objects = new GameObjectList($conditions, ['extraOpts' => $this->filterObj->extraOpts, 'calcTotal' => true]); - if (!$objects->error) - { - $tabData['data'] = array_values($objects->getListviewData()); - if ($objects->hasSetFields('reqSkill')) - $tabData['visibleCols'] = ['skill']; - - // create note if search limit was exceeded - if ($objects->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - - $this->lvTabs[] = [GameObjectList::$brickFile, $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::gameObject('cat', $this->category[0])); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/template/pages/object.tpl.php b/template/pages/object.tpl.php index 5bd28b69..b3e076e5 100644 --- a/template/pages/object.tpl.php +++ b/template/pages/object.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
      @@ -17,17 +20,17 @@
      brick('redButtons'); ?> -

      name; ?>

      +

      h1; ?>

      brick('article'); + $this->brick('markup', ['markup' => $this->article]); if ($this->relBoss): - echo "
      ".sprintf(Lang::gameObject('npcLootPH'), $this->name, $this->relBoss[0], $this->relBoss[1])."
      \n"; + echo "
      ".sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1])."
      \n"; echo '
      '; endif; -if (!empty($this->map)): +if ($this->map): $this->brick('mapper'); else: echo Lang::gameObject('unkPosition'); @@ -35,26 +38,15 @@ endif; $this->brick('book'); -if (isset($this->smartAI)): -?> -
      - +$this->brick('markup', ['markup' => $this->smartAI]); -
      -

      brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/objects.tpl.php b/template/pages/objects.tpl.php index ffc6f356..d5055534 100644 --- a/template/pages/objects.tpl.php +++ b/template/pages/objects.tpl.php @@ -1,10 +1,11 @@ - - brick('header'); -$f = $this->filterObj->values // shorthand -?> + namespace Aowow\Template; + use \Aowow\Lang; + +$this->brick('header'); +$f = $this->filter->values; // shorthand +?>
      @@ -12,20 +13,27 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [5]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); ?> - -
      +
      +
      +brick('headIcons'); + +$this->brick('redButtons'); +?> +

      h1; ?>

      +
      - +
       />
      ucFirst(Lang::main('name')).Lang::main('colon'); ?> />
      - /> /> + /> />
      @@ -39,7 +47,7 @@ $this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem'
      -brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From e876463f3be24fcf06ffe6d45cebfea3234b687a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 17:02:52 +0200 Subject: [PATCH 0966/1249] Template/Update (Part 31) * convert dbtype 'quest' * make use of separate GlobalStrings for spell rewards --- {pages => endpoints/quest}/quest.php | 840 ++++++++++++++------------- endpoints/quest/quest_power.php | 50 ++ endpoints/quests/quests.php | 142 +++++ includes/dbtypes/quest.class.php | 2 +- includes/game/misc.php | 2 +- localization/lang.class.php | 2 +- localization/locale_dede.php | 37 +- localization/locale_enus.php | 39 +- localization/locale_eses.php | 37 +- localization/locale_frfr.php | 37 +- localization/locale_ruru.php | 41 +- localization/locale_zhcn.php | 37 +- pages/quests.php | 125 ---- pages/zone.php | 2 +- setup/tools/filegen/profiler.ss.php | 2 +- template/pages/quest.tpl.php | 168 +++--- template/pages/quests.tpl.php | 50 +- 17 files changed, 834 insertions(+), 779 deletions(-) rename {pages => endpoints/quest}/quest.php (65%) create mode 100644 endpoints/quest/quest_power.php create mode 100644 endpoints/quests/quests.php delete mode 100644 pages/quests.php diff --git a/pages/quest.php b/endpoints/quest/quest.php similarity index 65% rename from pages/quest.php rename to endpoints/quest/quest.php index dfc37210..305900d6 100644 --- a/pages/quest.php +++ b/endpoints/quest/quest.php @@ -6,85 +6,88 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 3: Quest g_initPath() -// tabId 0: Database g_initHeader() -class QuestPage extends GenericPage +class QuestBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $objectiveList = []; - protected $providedItem = []; - protected $series = []; - protected $gains = []; - protected $mail = []; - protected $rewards = []; - protected $objectives = ''; - protected $details = ''; - protected $offerReward = ''; - protected $requestItems = ''; - protected $completed = ''; - protected $end = ''; - protected $suggestedPl = 1; - protected $unavailable = false; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $type = Type::QUEST; - protected $typeId = 0; - protected $tpl = 'quest'; - protected $path = [0, 3]; - protected $tabId = 0; - protected $mode = CACHE_TYPE_PAGE; - protected $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js'], [SC_CSS_FILE, 'css/Book.css']]; + protected string $template = 'quest'; + protected string $pageName = 'quest'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 3]; - protected $_get = ['domain' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; + protected array $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js']]; - private $powerTpl = '$WowheadPower.registerQuest(%d, %d, %s);'; + public int $type = Type::QUEST; + public int $typeId = 0; + public array $objectiveList = []; + public ?IconElement $providedItem = null; + public array $mail = []; + public ?array $gains = null; // why array|null ? because destructuring an array with less elements than expected is an error, destructuring null just returns false + public ?array $rewards = null; // so " if ([$spells, $items, $choice, $money] = $this->rewards): " will either work or cleanly branch to else + public string $objectives = ''; + public string $details = ''; + public string $offerReward = ''; + public string $requestItems = ''; + public string $completed = ''; + public string $end = ''; + public int $suggestedPl = 1; + public bool $unavailable = false; - public function __construct($pageCall, $id) + private QuestList $subject; + + public function __construct(string $id) { - parent::__construct($pageCall, $id); + parent::__construct($id); - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - $this->typeId = intVal($id); + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + protected function generate() : void + { $this->subject = new QuestList(array(['id', $this->typeId])); if ($this->subject->error) - $this->notFound(Lang::game('quest'), Lang::quest('notFound')); + $this->generateNotFound(Lang::game('quest'), Lang::quest('notFound')); - // may contain htmlesque tags - $this->name = Lang::unescapeUISequences(Util::htmlEscape($this->subject->getField('name', true)), Lang::FMT_HTML); - } + $this->h1 = Lang::unescapeUISequences(Util::htmlEscape($this->subject->getField('name', true)), Lang::FMT_HTML); - protected function generatePath() - { - // recreate path - $this->path[] = $this->subject->getField('cat2'); - if ($cat = $this->subject->getField('cat1')) - { - foreach (Game::$questSubCats as $parent => $children) - if (in_array($cat, $children)) - $this->path[] = $parent; + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_HTML) + ); - $this->path[] = $cat; - } - } - - protected function generateTitle() - { - // page title already escaped - array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('quest'))); - } - - protected function generateContent() - { $_level = $this->subject->getField('level'); $_minLevel = $this->subject->getField('minLevel'); $_flags = $this->subject->getField('flags'); $_specialFlags = $this->subject->getField('specialFlags'); $_side = ChrRace::sideFromMask($this->subject->getField('reqRaceMask')); + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('cat2'); + if ($cat = $this->subject->getField('cat1')) + { + foreach (Game::$questSubCats as $parent => $children) + if (in_array($cat, $children)) + $this->breadcrumb[] = $parent; + + $this->breadcrumb[] = $cat; + } + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('quest'))); + + /***********/ /* Infobox */ /***********/ @@ -95,7 +98,7 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('eventId')) { $this->extendGlobalIds(Type::WORLDEVENT, $_); - $infobox[] = Lang::game('eventShort').Lang::main('colon').'[event='.$_.']'; + $infobox[] = Lang::game('eventShort', ['[event='.$_.']']); } // level @@ -109,7 +112,7 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('maxLevel')) $lvl .= ' - '.$_; - $infobox[] = sprintf(Lang::game('reqLevel'), $lvl); + $infobox[] = Lang::game('reqLevel', [$lvl]); } // loremaster (i dearly hope those flags cover every case...) @@ -128,10 +131,10 @@ class QuestPage extends GenericPage case 0: break; case 1: - $infobox[] = Lang::quest('loremaster').Lang::main('colon').'[achievement='.$loremaster->id.']'; + $infobox[] = Lang::quest('loremaster').'[achievement='.$loremaster->id.']'; break; default: - $lm = Lang::quest('loremaster').Lang::main('colon').'[ul]'; + $lm = Lang::quest('loremaster').'[ul]'; foreach ($loremaster->iterate() as $id => $__) $lm .= '[li][achievement='.$id.'][/li]'; @@ -153,16 +156,15 @@ class QuestPage extends GenericPage $_[] = Lang::quest('questInfo', $t); if ($_) - $infobox[] = Lang::game('type').Lang::main('colon').implode(' ', $_); + $infobox[] = Lang::game('type').implode(' ', $_); // side - $_ = Lang::main('side').Lang::main('colon'); - switch ($_side) + $infobox[] = Lang::main('side') . match ($this->subject->getField('faction')) { - case 3: $infobox[] = $_.Lang::game('si', 3); break; - case 2: $infobox[] = $_.'[span class=icon-horde]'.Lang::game('si', 2).'[/span]'; break; - case 1: $infobox[] = $_.'[span class=icon-alliance]'.Lang::game('si', 1).'[/span]'; break; - } + SIDE_ALLIANCE => '[span class=icon-alliance]'.Lang::game('si', SIDE_ALLIANCE).'[/span]', + SIDE_HORDE => '[span class=icon-horde]'.Lang::game('si', SIDE_HORDE).'[/span]', + default => Lang::game('si', SIDE_BOTH) // 0, 3 + }; $jsg = []; // races @@ -189,17 +191,17 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('reqSkillPoints')) $sk .= ' ('.$_.')'; - $infobox[] = Lang::quest('profession').Lang::main('colon').$sk; + $infobox[] = Lang::quest('profession').$sk; } // timer if ($_ = $this->subject->getField('timeLimit')) - $infobox[] = Lang::quest('timer').Lang::main('colon').Util::formatTime($_ * 1000); + $infobox[] = Lang::quest('timer').Util::formatTime($_ * 1000); - $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE questId = ?d', $this->typeId); + $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE `questId` = ?d', $this->typeId); // start - $start = '[icon name=quest_start'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('start').Lang::main('colon').'[/icon]'; + $start = '[icon name=quest_start'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('start').'[/icon]'; $s = []; foreach ($startEnd as $se) { @@ -214,7 +216,7 @@ class QuestPage extends GenericPage $infobox[] = implode('[br]', $s); // end - $end = '[icon name=quest_end'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('end').Lang::main('colon').'[/icon]'; + $end = '[icon name=quest_end'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('end').'[/icon]'; $e = []; foreach ($startEnd as $se) { @@ -266,98 +268,13 @@ class QuestPage extends GenericPage $_[] = '[color=r4]'.($_level + 3 + ceil(12 * $_level / MAX_LEVEL)).'[/color]'; if ($_) - $infobox[] = Lang::game('difficulty').Lang::main('colon').implode('[small]  [/small]', $_); + $infobox[] = Lang::game('difficulty').implode('[small]  [/small]', $_); } - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); - /**********/ - /* Series */ - /**********/ - - // Assumption - // a chain always ends in a single quest, but can have an arbitrary amount of quests leading into it. - // so we fast forward to the last quest and go backwards from there. - - $lastQuestId = $this->subject->getField('nextQuestIdChain'); - while ($newLast = DB::Aowow()->selectCell('SELECT `nextQuestIdChain` FROM ?_quests WHERE `id` = ?d AND `id` <> `nextQuestIdChain`', $lastQuestId)) - $lastQuestId = $newLast; - - $end = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `id` = ?d', $lastQuestId ?: $this->typeId); - $chain = array(array(array( // series / step / quest - 'side' => ChrRace::sideFromMask($end['reqRaceMask']), - 'typeStr' => Type::getFileString(Type::QUEST), - 'typeId' => $end['id'], - 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($end, 'name'), 40)), - ))); - - $prevStepIds = [$lastQuestId ?: $this->typeId]; - while ($prevQuests = DB::Aowow()->select('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `nextQuestIdChain` IN (?a) AND `id` <> `nextQuestIdChain`', $prevStepIds)) - { - $step = []; - foreach ($prevQuests as $pQuest) - $step[$pQuest['id']] = array( - 'side' => ChrRace::sideFromMask($pQuest['reqRaceMask']), - 'typeStr' => Type::getFileString(Type::QUEST), - 'typeId' => $pQuest['id'], - 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($pQuest, 'name'), 40)), - ); - - $prevStepIds = array_keys($step); - $chain[] = $step; - } - - if (count($chain) > 1) - $this->series[] = [array_reverse($chain), null]; - - - // todo (low): sensibly merge the following lists into 'series' - $listGen = function($cnd) - { - $chain = []; - $list = new QuestList($cnd); - if ($list->error) - return null; - - foreach ($list->iterate() as $id => $__) - { - $n = $list->getField('name', true); - $chain[] = array(array( - 'side' => ChrRace::sideFromMask($list->getField('reqRaceMask')), - 'typeStr' => Type::getFileString(Type::QUEST), - 'typeId' => $id, - 'name' => Util::htmlEscape(Lang::trimTextClean($n, 40)) - )); - } - - return $chain; - }; - - $extraLists = array( - // Requires all of these quests (Quests that you must follow to get this quest) - ['reqQ', array('OR', ['AND', ['nextQuestId', $this->typeId], ['exclusiveGroup', 0, '<']], ['AND', ['id', $this->subject->getField('prevQuestId')], ['nextQuestIdChain', $this->typeId, '!']])], - - // Requires one of these quests (Requires one of the quests to choose from) - ['reqOneQ', array('OR', ['AND', ['exclusiveGroup', 0, '>'], ['nextQuestId', $this->typeId]], ['breadCrumbForQuestId', $this->typeId])], - - // Opens Quests (Quests that become available only after complete this quest (optionally only one)) - ['opensQ', array('OR', ['AND', ['prevQuestId', $this->typeId], ['id', $this->subject->getField('nextQuestIdChain'), '!']], ['id', $this->subject->getField('nextQuestId')], ['id', $this->subject->getField('breadcrumbForQuestId')])], - - // Closes Quests (Quests that become inaccessible after completing this quest) - ['closesQ', array(['exclusiveGroup', 0, '>'], ['exclusiveGroup', $this->subject->getField('exclusiveGroup')], ['id', $this->typeId, '!'])], - - // During the quest available these quests (Quests that are available only at run time this quest) - ['enablesQ', array(['prevQuestId', -$this->typeId])], - - // Requires an active quest (Quests during the execution of which is available on the quest) - ['enabledByQ', array(['id', -$this->subject->getField('prevQuestId')])] - ); - - foreach ($extraLists as $el) - if ($_ = $listGen($el[1])) - $this->series[] = [$_, sprintf(Util::$dfnString, Lang::quest($el[0].'Desc'), Lang::quest($el[0]))]; - /*******************/ /* Objectives List */ /*******************/ @@ -391,31 +308,45 @@ class QuestPage extends GenericPage $providedRequired = false; foreach ($olItems as $i => [$itemId, $qty, $provided]) { - if (!$i || !$itemId || !in_array($itemId, $olItemData->getFoundIDs())) + if (!$i || !$itemId) continue; if ($provided) $providedRequired = true; - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $itemId, - 'name' => Lang::unescapeUISequences($olItemData->json[$itemId]['name'], Lang::FMT_HTML), - 'qty' => $qty > 1 ? $qty : 0, - 'quality' => 7 - $olItemData->json[$itemId]['quality'], - 'extraText' => $provided ? ' ('.Lang::quest('provided').')' : '' - ); + if (!$olItemData->getEntry($itemId)) + { + $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $qty > 1 ? $qty : '', extraText: $provided ? Lang::quest('provided') : null)]; + continue; + } + + $this->objectiveList[] = [0, new IconElement( + Type::ITEM, + $itemId, + Lang::unescapeUISequences($olItemData->json[$itemId]['name'], Lang::FMT_HTML), + num: $qty > 1 ? $qty : '', + quality: 7 - $olItemData->json[$itemId]['quality'], + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: $provided ? Lang::quest('provided') : null + )]; } // if providd item is not required by quest, list it below other requirements - if (!$providedRequired && $olItems[0][0] && in_array($olItems[0][0], $olItemData->getFoundIDs())) + if (!$providedRequired && $olItems[0][0]) { - $this->providedItem = array( - 'id' => $olItems[0][0], - 'name' => Lang::unescapeUISequences($olItemData->json[$olItems[0][0]]['name'], Lang::FMT_HTML), - 'qty' => $olItems[0][1] > 1 ? $olItems[0][1] : 0, - 'quality' => 7 - $olItemData->json[$olItems[0][0]]['quality'] - ); + if (!$olItemData->getEntry($olItems[0][0])) + $this->providedItem = new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $olItems[0][1] > 1 ? $olItems[0][1] : ''); + else + $this->providedItem = new IconElement( + Type::ITEM, + $olItems[0][0], + Lang::unescapeUISequences($olItemData->json[$olItems[0][0]]['name'], Lang::FMT_HTML), + num: $olItems[0][1] > 1 ? $olItems[0][1] : '', + quality: 7 - $olItemData->json[$olItems[0][0]]['quality'], + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon' + ); } } @@ -449,24 +380,40 @@ class QuestPage extends GenericPage $olNPCs[$p][2][$id] = $olNPCData->getField('name', true); } - foreach ($olNPCs as $i => $pair) + foreach ($olNPCs as $i => [$qty, $altText, $proxies]) { - if (!$i || !in_array($i, $olNPCData->getFoundIDs())) + if (!$i) continue; - $ol = array( - 'typeStr' => Type::getFileString(Type::NPC), - 'id' => $i, - 'name' => $pair[1] ?: Util::localizedString($olNPCData->getEntry($i), 'name'), - 'qty' => $pair[0] > 1 ? $pair[0] : 0, - 'extraText' => (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $pair[1]) ? '' : ' '.Lang::achievement('slain'), - 'proxy' => $pair[2] - ); + if ($proxies) // has proxies assigned, add yourself as another proxy + { + $proxies[$i] = Util::localizedString($olNPCData->getEntry($i), 'name'); - if ($pair[2]) // has proxies assigned, add yourself as another proxy - $ol['proxy'][$i] = Util::localizedString($olNPCData->getEntry($i), 'name'); + // split in two blocks for display + $proxies = array( + array_slice($proxies, 0, ceil(count($proxies) / 2), true), + array_slice($proxies, ceil(count($proxies) / 2), null, true) + ); - $this->objectiveList[] = $ol; + $this->objectiveList[] = [2, array( + 'id' => $i, + 'text' => ($altText ?: Util::localizedString($olNPCData->getEntry($i), 'name')) . ((($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : ' '.Lang::achievement('slain')), + 'qty' => $qty > 1 ? $qty : 0, + 'proxy' => array_filter($proxies) + )]; + } + else if (!$olNPCData->getEntry($i)) + $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('npc')).' #'.$i, $qty > 1 ? $qty : '')]; + else + $this->objectiveList[] = [0, new IconElement( + Type::NPC, + $i, + $altText ?: Util::localizedString($olNPCData->getEntry($i), 'name'), + $qty > 1 ? $qty : '', + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : Lang::achievement('slain'), + )]; } } @@ -476,18 +423,22 @@ class QuestPage extends GenericPage $olGOData = new GameObjectList(array(['id', $ids])); $this->extendGlobalData($olGOData->getJSGlobals(GLOBALINFO_SELF)); - foreach ($olGOs as $i => $pair) + foreach ($olGOs as $i => [$qty, $altText]) { - if (!$i || !in_array($i, $olGOData->getFoundIDs())) + if (!$i) continue; - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::OBJECT), - 'id' => $i, - 'name' => $pair[1] ?: Lang::unescapeUISequences(Util::localizedString($olGOData->getEntry($i), 'name'), Lang::FMT_HTML), - 'qty' => $pair[0] > 1 ? $pair[0] : 0, - 'extraText' => '' - ); + if (!$olGOData->getEntry($i)) + $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('object')).' #'.$i, $qty > 1 ? $qty : '')]; + else + $this->objectiveList[] = [0, new IconElement( + Type::OBJECT, + $i, + $altText ?: Lang::unescapeUISequences(Util::localizedString($olGOData->getEntry($i), 'name'), Lang::FMT_HTML), + $qty > 1 ? $qty : '', + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + )]; } } @@ -512,13 +463,14 @@ class QuestPage extends GenericPage if (!$i || !in_array($i, $olFactionsData->getFoundIDs())) continue; - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::FACTION), - 'id' => $i, - 'name' => Util::localizedString($olFactionsData->getEntry($i), 'name'), - 'qty' => sprintf(Util::$dfnString, $val.' '.Lang::achievement('points'), Lang::getReputationLevelForPoints($val)), - 'extraText' => '' - ); + $this->objectiveList[] = [0, new IconElement( + Type::FACTION, + $i, + Util::localizedString($olFactionsData->getEntry($i), 'name'), + size: IconElement::SIZE_SMALL, + element: 'iconlist-icon', + extraText: sprintf(Util::$dfnString, $val.' '.Lang::achievement('points'), '('.Lang::getReputationLevelForPoints($val).')') + )]; } } @@ -526,29 +478,22 @@ class QuestPage extends GenericPage if ($_ = $this->subject->getField('sourceSpellId')) { $this->extendGlobalIds(Type::SPELL, $_); - $this->objectiveList[] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $_, - 'name' => SpellList::getName($_), - 'qty' => 0, - 'extraText' => ' ('.Lang::quest('provided').')' - ); + $this->objectiveList[] = [0, new IconElement(Type::SPELL, $_, SpellList::getName($_), extraText: Lang::quest('provided'), element: 'iconlist-icon')]; } // required money if ($this->subject->getField('rewardOrReqMoney') < 0) - $this->objectiveList[] = ['text' => Lang::quest('reqMoney').Lang::main('colon').Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))]; + $this->objectiveList[] = [1, Lang::quest('reqMoney', [Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))])]; // required pvp kills if ($_ = $this->subject->getField('reqPlayerKills')) - $this->objectiveList[] = ['text' => Lang::quest('playerSlain').' ('.$_.')']; + $this->objectiveList[] = [1, Lang::quest('playerSlain', [$_])]; + /**********/ /* Mapper */ /**********/ - $this->addScript([SC_JS_FILE, '?data=zones']); - // gather points of interest $mapNPCs = $mapGOs = []; // [typeId, start|end|objective, startItemId] @@ -560,7 +505,7 @@ class QuestPage extends GenericPage { /* todo (med): sanity check: - there are loot templates that are absolute tosh, containing hundrets of random items (e.g. Peacebloom for Quest "The Horde Needs Peacebloom!") + there are loot templates that are absolute tosh, containing hundreds of random items (e.g. Peacebloom for Quest "The Horde Needs Peacebloom!") even without these .. consider quests like "A Donation of Runecloth" .. oh my ..... should we... .. display only a maximum of sources? @@ -637,7 +582,7 @@ class QuestPage extends GenericPage $mObjectives[$zoneId]['levels'][$floor][] = $processing($objId, $objData); } } - }; + }; // POI: start + end @@ -674,10 +619,13 @@ class QuestPage extends GenericPage if ($_specialFlags & QUEST_FLAG_SPECIAL_EXT_COMPLETE) { // areatrigger - if ($atir = DB::Aowow()->selectCol('SELECT id FROM ?_areatrigger WHERE type = ?d AND quest = ?d', AT_TYPE_OBJECTIVE, $this->typeId)) + if ($atir = DB::Aowow()->selectCol('SELECT `id` FROM ?_areatrigger WHERE `type` = ?d AND `quest` = ?d', AT_TYPE_OBJECTIVE, $this->typeId)) { - if ($atSpawns = DB::AoWoW()->select('SELECT typeId AS ARRAY_KEY, posX, posY, floor, areaId FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a)', Type::AREATRIGGER, $atir)) + if ($atSpawns = DB::AoWoW()->select('SELECT `typeId` AS ARRAY_KEY, `posX`, `posY`, `floor`, `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a)', Type::AREATRIGGER, $atir)) { + if (User::isInGroup(U_GROUP_STAFF)) + $endTextWrapper = '%s'; + foreach ($atSpawns as $atId => $atsp) { $atSpawn = array ( @@ -705,7 +653,7 @@ class QuestPage extends GenericPage } } // complete-spell - else if ($endSpell = new SpellList(array('OR', ['AND', ['effect1Id', 16], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', 16], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', 16], ['effect3MiscValue', $this->typeId]]))) + else if ($endSpell = new SpellList(array('OR', ['AND', ['effect1Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect3MiscValue', $this->typeId]]))) if (!$endSpell->error) $endTextWrapper = '%s'; } @@ -901,24 +849,30 @@ class QuestPage extends GenericPage } } - $this->map = $mObjectives ? array( - 'mapperData' => [], // always empty - 'data' => array( - 'parent' => 'mapper-generic', - 'objectives' => $mObjectives, - 'zoneparent' => 'mapper-zone-generic', - 'zones' => $mZones, - 'missing' => count($mZones) > 1 || $hasStartEnd != 0x3 ? 1 : 0 // 0 if everything happens in one zone, else 1 - ) - ) : null; + if ($mObjectives) + { + $this->addDataLoader('zones'); + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'objectives' => $mObjectives, + 'zoneparent' => 'mapper-zone-generic', + 'zones' => $mZones, + 'missing' => count($mZones) > 1 || $hasStartEnd != 0x3 ? 1 : 0 // 0 if everything happens in one zone, else 1 + ), + new \StdClass(), // mapperData + null, // ShowOnMap + null // foundIn + ); + } /****************/ /* Main Content */ /****************/ + $this->series = $this->createSeries($_side); $this->gains = $this->createGains(); - $this->mail = $this->createMail($startEnd); $this->rewards = $this->createRewards($_side); $this->objectives = $this->subject->parseText('objectives', false); $this->details = $this->subject->parseText('details', false); @@ -939,36 +893,41 @@ class QuestPage extends GenericPage ) ); + if ($this->createMail($startEnd)) + $this->addScript([SC_CSS_FILE, 'css/Book.css']); + // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(horde_id = ?d, alliance_id, -horde_id) FROM player_factionchange_quests WHERE alliance_id = ?d OR horde_id = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_quests WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) { $altQuest = new QuestList(array(['id', abs($pendant)])); if (!$altQuest->error) { - $this->transfer = sprintf( - Lang::quest('_transfer'), + $this->transfer = Lang::quest('_transfer', array( $altQuest->id, $altQuest->getField('name', true), $pendant > 0 ? 'alliance' : 'horde', - $pendant > 0 ? Lang::game('si', 1) : Lang::game('si', 2) - ); + $pendant > 0 ? Lang::game('si', SIDE_ALLIANCE) : Lang::game('si', SIDE_HORDE) + )); } } + /**************/ /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: see also $seeAlso = new QuestList(array(['name_loc'.Lang::getLocale()->value, '%'.Util::htmlEscape($this->subject->getField('name', true)).'%'], ['id', $this->typeId, '!'])); if (!$seeAlso->error) { $this->extendGlobalData($seeAlso->getJSGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($seeAlso->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $seeAlso->getListviewData(), 'name' => '$LANG.tab_seealso', 'id' => 'see-also' - )]; + ), QuestList::$brickFile)); } // tab: criteria of @@ -976,27 +935,27 @@ class QuestPage extends GenericPage if (!$criteriaOf->error) { $this->extendGlobalData($criteriaOf->getJSGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($criteriaOf->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $criteriaOf->getListviewData(), 'name' => '$LANG.tab_criteriaof', 'id' => 'criteria-of' - )]; + ), AchievementList::$brickFile)); } // tab: spawning pool (for the swarm) - if ($qp = DB::World()->selectCol('SELECT qpm2.questId FROM quest_pool_members qpm1 JOIN quest_pool_members qpm2 ON qpm1.poolId = qpm2.poolId WHERE qpm1.questId = ?d', $this->typeId)) + if ($qp = DB::World()->selectCol('SELECT qpm2.`questId` FROM quest_pool_members qpm1 JOIN quest_pool_members qpm2 ON qpm1.`poolId` = qpm2.`poolId` WHERE qpm1.`questId` = ?d', $this->typeId)) { - $max = DB::World()->selectCell('SELECT numActive FROM quest_pool_template qpt JOIN quest_pool_members qpm ON qpm.poolId = qpt.poolId WHERE qpm.questId = ?d', $this->typeId); + $max = DB::World()->selectCell('SELECT `numActive` FROM quest_pool_template qpt JOIN quest_pool_members qpm ON qpm.`poolId` = qpt.`poolId` WHERE qpm.`questId` = ?d', $this->typeId); $pooledQuests = new QuestList(array(['id', $qp])); if (!$pooledQuests->error) { $this->extendGlobalData($pooledQuests->getJSGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($pooledQuests->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $pooledQuests->getListviewData(), 'name' => 'Quest Pool', 'id' => 'quest-pool', 'note' => Lang::quest('questPoolDesc', [$max]) - )]; + ), QuestList::$brickFile)); } } @@ -1015,27 +974,15 @@ class QuestPage extends GenericPage if ($tab = $cnd->toListviewTab()) { $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - } - - protected function generateTooltip() - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW); - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - if ($this->subject->isDaily()) - $power->daily = 1; + $this->lvTabs->addDataTab(...$tab); } - return sprintf($this->powerTpl, $this->typeId, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); + parent::generate(); } - private function createRewards($side) + private function createRewards(int $side) : ?array { - $rewards = []; + $rewards = [[], [], [], '']; // [spells, items, choice, money] // moneyReward / maxLevelCompensation $comp = $this->subject->getField('rewardMoneyMaxLevel'); @@ -1043,75 +990,68 @@ class QuestPage extends GenericPage $realComp = max($comp, $questMoney); if ($questMoney > 0) { - $rewards['money'] = Util::formatMoney($questMoney); + $rewards[3] = Util::formatMoney($questMoney); if ($realComp > $questMoney) - $rewards['money'] .= ' ' . sprintf(Lang::quest('expConvert'), Util::formatMoney($realComp), MAX_LEVEL); + $rewards[3] .= ' ' . Lang::quest('expConvert', [Util::formatMoney($realComp), MAX_LEVEL]); } else if ($questMoney <= 0 && $realComp > 0) - $rewards['money'] = sprintf(Lang::quest('expConvert2'), Util::formatMoney($realComp), MAX_LEVEL); + $rewards[3] = Lang::quest('expConvert2', [Util::formatMoney($realComp), MAX_LEVEL]); // itemChoices if (!empty($this->subject->choices[$this->typeId][Type::ITEM])) { - $c = $this->subject->choices[$this->typeId][Type::ITEM]; - $choiceItems = new ItemList(array(['id', array_keys($c)])); + $choices = $this->subject->choices[$this->typeId][Type::ITEM]; + $choiceItems = new ItemList(array(['id', array_keys($choices)])); if (!$choiceItems->error) { $this->extendGlobalData($choiceItems->getJSGlobals()); - foreach ($choiceItems->Iterate() as $id => $__) - { - $rewards['choice'][] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $id, - 'name' => $choiceItems->getField('name', true), - 'quality' => $choiceItems->getField('quality'), - 'qty' => $c[$id], - 'globalStr' => Type::getJSGlobalString(Type::ITEM) - ); - } + foreach ($choices as $id => $num) // itr over $choices to preserve display order + if ($choiceItems->getEntry($id)) + $rewards[2][] = new IconElement( + Type::ITEM, + $id, + Lang::unescapeUISequences($choiceItems->getField('name', true), Lang::FMT_HTML), + quality: $choiceItems->getField('quality'), + num: $num + ); } } // itemRewards if (!empty($this->subject->rewards[$this->typeId][Type::ITEM])) { - $ri = $this->subject->rewards[$this->typeId][Type::ITEM]; - $rewItems = new ItemList(array(['id', array_keys($ri)])); + $reward = $this->subject->rewards[$this->typeId][Type::ITEM]; + $rewItems = new ItemList(array(['id', array_keys($reward)])); if (!$rewItems->error) { $this->extendGlobalData($rewItems->getJSGlobals()); - foreach ($rewItems->Iterate() as $id => $__) - { - $rewards['items'][] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $id, - 'name' => Lang::unescapeUISequences($rewItems->getField('name', true), Lang::FMT_HTML), - 'quality' => $rewItems->getField('quality'), - 'qty' => $ri[$id], - 'globalStr' => Type::getJSGlobalString(Type::ITEM) - ); - } + foreach ($reward as $id => $num) // itr over $reward to preserve display order + if ($rewItems->getEntry($id)) + $rewards[1][] = new IconElement( + Type::ITEM, + $id, + Lang::unescapeUISequences($rewItems->getField('name', true), Lang::FMT_HTML), + quality: $rewItems->getField('quality'), + num: $num + ); } } if (!empty($this->subject->rewards[$this->typeId][Type::CURRENCY])) { - $rc = $this->subject->rewards[$this->typeId][Type::CURRENCY]; - $rewCurr = new CurrencyList(array(['id', array_keys($rc)])); + $currency = $this->subject->rewards[$this->typeId][Type::CURRENCY]; + $rewCurr = new CurrencyList(array(['id', array_keys($currency)])); if (!$rewCurr->error) { $this->extendGlobalData($rewCurr->getJSGlobals()); - foreach ($rewCurr->Iterate() as $id => $__) - { - $rewards['items'][] = array( - 'typeStr' => Type::getFileString(Type::CURRENCY), - 'id' => $id, - 'name' => $rewCurr->getField('name', true), - 'quality' => 1, - 'qty' => $rc[$id] * ($side == 2 ? -1 : 1), // toggles the icon - 'globalStr' => Type::getJSGlobalString(Type::CURRENCY) + foreach ($rewCurr->iterate() as $id => $__) + $rewards[1][] = new IconElement( + Type::CURRENCY, + $id, + $rewCurr->getField('name', true), + quality: ITEM_QUALITY_NORMAL, + num: $currency[$id] * ($side == SIDE_HORDE ? -1 : 1), // toggles the icon ); - } } } @@ -1133,18 +1073,14 @@ class QuestPage extends GenericPage { $extra = null; if ($_ = $rewSpells->getEntry($displ)) - $extra = sprintf(Lang::quest('spellDisplayed'), $displ, Util::localizedString($_, 'name')); + $extra = Lang::quest('spellDisplayed', [$displ, Util::localizedString($_, 'name')]); if ($_ = $rewSpells->getEntry($cast)) - { - $rewards['spells']['extra'] = $extra; - $rewards['spells']['cast'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $cast, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) + $rewards[0] = array( + 'title' => Lang::quest('rewardAura'), + 'cast' => [new IconElement(Type::SPELL, $cast, Util::localizedString($_, 'name'))], + 'extra' => $extra ); - } } else // if it has effect:learnSpell display the taught spell instead { @@ -1154,114 +1090,102 @@ class QuestPage extends GenericPage foreach ($_ as $idx) $teach[$rewSpells->getField('effect'.$idx.'TriggerSpell')] = $id; - if ($_ = $rewSpells->getEntry($displ)) - { - $rewards['spells']['extra'] = null; - $rewards['spells'][$teach ? 'learn' : 'cast'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $displ, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) - ); - } - else if (($_ = $rewSpells->getEntry($cast)) && !$teach) - { - $rewards['spells']['extra'] = null; - $rewards['spells']['cast'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $cast, - 'name' => Util::localizedString($_, 'name'), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) - ); - } - else + if ($teach) { $taught = new SpellList(array(['id', array_keys($teach)])); if (!$taught->error) { $this->extendGlobalData($taught->getJSGlobals()); - $rewards['spells']['extra'] = null; + $rewards[0] = ['cast' => [], 'extra' => null]; + + $isTradeSkill = 0; foreach ($taught->iterate() as $id => $__) { - $rewards['spells']['learn'][] = array( - 'typeStr' => Type::getFileString(Type::SPELL), - 'id' => $id, - 'name' => $taught->getField('name', true), - 'globalStr' => Type::getJSGlobalString(Type::SPELL) - ); + $isTradeSkill |= array_intersect($taught->getField('skillLines'), array_merge(SKILLS_TRADE_PRIMARY, SKILLS_TRADE_SECONDARY)) ? 1 : 0; + $rewards[0]['cast'][] = new IconElement(Type::SPELL, $id, $taught->getField('name', true)); } + + $rewards[0]['title'] = $isTradeSkill ? Lang::quest('rewardTradeSkill') : Lang::quest('rewardSpell'); } } - } - } - - return $rewards; - } - - private function createMail($startEnd) - { - $mail = []; - - if ($rmtId = $this->subject->getField('rewardMailTemplateId')) - { - $delay = $this->subject->getField('rewardMailDelay'); - $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE id = ?d', $rmtId); - - $mail = array( - 'id' => $rmtId, - 'delay' => $delay ? sprintf(Lang::mail('mailIn'), Util::formatTime($delay * 1000)) : null, - 'sender' => null, - 'attachments' => [], - 'text' => $letter ? Util::parseHtmlText(Util::localizedString($letter, 'text')) : null, - 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject')) - ); - - $senderTypeId = 0; - if ($_= DB::World()->selectCell('SELECT RewardMailSenderEntry FROM quest_mail_sender WHERE QuestId = ?d', $this->typeId)) - $senderTypeId = $_; - else - foreach ($startEnd as $se) - if (($se['method'] & 0x2) && $se['type'] == Type::NPC) - $senderTypeId = $se['typeId']; - - if ($ti = CreatureList::getName($senderTypeId)) - $mail['sender'] = sprintf(Lang::mail('mailBy'), $senderTypeId, $ti); - - // while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item. - $mailLoot = new Loot(); - if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId)) - { - $this->extendGlobalData($mailLoot->jsGlobals); - foreach ($mailLoot->getResult() as $loot) + else if (($_ = $rewSpells->getEntry($displ)) || ($_ = $rewSpells->getEntry($cast))) { - $mail['attachments'][] = array( - 'typeStr' => Type::getFileString(Type::ITEM), - 'id' => $loot['id'], - 'name' => substr($loot['name'], 1), - 'quality' => 7 - $loot['name'][0], - 'qty' => $loot['stack'][0], - 'globalStr' => Type::getJSGlobalString(Type::ITEM) + $rewards[0] = array( + 'title' => Lang::quest('rewardAura'), + 'cast' => [new IconElement(Type::SPELL, $cast, Util::localizedString($_, 'name'))], + 'extra' => null ); } } } - return $mail; + if (!array_filter($rewards)) + return null; + + return $rewards; } - private function createGains() + private function createMail(array $startEnd) : bool + { + $rmtId = $this->subject->getField('rewardMailTemplateId'); + if (!$rmtId) + return false; + + $delay = $this->subject->getField('rewardMailDelay'); + $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE `id` = ?d', $rmtId); + + $this->mail = array( + 'attachments' => [], + 'text' => $letter ? Util::parseHtmlText(Util::localizedString($letter, 'text')) : null, + 'subject' => Util::parseHtmlText(Util::localizedString($letter, 'subject')), + 'header' => array( + $rmtId, + null, + $delay ? Lang::mail('mailIn', [Util::formatTime($delay * 1000)]) : null, + ) + ); + + $senderTypeId = 0; + if ($_= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = ?d', $this->typeId)) + $senderTypeId = $_; + else + foreach ($startEnd as $se) + if (($se['method'] & 0x2) && $se['type'] == Type::NPC) + $senderTypeId = $se['typeId']; + + if ($ti = CreatureList::getName($senderTypeId)) + $this->mail['header'][1] = Lang::mail('mailBy', [$senderTypeId, $ti]); + + // while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item. + $mailLoot = new Loot(); + if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId)) + { + $this->extendGlobalData($mailLoot->jsGlobals); + foreach ($mailLoot->getResult() as $loot) + $this->mail['attachments'][] = new IconElement(Type::ITEM, $loot['id'], substr($loot['name'], 1), $loot['stack'][0], quality: 7 - $loot['name'][0]); + } + + return true; + } + + private function createGains() : ?array { $gains = []; // xp - if ($_ = $this->subject->getField('rewardXP')) - $gains['xp'] = $_; + $gains[0] = $this->subject->getField('rewardXP'); // talent points - if ($_ = $this->subject->getField('rewardTalents')) - $gains['tp'] = $_; + $gains[3] = $this->subject->getField('rewardTalents'); + + // title + if ($tId = $this->subject->getField('rewardTitleId')) + $gains[2] = [$tId, (new TitleList(array(['id', $tId])))->getHtmlizedName()]; + else + $gains[2] = null; // reputation + $repGains = []; for ($i = 1; $i < 6; $i++) { $fac = $this->subject->getField('rewardFactionId'.$i); @@ -1275,32 +1199,114 @@ class QuestPage extends GenericPage 'name' => FactionList::getName($fac) ); - if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE faction = ?d', $fac)) + if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE `faction` = ?d', $fac)) { - if ($dailyType = $this->subject->isDaily()) - { - if ($dailyType == 1 && $cuRates['quest_daily_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_daily_rate'] - 1); - else if ($dailyType == 2 && $cuRates['quest_weekly_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_weekly_rate'] - 1); - else if ($dailyType == 3 && $cuRates['quest_monthly_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_monthly_rate'] - 1); - } - else if ($this->subject->isRepeatable() && $cuRates['quest_repeatable_rate'] != 1.0) + if ($this->subject->isRepeatable()) $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_repeatable_rate'] - 1); - else if ($cuRates['quest_rate'] != 1.0) - $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_rate'] - 1); + else + $rep['qty'][1] = $rep['qty'][0] * match ($this->subject->isDaily()) + { + 1 => $cuRates['quest_daily_rate'] - 1, + 2 => $cuRates['quest_weekly_rate'] - 1, + 3 => $cuRates['quest_monthly_rate'] - 1, + default => $cuRates['quest_rate'] - 1 + }; } - $gains['rep'][] = $rep; - } + if (User::isInGroup(U_GROUP_STAFF)) + $rep['qty'][1] = $rep['qty'][0] . ($rep['qty'][1] ? $this->fmtStaffTip(($rep['qty'][1] > 0 ? '+' : '').$rep['qty'][1], Lang::faction('customRewRate')) : ''); + else + $rep['qty'][1] += $rep['qty'][0]; - // title - if ($_ = (new TitleList(array(['id', $this->subject->getField('rewardTitleId')])))->getHtmlizedName()) - $gains['title'] = $_; + $repGains[] = $rep; + } + $gains[1] = $repGains; + + if (!array_filter($gains)) + return null; return $gains; } + + private function createSeries() : array + { + $series = []; + + $makeSeriesItem = function (array $questData) : array + { + return array( + 'side' => ChrRace::sideFromMask($questData['reqRaceMask']), + 'typeStr' => Type::getFileString(Type::QUEST), + 'typeId' => $questData['id'], + 'name' => Util::htmlEscape(Lang::trimTextClean(Util::localizedString($questData, 'name'), 40)), + ); + }; + + // Assumption + // a chain always ends in a single quest, but can have an arbitrary amount of quests leading into it. + // so we fast forward to the last quest and go backwards from there. + + $lastQuestId = $this->subject->getField('nextQuestIdChain'); + while ($newLast = DB::Aowow()->selectCell('SELECT `nextQuestIdChain` FROM ?_quests WHERE `id` = ?d AND `id` <> `nextQuestIdChain`', $lastQuestId)) + $lastQuestId = $newLast; + + $end = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `id` = ?d', $lastQuestId ?: $this->typeId); + $chain = array(array($makeSeriesItem($end))); // series / step / quest + + $prevStepIds = [$lastQuestId ?: $this->typeId]; + while ($prevQuests = DB::Aowow()->select('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `nextQuestIdChain` IN (?a) AND `id` <> `nextQuestIdChain`', $prevStepIds)) + { + $step = []; + foreach ($prevQuests as $pQuest) + $step[$pQuest['id']] = $makeSeriesItem($pQuest); + + $prevStepIds = array_keys($step); + $chain[] = $step; + } + + if (count($chain) > 1) + $series[] = [array_reverse($chain), null]; + + // todo (low): sensibly merge the following lists into 'series' + $listGen = function($cnd) use ($makeSeriesItem) + { + $chain = []; + $list = new QuestList($cnd); + if ($list->error) + return null; + + foreach ($list->iterate() as $tpl) + $chain[] = [$makeSeriesItem($tpl)]; + + return $chain; + }; + + $extraLists = array( + // Requires all of these quests (Quests that you must follow to get this quest) + ['reqQ', array('OR', ['AND', ['nextQuestId', $this->typeId], ['exclusiveGroup', 0, '<']], ['AND', ['id', $this->subject->getField('prevQuestId')], ['nextQuestIdChain', $this->typeId, '!']])], + + // Requires one of these quests (Requires one of the quests to choose from) + ['reqOneQ', array('OR', ['AND', ['exclusiveGroup', 0, '>'], ['nextQuestId', $this->typeId]], ['breadCrumbForQuestId', $this->typeId])], + + // Opens Quests (Quests that become available only after complete this quest (optionally only one)) + ['opensQ', array('OR', ['AND', ['prevQuestId', $this->typeId], ['id', $this->subject->getField('nextQuestIdChain'), '!']], ['id', $this->subject->getField('nextQuestId')], ['id', $this->subject->getField('breadcrumbForQuestId')])], + + // Closes Quests (Quests that become inaccessible after completing this quest) + ['closesQ', array(['exclusiveGroup', 0, '>'], ['exclusiveGroup', $this->subject->getField('exclusiveGroup')], ['id', $this->typeId, '!'])], + + // During the quest available these quests (Quests that are available only at run time this quest) + ['enablesQ', array(['prevQuestId', -$this->typeId])], + + // Requires an active quest (Quests during the execution of which is available on the quest) + ['enabledByQ', array(['id', -$this->subject->getField('prevQuestId')])] + ); + + foreach ($extraLists as [$section, $condition]) + if ($_ = $listGen($condition)) + $series[] = [$_, sprintf(Util::$dfnString, Lang::quest($section.'Desc'), Lang::quest($section))]; + + return $series; + } } ?> diff --git a/endpoints/quest/quest_power.php b/endpoints/quest/quest_power.php new file mode 100644 index 00000000..d0eccf52 --- /dev/null +++ b/endpoints/quest/quest_power.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $quest = new QuestList(array(['id', $this->typeId])); + if ($quest->error) + $this->cacheType = CACHE_TYPE_NONE; + else + $opts = array( + 'name' => Lang::unescapeUISequences($quest->getField('name', true), Lang::FMT_RAW), + 'tooltip' => $quest->renderTooltip(), + 'daily' => $quest->isDaily() ? 1 : null + ); + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } +} + +?> diff --git a/endpoints/quests/quests.php b/endpoints/quests/quests.php new file mode 100644 index 00000000..fb4d944f --- /dev/null +++ b/endpoints/quests/quests.php @@ -0,0 +1,142 @@ + 3519, 4024 => 3537, 25 => 46, 1769 => 361, + // Startzones: Horde + 132 => 1, 9 => 12, 3431 => 3430, 154 => 85, + // Startzones: Alliance + 3526 => 3524, 363 => 14, 220 => 215, 188 => 141, + // Group: Caverns of Time + 2366 => 1941, 2367 => 1941, 4100 => 1941, + // Group: Hellfire Citadell + 3562 => 3535, 3713 => 3535, 3714 => 3535, + // Group: Auchindoun + 3789 => 3688, 3790 => 3688, 3791 => 3688, 3792 => 3688, + // Group: Tempest Keep + 3847 => 3842, 3848 => 3842, 3849 => 3842, + // Group: Coilfang Reservoir + 3715 => 3905, 3716 => 3905, 3717 => 3905, + // Group: Icecrown Citadel + 4809 => 4522, 4813 => 4522, 4820 => 4522 + ); + + protected int $type = Type::QUEST; + protected int $cacheType = CACHE_TYPE_PAGE; + + protected string $template = 'quests'; + protected string $pageName = 'quests'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 3]; + + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; + protected array $expectedGET = array( + 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = Game::QUEST_CLASSES; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new QuestListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('quests')); + + $conditions = []; + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->filter->evalCriteria(); + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + if (isset($this->category[1])) + $conditions[] = ['zoneOrSort', $this->category[1]]; + else if (isset($this->category[0])) + $conditions[] = ['zoneOrSort', $this->validCats[$this->category[0]]]; + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + if (isset($this->category[1]) && isset(self::SUB_SUB_CAT[$this->category[1]])) + array_splice($this->breadcrumb, 3, 0, self::SUB_SUB_CAT[$this->category[1]]); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if (isset($this->category[1])) + array_unshift($this->title, Lang::quest('cat', $this->category[0], $this->category[1])); + else if (isset($this->category[0])) + { + $c0 = Lang::quest('cat', $this->category[0]); + array_unshift($this->title, is_array($c0) ? $c0[0] : $c0); + } + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $quests = new QuestList($conditions, ['extraOpts' => $this->filter->extraOpts, 'calcTotal' => true]); + + $this->extendGlobalData($quests->getJSGlobals()); + + $tabData = ['data' => $quests->getListviewData()]; + + if ($rc = $this->filter->fiReputationCols) + $tabData['extraCols'] = '$fi_getReputationCols('.json_encode($rc, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; + else if ($this->filter->fiExtraCols) + $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; + + // create note if search limit was exceeded + if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + else if (isset($this->category[1]) && $this->category[1] > 0) + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questgivers, '.$this->category[1].', g_zones['.$this->category[1].'], '.$this->category[1].')'; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index f46a6ec5..5cd13bb7 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -36,7 +36,7 @@ class QuestList extends DBTypeList $_curTpl['cat1'] = $_curTpl['zoneOrSort']; // should probably be in a method... $_curTpl['cat2'] = 0; - foreach (Game::$questClasses as $k => $arr) + foreach (Game::QUEST_CLASSES as $k => $arr) { if (in_array($_curTpl['cat1'], $arr)) { diff --git a/includes/game/misc.php b/includes/game/misc.php index 44d6727d..67bfc553 100644 --- a/includes/game/misc.php +++ b/includes/game/misc.php @@ -31,7 +31,7 @@ class Game 1 => ['ability_rogue_eviscerate', 'ability_warrior_innerrage', 'ability_warrior_defensivestance' ] ); - public static $questClasses = array( + public const /* array */ QUEST_CLASSES = array( -2 => [ 0], 0 => [ 1, 3, 4, 8, 9, 10, 11, 12, 25, 28, 33, 36, 38, 40, 41, 44, 45, 46, 47, 51, 85, 130, 132, 139, 154, 267, 1497, 1519, 1537, 2257, 3430, 3431, 3433, 3487, 4080, 4298], 1 => [ 14, 15, 16, 17, 141, 148, 188, 215, 220, 331, 357, 361, 363, 400, 405, 406, 440, 490, 493, 618, 1377, 1637, 1638, 1657, 1769, 3524, 3525, 3526, 3557], diff --git a/localization/lang.class.php b/localization/lang.class.php index 62a54838..1bde86d4 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -490,7 +490,7 @@ class Lang public static function formatSkillBreakpoints(array $bp, int $fmt = self::FMT_MARKUP) : string { - $tmp = self::game('difficulty').self::main('colon'); + $tmp = self::game('difficulty'); $base = match ($fmt) { diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 2cd40810..ccf542bb 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Briefe", 'cooldown' => "%s Abklingzeit", - 'difficulty' => "Modus", + 'difficulty' => "Modus: ", 'dispelType' => "Bannart", 'duration' => "Dauer", 'eventShort' => "Ereignis: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Dieses Weltereignis existiert nicht.", - 'start' => "Anfang", - 'end' => "Ende", + 'start' => "Anfang: ", + 'end' => "Ende: ", 'interval' => "Intervall", 'inProgress' => "Ereignis findet gerade statt", 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Dieses Quest wird mit %s vertauscht, wenn Ihr zur %s wechselt.', 'questLevel' => "Stufe %s", 'requirements' => "Anforderungen", - 'reqMoney' => "Benötigtes Geld", + 'reqMoney' => "Benötigtes Geld: %s", 'money' => "Geld", 'additionalReq' => "Zusätzliche Anforderungen um das Quest zu erhalten", 'reqRepWith' => 'Eure Reputation mit %s %s %s sein', 'reqRepMin' => "muss mindestens", 'reqRepMax' => "darf höchstens", 'progress' => "Fortschritt", - 'provided' => "Bereitgestellt", + 'provided' => "(Bereitgestellt)", 'providedItem' => "Bereitgestellter Gegenstand", 'completion' => "Abschluss", 'description' => "Beschreibung", - 'playerSlain' => "Spieler getötet", - 'profession' => "Beruf", - 'timer' => "Zeitbegrenzung", - 'loremaster' => "Meister der Lehren", - 'suggestedPl' => "Empfohlene Spielerzahl", + 'playerSlain' => "Spieler getötet (%d)", + 'profession' => "Beruf: ", + 'timer' => "Zeitbegrenzung: ", + 'loremaster' => "Meister der Lehren: ", + 'suggestedPl' => "Empfohlene Spielerzahl: %d", 'keepsPvpFlag' => "Hält Euch im PvP", 'daily' => 'Täglich', 'weekly' => "Wöchentlich", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Aktiviert durch", 'enabledByQDesc'=> "Ihr könnt diese Quest nur annehmen, wenn eins der nachfolgenden Quests aktiv ist", 'gainsDesc' => "Bei Abschluss dieser Quest erhaltet Ihr", - 'theTitle' => 'den Titel "%s"', 'unavailable' => "Diese Quest wurde als nicht genutzt markiert und kann weder erhalten noch vollendet werden.", 'experience' => "Erfahrung", 'expConvert' => "(oder %s, wenn auf Stufe %d vollendet)", 'expConvert2' => "%s, wenn auf Stufe %d vollendet", - 'chooseItems' => "Auf Euch wartet eine dieser Belohnungen", - 'receiveItems' => "Ihr bekommt", - 'receiveAlso' => "Ihr bekommt außerdem", - 'spellCast' => "Der folgende Zauber wird auf Euch gewirkt", - 'spellLearn' => "Ihr erlernt", + 'rewardChoices' => "Auf Euch wartet eine dieser Belohnungen:", // REWARD_CHOICES + 'rewardItems' => "Ihr bekommt:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Ihr bekommt außerdem:", // REWARD_ITEMS + 'rewardSpell' => "Ihr erlernt:", // REWARD_SPELL + 'rewardAura' => "Der folgende Zauber wird auf Euch gewirkt:", // REWARD_AURA + 'rewardTradeSkill'=>"Ihr erlernt die Herstellung von:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Euch wird folgender Titel verliehen: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4Talentpunkt:Talentpunkte;", 'spellDisplayed'=> ' (%s wird angezeigt)', 'questPoolDesc' => 'Nur %d |4Quest kann:Quests können; aus diesem Tab gleichzeitig aktiv sein', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => 'Ihr werdet diesen Brief%s%s erhalten', 'mailBy' => ' von %s', 'mailIn' => " nach %s", - 'delay' => "Verzögerung", - 'sender' => "Absender", + 'delay' => "Verzögerung: %s", + 'sender' => "Absender: %s", 'untitled' => "Unbetitelter Brief #%d" ), 'pet' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index ba489eef..e250a297 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "%s cooldown", - 'difficulty' => "Difficulty", + 'difficulty' => "Difficulty: ", 'dispelType' => "Dispel type", 'duration' => "Duration", 'eventShort' => "Event: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "This world event doesn't exist.", - 'start' => "Start", - 'end' => "End", + 'start' => "Start: ", + 'end' => "End: ", 'interval' => "Interval", 'inProgress' => "Event is currently in progress", 'category' => ["Uncategorized", "Holidays", "Recurring", "Player vs. Player"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'This quest will be converted to %s if you transfer to %s.', 'questLevel' => "Level %s", 'requirements' => "Requirements", - 'reqMoney' => "Required money", // REQUIRED_MONEY + 'reqMoney' => "Required money: %s", // REQUIRED_MONEY 'money' => "Money", 'additionalReq' => "Additional requirements to obtain this quest", 'reqRepWith' => 'Your reputation with %s must be %s %s', 'reqRepMin' => "at least", 'reqRepMax' => "lower than", 'progress' => "Progress", - 'provided' => "Provided", + 'provided' => "(Provided)", 'providedItem' => "Provided item", 'completion' => "Completion", 'description' => "Description", - 'playerSlain' => "Players slain", - 'profession' => "Profession", - 'timer' => "Timer", - 'loremaster' => "Loremaster", - 'suggestedPl' => "Suggested players", + 'playerSlain' => "Players slain (%d)", + 'profession' => "Profession: ", + 'timer' => "Timer: ", + 'loremaster' => "Loremaster: ", + 'suggestedPl' => "Suggested players: %d", 'keepsPvpFlag' => "Keeps you PvP flagged", 'daily' => "Daily", 'weekly' => "Weekly", @@ -1308,17 +1308,18 @@ $lang = array( 'enabledByQ' => "Enabled by", 'enabledByQDesc'=> "This quest is available only, when one of these quests are active", 'gainsDesc' => "Upon completion of this quest you will gain", - 'theTitle' => 'the title "%s"', // partly REWARD_TITLE 'unavailable' => "This quest was marked obsolete and cannot be obtained or completed.", 'experience' => "experience", 'expConvert' => "(or %s if completed at level %d)", 'expConvert2' => "%s if completed at level %d", - 'chooseItems' => "You will be able to choose one of these rewards", // REWARD_CHOICES - 'receiveItems' => "You will receive", // REWARD_ITEMS_ONLY - 'receiveAlso' => "You will also receive", // REWARD_ITEMS - 'spellCast' => "The following spell will be cast on you", // REWARD_AURA - 'spellLearn' => "You will learn", // REWARD_SPELL - 'bonusTalents' => "%d talent |4point:points;", // partly LEVEL_UP_CHAR_POINTS + 'rewardChoices' => "You will be able to choose one of these rewards:", // REWARD_CHOICES + 'rewardItems' => "You will receive:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "You will also receive:", // REWARD_ITEMS + 'rewardSpell' => "You will learn:", // REWARD_SPELL + 'rewardAura' => "The following spell will be cast on you:", // REWARD_AURA + 'rewardTradeSkill'=>"You will learn how to create:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'You shall be granted the title: "%s"', // REWARD_TITLE + 'bonusTalents' => "%d talent |4point:points;", // partly LEVEL_UP_CHAR_POINTS 'spellDisplayed'=> ' (%s is displayed)', 'questPoolDesc' => 'Only %d |4Quest:Quests; from this tab will be available at a time', 'autoaccept' => 'Auto Accept', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => 'You will receive this letter%s%s', 'mailBy' => ' by %s', 'mailIn' => " after %s", - 'delay' => "Delay", - 'sender' => "Sender", + 'delay' => "Delay: %s", + 'sender' => "Sender: %s", 'untitled' => "Untitled Mail #%d" ), 'pet' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 84353d5f..b38316b9 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "%s de reutilización", - 'difficulty' => "Dificultad", + 'difficulty' => "Dificultad: ", 'dispelType' => "Tipo de disipación", 'duration' => "Duración", 'eventShort' => "Evento: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Este evento del mundo no existe.", - 'start' => "Empieza", - 'end' => "Termina", + 'start' => "Empieza: ", + 'end' => "Termina: ", 'interval' => "Intervalo", 'inProgress' => "El evento está en progreso actualmente", 'category' => ["Sin categoría", "Vacacionales", "Periódicos", "Jugador contra Jugador"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Esta misión será convertido a %s si lo transfieres a la %s.', 'questLevel' => 'Nivel %s', 'requirements' => 'Requisitos', - 'reqMoney' => 'Dinero necesario', + 'reqMoney' => 'Dinero necesario: %s', 'money' => 'Dinero', 'additionalReq' => "Requerimientos adicionales para obtener esta misión", 'reqRepWith' => 'Tu reputación con %s debe ser %s %s', 'reqRepMin' => "de al menos", 'reqRepMax' => "menor que", 'progress' => "Progreso", - 'provided' => "Provisto", + 'provided' => "(Provisto)", 'providedItem' => "Objeto provisto", 'completion' => "Terminación", 'description' => "Descripción", - 'playerSlain' => "Jugadores derrotados", - 'profession' => "Profesión", - 'timer' => "Tiempo", - 'loremaster' => "Maestro cultural", - 'suggestedPl' => "Jugadores sugeridos", + 'playerSlain' => "Jugadores derrotados (%d)", + 'profession' => "Profesión: ", + 'timer' => "Tiempo: ", + 'loremaster' => "Maestro cultural: ", + 'suggestedPl' => "Jugadores sugeridos: %d", 'keepsPvpFlag' => "Mantiene el JcJ activado", 'daily' => 'Diaria', 'weekly' => "Semanal", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Activada por", 'enabledByQDesc'=> "Para aceptar esta misión debes haber tener activa alguna de estas misiones", 'gainsDesc' => "Cuando completes esta misión ganarás", - 'theTitle' => 'el título "%s"', 'unavailable' => "Esta misión fue marcada como obsoleta y no puede ser obtenida o completada.", 'experience' => "experiencia", 'expConvert' => "(o %s si se completa al nivel %d)", 'expConvert2' => "%s si se completa al nivel %d", - 'chooseItems' => "Podrás elegir una de estas recompensas", - 'receiveItems' => "Recibirás", - 'receiveAlso' => "También recibirás", - 'spellCast' => "Te van a lanzar el siguiente hechizo", - 'spellLearn' => "Aprenderás", + 'rewardChoices' => "Podrás elegir una de estas recompensas:", // REWARD_CHOICES + 'rewardItems' => "Recibirás:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "También recibirás:", // REWARD_ITEMS + 'rewardSpell' => "Aprenderás:", // REWARD_SPELL + 'rewardAura' => "Te van a lanzar el siguiente hechizo:", // REWARD_AURA + 'rewardTradeSkill'=>"Aprenderás a crear:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Se te otorga el título de: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4punto:puntos; de talento", 'spellDisplayed'=> ' (mostrando %s)', 'questPoolDesc' => 'Solo %d |4misión:misiones; de esta pestaña estarán disponibles a la vez', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => "Usted recibirá esta carta%s%s", 'mailBy' => ' del %s', 'mailIn' => " después de %s", - 'delay' => "Retraso", - 'sender' => "Remitente", + 'delay' => "Retraso: %s", + 'sender' => "Remitente: %s", 'untitled' => "Correo sin título #%d" ), 'pet' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index a1f7d19a..bf728dd6 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "%s de recharge", - 'difficulty' => "Difficulté", + 'difficulty' => "Difficulté : ", 'dispelType' => "Type de dissipation", 'duration' => "Durée", 'eventShort' => "Évènement : %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Cet évènement mondial n'existe pas.", - 'start' => "Début", - 'end' => "Fin", + 'start' => "Début : ", + 'end' => "Fin : ", 'interval' => "Intervalle", 'inProgress' => "L'évènement est présentement en cours", 'category' => ["Non classés", "Vacances", "Récurrent", "Joueur ctr. Joueur"] @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Cette quête sera converti en %s si vous transférez en %s.', 'questLevel' => "Niveau %s", 'requirements' => "Conditions", - 'reqMoney' => "Argent requis", + 'reqMoney' => "Argent requis : %s", 'money' => "Argent", 'additionalReq' => "Conditions additionnelles requises pour obtenir cette quête", 'reqRepWith' => 'Votre reputation avec %s doît être %s %s', 'reqRepMin' => "d'au moins", 'reqRepMax' => "moins que", 'progress' => "Progrès", - 'provided' => "Fourni", + 'provided' => "(Fourni)", 'providedItem' => "Objet fourni", 'completion' => "Achèvement", 'description' => "Description", - 'playerSlain' => "Joueurs tués", - 'profession' => "Métier", - 'timer' => "Temps", - 'loremaster' => "Maitre des traditions", - 'suggestedPl' => "Joueurs suggérés", + 'playerSlain' => "Joueurs tués (%d)", + 'profession' => "Métier : ", + 'timer' => "Temps : ", + 'loremaster' => "Maitre des traditions : ", + 'suggestedPl' => "Joueurs suggérés : %d", 'keepsPvpFlag' => "Vous garde en mode JvJ", 'daily' => "Journalière", 'weekly' => "Chaque semaine", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Autorisée par", 'enabledByQDesc'=> "Vous pouvez faire cette quête seulement quand cette quête est active", 'gainsDesc' => "Lors de l'achèvement de cette quête vous gagnerez", - 'theTitle' => '"%s"', // empty on purpose! 'unavailable' => "Cette quête est marquée comme obsolète et ne peut être obtenue ou accomplie.", 'experience' => "points d'expérience", 'expConvert' => "(ou %s si completé au niveau %d)", 'expConvert2' => "%s si completé au niveau %d", - 'chooseItems' => "Vous pourrez choisir une de ces récompenses", - 'receiveItems' => "Vous recevrez", - 'receiveAlso' => "Vous recevrez également", - 'spellCast' => "Vous allez être la cible du sort suivant", - 'spellLearn' => "Vous apprendrez", + 'rewardChoices' => "Vous pourrez choisir une de ces récompenses :", // REWARD_CHOICES + 'rewardItems' => "Vous recevrez :", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Vous recevrez également :", // REWARD_ITEMS (Ainsi que :) + 'rewardSpell' => "Vous apprendrez :", // REWARD_SPELL + 'rewardAura' => "Vous allez être la cible du sort suivant :", // REWARD_AURA + 'rewardTradeSkill'=>"Vous apprendrez comment créer :", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Vous allez recevoir le titre suivant : "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4point:points; de talent", 'spellDisplayed'=> ' (%s affichés)', 'questPoolDesc' => 'Only %d |4Quest:Quests; from this tab will be available at a time', @@ -1444,8 +1445,8 @@ $lang = array( 'mailDelivery' => "Vous recevrez cette lettre%s%s", 'mailBy' => ' de %s', 'mailIn' => " après %s", - 'delay' => "Delay", - 'sender' => "Sender", + 'delay' => "Delay : %s", + 'sender' => "Sender : %s", 'untitled' => "Untitled Mail #%d" ), 'pet' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index a16699f7..8adbaec2 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -330,7 +330,7 @@ $lang = array( 'mails' => "Mails", 'cooldown' => "ВоÑÑтановление: %s", - 'difficulty' => "СложноÑть", + 'difficulty' => "СложноÑть: ", 'dispelType' => "Тип раÑÑеиваниÑ", 'duration' => "ДлительноÑть", 'eventShort' => "Игровое Ñобытие: %s", @@ -1135,8 +1135,8 @@ $lang = array( ), 'event' => array( 'notFound' => "Это игровое Ñобытие не ÑущеÑтвует.", - 'start' => "Ðачало", - 'end' => "Конец", + 'start' => "Ðачало: ", + 'end' => "Конец: ", 'interval' => "[Interval]", 'inProgress' => "Событие активно в данный момент", 'category' => array("Разное", "Праздники", "ПериодичеÑкие", "PvP") @@ -1272,22 +1272,22 @@ $lang = array( '_transfer' => 'Этот предмет превратитÑÑ Ð² %s, еÑли вы перейдете за %s.', 'questLevel' => "%s-го уровнÑ", 'requirements' => "ТребованиÑ", - 'reqMoney' => "ТребуетÑÑ Ð´ÐµÐ½ÐµÐ³", + 'reqMoney' => "ТребуетÑÑ Ð´ÐµÐ½ÐµÐ³: %s", 'money' => "Деньги", 'additionalReq' => "Дополнительные уÑÐ»Ð¾Ð²Ð¸Ñ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ð¾Ð³Ð¾ заданиÑ", 'reqRepWith' => 'Ваша Ñ€ÐµÐ¿ÑƒÑ‚Ð°Ñ†Ð¸Ñ Ñ %s должна быть %s %s', 'reqRepMin' => "не менее", 'reqRepMax' => "меньше чем", 'progress' => "ПрогреÑÑ", - 'provided' => "ПрилагаетÑÑ", + 'provided' => "(ПрилагаетÑÑ)", 'providedItem' => "ПрилагаетÑÑ Ð¿Ñ€ÐµÐ´Ð¼ÐµÑ‚", 'completion' => "Завершение", 'description' => "ОпиÑание", - 'playerSlain' => "Убито игроков", - 'profession' => "ПрофеÑÑиÑ", - 'timer' => "Таймер", - 'loremaster' => "Хранитель мудроÑти", - 'suggestedPl' => "Рекомендуемое количеÑтво игроков", + 'playerSlain' => "Убито игроков (%d)", + 'profession' => "ПрофеÑÑиÑ: ", + 'timer' => "Таймер: ", + 'loremaster' => "Хранитель мудроÑти: ", + 'suggestedPl' => "Рекомендуемое количеÑтво игроков: %d", 'keepsPvpFlag' => "Включает доÑтупноÑть PvP", 'daily' => "Ежедневно", 'weekly' => "Раз в неделю", @@ -1308,16 +1308,17 @@ $lang = array( 'enabledByQ' => "Включена по", 'enabledByQDesc'=> "Ð’Ñ‹ можете получить Ñто задание, только когда Ñти Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ð´Ð¾Ñтупны", 'gainsDesc' => "По завершении Ñтого заданиÑ, вы получите", - 'theTitle' => '"%s"', // empty on purpose! 'unavailable' => "пометили Ñто задание как уÑтаревшее — его Ð½ÐµÐ»ÑŒÐ·Ñ Ð¿Ð¾Ð»ÑƒÑ‡Ð¸Ñ‚ÑŒ или выполнить.", 'experience' => "опыта", 'expConvert' => "(или %s на %d-м уровне)", 'expConvert2' => "%s на %d-м уровне", - 'chooseItems' => "Вам дадут возможноÑть выбрать одну из Ñледующих наград", - 'receiveItems' => "Ð’Ñ‹ получите", - 'receiveAlso' => "Ð’Ñ‹ также получите", - 'spellCast' => "Следующее заклинание будет наложено на ваÑ", - 'spellLearn' => "Ð’Ñ‹ изучите", + 'rewardChoices' => "Ð’Ñ‹ Ñможете выбрать одну из наград:", // REWARD_CHOICES + 'rewardItems' => "Ð’Ñ‹ получите:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "Ð’Ñ‹ также получите:", // REWARD_ITEMS + 'rewardSpell' => "Ð’Ñ‹ узнаете:", // REWARD_SPELL + 'rewardAura' => "Ðа Ð²Ð°Ñ Ð±ÑƒÐ´ÐµÑ‚ наложено заклинание:", // REWARD_AURA + 'rewardTradeSkill'=>"Ð’Ñ‹ узнаете, как Ñоздавать:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => 'Вам будет приÑвоено звание: "%s"', // REWARD_TITLE 'bonusTalents' => "%d |4очко талантов:очка талантов:очков талантов;", 'spellDisplayed'=> ' (показано: %s)', 'questPoolDesc' => 'Only %d |4Quest:Quests; from this tab will be available at a time', @@ -1439,14 +1440,14 @@ $lang = array( ) ), 'mail' => array( - 'notFound' => "This mail doesn't exist.", + 'notFound' => "[This mail doesn't exist].", 'attachment' => "[Attachment]", 'mailDelivery' => "Ð’Ñ‹ получите Ñто пиÑьмо%s%s", 'mailBy' => ' от %s', 'mailIn' => " через %s", - 'delay' => "Delay", - 'sender' => "Sender", - 'untitled' => "Untitled Mail #%d" + 'delay' => "[Delay]: %s", + 'sender' => "[Sender]: %s", + 'untitled' => "[Untitled Mail] #%d" ), 'pet' => array( 'notFound' => "Такой породы питомцев не ÑущеÑтвует.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 2098281e..0ea4e318 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -329,7 +329,7 @@ $lang = array( 'mails' => "邮件", 'cooldown' => "%s冷崿—¶é—´", - 'difficulty' => "难度", + 'difficulty' => "难度:", 'dispelType' => "驱散类型", 'duration' => "æŒç»­æ—¶é—´", 'eventShort' => "事件:%s", @@ -1134,8 +1134,8 @@ $lang = array( ), 'event' => array( 'notFound' => "这个世界事件ä¸å­˜åœ¨ã€‚", - 'start' => "开始", - 'end' => "结æŸ", + 'start' => "开始:", + 'end' => "结æŸï¼š", 'interval' => "é—´éš”", 'inProgress' => "事件正在进行中", 'category' => ["未分类", "节日", "循环", "PvP"] @@ -1271,22 +1271,22 @@ $lang = array( '_transfer' => '这个任务将被转æ¢åˆ°%s,如果你转移到%s。', 'questLevel' => "等级%s", 'requirements' => "è¦æ±‚", - 'reqMoney' => "需è¦é‡‘é’±", + 'reqMoney' => "需è¦é‡‘钱:%s", 'money' => "金钱", 'additionalReq' => "获得这个任务的é¢å¤–è¦æ±‚", 'reqRepWith' => 'ä½ %s的声望需è¦%s %s', 'reqRepMin' => "至少", 'reqRepMax' => "低于", 'progress' => "进行", - 'provided' => "æä¾›çš„", + 'provided' => "(æä¾›çš„)", 'providedItem' => "æä¾›çš„物å“", 'completion' => "完æˆ", 'description' => "æè¿°", - 'playerSlain' => "玩家被æ€", - 'profession' => "专业", - 'timer' => "计时器", - 'loremaster' => "åšå­¦è€…", - 'suggestedPl' => "建议玩家数", + 'playerSlain' => "玩家被æ€ï¼ˆ%d)", + 'profession' => "专业:", + 'timer' => "计时器:", + 'loremaster' => "åšå­¦è€…:", + 'suggestedPl' => "建议玩家数:%d", 'keepsPvpFlag' => "ä¿æŒä½ çš„PvP标记", 'daily' => "æ¯æ—¥", 'weekly' => "æ¯å‘¨", @@ -1307,16 +1307,17 @@ $lang = array( 'enabledByQ' => "å¯ç”¨è‡ª", 'enabledByQDesc'=> "åªæœ‰å½“这些任务中的一个活跃时,这个任务æ‰å¯ç”¨", 'gainsDesc' => "完æˆè¿™ä¸ªä»»åŠ¡åŽï¼Œä½ å°†èŽ·å¾—", - 'theTitle' => '头衔 "%s"', 'unavailable' => "这项任务已被标记为过时,无法获得或完æˆã€‚", 'experience' => "ç»éªŒ", 'expConvert' => "(或%s如果在等级%d完æˆï¼‰", 'expConvert2' => "%s如果在等级%d完æˆ", - 'chooseItems' => "ä½ å¯ä»¥ä»Žè¿™äº›å¥–励å“中选择一件", - 'receiveItems' => "你将得到", - 'receiveAlso' => "你还将得到", - 'spellCast' => "该法术将被施放在你身上", - 'spellLearn' => "你将学会", + 'rewardChoices' => "ä½ å¯ä»¥ä»Žè¿™äº›å¥–励å“中选择一件:", // REWARD_CHOICES + 'rewardItems' => "你将得到:", // REWARD_ITEMS_ONLY + 'rewardAlso' => "你还将得到:", // REWARD_ITEMS + 'rewardSpell' => "你将学会:", // REWARD_SPELL + 'rewardAura' => "该法术将被施放在你身上:", // REWARD_AURA + 'rewardTradeSkill'=>"你将学会如何制造:", // REWARD_TRADESKILL_SPELL + 'rewardTitle' => '你将获得头衔:"%s"', // REWARD_TITLE 'bonusTalents' => "%d天赋|4点数:点数;", 'spellDisplayed'=> ' (%s 已显示)', 'questPoolDesc' => 'æ¯æ¬¡åªèƒ½åŒæ—¶æä¾› %d 个任务', @@ -1443,8 +1444,8 @@ $lang = array( 'mailDelivery' => '你将收到 è¿™å°ä¿¡%s%s', // "你会收到这å°ä¿¡%s%s", 'mailBy' => 'å‘件人:%s', 'mailIn' => "在 %s åŽ", - 'delay' => "延迟", - 'sender' => "寄件人", + 'delay' => "延迟:%s", + 'sender' => "寄件人:%s", 'untitled' => "无标题邮件 #%d" ), 'pet' => array( diff --git a/pages/quests.php b/pages/quests.php deleted file mode 100644 index a69f6210..00000000 --- a/pages/quests.php +++ /dev/null @@ -1,125 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->validCats = Game::$questClasses; // not allowed to set this as default - - $this->getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new QuestListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); - - $this->name = Util::ucFirst(Lang::game('quests')); - $this->subCat = $pageParam ? '='.$pageParam : ''; - } - - protected function generateContent() - { - $conditions = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if (isset($this->category[1])) - $conditions[] = ['zoneOrSort', $this->category[1]]; - else if (isset($this->category[0])) - $conditions[] = ['zoneOrSort', $this->validCats[$this->category[0]]]; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $quests = new QuestList($conditions, ['extraOpts' => $this->filterObj->extraOpts, 'calcTotal' => true]); - - $this->extendGlobalData($quests->getJSGlobals()); - - $tabData = ['data' => array_values($quests->getListviewData())]; - - if ($rCols = $this->filterObj->fiReputationCols) // never use pretty-print - $tabData['extraCols'] = '$fi_getReputationCols('.Util::toJSON($rCols, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE).')'; - else if ($this->filterObj->fiExtraCols) - $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; - - // create note if search limit was exceeded - if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - else if (isset($this->category[1]) && $this->category[1] > 0) - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questgivers, '.$this->category[1].', g_zones['.$this->category[1].'], '.$this->category[1].')'; - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = [QuestList::$brickFile, $tabData]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - if (isset($this->category[1])) - array_unshift($this->title, Lang::quest('cat', $this->category[0], $this->category[1])); - else if (isset($this->category[0])) - { - $c0 = Lang::quest('cat', $this->category[0]); - array_unshift($this->title, is_array($c0) ? $c0[0] : $c0); - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - - $hubs = array( - // Quest Hubs - 3679 => 3519, 4024 => 3537, 25 => 46, 1769 => 361, - // Startzones: Horde - 132 => 1, 9 => 12, 3431 => 3430, 154 => 85, - // Startzones: Alliance - 3526 => 3524, 363 => 14, 220 => 215, 188 => 141, - // Group: Caverns of Time - 2366 => 1941, 2367 => 1941, 4100 => 1941, - // Group: Hellfire Citadell - 3562 => 3535, 3713 => 3535, 3714 => 3535, - // Group: Auchindoun - 3789 => 3688, 3790 => 3688, 3791 => 3688, 3792 => 3688, - // Group: Tempest Keep - 3847 => 3842, 3848 => 3842, 3849 => 3842, - // Group: Coilfang Reservoir - 3715 => 3905, 3716 => 3905, 3717 => 3905, - // Group: Icecrown Citadel - 4809 => 4522, 4813 => 4522, 4820 => 4522 - ); - - if (isset($this->category[1]) && isset($hubs[$this->category[1]])) - array_splice($this->path, 3, 0, $hubs[$this->category[1]]); - } -} - -?> diff --git a/pages/zone.php b/pages/zone.php index 0469ac78..03020585 100644 --- a/pages/zone.php +++ b/pages/zone.php @@ -592,7 +592,7 @@ class ZonePage extends GenericPage { $tabData = ['data' => array_values($questsLV)]; - foreach (Game::$questClasses as $parent => $children) + foreach (Game::QUEST_CLASSES as $parent => $children) { if (!in_array($this->typeId, $children)) continue; diff --git a/setup/tools/filegen/profiler.ss.php b/setup/tools/filegen/profiler.ss.php index 8adf0f32..576d057b 100644 --- a/setup/tools/filegen/profiler.ss.php +++ b/setup/tools/filegen/profiler.ss.php @@ -63,7 +63,7 @@ CLISetup::registerSetup("build", new class extends SetupScript [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_DUNGEON_FINDER | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0] ]; - foreach (Game::$questClasses as $cat2 => $cat) + foreach (Game::QUEST_CLASSES as $cat2 => $cat) { if ($cat2 < 0) continue; diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index 6343a1f8..95c1a2d7 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
      @@ -17,7 +20,7 @@
      brick('redButtons'); ?> -

      name; ?>

      +

      h1; ?>

      unavailable): ?>
      @@ -35,80 +38,61 @@ elseif ($this->offerReward): echo $this->offerReward."\n"; endif; +$iconOffset = 0; if ($this->end || $this->objectiveList): ?> objectiveList as [$type, $data]): + switch ($type): + case 1: // just text line + echo ' \n"; + break; + case 2: // proxy npc data + ['id' => $id, 'text' => $text, 'qty' => $qty, 'proxy' => $proxies] = $data; + echo ' \n"; + break; + default: // has icon set (spell / item / ...) or unordered linked list + echo $data->renderContainer(20, $iconOffset, true); + endswitch; + endforeach; + if ($this->end): echo " \n"; endif; - if ($o = $this->objectiveList): - foreach ($o as $i => $ol): - if (isset($ol['text'])): - echo ' \n"; - elseif (!empty($ol['proxy'])): // this implies creatures - echo ' \n"; - elseif (isset($ol['typeStr'])): - if (in_array($ol['typeStr'], ['item', 'spell'])): - echo ' '; - else /* if (in_array($ol['typeStr'], ['npc', 'object', 'faction'])) */: - echo ' '; - endif; - - echo '\n"; - endif; - endforeach; - endif; - if ($this->suggestedPl): - echo ' \n"; + echo ' \n"; endif; ?>

       

      '.$data."

       

      '.$text.''.($qty ? ' ('.$qty.')' : '').'
      \n"; + foreach ($proxies as $block): + echo "
      \n"; + foreach ($block as $pId => $pName): + echo ' \n"; + endforeach; + echo "
      •  
      '.$pName."
      \n"; + endforeach; + echo "

       

      ".$this->end."

       

      '.$ol['text']."

       

      '.$ol['name'].$ol['extraText'].''.($ol['qty'] > 1 ? ' ('.$ol['qty'].')' : null).'
      \n"; - - $block1 = array_slice($ol['proxy'], 0, ceil(count($ol['proxy']) / 2), true); - $block2 = array_slice($ol['proxy'], ceil(count($ol['proxy']) / 2), null, true); - - echo "
      \n"; - foreach ($block1 as $pId => $name): - echo ' \n"; - endforeach; - echo "
      •  
      '.$name."
      \n"; - - if ($block2): // may be empty - echo "
      \n"; - foreach ($block2 as $pId => $name): - echo ' \n"; - endforeach; - echo "
      •  
      '.$name."
      \n"; - endif; - - echo "
      •  
      '.$ol['name'].''.($ol['extraText']).(!empty($ol['qty']) ? ' ('.$ol['qty'].')' : null)."

       

      '.Lang::quest('suggestedPl').Lang::main('colon').$this->suggestedPl."

       

      '.Lang::quest('suggestedPl', [$this->suggestedPl])."
      providedItem): - echo "
      \n"; - echo ' '.Lang::quest('providedItem').Lang::main('colon')."\n"; - echo " \n"; - echo ' '; - echo '\n"; + if ($this->providedItem): ?> +
      + +
      '.$p['name'].''.($p['qty'] ? ' ('.$ol['qty'].')' : null)."
      + providedItem->renderContainer(20, $iconOffset, true); ?>
      offerReward && ($this->requestItems || $this->objectives)): rewards): +if ([$spells, $items, $choice, $money] = $this->rewards): echo '

      '.Lang::main('rewards')."

      \n"; - if (!empty($r['choice'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('chooseItems'), 'rewards' => $r['choice'], 'offset' => $offset]); - $offset += count($r['choice']); + if ($choice): + $this->brick('rewards', ['rewTitle' => Lang::quest('rewardChoices'), 'rewards' => $choice, 'offset' => $iconOffset]); + $iconOffset += count($choice); endif; - if (!empty($r['spells'])): - if (!empty($r['choice'])): + if ($spells): + if ($choice): echo "
      \n"; endif; - if (!empty($r['spells']['learn'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('spellLearn'), 'rewards' => $r['spells']['learn'], 'offset' => $offset, 'extra' => $r['spells']['extra']]); - $offset += count($r['spells']['learn']); - elseif (!empty($r['spells']['cast'])): - $this->brick('rewards', ['rewTitle' => Lang::quest('spellCast'), 'rewards' => $r['spells']['cast'], 'offset' => $offset, 'extra' => $r['spells']['extra']]); - $offset += count($r['spells']['cast']); - endif; + $this->brick('rewards', ['rewTitle' => $spells['title'], 'rewards' => $spells['cast'], 'offset' => $iconOffset, 'extra' => $spells['extra']]); + $iconOffset += count($spells['cast']); endif; - if (!empty($r['items']) || !empty($r['money'])): - if (!empty($r['choice']) || !empty($r['spells'])): + if ($items || $money): + if ($choice || $spells): echo "
      \n"; endif; - $addData = ['rewards' => !empty($r['items']) ? $r['items'] : null, 'offset' => $offset, 'extra' => !empty($r['money']) ? $r['money'] : null]; - $addData['rewTitle'] = empty($r['choice']) ? Lang::quest('receiveItems') : Lang::quest('receiveAlso'); - - $this->brick('rewards', $addData); + $this->brick('rewards', array( + 'rewTitle' => $choice ? Lang::quest('rewardAlso') : Lang::quest('rewardItems'), + 'rewards' => $items ?: null, + 'offset' => $iconOffset, + 'extra' => $money ?: null + )); endif; endif; -if ($g = $this->gains): - echo '

      '.Lang::main('gains')."

      \n"; - echo ' '.Lang::quest('gainsDesc').Lang::main('colon')."\n"; - echo "
        \n"; - - if (!empty($g['xp'])): - echo '
      • '.Lang::nf($g['xp']).' '.Lang::quest('experience')."
      • \n"; +if ([$xp, $rep, $title, $tp] = $this->gains): +?> +

        + +
          +
          '.Lang::nf($xp).' '.Lang::quest('experience')."
          \n"; endif; - if (!empty($g['rep'])): - foreach ($g['rep'] as $r): - if ($r['qty'][1] && User::isInGroup(U_GROUP_EMPLOYEE)) - $qty = $r['qty'][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($r['qty'][1] > 0 ? '+' : '').$r['qty'][1]); - else - $qty = array_sum($r['qty']); - - echo '
        • '.($r['qty'][0] < 0 ? ''.$qty.'' : $qty).' '.Lang::npc('repWith').' '.$r['name']."
        • \n"; + if ($rep): + foreach ($rep as $r): + echo '
        • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name']."
        • \n"; endforeach; endif; - if (!empty($g['title'])): - echo '
        • '.Lang::quest('theTitle', [$g['title']])."
        • \n"; + if ($title): + echo '
        • '.Lang::quest('rewardTitle', $title)."
        • \n"; endif; - if (!empty($g['tp'])): - echo '
        • '.Lang::quest('bonusTalents', [$g['tp']])."
        • \n"; + if ($tp): + echo '
        • '.Lang::quest('bonusTalents', [$tp])."
        • \n"; endif; echo "
        \n"; endif; -$this->brick('mail', ['offset' => ++$offset]); +$this->brickIf($this->mail, 'mail', ['offset' => ++$iconOffset]); -if (!empty($this->transfer)): +if ($this->transfer): echo "
        "; echo "
        \n ".$this->transfer."\n"; endif; @@ -213,7 +189,7 @@ endif;
      brick('lvTabs', ['relTabs' => true]); +$this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index ef5fca5b..e7de3d19 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -1,10 +1,11 @@ - - brick('header'); -$f = $this->filterObj->values // shorthand -?> + namespace Aowow\Template; + use Aowow\Lang; + +$this->brick('header'); +$f = $this->filter->values; // shorthand +?>
      @@ -12,41 +13,44 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [3]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); ?> - -
      +
      +
      +brick('headIcons'); + +$this->brick('redButtons'); +?> +

      h1; ?>

      +
      - + - + @@ -54,11 +58,7 @@ endforeach;
      ucFirst(Lang::main('name')).Lang::main('colon'); ?> - - + +
       />  /> />  />
       /> - /> /> - /> - +
          /> - /> /> - />
       
      @@ -66,7 +66,7 @@ endforeach;
      - /> /> + /> />
      @@ -80,7 +80,7 @@ endforeach;
      -brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From d66a863f55fc5f93192bb207ac26220e4eae9109 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 18:07:35 +0200 Subject: [PATCH 0967/1249] Template/Update (Part 32) * convert dbtype 'event' --- endpoints/event/event.php | 337 ++++++++++++++++++++++++ endpoints/event/event_power.php | 79 ++++++ endpoints/events/events.php | 96 +++++++ includes/dbtypes/worldevent.class.php | 11 +- localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- localization/locale_zhcn.php | 2 +- pages/event.php | 365 -------------------------- pages/events.php | 109 -------- 12 files changed, 519 insertions(+), 490 deletions(-) create mode 100644 endpoints/event/event.php create mode 100644 endpoints/event/event_power.php create mode 100644 endpoints/events/events.php delete mode 100644 pages/event.php delete mode 100644 pages/events.php diff --git a/endpoints/event/event.php b/endpoints/event/event.php new file mode 100644 index 00000000..5e1530b8 --- /dev/null +++ b/endpoints/event/event.php @@ -0,0 +1,337 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new WorldEventList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('event'), Lang::event('notFound')); + + $this->h1 = $this->subject->getField('name', true); + $this->dates = array( + 'firstDate' => $this->subject->getField('startTime'), + 'lastDate' => $this->subject->getField('endTime'), + 'length' => $this->subject->getField('length'), + 'rec' => $this->subject->getField('occurence') + ); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_holidayId = $this->subject->getField('holidayId'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = match ($this->subject->getField('scheduleType')) + { + -1 => 1, + 0, 1 => 2, + 2 => 3, + '' => 0, + default => 0 + }; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucWords(Lang::game('event'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // boss + if ($_ = $this->subject->getField('bossCreature')) + { + $this->extendGlobalIds(Type::NPC, $_); + $infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; + } + + // display internal id to staff + if (User::isInGroup(U_GROUP_STAFF)) + $infobox[] = 'Event-Id'.Lang::main('colon').$this->typeId; + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + + /****************/ + /* Main Content */ + /****************/ + + // no entry in ?_articles? use default HolidayDescription + if ($_holidayId && empty($this->article)) + $this->article = new Markup($this->subject->getField('description', true), ['dbpage' => true]); + + if ($_holidayId) + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'event=', $_holidayId); + + $this->headIcons = [$this->subject->getField('iconString')]; + $this->redButtons = array( + BUTTON_WOWHEAD => $_holidayId > 0, + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] + ); + + parent::generate(); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: npcs + if ($npcIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(ec.`eventEntry` > 0, 1, 0) AS "added" FROM creature c, game_event_creature ec WHERE ec.`guid` = c.`guid` AND ABS(ec.`eventEntry`) = ?d', $this->typeId)) + { + $creatures = new CreatureList(array(['id', array_keys($npcIds)])); + if (!$creatures->error) + { + $data = $creatures->getListviewData(); + foreach ($data as &$d) + $d['method'] = $npcIds[$d['id']]; + + $tabData = ['data' => $data]; + + if ($_holidayId && CreatureListFilter::getCriteriaIndex(38, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=38;crs='.$_holidayId.';crv=0'); + + $this->result->addDataLoader('zones'); // req. by secondary tooltip in this tab + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); + } + } + + // tab: objects + if ($objectIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(eg.`eventEntry` > 0, 1, 0) AS "added" FROM gameobject g, game_event_gameobject eg WHERE eg.`guid` = g.`guid` AND ABS(eg.`eventEntry`) = ?d', $this->typeId)) + { + $objects = new GameObjectList(array(['id', array_keys($objectIds)])); + if (!$objects->error) + { + $data = $objects->getListviewData(); + foreach ($data as &$d) + $d['method'] = $objectIds[$d['id']]; + + $tabData = ['data' => $data]; + + if ($_holidayId && GameObjectListFilter::getCriteriaIndex(16, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=16;crs='.$_holidayId.';crv=0'); + + $this->result->addDataLoader('zones'); // req. by secondary tooltip in this tab + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + } + } + + // tab: achievements + if ($_ = $this->subject->getField('achievementCatOrId')) + { + $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; + $acvs = new AchievementList($condition); + if (!$acvs->error) + { + $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $tabData = array( + 'data' => $acvs->getListviewData(), + 'visibleCols' => ['category'] + ); + + if ($_holidayId && AchievementListFilter::getCriteriaIndex(11, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?achievements&filter=cr=11;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, AchievementList::$brickFile)); + } + } + + $itemCnd = []; + if ($_holidayId) + { + $itemCnd = array( + 'OR', + ['eventId', $this->typeId], // direct requirement on item + ); + + // tab: quests (by table, go & creature) + $quests = new QuestList(array(['eventId', $this->typeId])); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $tabData = ['data'=> $quests->getListviewData()]; + + if (QuestListFilter::getCriteriaIndex(33, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); + + $questItems = []; + foreach (array_column($quests->rewards, Type::ITEM) as $arr) + $questItems = array_merge($questItems, array_keys($arr)); + + foreach (array_column($quests->choices, Type::ITEM) as $arr) + $questItems = array_merge($questItems, array_keys($arr)); + + foreach (array_column($quests->requires, Type::ITEM) as $arr) + $questItems = array_merge($questItems, $arr); + + if ($questItems) + $itemCnd[] = ['id', $questItems]; + } + } + + // items from creature + if ($npcIds && !$creatures->error) + { + // vendor + $cIds = $creatures->getFoundIDs(); + if ($sells = DB::World()->selectCol( + 'SELECT `item` FROM npc_vendor nv WHERE `entry` IN (?a) UNION + SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` IN (?a) UNION + SELECT `item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` IN (?a)', + $cIds, $cIds, $cIds + )) + $itemCnd[] = ['id', $sells]; + } + + // tab: items + // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot + if ($itemCnd) + { + $eventItems = new ItemList($itemCnd); + if (!$eventItems->error) + { + $this->extendGlobalData($eventItems->getJSGlobals(GLOBALINFO_SELF)); + + $tabData = ['data'=> $eventItems->getListviewData()]; + + if ($_holidayId && ItemListFilter::getCriteriaIndex(160, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=160;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + } + + // tab: see also (event conditions) + if ($rel = DB::World()->selectCol('SELECT IF(`eventEntry` = `prerequisite_event`, NULL, IF(`eventEntry` = ?d, `prerequisite_event`, -`eventEntry`)) FROM game_event_prerequisite WHERE `prerequisite_event` = ?d OR `eventEntry` = ?d', $this->typeId, $this->typeId, $this->typeId)) + { + if (array_filter($rel, fn($x) => $x === null)) + trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); + + if ($seeAlso = array_filter($rel, fn($x) => $x > 0)) + { + $relEvents = new WorldEventList(array(['id', $seeAlso])); + $this->extendGlobalData($relEvents->getJSGlobals()); + $relData = $relEvents->getListviewData(); + foreach ($relEvents->getFoundIDs() as $id) + Conditions::extendListviewRow($relData[$id], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $this->typeId]); + + $this->extendGlobalData($this->subject->getJSGlobals()); + $d = $this->subject->getListviewData(); + foreach ($rel as $r) + if ($r > 0) + if (Conditions::extendListviewRow($d[$this->typeId], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $r])) + $this->extendGlobalIds(Type::WORLDEVENT, $r); + + $tabData = array( + 'data' => array_merge($relData, $d), + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso', + 'hiddenCols' => ['date'], + 'extraCols' => ['$Listview.extraCols.condition'] + ); + $this->lvTabs->addListviewTab(new Listview($tabData, WorldEventList::$brickFile)); + } + } + + // tab: condition for + $cnd = new Conditions(); + $cnd->getByCondition(Type::WORLDEVENT, $this->typeId)->prepare(); + if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + } + + // update dates to now() + public static function tabsHook(Template\PageTemplate &$pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if (is_object($listview) && $listview?->getTemplate() == 'holiday') + WorldEventList::updateListview($listview); + } + + /* finalize infobox */ + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$markup) : void + { + WorldEventList::updateDates($pt->dates, $start, $end, $rec); + $infobox = []; + + // start + if ($start) + $infobox[] = Lang::event('start').date(Lang::main('dateFmtLong'), $start); + + // end + if ($end) + $infobox[] = Lang::event('end').date(Lang::main('dateFmtLong'), $end); + + // interval + if ($rec > 0) + $infobox[] = Lang::event('interval').Util::formatTime($rec * 1000); + + // in progress + if ($start < time() && $end > time()) + $infobox[] = '[span class=q2]'.Lang::event('inProgress').'[/span]'; + + if ($infobox && !$markup) + $markup = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + else if ($markup) + foreach ($infobox as $ib) + $markup->addItem($ib); + } +} + +?> diff --git a/endpoints/event/event_power.php b/endpoints/event/event_power.php new file mode 100644 index 00000000..48c75683 --- /dev/null +++ b/endpoints/event/event_power.php @@ -0,0 +1,79 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private array $dates = []; + + public function __construct(string $id) + { + parent::__construct($id); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + $this->typeId = intVal($id); + } + + protected function generate() : void + { + $worldevent = new WorldEventList(array(['id', $this->typeId])); + if ($worldevent->error) + $this->cacheType = CACHE_TYPE_NONE; + else + { + $icon = $worldevent->getField('iconString'); + if ($icon == 'trade_engineering') + $icon = null; + + $opts = array( + 'name' => $worldevent->getField('name', true), + 'tooltip' => $worldevent->renderTooltip(), + 'icon' => $icon + ); + + $this->dates = array( + 'firstDate' => $worldevent->getField('startTime'), + 'lastDate' => $worldevent->getField('endTime'), + 'length' => $worldevent->getField('length'), + 'rec' => $worldevent->getField('occurence') + ); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay'], $this->dates); + } + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->typeId, $opts ?? []); + } + + public static function onBeforeDisplay(string $tooltip, array $dates) : string + { + // update dates to now() + WorldEventList::updateDates($dates, $start, $end); + + return sprintf( + $tooltip, + $start ? date(Lang::main('dateFmtLong'), $start) : null, + $end ? date(Lang::main('dateFmtLong'), $end) : null + ); + } +} + +?> diff --git a/endpoints/events/events.php b/endpoints/events/events.php new file mode 100644 index 00000000..2ae4a9b9 --- /dev/null +++ b/endpoints/events/events.php @@ -0,0 +1,96 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('events')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::event('category')[$this->category[0]]); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $condition = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + $condition[] = match ($this->category[0]) + { + 1 => ['h.scheduleType', -1], + 2 => ['h.scheduleType', [0, 1]], + 3 => ['h.scheduleType', 2], + default => ['e.holidayId', 0] // also cat 0 + }; + + $events = new WorldEventList($condition); + $this->extendGlobalData($events->getJSGlobals()); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $events->getListviewData()], WorldEventList::$brickFile)); + + if ($_ = array_filter($events->getListviewData(), fn($x) => $x['category'] > 0)) + $this->lvTabs->addListviewTab(new Listview(['data' => $_, 'hideCount' => 1], 'calendar')); + + parent::generate(); + + $this->result->registerDisplayHook('lvTabs', [self::class, 'tabsHook']); + } + + // recalculate dates with now() + public static function tabsHook(Template\PageTemplate &$pt, Tabs &$lvTabs) : void + { + foreach ($lvTabs->iterate() as &$listview) + if (is_object($listview) && ($listview?->getTemplate() == 'holiday' || $listview?->getTemplate() == 'holidaycal')) + WorldEventList::updateListview($listview); + } +} + +?> diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php index 18ba67c6..831b57c3 100644 --- a/includes/dbtypes/worldevent.class.php +++ b/includes/dbtypes/worldevent.class.php @@ -113,7 +113,7 @@ class WorldEventList extends DBTypeList } } - public function getListviewData(bool $forNow = false) : array + public function getListviewData() : array { $data = []; @@ -132,15 +132,6 @@ class WorldEventList extends DBTypeList ); } - if ($forNow) - { - foreach ($data as &$d) - { - self::updateDates($d['_date'], $d['startDate'], $d['endDate'], $d['rec']); - unset($d['_date']); - } - } - return $data; } diff --git a/localization/locale_dede.php b/localization/locale_dede.php index ccf542bb..9369227d 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Dieses Weltereignis existiert nicht.", 'start' => "Anfang: ", 'end' => "Ende: ", - 'interval' => "Intervall", + 'interval' => "Intervall: ", 'inProgress' => "Ereignis findet gerade statt", 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] ), diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e250a297..4628acc2 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "This world event doesn't exist.", 'start' => "Start: ", 'end' => "End: ", - 'interval' => "Interval", + 'interval' => "Interval: ", 'inProgress' => "Event is currently in progress", 'category' => ["Uncategorized", "Holidays", "Recurring", "Player vs. Player"] ), diff --git a/localization/locale_eses.php b/localization/locale_eses.php index b38316b9..82ba9a10 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Este evento del mundo no existe.", 'start' => "Empieza: ", 'end' => "Termina: ", - 'interval' => "Intervalo", + 'interval' => "Intervalo: ", 'inProgress' => "El evento está en progreso actualmente", 'category' => ["Sin categoría", "Vacacionales", "Periódicos", "Jugador contra Jugador"] ), diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index bf728dd6..96e90a94 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Cet évènement mondial n'existe pas.", 'start' => "Début : ", 'end' => "Fin : ", - 'interval' => "Intervalle", + 'interval' => "Intervalle : ", 'inProgress' => "L'évènement est présentement en cours", 'category' => ["Non classés", "Vacances", "Récurrent", "Joueur ctr. Joueur"] ), diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 8adbaec2..930b776a 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1137,7 +1137,7 @@ $lang = array( 'notFound' => "Это игровое Ñобытие не ÑущеÑтвует.", 'start' => "Ðачало: ", 'end' => "Конец: ", - 'interval' => "[Interval]", + 'interval' => "[Interval]: ", 'inProgress' => "Событие активно в данный момент", 'category' => array("Разное", "Праздники", "ПериодичеÑкие", "PvP") ), diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 0ea4e318..a5c5912c 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1136,7 +1136,7 @@ $lang = array( 'notFound' => "这个世界事件ä¸å­˜åœ¨ã€‚", 'start' => "开始:", 'end' => "结æŸï¼š", - 'interval' => "é—´éš”", + 'interval' => "间隔:", 'inProgress' => "事件正在进行中", 'category' => ["未分类", "节日", "循环", "PvP"] ), diff --git a/pages/event.php b/pages/event.php deleted file mode 100644 index 2af3aefb..00000000 --- a/pages/event.php +++ /dev/null @@ -1,365 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\Locale::tryFromDomain']]; - - private $powerTpl = '$WowheadPower.registerHoliday(%d, %d, %s);'; - private $hId = 0; - private $eId = 0; - - public function __construct($pageCall, $id) - { - parent::__construct($pageCall, $id); - - // temp locale - if ($this->mode == CACHE_TYPE_TOOLTIP && $this->_get['domain']) - Lang::load($this->_get['domain']); - - $this->typeId = intVal($id); - - $this->subject = new WorldEventList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('event'), Lang::event('notFound')); - - $this->hId = $this->subject->getField('holidayId'); - $this->eId = $this->typeId; - $this->name = $this->subject->getField('name', true); - $this->dates = array( - 'firstDate' => $this->subject->getField('startTime'), - 'lastDate' => $this->subject->getField('endTime'), - 'length' => $this->subject->getField('length'), - 'rec' => $this->subject->getField('occurence') - ); - } - - protected function generatePath() - { - switch ($this->subject->getField('scheduleType')) - { - case '': $this->path[] = 0; break; - case -1: $this->path[] = 1; break; - case 0: - case 1: $this->path[] = 2; break; - case 2: $this->path[] = 3; break; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('event'))); - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); - - /***********/ - /* Infobox */ - /***********/ - - $this->infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - - // boss - if ($_ = $this->subject->getField('bossCreature')) - { - $this->extendGlobalIds(Type::NPC, $_); - $this->infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; - } - - // display internal id to staff - if (User::isInGroup(U_GROUP_STAFF)) - $this->infobox[] = 'Event-Id'.Lang::main('colon').$this->eId; - - /****************/ - /* Main Content */ - /****************/ - - // no entry in ?_articles? use default HolidayDescription - if ($this->hId && empty($this->article)) - $this->article = ['text' => Util::jsEscape($this->subject->getField('description', true)), 'params' => []]; - - $this->headIcons = [$this->subject->getField('iconString')]; - $this->redButtons = array( - BUTTON_WOWHEAD => $this->hId > 0, - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] - ); - - /**************/ - /* Extra Tabs */ - /**************/ - - $hasFilter = in_array($this->hId, [372, 283, 285, 353, 420, 400, 284, 201, 374, 409, 141, 324, 321, 424, 335, 327, 341, 181, 404, 398, 301]); - - // tab: npcs - if ($npcIds = DB::World()->selectCol('SELECT id AS ARRAY_KEY, IF(ec.eventEntry > 0, 1, 0) AS added FROM creature c, game_event_creature ec WHERE ec.guid = c.guid AND ABS(ec.eventEntry) = ?d', $this->eId)) - { - $creatures = new CreatureList(array(['id', array_keys($npcIds)])); - if (!$creatures->error) - { - $data = $creatures->getListviewData(); - foreach ($data as &$d) - $d['method'] = $npcIds[$d['id']]; - - $tabData = ['data' => array_values($data)]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=38;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [CreatureList::$brickFile, $tabData]; - } - } - - // tab: objects - if ($objectIds = DB::World()->selectCol('SELECT id AS ARRAY_KEY, IF(eg.eventEntry > 0, 1, 0) AS added FROM gameobject g, game_event_gameobject eg WHERE eg.guid = g.guid AND ABS(eg.eventEntry) = ?d', $this->eId)) - { - $objects = new GameObjectList(array(['id', array_keys($objectIds)])); - if (!$objects->error) - { - $data = $objects->getListviewData(); - foreach ($data as &$d) - $d['method'] = $objectIds[$d['id']]; - - $tabData = ['data' => array_values($data)]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=16;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [GameObjectList::$brickFile, $tabData]; - } - } - - // tab: achievements - if ($_ = $this->subject->getField('achievementCatOrId')) - { - $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; - $acvs = new AchievementList($condition); - if (!$acvs->error) - { - $this->extendGlobalData($acvs->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - $tabData = array( - 'data' => array_values($acvs->getListviewData()), - 'visibleCols' => ['category'] - ); - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?achievements&filter=cr=11;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [AchievementList::$brickFile, $tabData]; - } - } - - $itemCnd = []; - if ($this->hId) - { - $itemCnd = array( - 'OR', - ['eventId', $this->eId], // direct requirement on item - ); - - // tab: quests (by table, go & creature) - $quests = new QuestList(array(['eventId', $this->eId])); - if (!$quests->error) - { - $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - - $tabData = ['data'=> array_values($quests->getListviewData())]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [QuestList::$brickFile, $tabData]; - - $questItems = []; - foreach (array_column($quests->rewards, Type::ITEM) as $arr) - $questItems = array_merge($questItems, array_keys($arr)); - - foreach (array_column($quests->choices, Type::ITEM) as $arr) - $questItems = array_merge($questItems, array_keys($arr)); - - foreach (array_column($quests->requires, Type::ITEM) as $arr) - $questItems = array_merge($questItems, $arr); - - if ($questItems) - $itemCnd[] = ['id', $questItems]; - } - } - - // items from creature - if ($npcIds && !$creatures->error) - { - // vendor - $cIds = $creatures->getFoundIDs(); - if ($sells = DB::World()->selectCol( - 'SELECT `item` FROM npc_vendor nv WHERE `entry` IN (?a) UNION - SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_Vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` IN (?a) UNION - SELECT `item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` IN (?a)', - $cIds, $cIds, $cIds - )) - $itemCnd[] = ['id', $sells]; - } - - // tab: items - // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot - if ($itemCnd) - { - $eventItems = new ItemList($itemCnd); - if (!$eventItems->error) - { - $this->extendGlobalData($eventItems->getJSGlobals(GLOBALINFO_SELF)); - - $tabData = ['data'=> array_values($eventItems->getListviewData())]; - - if ($hasFilter) - $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=160;crs='.$this->hId.';crv=0'); - - $this->lvTabs[] = [ItemList::$brickFile, $tabData]; - } - } - - // tab: see also (event conditions) - if ($rel = DB::World()->selectCol('SELECT IF(eventEntry = prerequisite_event, NULL, IF(eventEntry = ?d, prerequisite_event, -eventEntry)) FROM game_event_prerequisite WHERE prerequisite_event = ?d OR eventEntry = ?d', $this->eId, $this->eId, $this->eId)) - { - $list = []; - array_walk($rel, function($v, $k) use (&$list) { - if ($v > 0) - $list[] = $v; - else if ($v === null) - trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); - }); - - if ($list) - { - $relEvents = new WorldEventList(array(['id', $list])); - $this->extendGlobalData($relEvents->getJSGlobals()); - $relData = $relEvents->getListviewData(); - foreach ($relEvents->getFoundIDs() as $id) - Conditions::extendListviewRow($relData[$id], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $this->eId]); - - $this->extendGlobalData($this->subject->getJSGlobals()); - $d = $this->subject->getListviewData(); - foreach ($rel as $r) - if ($r > 0) - if (Conditions::extendListviewRow($d[$this->eId], Conditions::SRC_NONE, $this->typeId, [-Conditions::ACTIVE_EVENT, $r])) - $this->extendGlobalIds(Type::WORLDEVENT, $r); - - $relData = array_merge($relData, $d); - - $this->lvTabs[] = [WorldEventList::$brickFile, array( - 'data' => array_values($relData), - 'id' => 'see-also', - 'name' => '$LANG.tab_seealso', - 'hiddenCols' => ['date'], - 'extraCols' => ['$Listview.extraCols.condition'] - )]; - } - } - - // tab: condition for - $cnd = new Conditions(); - $cnd->getByCondition(Type::WORLDEVENT, $this->typeId)->prepare(); - if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) - { - $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - } - - protected function generateTooltip() : string - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = $this->subject->getField('name', true); - - if ($this->subject->getField('iconString') != 'trade_engineering') - $power->icon = rawurlencode($this->subject->getField('iconString', true, true)); - - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - } - - return sprintf($this->powerTpl, $this->typeId, Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); - } - - protected function postCache() - { - // update dates to now() - WorldEventList::updateDates($this->dates, $start, $end, $rec); - - if ($this->mode == CACHE_TYPE_TOOLTIP) - { - return array( - date(Lang::main('dateFmtLong'), $start), - date(Lang::main('dateFmtLong'), $end) - ); - } - else - { - if ($this->hId) - $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), 'event', $this->hId); - - /********************/ - /* finalize infobox */ - /********************/ - - // start - if ($start) - array_push($this->infobox, Lang::event('start').Lang::main('colon').date(Lang::main('dateFmtLong'), $start)); - - // end - if ($end) - array_push($this->infobox, Lang::event('end').Lang::main('colon').date(Lang::main('dateFmtLong'), $end)); - - // occurence - if ($rec > 0) - array_push($this->infobox, Lang::event('interval').Lang::main('colon').Util::formatTime($rec * 1000)); - - // in progress - if ($start < time() && $end > time()) - array_push($this->infobox, '[span class=q2]'.Lang::event('inProgress').'[/span]'); - - $this->infobox = '[ul][li]'.implode('[/li][li]', $this->infobox).'[/li][/ul]'; - - /***************************/ - /* finalize related events */ - /***************************/ - - foreach ($this->lvTabs as &$view) - { - if ($view[0] != WorldEventList::$brickFile) - continue; - - foreach ($view[1]['data'] as &$data) - { - WorldEventList::updateDates($data['_date'], $start, $end, $rec); - unset($data['_date']); - $data['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : false; - $data['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : false; - $data['rec'] = $rec; - } - } - } - } -} - -?> diff --git a/pages/events.php b/pages/events.php deleted file mode 100644 index 4ed714c1..00000000 --- a/pages/events.php +++ /dev/null @@ -1,109 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('events')); - } - - protected function generateContent() - { - $condition = []; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - switch ($this->category[0]) - { - case 0: $condition[] = ['e.holidayId', 0]; break; - case 1: $condition[] = ['h.scheduleType', -1]; break; - case 2: $condition[] = ['h.scheduleType', [0, 1]]; break; - case 3: $condition[] = ['h.scheduleType', 2]; break; - } - } - - $events = new WorldEventList($condition); - $this->extendGlobalData($events->getJSGlobals()); - - foreach ($events->iterate() as $__) - if ($d = $events->getField('requires')) - $this->dependency[$events->id] = $d; - - $data = array_values($events->getListviewData()); - - $this->lvTabs[] = [WorldEventList::$brickFile, ['data' => $data]]; - - if ($_ = array_values(array_filter($data, function($x) {return $x['category'] > 0;}))) - { - $this->lvTabs[] = ['calendar', array( - 'data' => $_, - 'hideCount' => 1 - )]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if ($this->category) - array_unshift($this->title, Lang::event('category')[$this->category[0]]); - } - - protected function generatePath() - { - if ($this->category) - $this->path[] = $this->category[0]; - } - - protected function postCache() - { - // recalculate dates with now() - foreach ($this->lvTabs as &$views) - { - foreach ($views[1]['data'] as &$data) - { - // is a followUp-event - if (!empty($this->dependency[$data['id']])) - { - $data['startDate'] = $data['endDate'] = false; - unset($data['_date']); - continue; - } - - WorldEventList::updateDates($data['_date'], $start, $end, $rec); - unset($data['_date']); - $data['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : false; - $data['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : false; - $data['rec'] = $rec; - } - } - } -} - -?> From 3f7f522d5087e2fd5de50e0e43ee0322d3995580 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 18:49:27 +0200 Subject: [PATCH 0968/1249] Template/Update (Part 33) * convert dbtype 'zone' --- {pages => endpoints/zone}/zone.php | 377 ++++++++++++++--------------- endpoints/zones/zones.php | 188 ++++++++++++++ pages/zones.php | 185 -------------- 3 files changed, 374 insertions(+), 376 deletions(-) rename {pages => endpoints/zone}/zone.php (72%) create mode 100644 endpoints/zones/zones.php delete mode 100644 pages/zones.php diff --git a/pages/zone.php b/endpoints/zone/zone.php similarity index 72% rename from pages/zone.php rename to endpoints/zone/zone.php index 03020585..548b4a0d 100644 --- a/pages/zone.php +++ b/endpoints/zone/zone.php @@ -6,39 +6,68 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 6: Zone g_initPath() -// tabId 0: Database g_initHeader() -class ZonePage extends GenericPage +class ZoneBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $path = [0, 6]; - protected $tabId = 0; - protected $type = Type::ZONE; - protected $typeId = 0; - protected $tpl = 'detail-page-generic'; - protected $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js']]; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $zoneMusic = []; + protected string $template = 'detail-page-generic'; + protected string $pageName = 'zone'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 6]; - public function __construct($pageCall, $id) + protected array $dataLoader = ['zones']; + protected array $scripts = [[SC_JS_FILE, 'js/ShowOnMap.js']]; + + public int $type = Type::ZONE; + public int $typeId = 0; + public array $zoneMusic = []; + public ?string $expansion = null; + + private ZoneList $subject; + + public function __construct(string $id) { - $this->typeId = intVal($id); + parent::__construct($id); - parent::__construct($pageCall, $id); - - $this->subject = new ZoneList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('zone'), Lang::zone('notFound')); - - $this->name = $this->subject->getField('name', true); + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; } - protected function generateContent() + protected function generate() : void { - $this->addScript([SC_JS_FILE, '?data=zones']); + $this->subject = new ZoneList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('zone'), Lang::zone('notFound')); - $parentArea = $this->subject->getField('parentArea'); + $this->h1 = $this->subject->getField('name', true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + $_parentArea = $this->subject->getField('parentArea'); + $_type = $this->subject->getField('type'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('category'); + + if (in_array($this->subject->getField('category'), [MAP_TYPE_DUNGEON, MAP_TYPE_RAID])) + $this->breadcrumb[] = $this->subject->getField('expansion'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('zone'))); /***********/ @@ -63,11 +92,11 @@ class ZonePage extends GenericPage $infobox = array_merge($infobox, $topRows); // City - if ($this->subject->getField('flags') & 0x8 && !$parentArea) + if ($this->subject->getField('flags') & AREA_FLAG_SLAVE_CAPITAL && !$_parentArea) $infobox[] = Lang::zone('city'); // Auto repop - if ($this->subject->getField('flags') & 0x1000 && !$parentArea) + if ($this->subject->getField('flags') & AREA_FLAG_NEED_FLY && !$_parentArea) $infobox[] = Lang::zone('autoRez'); // Level @@ -83,7 +112,7 @@ class ZonePage extends GenericPage if ($_ = $this->subject->getField('levelReq')) { if ($__ = $this->subject->getField('levelReqLFG')) - $buff = sprintf(Lang::zone('reqLevels'), $_, $__); + $buff = Lang::zone('reqLevels', [$_, $__]); else $buff = Lang::main('_reqLevel').Lang::main('colon').$_; @@ -92,12 +121,12 @@ class ZonePage extends GenericPage // Territory $faction = $this->subject->getField('faction'); - $wrap = match ((int)$faction) + $wrap = match ($faction) { - 0 => '[span class=icon-alliance]%s[/span]', - 1 => '[span class=icon-horde]%s[/span]', - 4, 5 => '[span class=icon-ffa]%s[/span]', - default => '%s' + TEAM_ALLIANCE => '[span class=icon-alliance]%s[/span]', + TEAM_HORDE => '[span class=icon-horde]%s[/span]', + 4, 5 => '[span class=icon-ffa]%s[/span]', + default => '%s' }; $infobox[] = Lang::zone('territory').sprintf($wrap, Lang::zone('territories', $faction)); @@ -132,6 +161,8 @@ class ZonePage extends GenericPage $infobox[] = Lang::concat($_, Lang::CONCAT_NONE, fn($x) => '[race='.$x.']').' '.Lang::race('startZone'); } + parent::generate(); // calls applyGlobals .. probably too early here, but addMoveLocationMenu requires PageTemplate to be initialized + // location (if instance) if ($pa = DB::Aowow()->selectRow('SELECT `areaId`, `posX`, `posY`, `floor` FROM ?_spawns WHERE `type`= ?d AND `typeId` = ?d ', Type::ZONE, $this->typeId)) { @@ -160,12 +191,15 @@ class ZonePage extends GenericPage if ($botRows = array_filter($quickFactsRows, fn($x) => $x > 0, ARRAY_FILTER_USE_KEY)) $infobox = array_merge($infobox, $botRows); + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + /****************/ /* Main Content */ /****************/ - $addToSOM = function ($what, $entry) use (&$som) + $addToSOM = function (string $what, array $entry) use (&$som) : void { // entry always contains: type, id, name, level, coords[] if (!isset($som[$what][$entry['name']])) // not found yet @@ -188,10 +222,10 @@ class ZonePage extends GenericPage } }; - if ($parentArea) + if ($_parentArea) { - $this->extraText = sprintf(Lang::zone('zonePartOf'), $parentArea); - $this->extendGlobalIds(Type::ZONE, $parentArea); + $this->extraText = new Markup(Lang::zone('zonePartOf', [$_parentArea]), ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + $this->extendGlobalIds(Type::ZONE, $_parentArea); } // we cannot fetch spawns via lists. lists are grouped by entry @@ -219,13 +253,22 @@ class ZonePage extends GenericPage } // see if we can actually display a map - $hasMap = file_exists('static/images/wow/maps/'.Lang::getLocale()->json().'/normal/'.$this->typeId.'.jpg'); - if (!$hasMap) // try multilayered - $hasMap = file_exists('static/images/wow/maps/'.Lang::getLocale()->json().'/normal/'.$this->typeId.'-1.jpg'); - if (!$hasMap) // try english fallback - $hasMap = file_exists('static/images/wow/maps/enus/normal/'.$this->typeId.'.jpg'); - if (!$hasMap) // try english fallback, multilayered - $hasMap = file_exists('static/images/wow/maps/enus/normal/'.$this->typeId.'-1.jpg'); + $mapFilePath = 'static/images/wow/maps/%s/normal/%d%s.jpg'; + $options = array( + [Lang::getLocale()->json(), ''], // default case + [Lang::getLocale()->json(), '-1'], // try multifloor + ['enus', ''], // try english fallback + ['enus', '-1'] // try english fallback, multifloor + ); + $hasMap = false; + foreach ($options as [$lang, $floor]) + { + if (!file_exists(sprintf($mapFilePath, $lang, $this->typeId, $floor))) + continue; + + $hasMap = true; + break; + } if ($hasMap) { @@ -238,33 +281,16 @@ class ZonePage extends GenericPage $n = Util::localizedString($tpl, 'name'); - $what = ''; - switch ($tpl['typeCat']) + $what = match ((int)$tpl['typeCat']) { - case -3: - $what = 'herb'; - break; - case -4: - $what = 'vein'; - break; - case 9: - $what = 'book'; - break; - case 25: - $what = 'pool'; - break; - case 0: - if ($tpl['type'] == 19) - $what = 'mail'; - break; - case -6: - if ($tpl['spellFocusId'] == 1) - $what = 'anvil'; - else if ($tpl['spellFocusId'] == 3) - $what = 'forge'; - - break; - } + -3 => 'herb', + -4 => 'vein', + 9 => 'book', + 25 => 'pool', + 0 => $tpl['type'] == 19 ? 'mail' : '', + -6 => $tpl['spellFocusId'] == 1 ? 'anvil' : ($tpl['spellFocusId'] == 3 ? 'forge' : ''), + default => '' + }; if ($what) { @@ -339,36 +365,33 @@ class ZonePage extends GenericPage $n = Util::localizedString($tpl, 'name'); $sn = Util::localizedString($tpl, 'subname'); - $what = ''; - if ($tpl['npcflag'] & NPC_FLAG_REPAIRER) - $what = 'repair'; - else if ($tpl['npcflag'] & NPC_FLAG_AUCTIONEER) - $what = 'auctioneer'; - else if ($tpl['npcflag'] & NPC_FLAG_BANKER) - $what = 'banker'; - else if ($tpl['npcflag'] & NPC_FLAG_BATTLEMASTER) - $what = 'battlemaster'; - else if ($tpl['npcflag'] & NPC_FLAG_INNKEEPER) - $what = 'innkeeper'; - else if ($tpl['npcflag'] & NPC_FLAG_TRAINER) - $what = 'trainer'; - else if ($tpl['npcflag'] & NPC_FLAG_VENDOR) - $what = 'vendor'; - else if ($tpl['npcflag'] & NPC_FLAG_FLIGHT_MASTER) - { - $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; - $what = 'flightmaster'; - } - else if ($tpl['npcflag'] & NPC_FLAG_STABLE_MASTER) - $what = 'stablemaster'; - else if ($tpl['npcflag'] & NPC_FLAG_GUILD_MASTER) - $what = 'guildmaster'; - else if ($tpl['npcflag'] & (NPC_FLAG_SPIRIT_HEALER | NPC_FLAG_SPIRIT_GUIDE)) - $what = 'spirithealer'; - else if ($creatureSpawns->isBoss()) + $flagsMap = array( + NPC_FLAG_REPAIRER => 'repair', + NPC_FLAG_AUCTIONEER => 'auctioneer', + NPC_FLAG_BANKER => 'banker', + NPC_FLAG_BATTLEMASTER => 'battlemaster', + NPC_FLAG_INNKEEPER => 'innkeeper', + NPC_FLAG_TRAINER => 'trainer', + NPC_FLAG_VENDOR => 'vendor', + NPC_FLAG_FLIGHT_MASTER => 'flightmaster', + NPC_FLAG_STABLE_MASTER => 'stablemaster', + NPC_FLAG_GUILD_MASTER => 'guildmaster', + NPC_FLAG_SPIRIT_HEALER | + NPC_FLAG_SPIRIT_GUIDE => 'spirithealer', + 0 => '' // set 'unused' if no match + ); + + if ($creatureSpawns->isBoss()) $what = 'boss'; else if ($tpl['rank'] == 2 || $tpl['rank'] == 4) $what = 'rare'; + else + foreach ($flagsMap as $flag => $what) + if ($tpl['npcflag'] & $flag) + break; + + if ($what == 'flightmaster') + $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; if ($what) $addToSOM($what, array( @@ -448,7 +471,7 @@ class ZonePage extends GenericPage 'name' => Util::localizedString($tpl, 'name', true, true), 'type' => Type::AREATRIGGER, 'id' => $spawn['typeId'], - 'description' => Lang::game('type').Lang::main('colon').Lang::areatrigger('types', $tpl['type']) + 'description' => Lang::game('type').Lang::areatrigger('types', $tpl['type']) )); } @@ -479,19 +502,13 @@ class ZonePage extends GenericPage { // neutral nodes come last as the line is colored by the node it's attached to usort($som['flightmaster'], function($a, $b) { - $n1 = $a['reactalliance'] == $a['reacthorde']; - $n2 = $b['reactalliance'] == $b['reacthorde']; + $n1 = (int)$a['reactalliance'] == $a['reacthorde']; + $n2 = (int)$b['reactalliance'] == $b['reacthorde']; - if ($n1 && !$n2) - return 1; - - if (!$n1 && $n2) - return -1; - - return 0; + return $n1 <=> $n2; }); - $paths = DB::Aowow()->select('SELECT n1.typeId AS "0", n2.typeId AS "1" FROM ?_taxipath p JOIN ?_taxinodes n1 ON n1.id = p.startNodeId JOIN ?_taxinodes n2 ON n2.id = p.endNodeId WHERE n1.typeId IN (?a) AND n2.typeId IN (?a)', array_keys($flightNodes), array_keys($flightNodes)); + $paths = DB::Aowow()->select('SELECT n1.`typeId` AS "0", n2.`typeId` AS "1" FROM ?_taxipath p JOIN ?_taxinodes n1 ON n1.`id` = p.`startNodeId` JOIN ?_taxinodes n2 ON n2.`id` = p.`endNodeId` WHERE n1.`typeId` IN (?a) AND n2.`typeId` IN (?a)', array_keys($flightNodes), array_keys($flightNodes)); foreach ($paths as $k => $path) { @@ -513,37 +530,39 @@ class ZonePage extends GenericPage } // preselect bosses for raids/dungeons - if (in_array($this->subject->getField('type'), [2, 3, 4, 5, 7, 8])) + if (in_array($_type, [MAP_TYPE_DUNGEON, MAP_TYPE_RAID, MAP_TYPE_BATTLEGROUND, MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC])) $som['instance'] = true; $this->map = array( - 'data' => ['parent' => 'mapper-generic', 'zone' => $this->typeId, 'zoneLink' => false], - 'som' => $som + array( // Mapper + 'parent' => 'mapper-generic', + 'zone' => $this->typeId, + 'zoneLink' => false + ), + null, // mapperData + $som, // ShowOnMap + null // foundIn ); } - else - $this->map = false; - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : null; $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] ); - /* - - associated with holiday? - */ /**************/ /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: NPCs if ($cSpawns && !$creatureSpawns->error) { $tabData = array( - 'data' => array_values($creatureSpawns->getListviewData()), + 'data' => $creatureSpawns->getListviewData(), 'note' => sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0') ); @@ -552,14 +571,14 @@ class ZonePage extends GenericPage $this->extendGlobalData($creatureSpawns->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [CreatureList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); } // tab: Objects if ($oSpawns && !$objectSpawns->error) { $tabData = array( - 'data' => array_values($objectSpawns->getListviewData()), + 'data' => $objectSpawns->getListviewData(), 'note' => sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0') ); @@ -568,7 +587,7 @@ class ZonePage extends GenericPage $this->extendGlobalData($objectSpawns->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [GameObjectList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); } $quests = new QuestList(array(['zoneOrSort', $this->typeId])); @@ -590,7 +609,7 @@ class ZonePage extends GenericPage // tab: Quests [including data collected by SOM-routine] if ($questsLV) { - $tabData = ['data' => array_values($questsLV)]; + $tabData = ['data' => $questsLV]; foreach (Game::QUEST_CLASSES as $parent => $children) { @@ -601,15 +620,15 @@ class ZonePage extends GenericPage break; } - $this->lvTabs[] = [QuestList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); } // tab: item-quest starter // select every quest starter, that is a drop - $questStartItem = DB::Aowow()->select(' - SELECT qse.typeId AS ARRAY_KEY, moreType, moreTypeId, moreZoneId - FROM ?_quests_startend qse JOIN ?_source src ON src.type = qse.type AND src.typeId = qse.typeId - WHERE src.src2 IS NOT NULL AND qse.type = ?d AND (moreZoneId = ?d OR (moreType = ?d AND moreTypeId IN (?a)) OR (moreType = ?d AND moreTypeId IN (?a)))', + $questStartItem = DB::Aowow()->select( + 'SELECT qse.`typeId` AS ARRAY_KEY, `moreType`, `moreTypeId`, `moreZoneId` + FROM ?_quests_startend qse JOIN ?_source src ON src.`type` = qse.`type` AND src.`typeId` = qse.`typeId` + WHERE src.`src2` IS NOT NULL AND qse.`type` = ?d AND (`moreZoneId` = ?d OR (`moreType` = ?d AND `moreTypeId` IN (?a)) OR (`moreType` = ?d AND `moreTypeId` IN (?a)))', Type::ITEM, $this->typeId, Type::NPC, array_unique(array_column($cSpawns, 'typeId')) ?: [0], Type::OBJECT, array_unique(array_column($oSpawns, 'typeId')) ?: [0] @@ -620,11 +639,11 @@ class ZonePage extends GenericPage $qsiList = new ItemList(array(['id', array_keys($questStartItem)])); if (!$qsiList->error) { - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($qsiList->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $qsiList->getListviewData(), 'name' => '$LANG.tab_startsquest', 'id' => 'starts-quest' - )]; + ), ItemList::$brickFile)); $this->extendGlobalData($qsiList->getJSGlobals(GLOBALINFO_SELF)); } @@ -636,12 +655,12 @@ class ZonePage extends GenericPage $rewards = new ItemList(array(['id', array_unique($rewardsLV)])); if (!$rewards->error) { - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($rewards->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $rewards->getListviewData(), 'name' => '$LANG.tab_questrewards', 'id' => 'quest-rewards', 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0') - )]; + ), ItemList::$brickFile)); $this->extendGlobalData($rewards->getJSGlobals(GLOBALINFO_SELF)); } @@ -656,28 +675,24 @@ class ZonePage extends GenericPage $this->extendGlobalData($fish->jsGlobals); $xCols = array_merge(['$Listview.extraCols.percent'], $fish->extraCols); - $note = ''; + $note = null; if ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $this->typeId)) $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); - else if ($parentArea && ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $parentArea))) + else if ($_parentArea && ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $_parentArea))) $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); - $tabData = array( - 'data' => array_values($fish->getResult()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $fish->getResult(), 'name' => '$LANG.tab_fishing', 'id' => 'fishing', 'extraCols' => array_unique($xCols), - 'hiddenCols' => ['side'] - ); - - if ($note) - $tabData['note'] = $note; - - $this->lvTabs[] = [ItemList::$brickFile, $tabData]; + 'hiddenCols' => ['side'], + 'note' => $note + ), ItemList::$brickFile)); } // tab: spells - if ($saData = DB::World()->select('SELECT * FROM spell_area WHERE area = ?d', $this->typeId)) + if ($saData = DB::World()->select('SELECT * FROM spell_area WHERE `area` = ?d', $this->typeId)) { $spells = new SpellList(array(['id', array_column($saData, 'spell')])); if (!$spells->error) @@ -710,15 +725,11 @@ class ZonePage extends GenericPage if ($cnd->toListviewColumn($lvSpells, $extraCols)) $this->extendGlobalData($cnd->getJsGlobals()); - $tabData = array( - 'data' => array_values($lvSpells), - 'hiddenCols' => ['skill'] - ); - - if ($extraCols) - $tabData['extraCols'] = $extraCols; - - $this->lvTabs[] = [SpellList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvSpells, + 'hiddenCols' => ['skill'], + 'extraCols' => $extraCols ?: null + ), SpellList::$brickFile)); } } @@ -726,12 +737,12 @@ class ZonePage extends GenericPage $subZones = new ZoneList(array(['parentArea', $this->typeId])); if (!$subZones->error) { - $this->lvTabs[] = [ZoneList::$brickFile, array( - 'data' => array_values($subZones->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $subZones->getListviewData(), 'name' => '$LANG.tab_zones', 'id' => 'subzones', 'hiddenCols' => ['territory', 'instancetype'] - )]; + ), ZoneList::$brickFile)); $this->extendGlobalData($subZones->getJSGlobals(GLOBALINFO_SELF)); } @@ -745,12 +756,12 @@ class ZonePage extends GenericPage $soundIds = []; $zoneMusic = DB::Aowow()->select( - 'SELECT x.soundId AS ARRAY_KEY, x.soundId, x.worldStateId, x.worldStateValue, x.type - FROM (SELECT `ambienceDay` AS soundId, `worldStateId`, `worldStateValue`, 1 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceDay` > 0 UNION - SELECT `ambienceNight` AS soundId, `worldStateId`, `worldStateValue`, 1 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceNight` > 0 UNION - SELECT `musicDay` AS soundId, `worldStateId`, `worldStateValue`, 2 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicDay` > 0 UNION - SELECT `musicNight` AS soundId, `worldStateId`, `worldStateValue`, 2 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicNight` > 0 UNION - SELECT `intro` AS soundId, `worldStateId`, `worldStateValue`, 3 AS `type` FROM ?_zones_sounds WHERE `id` IN (?a) AND `intro` > 0) x + 'SELECT x.`soundId` AS ARRAY_KEY, x.`soundId`, x.`worldStateId`, x.`worldStateValue`, x.`type` + FROM (SELECT `ambienceDay` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceDay` > 0 UNION + SELECT `ambienceNight` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceNight` > 0 UNION + SELECT `musicDay` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicDay` > 0 UNION + SELECT `musicNight` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicNight` > 0 UNION + SELECT `intro` AS "soundId", `worldStateId`, `worldStateValue`, 3 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `intro` > 0) x GROUP BY x.soundId, x.worldStateId, x.worldStateValue', $areaIds, $areaIds, $areaIds, $areaIds, $areaIds ); @@ -772,40 +783,38 @@ class ZonePage extends GenericPage if (array_filter(array_column($zoneMusic, 'worldStateId'))) { - $tabData['extraCols'] = ['$Listview.extraCols.condition']; + $tabData['extraCols'] = ['$Listview.extraCols.condition']; foreach ($soundIds as $sId) if (!empty($zoneMusic[$sId]['worldStateId'])) Conditions::extendListviewRow($data[$sId], Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $zoneMusic[$sId]['worldStateId'], $zoneMusic[$sId]['worldStateValue']]); } - $tabData['data'] = array_values($data); + $tabData['data'] = $data; - $this->lvTabs[] = [SoundList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); $this->extendGlobalData($music->getJSGlobals(GLOBALINFO_SELF)); $typeFilter = function(array $music, int $type) use ($data) : array { $result = []; - foreach (array_filter($music, function ($x) use ($type) { return $x['type'] == $type; } ) as $sId => $_) + foreach (array_filter($music, fn ($x) => $x['type'] == $type) as $sId => $_) $result = array_merge($result, $data[$sId]['files'] ?? []); return $result; }; - // audio controls - // ambience - if ($_ = $typeFilter($zoneMusic, 1)) - $this->zoneMusic['ambience'] = $_; - - // music + // audio controls (order how it appears on page) + // [title, data, divID, options] if ($_ = $typeFilter($zoneMusic, 2)) - $this->zoneMusic['music'] = $_; + $this->zoneMusic[] = [Lang::sound('music'), $_, 'zonemusic', (object)['loop' => true]]; - // intro if ($_ = $typeFilter($zoneMusic, 3)) - $this->zoneMusic['intro'] = $_; + $this->zoneMusic[] = [Lang::sound('intro'), $_, 'zonemusicintro', (object)[]]; + + if ($_ = $typeFilter($zoneMusic, 1)) + $this->zoneMusic[] = [Lang::sound('ambience'), $_, 'soundambience', (object)['loop' => true]]; } } @@ -815,11 +824,11 @@ class ZonePage extends GenericPage if ($tab = $cnd->toListviewTab('condition-for', '$LANG.tab_condition_for')) { $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; + $this->lvTabs->addDataTab(...$tab); } } - private function addMoveLocationMenu($parentArea, $parentFloor) + private function addMoveLocationMenu(int $_parentArea, int $parentFloor) : void { // hide for non-staff if (!User::isInGroup(U_GROUP_EMPLOYEE)) @@ -829,7 +838,7 @@ class ZonePage extends GenericPage if (!$worldPos) return; - $menu = Util::buildPosFixMenu($worldPos[-$this->typeId]['mapId'], $worldPos[-$this->typeId]['posX'], $worldPos[-$this->typeId]['posY'], Type::ZONE, -$this->typeId, $parentArea, $parentFloor); + $menu = Util::buildPosFixMenu($worldPos[-$this->typeId]['mapId'], $worldPos[-$this->typeId]['posX'], $worldPos[-$this->typeId]['posY'], Type::ZONE, -$this->typeId, $_parentArea, $parentFloor); if (!$menu) return; @@ -837,20 +846,6 @@ class ZonePage extends GenericPage $this->addScript([SC_JS_STRING, '$(document).ready(function () { mn_staff.push('.Util::toJSON(array_values($menu)).'); });']); } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('category'); - - if (in_array($this->subject->getField('category'), [2, 3])) - $this->path[] = $this->subject->getField('expansion'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('zone'))); - } - } ?> diff --git a/endpoints/zones/zones.php b/endpoints/zones/zones.php new file mode 100644 index 00000000..77247254 --- /dev/null +++ b/endpoints/zones/zones.php @@ -0,0 +1,188 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('zones')); + + + /*************/ + /* Menu Path */ + /*************/ + + foreach ($this->category as $c) + $this->breadcrumb[] = $c; + + + /**************/ + /* Page Title */ + /**************/ + + if (isset($this->category[1])) + array_unshift($this->title, Lang::game('expansions', $this->category[1])); + + if (isset($this->category[0])) + array_unshift($this->title, Lang::zone('cat', $this->category[0])); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + + $conditions = [Cfg::get('SQL_LIMIT_NONE')]; + $visibleCols = []; + $hiddenCols = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) // sub-areas and unused zones + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($this->category) + { + $conditions[] = ['z.category', $this->category[0]]; + $hiddenCols[] = 'category'; + + if (isset($this->category[1]) && in_array($this->category[0], [2, 3])) + $conditions[] = ['z.expansion', $this->category[1]]; + + switch ($this->category[0]) + { + case 6: + case 2: + case 3: + array_push($visibleCols, 'level', 'players'); + case 9: + $hiddenCols[] = 'territory'; + break; + } + } + + $zones = new ZoneList($conditions); + + if (!$zones->hasSetFields('type')) + $hiddenCols[] = 'instancetype'; + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $zones->getListviewData(), + 'visibleCols'=> $visibleCols ?: null, + 'hiddenCols' => $hiddenCols ?: null + ), ZoneList::$brickFile)); + + + /**************/ + /* Flight Map */ + /**************/ + + [$mapFile, $spawnMap] = match ($this->category[0] ?? null) + { + 0 => [-3, 0], + 1 => [-6, 1], + 8 => [-2, 530], + 10 => [-5, 571], + default => [ 0, -1] + }; + + if ($mapFile) + { + $somData = ['flightmaster' => []]; + $nodes = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, tn.* FROM ?_taxinodes tn WHERE `mapId` = ?d AND `type` <> 0 AND `typeId` <> 0', $spawnMap); + $paths = DB::Aowow()->select( + 'SELECT IF(tn1.`reactA` = tn1.`reactH` AND tn2.`reactA` = tn2.`reactH`, 1, 0) AS "neutral", + tp.`startNodeId` AS "startId", tn1.`posX` AS "startPosX", tn1.`posY` AS "startPosY", + tp.`endNodeId` AS "endId", tn2.`posX` AS "endPosX", tn2.`posY` AS "endPosY" + FROM ?_taxipath tp, ?_taxinodes tn1, ?_taxinodes tn2 + WHERE tn1.`Id` = tp.`endNodeId` AND tn2.`Id` = tp.`startNodeId` AND + tn1.`type` <> 0 AND tn2.`type` <> 0 AND + (tp.`startNodeId` IN (?a) OR tp.`EndNodeId` IN (?a))', + array_keys($nodes), array_keys($nodes) + ); + + foreach ($nodes as $i => $n) + { + $neutral = $n['reactH'] == $n['reactA']; + + $data = array( + 'coords' => [[$n['posX'], $n['posY']]], + 'level' => 0, // floor + 'name' => Util::localizedString($n, 'name'), + 'type' => $n['type'], + 'id' => $n['typeId'], + 'reacthorde' => $n['reactH'], + 'reactalliance' => $n['reactA'], + 'paths' => [] + ); + + foreach ($paths as $j => $p) + { + if ($i != $p['startId'] && $i != $p['endId']) + continue; + + if ($i == $p['startId'] && (!$neutral || $p['neutral'])) + { + $data['paths'][] = [$p['startPosX'], $p['startPosY']]; + unset($paths[$j]); + } + else if ($i == $p['endId'] && (!$neutral || $p['neutral'])) + { + $data['paths'][] = [$p['endPosX'], $p['endPosY']]; + unset($paths[$j]); + } + } + + if (empty($data['paths'])) + unset($data['paths']); + + $somData['flightmaster'][] = $data; + } + + $this->map = array( + array( // Mapper + 'parent' => 'mapper-generic', + 'zone' => $mapFile, + 'zoom' => 1, + 'overlay' => true, + 'zoomable' => false + ), + null, // mapperData + $somData, // ShowOnMap + null // foundIn + ); + } + + parent::generate(); + } +} + +?> diff --git a/pages/zones.php b/pages/zones.php deleted file mode 100644 index eb5e6efe..00000000 --- a/pages/zones.php +++ /dev/null @@ -1,185 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - $this->name = Util::ucFirst(Lang::game('zones')); - } - - protected function generateContent() - { - $conditions = [Cfg::get('SQL_LIMIT_NONE')]; - $visibleCols = []; - $hiddenCols = []; - $mapFile = 0; - $spawnMap = -1; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) // sub-areas and unused zones - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - if ($this->category) - { - $conditions[] = ['z.category', $this->category[0]]; - $hiddenCols[] = 'category'; - - if (isset($this->category[1]) && in_array($this->category[0], [2, 3])) - $conditions[] = ['z.expansion', $this->category[1]]; - - if (empty($this->category[1])) - { - switch ($this->category[0]) - { - case 0: $mapFile = -3; $spawnMap = 0; break; - case 1: $mapFile = -6; $spawnMap = 1; break; - case 8: $mapFile = -2; $spawnMap = 530; break; - case 10: $mapFile = -5; $spawnMap = 571; break; - } - } - - switch ($this->category[0]) - { - case 6: - case 2: - case 3: - array_push($visibleCols, 'level', 'players'); - case 9: - $hiddenCols[] = 'territory'; - break; - } - } - - $zones = new ZoneList($conditions); - - if (!$zones->hasSetFields('type')) - $hiddenCols[] = 'instancetype'; - - $tabData = ['data' => array_values($zones->getListviewData())]; - - if ($visibleCols) - $tabData['visibleCols'] = $visibleCols; - - if ($hiddenCols) - $tabData['hiddenCols'] = $hiddenCols; - - $this->lvTabs[] = [ZoneList::$brickFile, $tabData]; - - // create flight map - if ($mapFile) - { - $somData = ['flightmaster' => []]; - $nodes = DB::Aowow()->select('SELECT id AS ARRAY_KEY, tn.* FROM ?_taxinodes tn WHERE mapId = ?d AND type <> 0 AND typeId <> 0', $spawnMap); - $paths = DB::Aowow()->select(' - SELECT IF(tn1.reactA = tn1.reactH AND tn2.reactA = tn2.reactH, 1, 0) AS neutral, - tp.startNodeId AS startId, - tn1.posX AS startPosX, - tn1.posY AS startPosY, - tp.endNodeId AS endId, - tn2.posX AS endPosX, - tn2.posY AS endPosY - FROM ?_taxipath tp, - ?_taxinodes tn1, - ?_taxinodes tn2 - WHERE tn1.Id = tp.endNodeId AND - tn2.Id = tp.startNodeId AND - tn1.type <> 0 AND - tn2.type <> 0 AND - (tp.startNodeId IN (?a) OR tp.EndNodeId IN (?a)) - ', array_keys($nodes), array_keys($nodes)); - - foreach ($nodes as $i => $n) - { - $neutral = $n['reactH'] == $n['reactA']; - - $data = array( - 'coords' => [[$n['posX'], $n['posY']]], - 'level' => 0, // floor - 'name' => Util::localizedString($n, 'name'), - 'type' => $n['type'], - 'id' => $n['typeId'], - 'reacthorde' => $n['reactH'], - 'reactalliance' => $n['reactA'], - 'paths' => [] - ); - - foreach ($paths as $j => $p) - { - if ($i != $p['startId'] && $i != $p['endId']) - continue; - - if ($i == $p['startId'] && (!$neutral || $p['neutral'])) - { - $data['paths'][] = [$p['startPosX'], $p['startPosY']]; - unset($paths[$j]); - } - else if ($i == $p['endId'] && (!$neutral || $p['neutral'])) - { - $data['paths'][] = [$p['endPosX'], $p['endPosY']]; - unset($paths[$j]); - } - } - - if (empty($data['paths'])) - unset($data['paths']); - - $somData['flightmaster'][] = $data; - } - - $this->map = array( - 'data' => array( - 'zone' => $mapFile, - 'zoom' => 1, - 'overlay' => true, - 'zoomable' => false, - 'parent' => 'mapper-generic' - ), - 'som' => $somData, - 'mapperData' => null - ); - } - } - - protected function generateTitle() - { - if ($this->category) - { - if (isset($this->category[1])) - array_unshift($this->title, Lang::game('expansions', $this->category[1])); - - array_unshift($this->title, Lang::zone('cat', $this->category[0])); - } - } - - protected function generatePath() - { - foreach ($this->category as $c) - $this->path[] = $c; - } -} - - -?> From 1672883186cea4ad551fe1c0d0b422e3477ad95f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 19:23:10 +0200 Subject: [PATCH 0969/1249] Template/Update (Part 34) * convert dbtype 'areatrigger' --- endpoints/areatrigger/areatrigger.php | 141 ++++++++++++++++++++++++ endpoints/areatriggers/areatriggers.php | 100 +++++++++++++++++ pages/areatrigger.php | 125 --------------------- pages/areatriggers.php | 85 -------------- template/listviews/areatrigger.tpl | 11 +- template/pages/areatriggers.tpl.php | 40 ++++--- 6 files changed, 273 insertions(+), 229 deletions(-) create mode 100644 endpoints/areatrigger/areatrigger.php create mode 100644 endpoints/areatriggers/areatriggers.php delete mode 100644 pages/areatrigger.php delete mode 100644 pages/areatriggers.php diff --git a/endpoints/areatrigger/areatrigger.php b/endpoints/areatrigger/areatrigger.php new file mode 100644 index 00000000..b0ce07ca --- /dev/null +++ b/endpoints/areatrigger/areatrigger.php @@ -0,0 +1,141 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new AreaTriggerList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('areatrigger'), Lang::areatrigger('notFound')); + + $this->h1 = $this->subject->getField('name') ?: 'Areatrigger #'.$this->typeId; + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $this->subject->getField('type'); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('areatrigger'))); + + + /****************/ + /* Main Content */ + /****************/ + + $_type = $this->subject->getField('type'); + + // get spawns + if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) + { + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::areatrigger('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } + + // Smart AI + if ($_type == AT_TYPE_SMART) + { + $sai = new SmartAI(SmartAI::SRC_TYPE_AREATRIGGER, $this->typeId, ['teleportTargetArea' => $this->subject->getField('areaId')]); + if ($sai->prepare()) + { + $this->extendGlobalData($sai->getJSGlobals()); + $this->smartAI = $sai->getMarkup(); + } + } + + $this->redButtons = array( + BUTTON_LINKS => false, + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: conditions + $cnd = new Conditions(); + $cnd->getBySourceEntry($this->typeId, Conditions::SRC_AREATRIGGER_CLIENT)->prepare(); + if ($tab = $cnd->toListviewTab()) + { + $this->extendGlobalData($cnd->getJsGlobals()); + $this->lvTabs->addDataTab(...$tab); + } + + if ($_type == AT_TYPE_OBJECTIVE) + { + $relQuest = new QuestList(array(['id', $this->subject->getField('quest')])); + if (!$relQuest->error) + { + $this->extendGlobalData($relQuest->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + $this->lvTabs->addListviewTab(new Listview(['data' => $relQuest->getListviewData()], QuestList::$brickFile)); + } + } + else if ($_type == AT_TYPE_TELEPORT) + { + $relZone = new ZoneList(array(['id', $this->subject->getField('areaId')])); + if (!$relZone->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $relZone->getListviewData()], ZoneList::$brickFile)); + } + else if ($_type == AT_TYPE_SCRIPT) + { + $relTrigger = new AreaTriggerList(array(['id', $this->typeId, '!'], ['name', $this->subject->getField('name')])); + if (!$relTrigger->error) + $this->lvTabs->addListviewTab(new Listview(['data' => $relTrigger->getListviewData(), 'name' => Util::ucFirst(Lang::game('areatrigger'))]), AreaTriggerList::$brickFile, 'areatrigger'); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/areatriggers/areatriggers.php b/endpoints/areatriggers/areatriggers.php new file mode 100644 index 00000000..04b06d46 --- /dev/null +++ b/endpoints/areatriggers/areatriggers.php @@ -0,0 +1,100 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]]; + protected array $validCats = [0, 1, 2, 3, 4, 5]; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + if (isset($this->category[0])) + $this->forward('?areatriggers&filter=ty='.$this->category[0]); + + parent::__construct($pageParam); + + $this->filter = new AreaTriggerListFilter($this->_get['filter'] ?? ''); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('areatriggers')); + + $this->filter->evalCriteria(); + + $fiForm = $this->filter->values; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + if (count($fiForm['ty']) == 1) + array_unshift($this->title, Lang::areatrigger('types', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty']; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = false; + + $conditions = []; + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + $tabData = []; + $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); + if (!$trigger->error) + { + $tabData['data'] = $trigger->getListviewData(); + + // create note if search limit was exceeded; overwriting 'note' is intentional + if ($trigger->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, AreaTriggerList::$brickFile, 'areatrigger')); + + parent::generate(); + } +} + +?> diff --git a/pages/areatrigger.php b/pages/areatrigger.php deleted file mode 100644 index ecae884d..00000000 --- a/pages/areatrigger.php +++ /dev/null @@ -1,125 +0,0 @@ -typeId = intVal($id); - - $this->subject = new AreaTriggerList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('areatrigger'), Lang::areatrigger('notFound')); - - $this->name = $this->subject->getField('name') ?: 'AT #'.$this->typeId; - } - - protected function generatePath() - { - $this->path[] = $this->subject->getField('type'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('areatrigger'))); - } - - protected function generateContent() - { - $this->addScript([SC_JS_FILE, '?data=zones']); - - $_type = $this->subject->getField('type'); - - - /****************/ - /* Main Content */ - /****************/ - - // get spawns - $map = null; - if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) - { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns, 'foundIn' => Lang::areatrigger('foundIn')]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); - } - - // smart AI - $sai = null; - if ($_type == AT_TYPE_SMART) - { - $sai = new SmartAI(SmartAI::SRC_TYPE_AREATRIGGER, $this->typeId, ['teleportTargetArea' => $this->subject->getField('areaId')]); - if ($sai->prepare()) - $this->extendGlobalData($sai->getJSGlobals()); - } - - $this->map = $map; - $this->infobox = false; - $this->smartAI = $sai?->getMarkup(); - $this->redButtons = array( - BUTTON_LINKS => false, - BUTTON_WOWHEAD => false - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: conditions - $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_AREATRIGGER_CLIENT)->prepare(); - if ($tab = $cnd->toListviewTab()) - { - $this->extendGlobalData($cnd->getJsGlobals()); - $this->lvTabs[] = $tab; - } - - if ($_type == AT_TYPE_OBJECTIVE) - { - $relQuest = new QuestList(array(['id', $this->subject->getField('quest')])); - if (!$relQuest->error) - { - $this->extendGlobalData($relQuest->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - $this->lvTabs[] = [QuestList::$brickFile, ['data' => array_values($relQuest->getListviewData())]]; - } - } - else if ($_type == AT_TYPE_TELEPORT) - { - $relZone = new ZoneList(array(['id', $this->subject->getField('areaId')])); - if (!$relZone->error) - { - $this->lvTabs[] = [ZoneList::$brickFile, ['data' => array_values($relZone->getListviewData())]]; - } - } - else if ($_type == AT_TYPE_SCRIPT) - { - $relTrigger = new AreaTriggerList(array(['id', $this->typeId, '!'], ['name', $this->subject->getField('name')])); - if (!$relTrigger->error) - { - $this->lvTabs[] = [AreaTriggerList::$brickFile, ['data' => array_values($relTrigger->getListviewData()), 'name' => Util::ucFirst(Lang::game('areatrigger'))], 'areatrigger']; - } - } - } -} - -?> diff --git a/pages/areatriggers.php b/pages/areatriggers.php deleted file mode 100644 index 7c32bd24..00000000 --- a/pages/areatriggers.php +++ /dev/null @@ -1,85 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - if (isset($this->category[0])) - header('Location: ?areatriggers&filter=ty='.$this->category[0], true, 302); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new AreaTriggerListFilter($this->_get['filter'] ?? ''); - - $this->name = Util::ucFirst(Lang::game('areatriggers')); - } - - protected function generateContent() - { - $this->filterObj->evalCriteria(); - - $conditions = []; - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = []; - $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); - if (!$trigger->error) - { - $tabData['data'] = array_values($trigger->getListviewData()); - - // create note if search limit was exceeded; overwriting 'note' is intentional - if ($trigger->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - } - - $this->lvTabs[] = [AreaTriggerList::$brickFile, $tabData, 'areatrigger']; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - array_unshift($this->title, Lang::areatrigger('types', $form['ty'][0])); - } - - protected function generatePath() - { - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - $this->path[] = $form['ty']; - } -} - -?> diff --git a/template/listviews/areatrigger.tpl b/template/listviews/areatrigger.tpl index c9ee2816..0a428c0b 100644 --- a/template/listviews/areatrigger.tpl +++ b/template/listviews/areatrigger.tpl @@ -11,7 +11,9 @@ Listview.templates.areatrigger = { value: 'id', compute: function(data, td) { if (data.id) { - $WH.ae(td, $WH.ct(data.id)); + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); } } }, @@ -75,5 +77,12 @@ Listview.templates.areatrigger = { ], getItemLink: function(areatrigger) { return '?areatrigger=' + areatrigger.id; + }, + onBeforeCreate : function() { + // hide duplicate id col + if (this.debug || g_user?.debug) { + let colId = this.columns.findIndex(x => x.id == 'id'); + this.visibility = this.visibility.filter(x => x != colId); + } } } diff --git a/template/pages/areatriggers.tpl.php b/template/pages/areatriggers.tpl.php index 5709d0e3..3f187918 100644 --- a/template/pages/areatriggers.tpl.php +++ b/template/pages/areatriggers.tpl.php @@ -1,10 +1,11 @@ - - brick('header'); -$f = $this->filterObj->values // shorthand -?> + namespace Aowow\Template; + use \Aowow\Lang; + +$this->brick('header'); +$f = $this->filter->values; // shorthand +?>
      @@ -12,29 +13,32 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [101]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [102]]); ?> -

      name; ?> brick('redButtons'); ?>

      -
      +
      +
      +brick('headIcons'); + +$this->brick('redButtons'); +?> +

      h1; ?>

      +
      -
      +
      - + @@ -43,7 +47,7 @@ endforeach;
      - /> /> + /> />
      @@ -57,7 +61,7 @@ endforeach;
      -brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From 0cf9069eb1fd71803f18f0194a715f12f414badf Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 21:23:09 +0200 Subject: [PATCH 0970/1249] Template/Update (Part 35) * convert dbtype 'icon' * improve on IconlistFilter --- endpoints/icon/icon.php | 158 ++++++++++++++++++++++++++++++++ endpoints/icons/icons.php | 109 ++++++++++++++++++++++ includes/dbtypes/icon.class.php | 120 ++++++++++-------------- pages/icon.php | 120 ------------------------ pages/icons.php | 96 ------------------- template/pages/icon.tpl.php | 13 ++- template/pages/icons.tpl.php | 32 ++++--- 7 files changed, 344 insertions(+), 304 deletions(-) create mode 100644 endpoints/icon/icon.php create mode 100644 endpoints/icons/icons.php delete mode 100644 pages/icon.php delete mode 100644 pages/icons.php diff --git a/endpoints/icon/icon.php b/endpoints/icon/icon.php new file mode 100644 index 00000000..2a67b53e --- /dev/null +++ b/endpoints/icon/icon.php @@ -0,0 +1,158 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new IconList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('icon'), Lang::icon('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = $this->subject->getField('name'); + $this->icon = $this->subject->getField('name', true, true); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); + + + /*************/ + /* Menu Path */ + /*************/ + + $cats = [1 => 'nItems', 2 => 'nSpells', 3 => 'nAchievements', 6 => 'nCurrencies', 9 => 'nPets'/* , 11 => '' */]; + $crumb = ''; + foreach ($cats as $cat => $field) + { + if (!$this->subject->getField($field)) + continue; + + if ($crumb) + { + $crumb = 0; + break; + } + + $crumb = $cat; + } + + if ($crumb) + $this->breadcrumb[] = $crumb; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('icon'))); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // used by: spell + $ubSpells = new SpellList(array(['iconId', $this->typeId])); + if (!$ubSpells->error) + { + $this->extendGlobalData($ubSpells->getJsGlobals(GLOBALINFO_RELATED | GLOBALINFO_SELF)); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubSpells->getListviewData(), + 'id' => 'used-by-spell' + ), SpellList::$brickFile)); + } + + // used by: item + $ubItems = new ItemList(array(['iconId', $this->typeId])); + if (!$ubItems->error) + { + $this->extendGlobalData($ubItems->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubItems->getListviewData(), + 'id' => 'used-by-item' + ), ItemList::$brickFile)); + } + + // used by: achievement + $ubAchievements = new AchievementList(array(['iconId', $this->typeId])); + if (!$ubAchievements->error) + { + $this->extendGlobalData($ubAchievements->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubAchievements->getListviewData(), + 'id' => 'used-by-achievement' + ), AchievementList::$brickFile)); + } + + // used by: currency + $ubCurrencies = new CurrencyList(array(['iconId', $this->typeId])); + if (!$ubCurrencies->error) + { + $this->extendGlobalData($ubCurrencies->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubCurrencies->getListviewData(), + 'id' => 'used-by-currency' + ), CurrencyList::$brickFile)); + } + + // used by: hunter pet + $ubPets = new PetList(array(['iconId', $this->typeId])); + if (!$ubPets->error) + { + $this->extendGlobalData($ubPets->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubPets->getListviewData(), + 'id' => 'used-by-pet' + ), PetList::$brickFile)); + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/icons/icons.php b/endpoints/icons/icons.php new file mode 100644 index 00000000..7613240e --- /dev/null +++ b/endpoints/icons/icons.php @@ -0,0 +1,109 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [0, 1, 2, 3]; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new IconListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucWords(Lang::game('icons')); + + $conditions = [600]; // LIMIT 600 - fits better onto the grid + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + $this->filter->evalCriteria(); + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + + /**************/ + /* Page Title */ + /**************/ + + $title = $this->h1; + $setCr = $this->filter->getSetCriteria(1, 2, 3, 6, 9, 11); + if (count($setCr) == 1) + $title = match ($setCr[0]) + { + 1 => Util::ucFirst(Lang::game('item')), + 2 => Util::ucFirst(Lang::game('spell')), + 3 => Util::ucFirst(Lang::game('achievement')), + 6 => Util::ucFirst(Lang::game('currency')), + 9 => Util::ucFirst(Lang::game('pet')), + 11 => Util::ucFirst(Lang::game('class')), + } . ' ' . $this->h1; + + array_unshift($this->title, $title); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($setCr) == 1) + $this->breadcrumb[] = $setCr[0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $icons = new IconList($conditions, ['calcTotal' => true]); + + $tabData['data'] = $icons->getListviewData(); + $this->extendGlobalData($icons->getJSGlobals()); + + if ($icons->getMatches() > $conditions[0]) // LIMIT + { + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $icons->getMatches(), 'LANG.types[29][3]', $conditions[0]); + $tabData['_truncated'] = 1; + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, IconList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php index 0ec46b4e..c324847f 100644 --- a/includes/dbtypes/icon.class.php +++ b/includes/dbtypes/icon.class.php @@ -101,7 +101,7 @@ class IconList extends DBTypeList class IconListFilter extends Filter { - private array $totalUses = []; + private array $iconTotals = []; private array $criterion2field = array( 1 => '?_items', // items [num] 2 => '?_spell', // spells [num] @@ -119,13 +119,13 @@ class IconListFilter extends Filter protected string $type = 'icons'; protected static array $genericFilter = array( - 1 => [parent::CR_CALLBACK, 'cbUseAny' ], // items [num] - 2 => [parent::CR_CALLBACK, 'cbUseAny' ], // spells [num] - 3 => [parent::CR_CALLBACK, 'cbUseAny' ], // achievements [num] - 6 => [parent::CR_CALLBACK, 'cbUseAny' ], // currencies [num] - 9 => [parent::CR_CALLBACK, 'cbUseAny' ], // hunterpets [num] - 11 => [parent::CR_NYI_PH, null, 0], // classes [num] - 13 => [parent::CR_CALLBACK, 'cbUseAll' ] // used [num] + 1 => [parent::CR_CALLBACK, 'cbUsedBy' ], // items [num] + 2 => [parent::CR_CALLBACK, 'cbUsedBy' ], // spells [num] + 3 => [parent::CR_CALLBACK, 'cbUsedBy' ], // achievements [num] + 6 => [parent::CR_CALLBACK, 'cbUsedBy' ], // currencies [num] + 9 => [parent::CR_CALLBACK, 'cbUsedBy' ], // hunterpets [num] + 11 => [parent::CR_NYI_PH, null, 0 ], // classes [num] + 13 => [parent::CR_CALLBACK, 'cbUsedBy', true] // used [num] ); protected static array $inputFields = array( @@ -138,35 +138,6 @@ class IconListFilter extends Filter public array $extraOpts = []; - private function _getCnd(string $op, int $val, string $tbl) : ?array - { - switch ($op) - { - case '>': - case '>=': - case '=': - $ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl); - return $ids ? ['id', array_keys($ids)] : [1]; - case '<=': - if ($val) - $op = '>'; - break; - case '<': - if ($val) - $op = '>='; - break; - case '!=': - if ($val) - $op = '='; - break; - default: - return null; - } - - $ids = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId` HAVING n '.$op.' '.$val, $tbl); - return $ids ? ['id', array_keys($ids), '!'] : [1]; - } - protected function createSQLForValues() : array { $parts = []; @@ -180,47 +151,54 @@ class IconListFilter extends Filter return $parts; } - protected function cbUseAny(int $cr, int $crs, string $crv) : ?array + protected function cbUsedBy(int $cr, int $crs, string $crv, ?bool $all = false) : ?array { - if (Util::checkNumeric($crv, NUM_CAST_INT) && $this->int2Op($crs)) - return $this->_getCnd($crs, $crv, $this->criterion2field[$cr]); - - return null; - } - - protected function cbUseAll(int $cr, int $crs, string $crv) : ?array - { - if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) + if (!Util::checkNumeric($crv, NUM_CAST_INT) || ![$filter, $negate] = $this->int2Filter($crs, $crv)) return null; - if (!$this->totalUses) - { - foreach ($this->criterion2field as $tbl) - { - if (!$tbl) - continue; + $total = $this->prepareIconTotals($all ? 0 : $cr); - $res = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl); - Util::arraySumByKey($this->totalUses, $res); - } - } + $ids = array_filter($total, $filter); - if ($crs == '=') - $crs = '=='; - - $op = $crs; - if ($crs == '<=' && $crv) - $op = '>'; - else if ($crs == '<' && $crv) - $op = '>='; - else if ($crs == '!=' && $crv) - $op = '=='; - $ids = array_filter($this->totalUses, fn($x) => eval('return '.$x.' '.$op.' '.$crv.';')); - - if ($crs != $op) + if ($negate) return $ids ? ['id', array_keys($ids), '!'] : [1]; else - return $ids ? ['id', array_keys($ids)] : ['id', array_keys($this->totalUses), '!']; + return $ids ? ['id', array_keys($ids)] : ['id', array_keys($total), '!']; + } + + private function int2Filter(mixed $op, int $y) : ?array + { + return match ($op) { + 1 => [fn($x) => $x > $y, false], + 2 => [fn($x) => $x >= $y, false], + 3 => [fn($x) => $x == $y, false], + 4 => [fn($x) => $x > $y, true], + 5 => [fn($x) => $x >= $y, true], + 6 => [fn($x) => $x == $y, true], + default => null + }; + } + + private function prepareIconTotals(int $forCr = 0) : array + { + foreach ($this->criterion2field as $cr => $tbl) + { + if (!$tbl || isset($this->iconTotals[$cr]) || ($forCr && $forCr != $cr)) + continue; + + $this->iconTotals[$cr] = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl); + } + + if ($forCr) + return $this->iconTotals[$forCr]; + + if (!isset($this->iconTotals['all'])) + { + $this->iconTotals['all'] = []; + Util::arraySumByKey($this->iconTotals['all'], ...$this->iconTotals); + } + + return $this->iconTotals['all']; } } diff --git a/pages/icon.php b/pages/icon.php deleted file mode 100644 index cb56cc95..00000000 --- a/pages/icon.php +++ /dev/null @@ -1,120 +0,0 @@ -typeId = intVal($id); - - $this->subject = new IconList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('icon'), Lang::icon('notFound')); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - $this->name = $this->subject->getField('name'); - $this->icon = $this->subject->getField('name', true, true); - } - - protected function generateContent() - { - /****************/ - /* Main Content */ - /****************/ - - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // used by: spell - $ubSpells = new SpellList(array(['iconId', $this->typeId])); - if (!$ubSpells->error) - { - $this->extendGlobalData($ubSpells->getJsGlobals(GLOBALINFO_RELATED | GLOBALINFO_SELF)); - $this->lvTabs[] = [SpellList::$brickFile, array( - 'data' => array_values($ubSpells->getListviewData()), - 'id' => 'used-by-spell' - )]; - } - - // used by: item - $ubItems = new ItemList(array(['iconId', $this->typeId])); - if (!$ubItems->error) - { - $this->extendGlobalData($ubItems->getJsGlobals()); - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($ubItems->getListviewData()), - 'id' => 'used-by-item' - )]; - } - - // used by: achievement - $ubAchievements = new AchievementList(array(['iconId', $this->typeId])); - if (!$ubAchievements->error) - { - $this->extendGlobalData($ubAchievements->getJsGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($ubAchievements->getListviewData()), - 'id' => 'used-by-achievement' - )]; - } - - // used by: currency - $ubCurrencies = new CurrencyList(array(['iconId', $this->typeId])); - if (!$ubCurrencies->error) - { - $this->extendGlobalData($ubCurrencies->getJsGlobals()); - $this->lvTabs[] = [CurrencyList::$brickFile, array( - 'data' => array_values($ubCurrencies->getListviewData()), - 'id' => 'used-by-currency' - )]; - } - - // used by: hunter pet - $ubPets = new PetList(array(['iconId', $this->typeId])); - if (!$ubPets->error) - { - $this->extendGlobalData($ubPets->getJsGlobals()); - $this->lvTabs[] = [PetList::$brickFile, array( - 'data' => array_values($ubPets->getListviewData()), - 'id' => 'used-by-pet' - )]; - } - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('icon'))); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/icons.php b/pages/icons.php deleted file mode 100644 index 52e9d218..00000000 --- a/pages/icons.php +++ /dev/null @@ -1,96 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall) - { - parent::__construct($pageCall); - - $this->filterObj = new IconListFilter($this->_get['filter'] ?? ''); - - $this->name = Util::ucFirst(Lang::game('icons')); - } - - protected function generateContent() - { - $tabData = array( - 'data' => [], - ); - - $sqlLimit = 600; // fits better onto the grid - - $conditions = [$sqlLimit]; - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - - $this->filterObj->evalCriteria(); - - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $icons = new IconList($conditions, ['calcTotal' => true]); - - $tabData['data'] = array_values($icons->getListviewData()); - $this->extendGlobalData($icons->getJSGlobals()); - - if ($icons->getMatches() > $sqlLimit) - { - $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $icons->getMatches(), 'LANG.types[29][3]', $sqlLimit); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - - $this->lvTabs[] = [IconList::$brickFile, $tabData]; - } - - protected function generateTitle() - { - $title = $this->name; - $setCr = $this->filterObj->getSetCriteria(1, 2, 3, 6, 9, 11); - if (count($setCr) == 1) - { - $title = match($setCr[0]) - { - 1 => Util::ucFirst(Lang::game('item')), - 2 => Util::ucFirst(Lang::game('spell')), - 3 => Util::ucFirst(Lang::game('achievement')), - 6 => Util::ucFirst(Lang::game('currency')), - 9 => Util::ucFirst(Lang::game('pet')), - 11 => Util::ucFirst(Lang::game('class')) - } . ' ' . $title; - } - - array_unshift($this->title, $title); - } - - protected function generatePath() - { - $setCr = $this->filterObj->getSetCriteria(1, 2, 3, 6, 9, 11); - if (count($setCr) == 1) - $this->path[] = $setCr[0]; - } -} - -?> diff --git a/template/pages/icon.tpl.php b/template/pages/icon.tpl.php index 727c1ade..5ec5f25a 100644 --- a/template/pages/icon.tpl.php +++ b/template/pages/icon.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
      @@ -19,20 +22,20 @@ $this->brick('redButtons'); ?> -

      name; ?>

      +

      h1; ?>

      brick('article'); + $this->brick('markup', ['markup' => $this->article]); ?>

      brick('lvTabs', ['relTabs' => true]); + $this->brick('lvTabs'); $this->brick('contribute'); ?> diff --git a/template/pages/icons.tpl.php b/template/pages/icons.tpl.php index 11f6c9c9..2ab62c48 100644 --- a/template/pages/icons.tpl.php +++ b/template/pages/icons.tpl.php @@ -1,10 +1,11 @@ - - brick('header'); -$f = $this->filterObj->values // shorthand -?> + namespace Aowow\Template; + use \Aowow\Lang; + +$this->brick('header'); +$f = $this->filter->values; // shorthand +?>
      @@ -12,17 +13,24 @@ $f = $this->filterObj->values // shorthand brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem' => [101]]); +$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); ?> - -
      +
      +
      +brick('headIcons'); + +$this->brick('redButtons'); +?> +

      h1; ?>

      +
      ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
       /> />
      - + @@ -31,7 +39,7 @@ $this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem'
      - /> /> + /> />
      @@ -45,7 +53,7 @@ $this->brick('pageTemplate', ['fiQuery' => $this->filterObj->query, 'fiMenuItem'
      -brick('filter'); ?> +renderFilter(12); ?> brick('lvTabs'); ?> From 3f8d5d90e1df5a7d12f8af3319eeee6afac7789f Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 22:20:24 +0200 Subject: [PATCH 0971/1249] Template/Update (Part 36) * convert dbtype 'mail' --- endpoints/mail/mail.php | 178 +++++++++++++++++++++++++ endpoints/mails/mails.php | 59 ++++++++ pages/mail.php | 169 ----------------------- pages/mails.php | 48 ------- setup/tools/sqlgen/mailtemplate.ss.php | 24 ++-- template/listviews/mail.tpl | 11 +- 6 files changed, 259 insertions(+), 230 deletions(-) create mode 100644 endpoints/mail/mail.php create mode 100644 endpoints/mails/mails.php delete mode 100644 pages/mail.php delete mode 100644 pages/mails.php diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php new file mode 100644 index 00000000..705c6aca --- /dev/null +++ b/endpoints/mail/mail.php @@ -0,0 +1,178 @@ +typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new MailList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('mail'), Lang::mail('notFound')); + + $this->extendGlobalData($this->subject->getJSGlobals()); + + $this->h1 = Util::htmlEscape($this->subject->getField('name', true)); + + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->subject->getField('name', true) + ); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('name', true), Util::ucFirst(Lang::game('mail'))); + + + /***********/ + /* Infobox */ + /***********/ + + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + + // sender + delay + if ($this->typeId < 0) // def. achievement + { + if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `ID` = ?d', -$this->typeId)) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + } + else if ($mlr = DB::World()->selectRow('SELECT * FROM mail_level_reward WHERE `mailTemplateId` = ?d', $this->typeId)) // level rewards + { + if ($mlr['level']) + $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; + + if ($r = Lang::getRaceString($mlr['raceMask'], $rIds, Lang::FMT_MARKUP)) + { + $infobox[] = Lang::game('races').Lang::main('colon').$r; + $this->extendGlobalIds(Type::CHR_RACE, ...$rIds); + } + + $infobox[] = Lang::mail('sender', ['[npc='.$mlr['senderEntry'].']']); + $this->extendGlobalIds(Type::NPC, $mlr['senderEntry']); + } + else // achievement or quest + { + if ($q = DB::Aowow()->selectRow('SELECT `id`, `rewardMailDelay` FROM ?_quests WHERE `rewardMailTemplateId` = ?d', $this->typeId)) + { + if ($npcId= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = ?d', $q['id'])) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + else if ($npcId = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_quests_startend WHERE `questId` = ?d AND `type` = ?d AND `method` & ?d', $q['id'], Type::NPC, 0x2)) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + + if ($q['rewardMailDelay'] > 0) + $infobox[] = Lang::mail('delay', [Util::formatTime($q['rewardMailDelay'] * 1000)]); + } + else if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId)) + { + $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); + $this->extendGlobalIds(Type::NPC, $npcId); + } + } + + if ($infobox) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], + BUTTON_WOWHEAD => false + ); + + $this->extraText = new Markup(Util::parseHtmlText($this->subject->getField('text', true), true), ['dbpage' => true, 'allow' => Markup::CLASS_ADMIN], 'text-generic'); + + + /**************/ + /* Extra Tabs */ + /**************/ + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + + // tab: attachment + if ($itemId = $this->subject->getField('attachment')) + { + $attachment = new ItemList(array(['id', $itemId])); + if (!$attachment->error) + { + $this->extendGlobalData($attachment->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $attachment->getListviewData(), + 'name' => Lang::mail('attachment'), + 'id' => 'attachment' + ), ItemList::$brickFile)); + } + } + + if ($this->typeId < 0 || // used by: achievement + ($acvId = DB::World()->selectCell('SELECT `ID` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId))) + { + $ubAchievements = new AchievementList(array(['id', $this->typeId < 0 ? -$this->typeId : $acvId])); + if (!$ubAchievements->error) + { + $this->extendGlobalData($ubAchievements->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubAchievements->getListviewData(), + 'id' => 'used-by-achievement' + ), AchievementList::$brickFile)); + } + } + else // used by: quest + { + $ubQuests = new QuestList(array(['rewardMailTemplateId', $this->typeId])); + if (!$ubQuests->error) + { + $this->extendGlobalData($ubQuests->getJsGlobals()); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $ubQuests->getListviewData(), + 'id' => 'used-by-quest' + ), QuestList::$brickFile)); + } + } + + parent::generate(); + } +} + +?> diff --git a/endpoints/mails/mails.php b/endpoints/mails/mails.php new file mode 100644 index 00000000..e892948f --- /dev/null +++ b/endpoints/mails/mails.php @@ -0,0 +1,59 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('mails')); + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1); + + + /****************/ + /* Main Content */ + /****************/ + + $tabData = []; + $mails = new MailList(); + if (!$mails->error) + $tabData['data'] = $mails->getListviewData(); + + $this->extendGlobalData($mails->getJsGlobals()); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(['data' => $mails->getListviewData()], MailList::$brickFile, 'mail')); + + parent::generate(); + } +} + +?> diff --git a/pages/mail.php b/pages/mail.php deleted file mode 100644 index 157188f4..00000000 --- a/pages/mail.php +++ /dev/null @@ -1,169 +0,0 @@ -typeId = intVal($id); - - $this->subject = new MailList(array(['id', $this->typeId])); - - if ($this->subject->error) - $this->notFound(lang::game('mail'), Lang::mail('notFound')); - - $this->extendGlobalData($this->subject->getJSGlobals()); - - $this->name = Util::htmlEscape(Util::ucFirst($this->subject->getField('name', true))); - } - - protected function generateContent() - { - /***********/ - /* Infobox */ - /***********/ - - $infobox = []; - - // sender + delay - if ($this->typeId < 0) // def. achievement - { - if ($npcId = DB::World()->selectCell('SELECT Sender FROM achievement_reward WHERE ID = ?d', -$this->typeId)) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - } - else if ($mlr = DB::World()->selectRow('SELECT * FROM mail_level_reward WHERE mailTemplateId = ?d', $this->typeId)) // level rewards - { - if ($mlr['level']) - $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; - - $rIds = []; - if ($r = Lang::getRaceString($mlr['raceMask'], $rIds, Lang::FMT_MARKUP)) - { - $infobox[] = Lang::game('races').Lang::main('colon').$r; - $this->extendGlobalIds(Type::CHR_RACE, ...$rIds); - } - - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$mlr['senderEntry'].']'; - $this->extendGlobalIds(Type::NPC, $mlr['senderEntry']); - } - else // achievement or quest - { - if ($q = DB::Aowow()->selectRow('SELECT id, rewardMailDelay FROM ?_quests WHERE rewardMailTemplateId = ?d', $this->typeId)) - { - if ($npcId= DB::World()->selectCell('SELECT RewardMailSenderEntry FROM quest_mail_sender WHERE QuestId = ?d', $q['id'])) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - else if ($npcId = DB::Aowow()->selectCell('SELECT typeId FROM ?_quests_startend WHERE questId = ?d AND type = ?d AND method & ?d', $q['id'], Type::NPC, 0x2)) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - - if ($q['rewardMailDelay'] > 0) - $infobox[] = Lang::mail('delay').Lang::main('colon').''.Util::formatTime($q['rewardMailDelay'] * 1000); - } - else if ($npcId = DB::World()->selectCell('SELECT Sender FROM achievement_reward WHERE MailTemplateId = ?d', $this->typeId)) - { - $infobox[] = Lang::mail('sender').Lang::main('colon').'[npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - } - - /****************/ - /* Main Content */ - /****************/ - - $this->infobox = $infobox ? '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]' : ''; - $this->redButtons = array( - BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], - BUTTON_WOWHEAD => false - ); - - $this->extraText = Util::parseHtmlText($this->subject->getField('text', true), true); - - - /**************/ - /* Extra Tabs */ - /**************/ - - // tab: attachment - if ($itemId = $this->subject->getField('attachment')) - { - $attachment = new ItemList(array(['id', $itemId])); - if (!$attachment->error) - { - $this->extendGlobalData($attachment->getJsGlobals()); - $this->lvTabs[] = [ItemList::$brickFile, array( - 'data' => array_values($attachment->getListviewData()), - 'name' => Lang::mail('attachment'), - 'id' => 'attachment' - )]; - } - } - - - if ($this->typeId < 0 || // used by: achievement - ($acvId = DB::World()->selectCell('SELECT ID FROM achievement_reward WHERE MailTemplateId = ?d', $this->typeId))) - { - $ubAchievements = new AchievementList(array(['id', $this->typeId < 0 ? -$this->typeId : $acvId])); - if (!$ubAchievements->error) - { - $this->extendGlobalData($ubAchievements->getJsGlobals()); - $this->lvTabs[] = [AchievementList::$brickFile, array( - 'data' => array_values($ubAchievements->getListviewData()), - 'id' => 'used-by-achievement' - )]; - } - } - else if ($npcId = DB::World()->selectCell('SELECT ID FROM achievement_reward WHERE MailTemplateId = ?d', $this->typeId)) - { - $infobox[] = '[Sender]: [npc='.$npcId.']'; - $this->extendGlobalIds(Type::NPC, $npcId); - } - - else // used by: quest - { - $ubQuests = new QuestList(array(['rewardMailTemplateId', $this->typeId])); - if (!$ubQuests->error) - { - $this->extendGlobalData($ubQuests->getJsGlobals()); - $this->lvTabs[] = [QuestList::$brickFile, array( - 'data' => array_values($ubQuests->getListviewData()), - 'id' => 'used-by-quest' - )]; - } - } - } - - protected function generateTitle() - { - array_unshift($this->title, Util::ucFirst($this->subject->getField('name', true)), Util::ucFirst(Lang::game('mail'))); - } - - protected function generatePath() { } -} - -?> diff --git a/pages/mails.php b/pages/mails.php deleted file mode 100644 index 391b575f..00000000 --- a/pages/mails.php +++ /dev/null @@ -1,48 +0,0 @@ -name = Util::ucFirst(Lang::game('mails')); - } - - protected function generateContent() - { - $tabData = []; - $mails = new MailList(); - if (!$mails->error) - $tabData['data'] = array_values($mails->getListviewData()); - - $this->extendGlobalData($mails->getJsGlobals()); - - $this->lvTabs[] = [MailList::$brickFile, $tabData, 'mail']; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - } - - protected function generatePath() { } -} - -?> diff --git a/setup/tools/sqlgen/mailtemplate.ss.php b/setup/tools/sqlgen/mailtemplate.ss.php index 32c21948..d2e17d1e 100644 --- a/setup/tools/sqlgen/mailtemplate.ss.php +++ b/setup/tools/sqlgen/mailtemplate.ss.php @@ -28,17 +28,17 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[mails] - loading data from achievement_reward'); $acvMail = DB::World()->select( - 'SELECT -ar.ID, 0, - IFNULL(ar.Subject, "") AS s0, IFNULL(arl2.Subject, "") AS s2, IFNULL(arl3.Subject, "") AS s3, IFNULL(arl4.Subject, "") AS s4, IFNULL(arl6.Subject, "") AS s6, IFNULL(arl8.Subject, "") AS s8, - IFNULL(ar.Body, "") AS t0, IFNULL(arl2.Body, "") AS t2, IFNULL(arl3.Body, "") AS t3, IFNULL(arl4.Body, "") AS t4, IFNULL(arl6.Body, "") AS t6, IFNULL(arl8.Body, "") AS t8, - ItemID + 'SELECT -ar.`ID`, 0, + IFNULL(ar.`Subject`, "") AS "s0", IFNULL(arl2.`Subject`, "") AS "s2", IFNULL(arl3.`Subject`, "") AS "s3", IFNULL(arl4.`Subject`, "") AS "s4", IFNULL(arl6.`Subject`, "") AS "s6", IFNULL(arl8.`Subject`, "") AS "s8", + IFNULL(ar.`Body`, "") AS "t0", IFNULL(arl2.`Body`, "") AS "t2", IFNULL(arl3.`Body`, "") AS "t3", IFNULL(arl4.`Body`, "") AS "t4", IFNULL(arl6.`Body`, "") AS "t6", IFNULL(arl8.`Body`, "") AS "t8", + `ItemID` FROM achievement_reward ar - LEFT JOIN achievement_reward_locale arl2 ON ar.ID = arl2.ID AND arl2.Locale = "frFR" - LEFT JOIN achievement_reward_locale arl3 ON ar.ID = arl3.ID AND arl3.Locale = "deDE" - LEFT JOIN achievement_reward_locale arl4 ON ar.ID = arl4.ID AND arl4.Locale = "zhCN" - LEFT JOIN achievement_reward_locale arl6 ON ar.ID = arl6.ID AND arl6.Locale = "esES" - LEFT JOIN achievement_reward_locale arl8 ON ar.ID = arl8.ID AND arl8.Locale = "ruRU" - WHERE ar.MailTemplateID = 0 AND ar.Body <> ""' + LEFT JOIN achievement_reward_locale arl2 ON ar.`ID` = arl2.`ID` AND arl2.`Locale` = "frFR" + LEFT JOIN achievement_reward_locale arl3 ON ar.`ID` = arl3.`ID` AND arl3.`Locale` = "deDE" + LEFT JOIN achievement_reward_locale arl4 ON ar.`ID` = arl4.`ID` AND arl4.`Locale` = "zhCN" + LEFT JOIN achievement_reward_locale arl6 ON ar.`ID` = arl6.`ID` AND arl6.`Locale` = "esES" + LEFT JOIN achievement_reward_locale arl8 ON ar.`ID` = arl8.`ID` AND arl8.`Locale` = "ruRU" + WHERE ar.`MailTemplateID` = 0 AND ar.`Body` <> ""' ); DB::Aowow()->query('INSERT INTO ?_mails VALUES (?a)', array_values($acvMail)); @@ -46,9 +46,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[mails] - loading data from mail_loot_template'); // assume mails to only contain one single item, wich works for an unmodded installation - $mlt = DB::World()->selectCol('SELECT Entry AS ARRAY_KEY, Item FROM mail_loot_template'); + $mlt = DB::World()->selectCol('SELECT `Entry` AS ARRAY_KEY, `Item` FROM mail_loot_template'); foreach ($mlt as $k => $v) - DB::Aowow()->query('UPDATE ?_mails SET attachment = ?d WHERE id = ?d', $v, $k); + DB::Aowow()->query('UPDATE ?_mails SET `attachment` = ?d WHERE `id` = ?d', $v, $k); return true; } diff --git a/template/listviews/mail.tpl b/template/listviews/mail.tpl index 2ebe19f4..72e05fda 100644 --- a/template/listviews/mail.tpl +++ b/template/listviews/mail.tpl @@ -11,7 +11,9 @@ Listview.templates.mail = { value: 'id', compute: function(data, td) { if (data.id) { - $WH.ae(td, $WH.ct(data.id)); + let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id)); + $WH.clickToCopy(pre); + $WH.ae(td, pre); } } }, @@ -89,5 +91,12 @@ Listview.templates.mail = { ], getItemLink: function(mail) { return '?mail=' + mail.id; + }, + onBeforeCreate : function() { + // hide duplicate id col + if (this.debug || g_user?.debug) { + let colId = this.columns.findIndex(x => x.id == 'id'); + this.visibility = this.visibility.filter(x => x != colId); + } } } From 3d3e2211e50d8fc1a298cfb2bcc37d411dff1a89 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 13 Aug 2025 23:12:34 +0200 Subject: [PATCH 0972/1249] Template/Update (Part 37) * convert dbtype 'sound' --- {pages => endpoints/sound}/sound.php | 159 ++++++++++++-------------- endpoints/sound/sound_playlist.php | 26 +++++ endpoints/sounds/sounds.php | 117 +++++++++++++++++++ pages/sounds.php | 93 --------------- template/pages/sound-playlist.tpl.php | 68 +++++++++++ template/pages/sound.tpl.php | 71 ++---------- template/pages/sounds.tpl.php | 38 +++--- 7 files changed, 315 insertions(+), 257 deletions(-) rename {pages => endpoints/sound}/sound.php (72%) create mode 100644 endpoints/sound/sound_playlist.php create mode 100644 endpoints/sounds/sounds.php delete mode 100644 pages/sounds.php create mode 100644 template/pages/sound-playlist.tpl.php diff --git a/pages/sound.php b/endpoints/sound/sound.php similarity index 72% rename from pages/sound.php rename to endpoints/sound/sound.php index c90a0a69..6207cf2a 100644 --- a/pages/sound.php +++ b/endpoints/sound/sound.php @@ -6,97 +6,82 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -// menuId 19: Sound g_initPath() -// tabId 0: Database g_initHeader() -class SoundPage extends GenericPage +class SoundBaseResponse extends TemplateResponse implements ICache { - use TrDetailPage; + use TrDetailPage, TrCache; - protected $type = Type::SOUND; - protected $typeId = 0; - protected $tpl = 'sound'; - protected $path = [0, 19]; - protected $tabId = 0; - protected $mode = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_PAGE; - protected $special = false; - protected $_get = ['playlist' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet']]; + protected string $template = 'sound'; + protected string $pageName = 'sound'; + protected ?int $activeTab = parent::TAB_DATABASE; + protected array $breadcrumb = [0, 19]; - private $cat = 0; + public int $type = Type::SOUND; + public int $typeId = 0; - public function __construct($pageCall, $id) + private SoundList $subject; + + public function __construct(string $id) { - parent::__construct($pageCall, $id); + parent::__construct($id); - // special case - if (!$id && $this->_get['playlist']) - { - $this->special = true; - $this->name = Lang::sound('cat', 1000); - $this->cat = 1000; - $this->articleUrl = 'sound&playlist'; - $this->contribute = CONTRIBUTE_NONE; - $this->mode = CACHE_TYPE_NONE; - } - // regular case - else - { - $this->typeId = intVal($id); - - $this->subject = new SoundList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('sound'), Lang::sound('notFound')); - - $this->name = $this->subject->getField('name'); - $this->cat = $this->subject->getField('cat'); - } + $this->typeId = intVal($id); + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; } - protected function generatePath() + protected function generate() : void { - $this->path[] = $this->cat; - } + $this->subject = new SoundList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('sound'), Lang::sound('notFound')); - protected function generateTitle() - { - array_unshift($this->title, $this->name, Util::ucFirst(Lang::game('sound'))); - } + $this->h1 = $this->subject->getField('name'); - protected function generateContent() - { - if ($this->special) - $this->generatePlaylistContent(); - else - $this->generateDefaultContent(); - } + $this->gPageInfo += array( + 'type' => $this->type, + 'typeId' => $this->typeId, + 'name' => $this->h1 + ); - private function generatePlaylistContent() - { + $_cat = $this->subject->getField('cat'); + + + /*************/ + /* Menu Path */ + /*************/ + + $this->breadcrumb[] = $_cat; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('sound'))); - } - private function generateDefaultContent() - { /****************/ /* Main Content */ /****************/ - $this->addScript([SC_JS_FILE, '?data=zones']); - // get spawns - $map = null; if ($spawns = $this->subject->getSpawns(SPAWNINFO_FULL)) { - $map = ['data' => ['parent' => 'mapper-generic'], 'mapperData' => &$spawns, 'foundIn' => Lang::sound('foundIn')]; - foreach ($spawns as $areaId => &$areaData) - $map['extra'][$areaId] = ZoneList::getName($areaId); + $this->addDataLoader('zones'); + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $spawns, // mapperData + null, // ShowOnMap + [Lang::sound('foundIn')] // foundIn + ); + foreach ($spawns as $areaId => $__) + $this->map[3][$areaId] = ZoneList::getName($areaId); } - // get full path ingame for sound (workaround for missing PlaySoundKit()) + // get full path in-game for sound (workaround for missing PlaySoundKit()) $fullpath = DB::Aowow()->selectCell('SELECT IF(sf.`path` <> "", CONCAT(sf.`path`, "\\\\", sf.`file`), sf.`file`) FROM ?_sounds_files sf JOIN ?_sounds s ON s.`soundFile1` = sf.`id` WHERE s.`id` = ?d', $this->typeId); - $this->map = $map; - $this->headIcons = [$this->subject->getField('iconString')]; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_PLAYLIST => true, @@ -109,11 +94,15 @@ class SoundPage extends GenericPage $this->extendGlobalData($this->subject->getJSGlobals()); + parent::generate(); + /**************/ /* Extra Tabs */ /**************/ + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); + // tab: Spells // skipping (always empty): ready, castertargeting, casterstate, targetstate $displayIds = DB::Aowow()->selectCol( @@ -134,9 +123,9 @@ class SoundPage extends GenericPage $cnd = array( 'OR', - ['AND', ['effect1Id', 132], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', 132], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', 132], ['effect3MiscValue', $this->typeId]] + ['AND', ['effect1Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect1MiscValue', $this->typeId]], + ['AND', ['effect2Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect2MiscValue', $this->typeId]], + ['AND', ['effect3Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect3MiscValue', $this->typeId]] ); if ($displayIds) @@ -145,19 +134,18 @@ class SoundPage extends GenericPage if ($seMiscValues) $cnd[] = array( 'OR', - ['AND', ['effect1AuraId', 260], ['effect1MiscValue', $seMiscValues]], - ['AND', ['effect2AuraId', 260], ['effect2MiscValue', $seMiscValues]], - ['AND', ['effect3AuraId', 260], ['effect3MiscValue', $seMiscValues]] + ['AND', ['effect1AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect1MiscValue', $seMiscValues]], + ['AND', ['effect2AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect2MiscValue', $seMiscValues]], + ['AND', ['effect3AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect3MiscValue', $seMiscValues]] ); $spells = new SpellList($cnd); if (!$spells->error) { $this->extendGlobalData($spells->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [SpellList::$brickFile, ['data' => array_values($spells->getListviewData())]]; + $this->lvTabs->addListviewTab(new Listview(['data' => $spells->getListviewData()], SpellList::$brickFile)); } - // tab: Items $subClasses = []; if ($subClassMask = DB::Aowow()->selectCell('SELECT `subClassMask` FROM ?_items_sounds WHERE `soundId` = ?d', $this->typeId)) @@ -179,11 +167,10 @@ class SoundPage extends GenericPage if (!$items->error) { $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [ItemList::$brickFile, ['data' => array_values($items->getListviewData())]]; + $this->lvTabs->addListviewTab(new Listview(['data' => $items->getListviewData()], ItemList::$brickFile)); } } - // tab: Zones if ($zoneIds = DB::Aowow()->select('SELECT `id`, `worldStateId`, `worldStateValue` FROM ?_zones_sounds WHERE `ambienceDay` = ?d OR `ambienceNight` = ?d OR `musicDay` = ?d OR `musicNight` = ?d OR `intro` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId)) { @@ -240,14 +227,13 @@ class SoundPage extends GenericPage } } - $tabData['data'] = array_values($zoneData); + $tabData['data'] = $zoneData; $tabData['hiddenCols'] = ['territory']; - $this->lvTabs[] = [ZoneList::$brickFile, $tabData]; + $this->lvTabs->addListviewTab(new Listview($tabData, ZoneList::$brickFile)); } } - // tab: Races (VocalUISounds (containing error voice overs)) if ($vo = DB::Aowow()->selectCol('SELECT `raceId` FROM ?_races_sounds WHERE `soundId` = ?d GROUP BY `raceId`', $this->typeId)) { @@ -255,11 +241,10 @@ class SoundPage extends GenericPage if (!$races->error) { $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [CharRaceList::$brickFile, ['data' => array_values($races->getListviewData())]]; + $this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile)); } } - // tab: Emotes (EmotesTextSound (containing emote audio)) if ($em = DB::Aowow()->selectCol('SELECT `emoteId` FROM ?_emotes_sounds WHERE `soundId` = ?d GROUP BY `emoteId` UNION SELECT `id` FROM ?_emotes WHERE `soundId` = ?d', $this->typeId, $this->typeId)) { @@ -267,10 +252,10 @@ class SoundPage extends GenericPage if (!$races->error) { $this->extendGlobalData($races->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [EmoteList::$brickFile, array( - 'data' => array_values($races->getListviewData()), + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $races->getListviewData(), 'name' => Util::ucFirst(Lang::game('emotes')) - ), 'emote']; + ), EmoteList::$brickFile, 'emote')); } } @@ -289,7 +274,7 @@ class SoundPage extends GenericPage `injury` = ?d OR `injurycritical` = ?d OR `death` = ?d OR `stun` = ?d OR `stand` = ?d OR `aggro` = ?d OR `wingflap` = ?d OR `wingglide` = ?d OR `alert` = ?d OR `fidget` = ?d OR `customattack` = ?d OR `loop` = ?d OR `jumpstart` = ?d OR `jumpend` = ?d OR `petattack` = ?d OR - `petorder` = ?d OR `petdismiss` = ?d OR `birth` = ?d OR `spellcast` = ?d OR `submerge` = ?d OR `submerged` = ?d', + `petorder` = ?d OR `petdismiss` = ?d OR `birth` = ?d OR `spellcast` = ?d OR `submerge` = ?d OR `submerged` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, @@ -319,10 +304,10 @@ class SoundPage extends GenericPage $npcs = new CreatureList($cnds); if (!$npcs->error) { - $this->addScript([SC_JS_FILE, '?data=zones']); - $this->extendGlobalData($npcs->getJSGlobals(GLOBALINFO_SELF)); - $this->lvTabs[] = [CreatureList::$brickFile, ['data' => array_values($npcs->getListviewData())]]; + + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(['data' => $npcs->getListviewData()], CreatureList::$brickFile)); } } } diff --git a/endpoints/sound/sound_playlist.php b/endpoints/sound/sound_playlist.php new file mode 100644 index 00000000..334117ae --- /dev/null +++ b/endpoints/sound/sound_playlist.php @@ -0,0 +1,26 @@ +h1 = Lang::sound('cat', 1000); + + array_unshift($this->title, $this->h1, Util::ucFirst(Lang::game('sound'))); + + parent::generate(); + } +} + +?> diff --git a/endpoints/sounds/sounds.php b/endpoints/sounds/sounds.php new file mode 100644 index 00000000..3ed6eaa6 --- /dev/null +++ b/endpoints/sounds/sounds.php @@ -0,0 +1,117 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] + ); + protected array $validCats = [1, 2, 3, 4, 6, 9, 10, 12, 13, 14, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 50, 52, 53]; + + public function __construct(string $pageParam) + { + $this->getCategoryFromUrl($pageParam); + if ($this->category) + $this->forward('?sounds&filter=ty='.$this->category[0]); + + parent::__construct($pageParam); + + $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + $this->filter = new SoundListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + $this->filterError = $this->filter->error; + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('sounds')); + + $this->filter->evalCriteria(); + + $conditions = []; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; + + if ($_ = $this->filter->getConditions()) + $conditions[] = $_; + + $this->filterError = $this->filter->error; // maybe the evalX() caused something + + + /**************/ + /* Page Title */ + /**************/ + + $fiForm = $this->filter->values; + + array_unshift($this->title, $this->h1); + if (count($fiForm['ty']) == 1) + array_unshift($this->title, Lang::sound('cat', $fiForm['ty'][0])); + + + /*************/ + /* Menu Path */ + /*************/ + + if (count($fiForm['ty']) == 1) + $this->breadcrumb[] = $fiForm['ty'][0]; + + + /****************/ + /* Main Content */ + /****************/ + + $this->redButtons = array( + BUTTON_WOWHEAD => true, + BUTTON_PLAYLIST => true + ); + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; + + $tabData = []; + $sounds = new SoundList($conditions, ['calcTotal' => true]); + if (!$sounds->error) + { + $tabData['data'] = $sounds->getListviewData(); + + // create note if search limit was exceeded; overwriting 'note' is intentional + if ($sounds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + { + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['_truncated'] = 1; + } + } + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview($tabData, SoundList::$brickFile)); + + parent::generate(); + + $this->setOnCacheLoaded([self::class, 'onBeforeDisplay']); + } + + public static function onBeforeDisplay() + { + // sort for dropdown-menus in filter + Lang::sort('sound', 'cat'); + } +} + +?> diff --git a/pages/sounds.php b/pages/sounds.php deleted file mode 100644 index ea064f36..00000000 --- a/pages/sounds.php +++ /dev/null @@ -1,93 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW]]; - - public function __construct($pageCall, $pageParam) - { - $this->getCategoryFromUrl($pageParam); - if (isset($this->category[0])) - header('Location: ?sounds&filter=ty='.$this->category[0], true, 302); - - parent::__construct($pageCall, $pageParam); - - $this->filterObj = new SoundListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); - - $this->name = Util::ucFirst(Lang::game('sounds')); - } - - protected function generateContent() - { - $this->redButtons = array( - BUTTON_WOWHEAD => true, - BUTTON_PLAYLIST => true - ); - - $this->filterObj->evalCriteria(); - - $conditions = []; - if ($_ = $this->filterObj->getConditions()) - $conditions[] = $_; - - $tabData = []; - $sounds = new SoundList($conditions, ['calcTotal' => true]); - if (!$sounds->error) - { - $tabData['data'] = array_values($sounds->getListviewData()); - - // create note if search limit was exceeded; overwriting 'note' is intentional - if ($sounds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); - $tabData['_truncated'] = 1; - } - - if ($this->filterObj->error) - $tabData['_errors'] = 1; - } - $this->lvTabs[] = [SoundList::$brickFile, $tabData]; - } - - protected function postCache() - { - // sort for dropdown-menus - Lang::sort('sound', 'cat'); - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - array_unshift($this->title, Lang::sound('cat', $form['ty'][0])); - } - - protected function generatePath() - { - $form = $this->filterObj->values; - if (count($form['ty']) == 1) - $this->path[] = $form['ty'][0]; - } -} - -?> diff --git a/template/pages/sound-playlist.tpl.php b/template/pages/sound-playlist.tpl.php new file mode 100644 index 00000000..1d561627 --- /dev/null +++ b/template/pages/sound-playlist.tpl.php @@ -0,0 +1,68 @@ +brick('header'); +?> +
      +
      +
      + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
      +

      h1; ?>

      + +brick('markup', ['markup' => $this->article]); ?> + +
      +
      + +
      +
      +
      + +brick('footer'); ?> diff --git a/template/pages/sound.tpl.php b/template/pages/sound.tpl.php index b52e3841..6dbb1a32 100644 --- a/template/pages/sound.tpl.php +++ b/template/pages/sound.tpl.php @@ -1,7 +1,10 @@ - +brick('header'); ?> + use \Aowow\Lang; + $this->brick('header'); +?>
      @@ -17,68 +20,19 @@ $this->brick('redButtons'); ?> -

      name; ?>

      +

      h1; ?>

      brick('article'); + $this->brick('markup', ['markup' => $this->article]); - if ($this->special): -?> -
      -
      - -
      - -map)): - $this->brick('mapper'); - endif; + $this->brickIf($this->map, 'mapper'); ?>
        From cb523353fd448a86d10d2b02677f4f39f06eebbd Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 14 Aug 2025 01:12:42 +0200 Subject: [PATCH 0974/1249] Template/Update (Part 39) * implement video suggestion & management --- endpoints/admin/videos.php | 68 ++ endpoints/admin/videos_approve.php | 46 + endpoints/admin/videos_delete.php | 43 + endpoints/admin/videos_edittitle.php | 31 + endpoints/admin/videos_list.php | 23 + endpoints/admin/videos_manage.php | 31 + endpoints/admin/videos_order.php | 57 ++ endpoints/admin/videos_relocate.php | 49 + endpoints/admin/videos_sticky.php | 56 ++ endpoints/video/add.php | 124 +++ endpoints/video/complete.php | 92 ++ endpoints/video/confirm.php | 81 ++ endpoints/video/thankyou.php | 60 ++ includes/components/videomgr.class.php | 229 +++++ includes/defines.php | 4 +- includes/utilities.php | 4 +- localization/lang.class.php | 1 + localization/locale_dede.php | 14 + localization/locale_enus.php | 14 + localization/locale_eses.php | 14 + localization/locale_frfr.php | 14 + localization/locale_ruru.php | 14 + localization/locale_zhcn.php | 15 + setup/updates/1758578400_07.sql | 8 + setup/updates/1758578400_08.sql | 8 + setup/updates/1758578400_09.sql | 4 + static/js/global.js | 13 +- static/js/locale_dede.js | 2 +- static/js/locale_enus.js | 2 +- static/js/locale_eses.js | 2 +- static/js/locale_frfr.js | 2 +- static/js/locale_ruru.js | 2 +- static/js/locale_zhcn.js | 2 +- static/js/video.js | 1135 ++++++++++++++++++++++++ template/pages/admin/videos.tpl.php | 135 +++ template/pages/video.tpl.php | 82 ++ 36 files changed, 2465 insertions(+), 16 deletions(-) create mode 100644 endpoints/admin/videos.php create mode 100644 endpoints/admin/videos_approve.php create mode 100644 endpoints/admin/videos_delete.php create mode 100644 endpoints/admin/videos_edittitle.php create mode 100644 endpoints/admin/videos_list.php create mode 100644 endpoints/admin/videos_manage.php create mode 100644 endpoints/admin/videos_order.php create mode 100644 endpoints/admin/videos_relocate.php create mode 100644 endpoints/admin/videos_sticky.php create mode 100644 endpoints/video/add.php create mode 100644 endpoints/video/complete.php create mode 100644 endpoints/video/confirm.php create mode 100644 endpoints/video/thankyou.php create mode 100644 includes/components/videomgr.class.php create mode 100644 setup/updates/1758578400_07.sql create mode 100644 setup/updates/1758578400_08.sql create mode 100644 setup/updates/1758578400_09.sql create mode 100644 static/js/video.js create mode 100644 template/pages/admin/videos.tpl.php create mode 100644 template/pages/video.tpl.php diff --git a/endpoints/admin/videos.php b/endpoints/admin/videos.php new file mode 100644 index 00000000..10beece8 --- /dev/null +++ b/endpoints/admin/videos.php @@ -0,0 +1,68 @@ + Content > Videos + + protected array $scripts = array( + [SC_JS_FILE, 'js/video.js'], + [SC_CSS_STRING, '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'], + [SC_CSS_STRING, '#highlightedRow { background-color: #322C1C; }'] + ); + protected array $expectedGET = array( + 'action' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'all' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']], + 'type' => ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode' ] + ); + + public ?bool $getAll = null; + public array $viPages = []; + public array $viData = []; + public int $viNFound = 0; + public array $pageTypes = []; + + protected function generate() : void + { + $this->h1 = 'Video Manager'; + + // types that can have videos + foreach (Type::getClassesFor(0, 'contribute', CONTRIBUTE_SS) as $type => $obj) + $this->pageTypes[$type] = Util::ucWords(Lang::game(Type::getFileString($type))); + + $viGetAll = $this->_get['all']; + $viPages = []; + $viData = []; + $nMatches = 0; + + if ($this->_get['type'] && $this->_get['typeid']) + $viData = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid'], nFound: $nMatches); + else if ($this->_get['user']) + { + if (mb_strlen($this->_get['user']) >= 3) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + $viData = VideoMgr::getVideos(userId: $uId, nFound: $nMatches); + } + else + $viPages = VideoMgr::getPages($viGetAll, $nMatches); + + $this->getAll = $viGetAll; + $this->viPages = $viPages; + $this->viData = $viData; + $this->viNFound = $nMatches; // ssm_numPagesFound + + parent::generate(); + } +} diff --git a/endpoints/admin/videos_approve.php b/endpoints/admin/videos_approve.php new file mode 100644 index 00000000..32e866e9 --- /dev/null +++ b/endpoints/admin/videos_approve.php @@ -0,0 +1,46 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionApproveResponse - videoId empty', E_USER_ERROR); + return; + } + + $viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_APPROVED, $this->_get['id']); + foreach ($viEntries as $id => $viData) + { + // set as approved in DB + DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + + // gain siterep + Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]); + + // flag DB entry as having videos + if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable')) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + + unset($viEntries[$id]); + } + + if (!$viEntries) + trigger_error('AdminVideosActionApproveResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or already approved', E_USER_WARNING); + } +} diff --git a/endpoints/admin/videos_delete.php b/endpoints/admin/videos_delete.php new file mode 100644 index 00000000..d3b6c19e --- /dev/null +++ b/endpoints/admin/videos_delete.php @@ -0,0 +1,43 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + // 2 steps: 1) remove from sight, 2) remove from disk + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionDeleteResponse - videoId empty', E_USER_ERROR); + return; + } + + // irrevocably purge files already flagged as deleted (should only exist as pending) + if (User::isInGroup(U_GROUP_ADMIN)) + DB::Aowow()->selectCell('SELECT 1 FROM ?_videos WHERE `status` & ?d AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + + // flag as deleted if not aready + $oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ?_videos WHERE `id` IN (?a) GROUP BY `type`', $this->_get['id']); + DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdDelete` = ?d WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, User::$id, CC_FLAG_DELETED, $this->_get['id']); + + // deflag db entry as having videos + foreach ($oldEntries as $type => $typeIds) + { + $typeIds = explode(',', $typeIds); + $toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds); + if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable'))) + DB::Aowow()->query('UPDATE ?# SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', $tbl, CUSTOM_HAS_VIDEO, array_keys($toUnflag)); + } + } +} diff --git a/endpoints/admin/videos_edittitle.php b/endpoints/admin/videos_edittitle.php new file mode 100644 index 00000000..d9c92dfe --- /dev/null +++ b/endpoints/admin/videos_edittitle.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + protected array $expectedPOST = array( + 'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + return; + + $caption = $this->handleCaption($this->_post['title']); + + DB::Aowow()->query('UPDATE ?_videos SET `caption` = ? WHERE `id` = ?d', $caption, $this->_get['id'][0]); + } +} diff --git a/endpoints/admin/videos_list.php b/endpoints/admin/videos_list.php new file mode 100644 index 00000000..7b02d12b --- /dev/null +++ b/endpoints/admin/videos_list.php @@ -0,0 +1,23 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] + ); + + protected function generate() : void + { + $pages = VideoMgr::getPages($this->_get['all'], $nPages); + $this->result = 'vim_videoPages = '.Util::toJSON($pages).";\n"; + $this->result .= 'vim_numPagesFound = '.$nPages.';'; + } +} diff --git a/endpoints/admin/videos_manage.php b/endpoints/admin/videos_manage.php new file mode 100644 index 00000000..0bd4e31e --- /dev/null +++ b/endpoints/admin/videos_manage.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ], + 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] + ); + + protected function generate() : void + { + $res = []; + + if ($this->_get['type'] && $this->_get['typeid']) + $res = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid']); + else if ($this->_get['user']) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + $res = VideoMgr::getVideos(userId: $uId); + + $this->result = 'vim_videoData = '.Util::toJSON($res); + } +} diff --git a/endpoints/admin/videos_order.php b/endpoints/admin/videos_order.php new file mode 100644 index 00000000..f11f64eb --- /dev/null +++ b/endpoints/admin/videos_order.php @@ -0,0 +1,57 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'] ], + 'move' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -1, 'max_range' => 1]] // -1 = up, 1 = down + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'move') || $this->_get['move'] === 0) + { + trigger_error('AdminVideosActionOrderResponse - id or move empty', E_USER_ERROR); + return; + } + + $id = $this->_get['id'][0]; + + $videos = DB::Aowow()->selectCol('SELECT a.`id` AS ARRAY_KEY, a.`pos` FROM ?_videos a, ?_videos b WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND (a.`status` & ?d) = 0 AND b.`id` = ?d ORDER BY a.`pos` ASC', CC_FLAG_DELETED, $id); + if (!$videos || count($videos) == 1) + { + trigger_error('AdminVideosActionOrderResponse - not enough videos to sort', E_USER_WARNING); + return; + } + + $dir = $this->_get['move']; + $curPos = $videos[$id]; + + if ($dir == -1 && $curPos == 0) + { + trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in top position', E_USER_WARNING); + return; + } + + if ($dir == 1 && $curPos + 1 == count($videos)) + { + trigger_error('AdminVideosActionOrderResponse - video #'.$id.' already in bottom position', E_USER_WARNING); + return; + } + + $oldKey = array_search($curPos + $dir, $videos); + $videos[$oldKey] -= $dir; + $videos[$id] += $dir; + + foreach ($videos as $id => $pos) + DB::Aowow()->query('UPDATE ?_videos SET `pos` = ?d WHERE `id` = ?d', $pos, $id); + } +} diff --git a/endpoints/admin/videos_relocate.php b/endpoints/admin/videos_relocate.php new file mode 100644 index 00000000..aa2bd6a1 --- /dev/null +++ b/endpoints/admin/videos_relocate.php @@ -0,0 +1,49 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + 'typeid' => ['filter' => FILTER_VALIDATE_INT ] + // (but not type..?) + ); + + protected function generate() : void + { + if (!$this->assertGET('id', 'typeid')) + { + trigger_error('AdminVideosActionRelocateResponse - videoId or typeId empty', E_USER_ERROR); + return; + } + + $id = $this->_get['id'][0]; + [$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ?_videos WHERE `id` = ?d', $id)); + $typeId = $this->_get['typeid']; + + if (Type::validateIds($type, $typeId)) + { + $tbl = Type::getClassAttrib($type, 'dataTable'); + + // move video + DB::Aowow()->query('UPDATE ?_videos SET `typeId` = ?d WHERE `id` = ?d', $typeId, $id); + + // flag target as having video + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $typeId); + + // deflag source for having had videos (maybe) + $viInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `status`& ?d AND `type` = ?d AND `typeId` = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId); + if ($viInfo || !$viInfo['hasMore']) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $oldTypeId); + } + else + trigger_error('AdminVideosActionRelocateResponse - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR); + } +} diff --git a/endpoints/admin/videos_sticky.php b/endpoints/admin/videos_sticky.php new file mode 100644 index 00000000..ea79ac4b --- /dev/null +++ b/endpoints/admin/videos_sticky.php @@ -0,0 +1,56 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('AdminVideosActionStickyResponse - videoId empty', E_USER_ERROR); + return; + } + + // this one is a bit strange: as far as i've seen, the only thing a 'sticky' video does is show up in the infobox + // this also means, that only one video per page should be sticky + // so, handle it one by one and the last one affecting one particular type/typId-key gets the cake + $viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + foreach ($viEntries as $id => $viData) + { + // approve yet unapproved videos + if (!($viData['status'] & CC_FLAG_APPROVED)) + { + // set as approved in DB + DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + + // gain siterep + Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]); + + // flag DB entry as having videos + if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable')) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + } + + // reset all others + DB::Aowow()->query('UPDATE ?_videos a, ?_videos b SET a.`status` = a.`status` & ~?d WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = ?d', CC_FLAG_STICKY, $id); + + // toggle sticky status + DB::Aowow()->query('UPDATE ?_videos SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE `id` = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); + + unset($viEntries[$id]); + } + + if ($viEntries) + trigger_error('AdminVideosActionStickyResponse - video(s) # '.implode(', ', array_keys($viEntries)).' not in db or flagged as deleted', E_USER_WARNING); + } +} diff --git a/endpoints/video/add.php b/endpoints/video/add.php new file mode 100644 index 00000000..36c67ebc --- /dev/null +++ b/endpoints/video/add.php @@ -0,0 +1,124 @@ + 1. =add: receives user upload + 1.1. checks and processing on the upload + 1.2. forward to =confirm or blank response + 2. =confirm: user edites upload + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoAddResponse extends TextResponse +{ + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'videourl' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private string $videoHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + + // only accept/expect hash for confirm & complete + if ($videoHash) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleAdd()) + $this->redirectTo = '?video=confirm&'.$this->destType.'.'.$this->destTypeId.'.'.$this->videoHash; + else if ($this->destType && $this->destTypeId) + $this->redirectTo = '?'.Type::getFileString($this->destType).'='.$this->destTypeId.'#suggest-a-video'; + else + $this->generate404(); + } + + private function handleAdd() : bool + { + if (!User::canSuggestVideo()) + { + $_SESSION['error']['vi'] = Lang::video('error', 'notAllowed'); + return false; + } + + if (!$this->assertPOST('videourl')) + { + $_SESSION['error']['vi'] = Lang::video('error', 'selectVI'); + return false; + } + + $videoId = ''; + if (preg_match('/^https?:\/\/(www\.)?youtu(\.be|be\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/', $this->_post['videourl'], $m)) + $videoId = $m[3]; + else + { + $_SESSION['error']['vi'] = Lang::video('error', 'selectVI'); + return false; + } + + $curl = curl_init('https://youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v='.$videoId); + if (!$curl) + { + trigger_error('VideoAddResponse - curl_init fail', E_USER_ERROR); + $_SESSION['error']['vi'] = Lang::main('intError'); + return false; + } + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + $ytOembed = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + curl_close($curl); + + if ($status == 401) + { + $_SESSION['error']['vi'] = Lang::video('error', 'isPrivate'); + return false; + } + else if ($status != 200) // 404, 500 seen .. does it matter why its inaccessible? + { + $_SESSION['error']['vi'] = Lang::video('error', 'noExist'); + return false; + } + + $videoInfo = json_decode($ytOembed); + $videoInfo->id = $videoId; + + if (!VideoMgr::saveSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + { + $_SESSION['error']['ss'] = Lang::main('intError'); + return false; + } + + return true; + } +} + +?> diff --git a/endpoints/video/complete.php b/endpoints/video/complete.php new file mode 100644 index 00000000..46cf9d0b --- /dev/null +++ b/endpoints/video/complete.php @@ -0,0 +1,92 @@ + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoCompleteResponse extends TextResponse +{ + use TrCommunityHelper; + + protected bool $requiresLogin = true; + + protected array $expectedPOST = array( + 'caption' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] + ); + + private string $videoHash = ''; + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->destType, $this->destTypeId, , $this->videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generate404(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generate404(); + } + + protected function generate() : void + { + if ($this->handleComplete()) + $this->forward('?video=thankyou&'.$this->destType.'.'.$this->destTypeId); + else + $this->generate404(); + } + + private function handleComplete() : bool + { + if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + $this->generate404(); + + $pos = DB::Aowow()->selectCell('SELECT MAX(`pos`) FROM ?_videos WHERE `type` = ?d AND `typeId` = ?d AND (`status` & ?d) = 0', $this->destType, $this->destTypeId, CC_FLAG_DELETED); + if (!is_int($pos)) + $pos = -1; + + // write to db + $newId = DB::Aowow()->query( + 'INSERT INTO ?_videos (`type`, `typeId`, `userIdOwner`, `date`, `videoId`, `pos`, `url`, `width`, `height`, `name`, `caption`, `status`) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?, ?d, ?, ?d, ?d, ?, ?, 0)', + $this->destType, $this->destTypeId, User::$id, + $videoInfo->id, + $pos + 1, + $videoInfo->thumbnail_url, + $videoInfo->thumbnail_width, + $videoInfo->thumbnail_height, + $videoInfo->title, + $this->handleCaption($this->_post['caption']) + ); + + if (!is_int($newId)) // 0 is valid, NULL or FALSE is not + { + trigger_error('VideoCompleteResponse - video query failed', E_USER_ERROR); + return false; + } + + VideoMgr::dropTempFile(); + + return true; + } +} + +?> diff --git a/endpoints/video/confirm.php b/endpoints/video/confirm.php new file mode 100644 index 00000000..055670ef --- /dev/null +++ b/endpoints/video/confirm.php @@ -0,0 +1,81 @@ + 2. =crop: user edites upload + 2.1. just show edit page + 2.2. user submits coords and description to =complete + 3. =complete: store edited video file and data + 4. =thankyou +*/ + +class VideoConfirmResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'video'; + protected string $pageName = 'video'; + + public ?Markup $infobox = null; + public string $videoHash = ''; + public int $destType = 0; + public int $destTypeId = 0; + public string $url = ''; + public int $width = 0; + public int $height = 0; + public array $video = []; + public string $viTitle = ''; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&.. (hash is optional) + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)(\.(\w{16}))?$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId, , $this->videoHash] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::video('submission'); + array_unshift($this->title, $this->h1); + + if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) + $this->generateError(); + + $this->viTitle = $videoInfo->title; + $this->url = $videoInfo->thumbnail_url; + $this->width = $videoInfo->thumbnail_width; + $this->height = $videoInfo->thumbnail_height; + $this->video = [[ + 'videoType' => VideoMgr::TYPE_YOUTUBE, + 'videoId' => $videoInfo->id, + 'caption' => $videoInfo->title + ]]; + + // target + $this->infobox = new Markup(Lang::screenshot('displayOn', [Lang::typeName($this->destType), Type::getFileString($this->destType), $this->destTypeId]), ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->extendGlobalIds($this->destType, $this->destTypeId); + + parent::generate(); + } +} + +?> diff --git a/endpoints/video/thankyou.php b/endpoints/video/thankyou.php new file mode 100644 index 00000000..c90e7922 --- /dev/null +++ b/endpoints/video/thankyou.php @@ -0,0 +1,60 @@ + 4. =thankyou +*/ + +class VideoThankyouResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'video'; + + private int $destType = 0; + private int $destTypeId = 0; + + public function __construct(string $pageParam) + { + parent::__construct($pageParam); + + // get video destination + // target delivered as video=&. + if (!preg_match('/^video=\w+&(-?\d+)\.(-?\d+)$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generateError(); + + [, $this->destType, $this->destTypeId] = $m; + + // no such type or this type cannot receive videos + if (!Type::checkClassAttrib($this->destType, 'contribute', CONTRIBUTE_VI)) + $this->generateError(); + + // no such typeId + if (!Type::validateIds($this->destType, $this->destTypeId)) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::video('submission'); + + array_unshift($this->title, $this->h1); + + $this->extraHTML = Lang::video('thanks', 'contrib').'

        '; + $this->extraHTML .= Lang::video('thanks', 'goBack', [Type::getFileString($this->destType), $this->destTypeId])."

        \n"; + $this->extraHTML .= ''.Lang::video('thanks', 'note').''; + + parent::generate(); + } +} + +?> diff --git a/includes/components/videomgr.class.php b/includes/components/videomgr.class.php new file mode 100644 index 00000000..a9b6e07f --- /dev/null +++ b/includes/components/videomgr.class.php @@ -0,0 +1,229 @@ +id . PHP_EOL); + fwrite($tmpFile, $videoInfo->title . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_url . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_height . PHP_EOL); + fwrite($tmpFile, $videoInfo->thumbnail_width . PHP_EOL); + + return fclose($tmpFile); + } + + public static function loadSuggestion(?\stdClass &$videoInfo, int $destType, int $destTypeId, ?string $uid) : bool + { + self::$tmpFile = sprintf(self::PATH_TEMP, User::$username.'-'.$destType.'-'.$destTypeId.'-'.$uid); + + if (!file_exists(self::$tmpFile)) + return false; + + if ($info = file(self::$tmpFile, FILE_IGNORE_NEW_LINES)) + { + $videoInfo = new \stdClass; + $videoInfo->id = $info[0]; + $videoInfo->title = $info[1]; + $videoInfo->thumbnail_url = $info[2]; + $videoInfo->thumbnail_height = (int)$info[3]; + $videoInfo->thumbnail_width = (int)$info[4]; + + return true; + } + + return false; + } + + public static function dropTempFile() + { + if (!self::$tmpFile || !file_exists(self::$tmpFile)) + return; + + unlink(self::$tmpFile); + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + public static function getVideos(int $type = 0, int $typeId = 0, $userId = 0, ?int &$nFound = 0) : array + { + /* VideoData + * caption: caption + * date: isodate + * height: ytPreviewImgHeight? + * width: ytPreviewImgWidth? + * id: id + * next: idx || null + * prev: idx || null + * name: ytTitle? + * pending: bool + * status: statusCode + * type: dbType + * typeId: typeId + * user: userName + * url: ytPreviewImg? + * videoType: always 1 + * videoId: videoId + * unique: bool || null + */ + + $videos = DB::Aowow()->select( + 'SELECT v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`type`, v.`typeId`, v.`caption`, v.`status` AS "flags", v.`url`, v.`name` + FROM ?_videos v + LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id` + WHERE + { v.`type` = ?d } + { AND v.`typeId` = ?d } + { v.`userIdOwner` = ?d } + { LIMIT ?d } + ORDER BY `type`, `typeId`, `pos` ASC', + $userId ? DBSIMPLE_SKIP : $type, + $userId ? DBSIMPLE_SKIP : $typeId, + $userId ? $userId : DBSIMPLE_SKIP, + $userId || $type ? DBSIMPLE_SKIP : 100 + ); + + $num = []; + foreach ($videos as $v) + { + if (empty($num[$v['type']][$v['typeId']])) + $num[$v['type']][$v['typeId']] = 1; + else + $num[$v['type']][$v['typeId']]++; + } + + $nFound = 0; + + // format data to meet requirements of the js + foreach ($videos as $i => &$v) + { + $nFound++; + + $v['date'] = date(Util::$dateFormatInternal, $v['date']); + $v['videoType'] = self::TYPE_YOUTUBE; + + if ($i > 0) + $v['prev'] = $i - 1; + + if (($i + 1) < count($videos)) + $v['next'] = $i + 1; + + // order gives priority for 'status' + if (!($v['flags'] & CC_FLAG_APPROVED)) + { + $v['pending'] = 1; + $v['status'] = self::STATUS_PENDING; + } + else + $v['status'] = self::STATUS_APPROVED; + + if ($v['flags'] & CC_FLAG_STICKY) + { + $v['sticky'] = 1; + $v['status'] = self::STATUS_STICKY; + } + + if ($v['flags'] & CC_FLAG_DELETED) + { + $v['deleted'] = 1; + $v['status'] = self::STATUS_DELETED; + } + + // something todo with massSelect .. am i doing this right? + if ($num[$v['type']][$v['typeId']] == 1) + $v['unique'] = 1; + + if (!$v['user']) + unset($v['user']); + } + + return $videos; + } + + public static function getPages(?bool $all, ?int &$nFound) : array + { + // i GUESS .. vi_getALL ? everything : pending + $nFound = 0; + $pages = DB::Aowow()->select( + 'SELECT v.`type`, v.`typeId`, COUNT(1) AS "count", MIN(v.`date`) AS "date" + FROM ?_videos v + { WHERE (v.`status` & ?d) = 0 } + GROUP BY v.`type`, v.`typeId`', + $all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED | CC_FLAG_DELETED + ); + + if ($pages) + { + // limit to one actually existing type each + foreach (array_unique(array_column($pages, 'type')) as $t) + { + $ids = []; + foreach ($pages as $row) + if ($row['type'] == $t) + $ids[] = $row['typeId']; + + if (!$ids) + continue; + + $obj = Type::newList($t, [Cfg::get('SQL_LIMIT_NONE'), ['id', $ids]]); + if (!$obj || $obj->error) + continue; + + foreach ($pages as &$p) + if ($p['type'] == $t) + if ($obj->getEntry($p['typeId'])) + $p['name'] = $obj->getField('name', true); + } + + foreach ($pages as &$p) + { + if (empty($p['name'])) + { + trigger_error('VideoMgr::getPages - video linked to nonexistent type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); + unset($p); + } + else + { + $nFound += $p['count']; + $p['date'] = date(Util::$dateFormatInternal, $p['date']); + } + } + } + + return $pages; + } +} + +?> diff --git a/includes/defines.php b/includes/defines.php index 49e18275..a9f440ef 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -90,7 +90,7 @@ define('SITEREP_ACTION_DAILYVISIT', 2); // Daily visit define('SITEREP_ACTION_COMMENT', 3); // Posted comment define('SITEREP_ACTION_UPVOTED', 4); // Your comment was upvoted define('SITEREP_ACTION_DOWNVOTED', 5); // Your comment was downvoted -define('SITEREP_ACTION_SUBMIT_SCREENSHOT', 6); // Submitted screenshot (suggested video) +define('SITEREP_ACTION_SUBMIT_SCREENSHOT', 6); // Submitted screenshot // Cast vote // Uploaded data define('SITEREP_ACTION_GOOD_REPORT', 9); // Report accepted @@ -98,7 +98,7 @@ define('SITEREP_ACTION_BAD_REPORT', 10); // Report declined // Copper Achievement // Silver Achievement // Gold Achievement - // Test 1 +define('SITEREP_ACTION_SUGGEST_VIDEO', 14); // repurposed, originally: Test 1 // Test 2 define('SITEREP_ACTION_ARTICLE', 16); // Guide approved (article approved) define('SITEREP_ACTION_USER_WARNED', 17); // Moderator Warning diff --git a/includes/utilities.php b/includes/utilities.php index 5ead86dc..10d84995 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -727,12 +727,12 @@ abstract class Util $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? Cfg::get('REP_REWARD_UPVOTED') : Cfg::get('REP_REWARD_DOWNVOTED'); break; case SITEREP_ACTION_SUBMIT_SCREENSHOT: + case SITEREP_ACTION_SUGGEST_VIDEO: if (empty($miscData['id']) || empty($miscData['what'])) return false; $x['sourceA'] = $miscData['id']; // screenshotId or videoId - $x['sourceB'] = $miscData['what']; // screenshot:1 - $x['amount'] = Cfg::get('REP_REWARD_UPLOAD'); + $x['amount'] = $action == SITEREP_ACTION_SUBMIT_SCREENSHOT ? Cfg::get('REP_REWARD_SUBMIT_SCREENSHOT') : Cfg::get('REP_REWARD_SUGGEST_VIDEO'); break; case SITEREP_ACTION_GOOD_REPORT: // NYI case SITEREP_ACTION_BAD_REPORT: diff --git a/localization/lang.class.php b/localization/lang.class.php index 1bde86d4..e496c36b 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -16,6 +16,7 @@ class Lang private static $maps; private static $profiler; private static $screenshot; + private static $video; private static $privileges; private static $smartAI; private static $unit; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 9369227d..c3e0007b 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "Es ist euch nicht erlaubt einen Screenshot hochzuladen!", ) ), + 'video' => array( + 'submission' => "Video-Einsendung", + 'thanks' => array( + 'contrib' => "Vielen Dank für Euren Beitrag!", + 'goBack' => 'Klickt hier, um zu der vorherigen Seite zurückzukehren.', + 'note' => "Hinweis: Euer Video muss zunächst zugelassen werden, bevor es auf der Seite erscheint. Dies kann bis zu 72 Stunden dauern." + ), + 'error' => array( + 'isPrivate' => "Das vorgeschlagene Video ist privat.", + 'noExist' => "An der eingereichten Url existiert kein Video.", + 'selectVI' => "Bitte gebt gültige Videoinformationen ein.", + 'notAllowed' => "Es ist euch nicht erlaubt Videos vorzuschlagen!" + ) + ), 'game' => array( // type strings 'npc' => "NPC", // 1 diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 4628acc2..e4a7fe76 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "You are not allowed to upload screenshots!", ) ), + 'video' => array( + 'submission' => "Video Suggestion", + 'thanks' => array( + 'contrib' => "Thanks a lot for your contribution!", + 'goBack' => 'Click here to go back to the page you came from.', + 'note' => "Note: Your video will need to be approved before appearing on the site. This can take up to 72 hours." + ), + 'error' => array( + 'isPrivate' => "The suggested video is private.", + 'noExist' => "No video found at the provided Url.", + 'selectVI' => "Please enter valid video information.", // message_novideo + 'notAllowed' => "You are not allowed to suggest videos!", + ) + ), 'game' => array( // type strings 'npc' => "NPC", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 82ba9a10..95a75707 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "¡No estás permitido para subir capturas de pantalla!", ) ), + 'video' => array( + 'submission' => "Sugerencia de video", + 'thanks' => array( + 'contrib' => "¡Muchísimas gracias por tu aportación!", + 'goBack' => 'aquí vuelve a la página de la que viniste.', + 'note' => "Nota: Tu video tiene que ser aprobado antes de que pueda aparecer en el sitio. Esto puede tomar hasta 72 horas." + ), + 'error' => array( + 'isPrivate' => "El video sugerido es privado.", + 'noExist' => "No se encontró ningún video en la URL proporcionada.", + 'selectVI' => "Por favor, introduce información válida del vídeo.", // message_novideo + 'notAllowed' => "¡No tienes permiso para sugerir videos!", + ) + ), 'game' => array( // type strings 'npc' => "PNJ", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 96e90a94..7123dde1 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "Vous n'êtes pas autorisés à exporter des captures d'écran.", ) ), + 'video' => array( + 'submission' => "Suggestion de vidéo", + 'thanks' => array( + 'contrib' => "Merci beaucoup de votre contribution!", + 'goBack' => 'ici pour retourner à la page d\'où vous venez.', + 'note' => "Note : Votre vidéo devra être approuvée avant d'apparaître sur le site. Cela peut prendre jusqu'à 72 heures." + ), + 'error' => array( + 'isPrivate' => "La vidéo suggérée est privée.", + 'noExist' => "Aucune vidéo trouvée à l'URL fournie.", + 'selectVI' => "Veuillez entrer des informations valides pour la vidéo.", // message_novideo + 'notAllowed' => "Vous n'êtes pas autorisé à suggérer des vidéos!", + ) + ), 'game' => array( // type strings 'npc' => "PNJ", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 930b776a..6e0de9a1 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -278,6 +278,20 @@ $lang = array( 'notAllowed' => "[You are not allowed to upload screenshots!]", ) ), + 'video' => array( + 'submission' => "Предложить видео", + 'thanks' => array( + 'contrib' => "СпаÑибо за ваш вклад!", + 'goBack' => 'здеÑÑŒ чтобы перейти к предыдущей Ñтранице.', + 'note' => "Примечание: Ваше видео должно быть одобрено, прежде чем поÑвитÑÑ Ð½Ð° Ñайте. Это может занÑть до 72 чаÑов." + ), + 'error' => array( + 'isPrivate' => "Предложенное видео ÑвлÑетÑÑ Ð¿Ñ€Ð¸Ð²Ð°Ñ‚Ð½Ñ‹Ð¼.", + 'noExist' => "Видео по предоÑтавленной ÑÑылке не найдено.", + 'selectVI' => "введите корректную информацию о видео.", // message_novideo + 'notAllowed' => "У Ð²Ð°Ñ Ð½ÐµÑ‚ прав предлагать видео!", + ) + ), 'game' => array( // type strings 'npc' => "ÐИП", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index a5c5912c..42e090cb 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -278,7 +278,22 @@ $lang = array( 'notAllowed' => "ä½ ä¸å…许上传截图ï¼", ) ), + 'video' => array( + 'submission' => "视频建议", + 'thanks' => array( + 'contrib' => "éžå¸¸æ„Ÿè°¢ä½ çš„贡献ï¼", + 'goBack' => '点击这里返回上一页。', + 'note' => "注æ„:您的视频需è¦ç»è¿‡å®¡æ ¸åŽæ‰èƒ½æ˜¾ç¤ºåœ¨ç½‘ç«™ä¸Šã€‚è¿™éœ€è¦æœ€å¤š72å°æ—¶ã€‚" + ), + 'error' => array( + 'isPrivate' => "å»ºè®®çš„è§†é¢‘ä¸ºç§æœ‰ã€‚", + 'noExist' => "在æä¾›çš„链接中未找到视频。", + 'selectVI' => "请输入有效的视频信æ¯ã€‚", // message_novideo + 'notAllowed' => "您没有æƒé™å»ºè®®è§†é¢‘ï¼", + ) + ), 'game' => array( + // type strings 'npc' => "NPC", 'npcs' => "NPC", 'object' => "对象", diff --git a/setup/updates/1758578400_07.sql b/setup/updates/1758578400_07.sql new file mode 100644 index 00000000..4ba7523f --- /dev/null +++ b/setup/updates/1758578400_07.sql @@ -0,0 +1,8 @@ +-- `key` is too small for our new configs +ALTER TABLE `aowow_config` + MODIFY COLUMN `key` varchar(50) NOT NULL; + +-- split generic upload in ss / vi +UPDATE `aowow_config` SET `key` = 'rep_reward_submit_screenshot', `comment` = 'uploaded screenshot was approved' WHERE `key` = 'rep_reward_upload'; +DELETE FROM `aowow_config` WHERE `key` = 'rep_reward_suggest_video'; +INSERT INTO `aowow_config` VALUES ('rep_reward_suggest_video', '10', '10', 5, 129, 'suggested video was approved'); diff --git a/setup/updates/1758578400_08.sql b/setup/updates/1758578400_08.sql new file mode 100644 index 00000000..f7ad1861 --- /dev/null +++ b/setup/updates/1758578400_08.sql @@ -0,0 +1,8 @@ +-- update video storage +ALTER TABLE `aowow_videos` + ADD COLUMN `pos` tinyint unsigned NOT NULL AFTER `videoId`, + ADD COLUMN `url` varchar(64) NOT NULL COMMENT 'preview thumb' AFTER `pos`, + ADD COLUMN `width` smallint unsigned NOT NULL AFTER `url`, + ADD COLUMN `height` smallint unsigned NOT NULL AFTER `width`, + ADD COLUMN `name` varchar(64) DEFAULT NULL AFTER `height`, + MODIFY COLUMN `caption` varchar(200) DEFAULT NULL; diff --git a/setup/updates/1758578400_09.sql b/setup/updates/1758578400_09.sql new file mode 100644 index 00000000..4af1c23d --- /dev/null +++ b/setup/updates/1758578400_09.sql @@ -0,0 +1,4 @@ +-- update article affected by cfg change +UPDATE `aowow_acticles` SET + `article` = '[b]Reputation[/b] is a rough measurement of how much you participate in the community--it is earned by convincing your peers that you know what you’re talking about. Our community puts just as much work as our developers do into making our site as awesome as it is and reputation is meant as a way for you to track just how much work you\'re putting into us.\r\n\r\nThe primary means of gaining reputation is by posting quality comments on database entries (which are then voted up by other site members) and by general contributions to the site which can include actions like data and screenshot submissions. Whenever you leave a comment on a database entry, your peers can then vote on these comments, and those votes will cause you to gain reputation. You can also earn reputation by voting on other users\' comments and by sending in reports!\r\n\r\nBy being a good-standing and contributing user you will be able to earn both reputation and achievements for many of the same actions!\r\n\r\n[h3]Reputation Gains[/h3]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td][url=?account=signup]Registering[/url] an account[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_REGISTER reputation[/td]\r\n[/tr]\r\n[tr][td]Daily visit[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_DAILYVISIT reputation[/td]\r\n[/tr]\r\n[tr][td]Posting a comment[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Your comment was voted up (each upvote)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_UPVOTED reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a screenshot[/td]\r\n[td align=right class=no-wrap]REP_REWARD_SUBMIT_SCREENSHOT reputation[/td]\r\n[/tr]\r\n[tr][td]Suggesting a video[/td]\r\n[td align=right class=no-wrap]REP_REWARD_SUGGEST_VIDEO reputation[/td]\r\n[/tr]\r\n[tr][td]Submitting a guide (approved)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_ARTICLE reputation[/td]\r\n[/tr]\r\n[tr][td]Filing a report (accepted)[/td]\r\n[td align=right class=no-wrap]CFG_REP_REWARD_GOOD_REPORT reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n\r\n\r\n[h3]Site Privileges[/h3]\r\nThe higher your reputation level, the more privileges you gain. Earn a high enough reputation to unlock additional rewards, in the form of new privileges around the site!\r\n[pad]\r\n[div style=\"max-width:400px\"][table class=grid]\r\n[tr][td]Post comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_COMMENT reputation[/td]\r\n[/tr]\r\n[tr][td]Upvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_UPVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]Downvote on comments[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_DOWNVOTE reputation[/td]\r\n[/tr]\r\n[tr][td]More votes per day[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_VOTEMORE_BASE reputation[/td]\r\n[/tr]\r\n[tr][td]Comment votes worth more[/td]\r\n[td align=right class=no-wrap]CFG_REP_REQ_SUPERVOTE reputation[/td]\r\n[/tr]\r\n[/table][/div]\r\n[pad]\r\n[url=?privileges]Check out full details on site privileges you can earn![/url]\r\n' +WHERE `url` = 'reputation' AND `locale` = 0; diff --git a/static/js/global.js b/static/js/global.js index 91d49851..453f644e 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -2831,9 +2831,10 @@ var vi_siteurls = { 1: 'https://www.youtube.com/watch?v=$1' // YouTube }; -var vi_sitevalidation = { - 1: /^https?:\/\/www\.youtube\.com\/watch\?v=([^& ]{11})/ // YouTube -}; +var vi_sitevalidation = [ + /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([^& ]{11})/i, + /https?:\/\/(?:www\.)?youtu\.be\/([^& ]{11})/i +]; function vi_submitAVideo() { tabsContribute.focus(2); @@ -2882,7 +2883,7 @@ function vi_appendSticky() { }; var img = $WH.ce('img'); - img.src = $WH.sprintf(vi_thumbnails[video.videoType], video.videoId); + img.src = $WH.sprintf(vi_thumbnails[video.videoType].replace(/\/default\.jpg/, '/mqdefault.jpg'), video.videoId); img.className = 'border'; $WH.ae(a, img); @@ -3227,7 +3228,7 @@ var VideoViewer = new function() { aCover.onclick = Lightbox.hide; var foo = $WH.ce('span'); var b = $WH.ce('b'); - $WH.ae(b, $WH.ct(LANG.close)); + // $WH.ae(b, $WH.ct(LANG.close)); $WH.ae(foo, b); $WH.ae(aCover, foo); @@ -3312,7 +3313,7 @@ var VideoViewer = new function() { onShow: onShow, onHide: onHide, onResize: onResize - },opt); + }, opt); return false; } diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 7ee34a44..32d67761 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Bronzeerfolg", "Silbererfolg", "Golderfolg", - 'Test 1', + 'Video vorgeschlagen', // aowow - originally: Test 1 'Test 2', "Leitfaden zugelassen", "Warnung durch Moderator", diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index a06c0c6f..4aacd372 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Copper Achievement", "Silver Achievement", "Gold Achievement", - 'Test 1', + 'Video suggested', // aowow - originally: Test 1 'Test 2', "Guide approved", "Moderator Warning", diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index e4f82e5a..6cbf3d68 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Logro de Cobre", "Logro de Plata", "Logro de Oro", - 'Test 1', + "Vídeo sugerido", // aowow - originally: Test 1 'Test 2', "Guía aprobada", "Aviso de moderador", diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 42128086..747196f1 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Haut-fait de bronze", "Haut-fait d'argent", "Haut-fait d'or", - 'Test 1', + "Vidéo suggérée", // aowow - originally: Test 1 'Test 2', "Guide approuvé", "Avertissement d'un modérateur", diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 8851c21b..0211ba2c 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "Бронзовое доÑтижение", "СеребрÑное доÑтижение", "Золотое доÑтижение", - "Test 1", + "Видео предложено", // aowow - originally: Test 1 "Test 2", "Гайд одобрен", "ПожаловатьÑÑ Ð¼Ð¾Ð´ÐµÑ€Ð°Ñ‚Ð¾Ñ€Ñƒ", diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 53d7cfda..186bb754 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -13,7 +13,7 @@ var l_reputation_names = [ "铜牌æˆå°±", "银牌æˆå°±", "金牌æˆå°±", - 'Test 1', + '建议视频', // aowow - originally: Test 1 'Test 2', "指å—通过审核", "管ç†å‘˜è­¦å‘Š", diff --git a/static/js/video.js b/static/js/video.js new file mode 100644 index 00000000..545ca01c --- /dev/null +++ b/static/js/video.js @@ -0,0 +1,1135 @@ +var vi_managedRow = null; +var vi_getAll = false; +var vim_ViewedRow = null; +var vim_videoData = []; +var vim_videoPages = []; +var vim_numPagesFound = 0; +var vim_numPages = 0; +var vim_numPending = 0; +var vim_statuses = { + 0 : 'Pending', + 999: 'Deleted', + 100: 'Approved', + 105: 'Sticky' +}; + +function makePipe() { + var sp = $WH.ce('span'); + $WH.ae(sp, $WH.ct(' ')); + + var b = $WH.ce('small'); + b.className = 'q0'; + $WH.ae(b, $WH.ct('|')); + + $WH.ae(sp, b); + $WH.ae(sp, $WH.ct(' ')); + + return sp; +} + +function vi_OnResize() { + var a = Math.max(100, Math.min($WH.g_getWindowSize().h - 50, 700)); + + $WH.ge('menu-container').style.height = $WH.ge('pages-container').style.height = a + 'px'; + $WH.ge('data-container').style.height = a + 'px'; +} + +$WH.aE(window, 'resize', vi_OnResize); + +function vi_Refresh(openNext, type, typeId) { + new Ajax('?admin=videos&action=list' + (vi_getAll ? '&all': ''), { + method: 'get', + onSuccess: function (xhr) { + eval(xhr.responseText); + + if (vim_videoPages.length > 0) { + $WH.ge('show-all-pages').innerHTML = ' – Show All (' + vim_numPagesFound + ')'; + + vim_UpdatePages(); + + if (openNext) + vi_Manage($WH.ge('pages-container').firstChild.firstChild, vim_videoPages[0].type, vim_videoPages[0].typeId, true); + else if (type && typeId) + vi_Manage(null, type, typeId, true); + } + else { + $WH.ee($WH.ge('show-all-pages')); + $WH.ge('pages-container').innerHTML = 'NO VIDEOZ NEEDS 2 BE APPRVED NOW KTHX. :)'; + if (type && typeId) + vi_Manage(null, type, typeId, true); + } + } + }) +} + +function vi_Manage(_this, type, typeId, openNext) { + new Ajax('?admin=videos&action=manage&type=' + type + '&typeid=' + typeId, { + method: 'get', + onSuccess: function (xhr) { + + eval(xhr.responseText); + vim_numPending = 0; + + for (var i in vim_videoData) + if (vim_videoData[i].pending) + vim_numPending++; + + var nRows = vim_videoData.length; + $WH.ge('videoTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); + + vim_UpdateList(openNext); + vim_UpdateMassLinks(); + + if (vi_managedRow != null) + vi_ColorizeRow('transparent'); + + vi_managedRow = _this; + + if (vi_managedRow != null) + vi_ColorizeRow('#282828'); + } + }); +} + +function vi_ManageUser() { + var username = $WH.ge('usermanage'); + username.value = $WH.trim(username.value); + + if (username.value.length < 4) { + alert('Username must be at least 4 characters long.'); + username.focus(); + + return false + } + + if (username.value.match(/[^a-z0-9]/i) != null) { + alert('Username can only contain letters and numbers.'); + username.focus(); + + return false + } + + new Ajax('?admin=videos&action=manage&user=' + username.value, { + method: 'get', + onSuccess: function (xhr) { + eval(xhr.responseText); + + var nRows = vim_videoData.length; + $WH.ge('videoTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); + + vim_UpdateList(); + vim_UpdateMassLinks(); + + if (vi_managedRow != null) + vi_ColorizeRow('transparent'); + } + }); + + return true +} + +function vi_ColorizeRow(color) { + for (var i = 0; i < vi_managedRow.childNodes.length; ++i) + vi_managedRow.childNodes[i].style.backgroundColor = color; +} + +function vim_GetVideo(id) { + for (var i in vim_videoData) + if (vim_videoData[i].id == id) + return vim_videoData[i]; + + return null +} + +function vim_View(row, id) { + if (vim_ViewedRow != null) + vim_ColorizeRow('transparent'); + + vim_ViewedRow = row; + vim_ColorizeRow('#282828'); + + var video = vim_GetVideo(id); + if (video != null) + VideoManager.show(video); +} + +function vim_ColorizeRow(color) { + for (var i = 0; i < vim_ViewedRow.childNodes.length; ++i) + vim_ViewedRow.childNodes[i].style.backgroundColor = color; +} + +function vim_ConfirmMassApprove() { + ajaxAnchor(this); // aowow custom - same endpoint gets used as ajax and page .. what? + + return false; + // return true; +} + +function vim_ConfirmMassDelete() { + if (confirm('Delete selected video(s)?')) // aowow custom - see above + ajaxAnchor(this); + + return false; + // return confirm('Delete selected video(s)?'); +} + +function vim_ConfirmMassSticky() { + if (confirm('Sticky selected video(s)?')) // aowow custom - see above + ajaxAnchor(this); + + return false; + // return confirm('Sticky selected video(s)?'); +} + +function vim_UpdatePages(UNUSED) { + var pc = $WH.ge('pages-container'); + $WH.ee(pc); + + var tbl = $WH.ce('table'); + tbl.className = 'grid'; + tbl.style.width = '400px'; + + var tr = $WH.ce('tr'); + + var th = $WH.ce('th'); + $WH.ae(th, $WH.ct('Page')); + $WH.ae(tr, th); + + th = $WH.ce('th'); + $WH.ae(th, $WH.ct('Submitted')); + $WH.ae(tr, th); + + th = $WH.ce('th'); + th.align = 'right'; + $WH.ae(th, $WH.ct('#')); + $WH.ae(tr, th); + + $WH.ae(tbl, tr); + + var now = new Date(); + for (var i in vim_videoPages) { + var viPage = vim_videoPages[i]; + tr = $WH.ce('tr'); + tr.onclick = vi_Manage.bind(tr, tr, viPage.type, viPage.typeId, true, i); + + var td = $WH.ce('td'); + var a = $WH.ce('a'); + a.href = '?' + g_types[viPage.type] + '=' + viPage.typeId; + a.target = '_blank'; + $WH.ae(a, $WH.ct(viPage.name)); + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var elapsed = new Date(viPage.date); + $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); + $WH.ae(tr, td); + + td = $WH.ce('td'); + td.align = 'right'; + $WH.ae(td, $WH.ct(viPage.count)); + $WH.ae(tr, td); + + $WH.ae(tbl, tr); + } + + $WH.ae(pc, tbl); +} + +function vim_UpdateList(k) { + var tbl = $WH.ge('theVideosList'); + var tBody = false; + var i = 1; + + while (tbl.childNodes.length > i) { + if (tbl.childNodes[i].nodeName == 'TR' && tBody) + $WH.de(tbl.childNodes[i]); + else if (tbl.childNodes[i].nodeName == 'TR') + tBody = true; + else + i++; + } + + var now = new Date(); + var viId = 0; + for (var i in vim_videoData) { + var video = vim_videoData[i]; + var tr = $WH.ce('tr'); + if (viId == 0 && video.pending) { + viId = video.id; + tr.id = 'highlightedRow'; + } + + var td = $WH.ce('td'); + td.align = 'center'; + + // if (video.status != 999 && !video.pending) { // Aowow - removed + var a = $WH.ce('a'); + a.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId); + a.target = '_blank'; + a.onclick = function (id, e) { + $WH.sp(e); + (vim_View.bind(null, this, id))(); + return false; + }.bind(tr, video.id); + + var previewImg = $WH.ce('img'); + previewImg.src = $WH.sprintf(vi_thumbnails[video.videoType], video.videoId); + previewImg.height = 50; + $WH.ae(a, previewImg); + $WH.ae(td, a); + // } + $WH.ae(tr, td); + + td = $WH.ce('td'); + if (video.status != 999 && !video.pending) { + var a = $WH.ce('a'); + a.href = '?' + g_types[video.type] + '=' + video.typeId + '#videos:id=' + video.id; + a.target = '_blank'; + a.onclick = function (a) { $WH.sp(a); }; + $WH.ae(a, $WH.ct(video.id)); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct(video.id)); + + $WH.ae(tr, td); + + td = $WH.ce('td'); + td.id = 'title-' + video.id; + + var sp = $WH.ce('span'); + sp.style.paddingRight = '8px'; + if (video.caption) { + var sp2 = $WH.ce('span'); + sp2.className = 'q2'; + var b = $WH.ce('b'); + $WH.ae(b, $WH.ct(video.caption)); + $WH.ae(sp2, b); + $WH.ae(sp, sp2); + } + else { + var it = $WH.ce('i'); + it.className = 'q0'; + $WH.ae(it, $WH.ct('NULL')); + $WH.ae(sp, it); + } + $WH.ae(td, sp); + + sp = $WH.ce('span'); + sp.style.whiteSpace = 'nowrap'; + + var a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (vi, e) { + $WH.sp(e); + (vim_ShowEdit.bind(this, vi))(); + }.bind(a, video); + $WH.ae(a, $WH.ct('Edit')); + $WH.ae(sp, a); + $WH.ae(sp, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (vi, e) { + $WH.sp(e); + (vim_Clear.bind(this, vi))(); + }.bind(a, video); + $WH.ae(a, $WH.ct('Clear')); + $WH.ae(sp, a); + $WH.ae(td, sp); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var elapsed = new Date(video.date); + $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); + $WH.ae(tr, td); + + td = $WH.ce('td'); + a = $WH.ce('a'); + a.href = '?user=' + video.user; + a.target = '_blank'; + a.onclick = function (a) { $WH.sp(a); }; + $WH.ae(a, $WH.ct(video.user)); + $WH.ae(td, a); + $WH.ae(tr, td); + + td = $WH.ce('td'); + $WH.ae(td, $WH.ct(vim_statuses[video.status])); + $WH.ae(tr, td); + + td = $WH.ce('td'); + var cb = $WH.ce('input'); + cb.type = 'checkbox'; + cb.value = video.id; + cb.onclick = function (e) { + $WH.sp(e); + (vim_UpdateMassLinks.bind(this))(); + }.bind(cb); + $WH.ae(td, cb); + + $WH.ae(td, $WH.ct(' ')); + + if (video.status != 999) { + tr.onclick = function (id) { + vim_View(this, id); + return false; + }.bind(tr, video.id); + + if (video.id == viId && k) + vim_View(tr, video.id); + + if (video.pending) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Approve.bind(this, false))(); + }.bind(video); + $WH.ae(a, $WH.ct('Approve')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Approve')); + + $WH.ae(td, makePipe()); + + if (video.status != 105) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Sticky.bind(this, false))(); + }.bind(video); + $WH.ae(a, $WH.ct('Make sticky')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Make sticky')); + + $WH.ae(td, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Delete.bind(this, false))(); + }.bind(video); + $WH.ae(a, $WH.ct('Delete')); + $WH.ae(td, a); + + $WH.ae(td, makePipe()); + + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + var a = prompt('Enter the ID to move this video to:'); + (vim_Relocate.bind(this, a))(); + }.bind(video); + $WH.ae(a, $WH.ct('Relocate')); + $WH.ae(td, a); + + $WH.ae(td, makePipe()); + + if (i > 0) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Move.bind(this, -1))() + }.bind(video); + $WH.ae(a, $WH.ct('Move up')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Move up')); + + $WH.ae(td, makePipe()); + + if (i < vim_videoData.length - 1) { + a = $WH.ce('a'); + a.href = 'javascript:;'; + a.onclick = function (e) { + $WH.sp(e); + (vim_Move.bind(this, 1))(); + }.bind(video); + $WH.ae(a, $WH.ct('Move down')); + $WH.ae(td, a); + } + else + $WH.ae(td, $WH.ct('Move down')); + } + + $WH.ae(tr, td); + $WH.ae(tbl, tr); + } +} + +function vim_UpdateMassLinks() { + var idBuff = ''; + var i = 0; + var e = $WH.ge('theVideosList'); + var inp = $WH.gE(e, 'input'); + + $WH.array_walk(inp, function (i) { + if (i.checked) { + idBuff += i.value + ','; ++i + } + }); + + idBuff = $WH.rtrim(idBuff, ','); + + var selCnt = $WH.ge('withselected'); + if (i > 0) { + selCnt.style.display = ''; + $WH.gE(selCnt, 'b')[0].firstChild.nodeValue = '(' + i + ')'; + + var c = $WH.ge('massapprove'); + var b = $WH.ge('massdelete'); + var a = $WH.ge('masssticky'); + + c.href = '?admin=videos&action=approve&id=' + idBuff; + c.onclick = vim_ConfirmMassApprove; + + b.href = '?admin=videos&action=delete&id=' + idBuff; + b.onclick = vim_ConfirmMassDelete; + + a.href = '?admin=videos&action=sticky&id=' + idBuff; + a.onclick = vim_ConfirmMassSticky; + } + else + selCnt.style.display = 'none'; +} + +function vim_MassSelect(action) { + var tbl = $WH.ge('theVideosList'); + var inp = $WH.gE(tbl, 'input'); + + switch (parseInt(action)) { + case 1: + $WH.array_walk(inp, function (x) { x.checked = true; }); + break; + case 0: + $WH.array_walk(inp, function (x) { x.checked = false; }); + break; + case -1: + $WH.array_walk(inp, function (x) { x.checked = !x.checked; }); + break; + case 2: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).status == 0; }); + break; + case 5: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).unique == 1 && vim_GetVideo(x.value).status == 0; }); + break; + case 3: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).status == 100; }); + break; + case 4: + $WH.array_walk(inp, function (x) { x.checked = vim_GetVideo(x.value).status == 105; }); + break; + default: + return; + } + + vim_UpdateMassLinks(); +} + +function vim_ShowEdit(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var sp = $WH.gE(node, 'span')[0]; + var div = $WH.ce('div'); + div.style.whiteSpace = 'nowrap'; + var iCaption = $WH.ce('input'); + iCaption.type = 'text'; + iCaption.value = video.caption; + iCaption.maxLength = 200; + iCaption.size = 35; + iCaption.onclick = function (e) { $WH.sp(e); } // aowow - custom to inhibit screenshot popup, when clicking into input element + div.appendChild(iCaption); + + var btn = $WH.ce('input'); + btn.type = 'button'; + btn.value = 'Update'; + btn.onclick = function (vi, isAlt, e) { + if (!isAlt) + $WH.sp(e); + + (vim_Edit.bind(this, vi, isAlt))(); + }.bind(btn, video, isAlt); + div.appendChild(btn); + + var sp2 = $WH.ce('span'); + sp2.appendChild($WH.ct(' ')); + div.appendChild(sp2); + + btn = $WH.ce('input'); + btn.type = 'button'; + btn.value = 'Cancel'; + btn.onclick = function (vi, isAlt, e) { + if (!isAlt) + $WH.sp(e); + + (vim_CancelEdit.bind(this, vi, isAlt))(); + }.bind(btn, video, isAlt); + div.appendChild(btn); + + sp.style.display = 'none'; + sp.nextSibling.style.display = 'none'; + node.insertBefore(div, sp); + + iCaption.focus(); +} + +function vim_CancelEdit(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var b = $WH.gE(node, 'span')[1]; + b.style.display = ''; + b.nextSibling.style.display = ''; + + node.removeChild(node.firstChild); +} + +function vim_Edit(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var desc = node.firstChild.childNodes; + if (desc[0].value == video.caption) { + vim_CancelEdit(video, isAlt); + return; + } + + video.caption = desc[0].value; + + vim_CancelEdit(video, isAlt); + + node = node.firstChild; + while (node.childNodes.length > 0) + node.removeChild(node.firstChild); + + $WH.ae(node, $WH.ct(video.caption)); + + new Ajax('?admin=videos&action=edittitle&id=' + video.id, { + method: 'POST', + params: 'title=' + $WH.urlencode(video.caption) + }); +} + +function vim_Clear(video, isAlt) { + var node; + if (isAlt) + node = $WH.ge('title2-' + video.id); + else + node = $WH.ge('title-' + video.id); + + var sp = $WH.gE(node, 'span'); + var a = $WH.gE(sp[1], 'a'); + sp = sp[0]; + + if (video.caption == '') + return; + + video.caption = ''; + sp.innerHTML = "NULL"; + + new Ajax('?admin=videos&action=edittitle&id=' + video.id, { + method: 'POST', + params: 'title=' + $WH.urlencode('') + }); +} + +function vim_Approve(openNext) { + var vi = this; + new Ajax('?admin=videos&action=approve&id=' + vi.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (vim_numPending == 1 && vi.pending) + vi_Refresh(true); + else { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId, openNext, 0); + } + } + }); +} + +function vim_Sticky(openNext) { + var vi = this; + new Ajax('?admin=videos&action=sticky&id=' + vi.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (vim_numPending == 1 && vi.pending) + vi_Refresh(true); + else { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId, openNext, 0); + } + } + }); +} + +function vim_Delete(openNext) { + var vi = this; + new Ajax('?admin=videos&action=delete&id=' + vi.id, { + method: 'get', + onSuccess: function (x) { + Lightbox.hide(); + if (vim_numPending == 1 && vi.pending) + vi_Refresh(true); + else { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId, openNext, 0); + } + } + }); +} + +function vim_Relocate(typeid) { + var vi = this; + new Ajax('?admin=videos&action=relocate&id=' + vi.id + '&typeid=' + typeid, { + method: 'get', + onSuccess: function (x) { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, typeid); + } + }); +} + +function vim_Move(direction) { + var vi = this; + new Ajax('?admin=videos&action=order&id=' + vi.id + '&move=' + direction, { + method: 'get', + onSuccess: function (x) { + vi_Refresh(); + vi_Manage(vi_managedRow, vi.type, vi.typeId); + } + }); +} + +var VideoManager = new +function () { + var + video, + pos, + prevImgWidth, + prevImgHeight, + scale, + desiredScale, + container, screen, + prevImgDiv, + aPrev, aNext, aCover, + aOriginal, + divFrom, + spCaption, + divCaption, + h2Name, + controlsCOPY, + aEdit, + aClear, + spApprove, + aApprove, + aMakeSticky, + aDelete, + loadingImage, + lightboxComponents; + + function computeDimensions(captionExtraHeight) { + var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 122 - captionExtraHeight)); + + if (video.id) { + desiredScale = Math.min(772 / video.width, 618 / video.height); + scale = Math.min(772 / video.width, availHeight / video.height) + } + else + desiredScale = scale = 1; + + if (desiredScale > 1) + desiredScale = 1; + + if (scale > 1) + scale = 1; + + prevImgWidth = Math.round(scale * video.width); + prevImgHeight = Math.round(scale * video.height); + var M = Math.max(480, prevImgWidth); + + Lightbox.setSize(M + 20, prevImgHeight + 116 + captionExtraHeight); + + if (captionExtraHeight) { + prevImgDiv.firstChild.width = prevImgWidth; + prevImgDiv.firstChild.height = prevImgHeight; + } + } + + function render(resizing) { + if (resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) + return; + + container.style.visibility = 'hidden'; + + var resized = (video.width > 772 || video.height > 618); + + computeDimensions(0); + + // Aowow - /uploads/videos/ not seen on server + // var url = g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'; + var url = video.url; + + var html = ''; + + spCaption.innerHTML = html; + } + else + spCaption.innerHTML = "NULL"; + + divCaption.id = 'title2-' + video.id; + + aEdit.onclick = vim_ShowEdit.bind(aEdit, video, true); + aClear.onclick = vim_Clear.bind(aClear, video, true); + + if (video.next !== undefined) { + aPrev.style.display = aNext.style.display = ''; + aCover.style.display = 'none'; + } + else { + aPrev.style.display = aNext.style.display = 'none'; + aCover.style.display = ''; + } + } + + Lightbox.reveal(); + + if (spCaption.offsetHeight > 18) + computeDimensions(spCaption.offsetHeight - 18); + + container.style.visibility = 'visible'; + } + + function nextVideo() { + if (video.next !== undefined) + video = vim_videoData[video.next]; + + onRender(); + } + + function prevVideo() { + if (video.prev !== undefined) + video = vim_videoData[video.prev]; + + onRender(); + } + function onResize() { + render(1); + } + + function onHide() { + aApprove.onclick = aMakeSticky.onclick = aDelete.onclick = null; + cancelImageLoading(); + } + + function onShow(dest, first, opt) { + video = opt; + container = dest; + + if (first) { + dest.className = 'screenshotviewer'; + + screen = $WH.ce('div'); + screen.className = 'screenshotviewer-screen'; + + aPrev = $WH.ce('a'); + aNext = $WH.ce('a'); + aPrev.className = 'screenshotviewer-prev'; + aNext.className = 'screenshotviewer-next'; + aPrev.href = 'javascript:;'; + aNext.href = 'javascript:;'; + + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aPrev, foo); + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aNext, foo); + + aPrev.onclick = prevVideo; + aNext.onclick = nextVideo; + + aCover = $WH.ce('a'); + aCover.className = 'screenshotviewer-cover'; + aCover.href = 'javascript:;'; + aCover.onclick = Lightbox.hide; + + var foo = $WH.ce('span'); + $WH.ae(foo, $WH.ce('b')); + $WH.ae(aCover, foo); + $WH.ae(screen, aPrev); + $WH.ae(screen, aNext); + $WH.ae(screen, aCover); + + var _div = $WH.ce('div'); + _div.className = 'text'; + h2Name = $WH.ce('h2'); + h2Name.className = 'first'; + $WH.ae(h2Name, $WH.ct(video.name)); + $WH.ae(_div, h2Name); + $WH.ae(dest, _div); + + prevImgDiv = $WH.ce('div'); + $WH.ae(screen, prevImgDiv); + + $WH.ae(dest, screen); + + var _div = $WH.ce('div'); + _div.style.paddingTop = '6px'; + _div.style.cssFloat = _div.style.styleFloat = 'right'; + _div.className = 'bigger-links'; + + aApprove = $WH.ce('a'); + aApprove.href = 'javascript:;'; + $WH.ae(aApprove, $WH.ct('Approve')); + $WH.ae(_div, aApprove); + + spApprove = $WH.ce('span'); + spApprove.style.display = 'none'; + $WH.ae(spApprove, $WH.ct('Approve')); + $WH.ae(_div, spApprove); + + $WH.ae(_div, makePipe()); + + aMakeSticky = $WH.ce('a'); + aMakeSticky.href = 'javascript:;'; + $WH.ae(aMakeSticky, $WH.ct('Make sticky')); + $WH.ae(_div, aMakeSticky); + + $WH.ae(_div, makePipe()); + + aDelete = $WH.ce('a'); + aDelete.href = 'javascript:;'; + $WH.ae(aDelete, $WH.ct('Delete')); + $WH.ae(_div, aDelete); + + controlsCOPY = _div; + + $WH.ae(dest, _div); + + divFrom = $WH.ce('div'); + divFrom.className = 'screenshotviewer-from'; + + var sp = $WH.ce('span'); + $WH.ae(sp, $WH.ct(LANG.lvvideo_from)); + $WH.ae(sp, $WH.ce('a')); + $WH.ae(sp, $WH.ct(' ')); + $WH.ae(sp, $WH.ce('span')); + $WH.ae(divFrom, sp); + $WH.ae(dest, divFrom); + + _div = $WH.ce('div'); + _div.className = 'clear'; + $WH.ae(dest, _div); + + var aClose = $WH.ce('a'); + aClose.className = 'screenshotviewer-close'; + aClose.href = 'javascript:;'; + aClose.onclick = Lightbox.hide; + $WH.ae(aClose, $WH.ce('span')); + $WH.ae(dest, aClose); + + aOriginal = $WH.ce('a'); + aOriginal.className = 'screenshotviewer-original'; + aOriginal.href = 'javascript:;'; + aOriginal.target = '_blank'; + $WH.ae(aOriginal, $WH.ce('span')); + $WH.ae(dest, aOriginal); + + divCaption = $WH.ce('div'); + spCaption = $WH.ce('span'); + spCaption.style.paddingRight = '8px'; + $WH.ae(divCaption, spCaption); + + var sp = $WH.ce('span'); + sp.style.whiteSpace = 'nowrap'; + aEdit = $WH.ce('a'); + aEdit.href = 'javascript:;'; + $WH.ae(aEdit, $WH.ct('Edit')); + $WH.ae(sp, aEdit); + + $WH.ae(sp, makePipe()); + + aClear = $WH.ce('a'); + aClear.href = 'javascript:;'; + $WH.ae(aClear, $WH.ct('Clear')); + $WH.ae(sp, aClear); + $WH.ae(divCaption, sp); + $WH.ae(dest, divCaption); + + _div = $WH.ce('div'); + _div.className = 'clear'; + $WH.ae(dest, _div); + } + else { + $WH.ee(h2Name); + $WH.ae(h2Name, $WH.ct(video.name)); + } + + onRender(); + } + function onRender() { + if (video.pending) { + aApprove.onclick = vim_Approve.bind(video, true); + aMakeSticky.onclick = vim_Sticky.bind(video, true); + aDelete.onclick = vim_Delete.bind(video, true); + } + else { + aMakeSticky.onclick = vim_Sticky.bind(video, true); + aDelete.onclick = vim_Delete.bind(video, true); + } + aApprove.style.display = video.pending ? '' : 'none'; + spApprove.style.display = video.pending ? 'none' : ''; + + if (!video.width || !video.height) { + if (loadingImage) { + loadingImage.onload = null; + loadingImage.onerror = null; + } + else { + container.className = ''; + lightboxComponents = []; + + while (container.firstChild) { + lightboxComponents.push(container.firstChild); + $WH.de(container.firstChild); + } + } + + var lightboxTimer = setTimeout(function () { + video.width = 126; + video.height = 22; + + computeDimensions(0); + + video.width = null; + video.height = null; + + var div = $WH.ce('div'); + div.style.margin = '0 auto'; + div.style.width = '126px'; + + var img = $WH.ce('img'); + img.src = g_staticUrl + '/images/ui/misc/progress-anim.gif'; + img.width = 126; + img.height = 22; + + $WH.ae(div, img); + $WH.ae(container, div); + + Lightbox.reveal(); + container.style.visiblity = 'visible' + }, 150); + + loadingImage = new Image(); + loadingImage.onload = (function (vi, timer) { + clearTimeout(timer); + vi.width = this.width; + vi.height = this.height; + loadingImage = null; + restoreLightbox(); + render() + }).bind(loadingImage, video, lightboxTimer); + + loadingImage.onerror = (function (timer) { + clearTimeout(timer); + loadingImage = null; + Lightbox.hide(); + restoreLightbox() + }).bind(loadingImage, lightboxTimer); + + loadingImage.src = (video.url ? video.url : g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'); + } + else + render(); + } + + function cancelImageLoading() { + if (!loadingImage) + return; + + loadingImage.onload = null; + loadingImage.onerror = null; + loadingImage = null; + + restoreLightbox(); + } + + function restoreLightbox() { + if (!lightboxComponents) + return; + + $WH.ee(container); + container.className = 'screenshotviewer'; + for (var K = 0; K < lightboxComponents.length; ++K) + $WH.ae(container, lightboxComponents[K]); + + lightboxComponents = null; + } + + this.show = function (opt) { + Lightbox.show('videomanager', { + onShow: onShow, + onHide: onHide, + onResize: onResize + }, opt); + } +}; diff --git a/template/pages/admin/videos.tpl.php b/template/pages/admin/videos.tpl.php new file mode 100644 index 00000000..da89e0a6 --- /dev/null +++ b/template/pages/admin/videos.tpl.php @@ -0,0 +1,135 @@ +brick('header'); +?> +
        +
        +
        + +brick('announcement'); + +$this->brick('pageTemplate'); +?> +
        +

        h1; ?>

        + +
        ucFirst(Lang::main('name')).Lang::main('colon'); ?> - +
         /> />
        + + + + + + + + + + + +
        User: » Search by User
        Page: + + #» Search by Page
        +
        + + + + + + + +
        Menu
        PagesVideos:
        + + + + + + + +
        VideoIdTitleDateUploaderStatusOptions
        + + +
        +
        +
        + +brick('footer'); ?> diff --git a/template/pages/video.tpl.php b/template/pages/video.tpl.php new file mode 100644 index 00000000..f3cded7d --- /dev/null +++ b/template/pages/video.tpl.php @@ -0,0 +1,82 @@ +brick('header'); +?> +
        +
        +
        + +brick('announcement'); + +$this->brick('pageTemplate'); + +$this->brick('infobox'); + +?> +
        +

        h1; ?>

        + +

        viTitle;?>

        +
        +
        + + +
        + + +
        +
        + + + + + +
        +
        +
        + +brick('footer'); ?> From fef27c58e60333f3e4f44a66d7dd1488c6c949b8 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 18 Aug 2025 00:22:24 +0200 Subject: [PATCH 0975/1249] Template/Update (Part 40) * convert 'guides' (listing, viewing, writing & management) * don't allow comments on WIP guides --- endpoints/admin/guide.php | 81 +++ endpoints/admin/guides.php | 46 ++ endpoints/edit/image.php | 48 ++ endpoints/get-description/get-description.php | 35 ++ endpoints/guide/changelog.php | 104 ++++ endpoints/guide/edit.php | 214 +++++++ endpoints/guide/guide.php | 250 ++++++++ endpoints/guide/guide_power.php | 59 ++ endpoints/guide/new.php | 66 +++ endpoints/guide/vote.php | 50 ++ endpoints/guides/guides.php | 73 +++ endpoints/my-guides/my-guides.php | 52 ++ endpoints/user/user.php | 2 +- includes/ajaxHandler/admin.class.php | 60 +- includes/ajaxHandler/edit.class.php | 82 --- includes/ajaxHandler/getdescription.class.php | 37 -- includes/ajaxHandler/guide.class.php | 63 -- includes/components/guidemgr.class.php | 106 ++++ includes/components/pagetemplate.class.php | 1 - .../response/templateresponse.class.php | 6 +- includes/dbtypes/guide.class.php | 35 +- includes/defines.php | 7 - includes/libs/qqFileUploader.class.php | 2 +- includes/type.class.php | 2 +- includes/user.class.php | 2 +- includes/utilities.php | 2 - localization/locale_dede.php | 28 +- localization/locale_enus.php | 28 +- localization/locale_eses.php | 28 +- localization/locale_frfr.php | 28 +- localization/locale_ruru.php | 28 +- localization/locale_zhcn.php | 28 +- pages/admin.php | 2 +- pages/guide.php | 549 ------------------ pages/guides.php | 102 ---- static/js/global.js | 235 ++++---- template/listviews/guideAdminCol.tpl | 4 +- template/pages/guide-edit.tpl.php | 57 +- 38 files changed, 1437 insertions(+), 1165 deletions(-) create mode 100644 endpoints/admin/guide.php create mode 100644 endpoints/admin/guides.php create mode 100644 endpoints/edit/image.php create mode 100644 endpoints/get-description/get-description.php create mode 100644 endpoints/guide/changelog.php create mode 100644 endpoints/guide/edit.php create mode 100644 endpoints/guide/guide.php create mode 100644 endpoints/guide/guide_power.php create mode 100644 endpoints/guide/new.php create mode 100644 endpoints/guide/vote.php create mode 100644 endpoints/guides/guides.php create mode 100644 endpoints/my-guides/my-guides.php delete mode 100644 includes/ajaxHandler/edit.class.php delete mode 100644 includes/ajaxHandler/getdescription.class.php delete mode 100644 includes/ajaxHandler/guide.class.php create mode 100644 includes/components/guidemgr.class.php delete mode 100644 pages/guide.php delete mode 100644 pages/guides.php diff --git a/endpoints/admin/guide.php b/endpoints/admin/guide.php new file mode 100644 index 00000000..8a6d93d8 --- /dev/null +++ b/endpoints/admin/guide.php @@ -0,0 +1,81 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => GuideMgr::STATUS_APPROVED, 'max_range' => GuideMgr::STATUS_REJECTED]], + 'msg' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'status')) + { + trigger_error('AdminGuideResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + $guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']); + if (!$guide) + { + trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' not found', E_USER_ERROR); + $this->result = self::ERR_GUIDE; + return; + } + + if ($this->_post['status'] == $guide['status']) + { + trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' already has status #'.$this->_post['status'], E_USER_ERROR); + $this->result = self::ERR_STATUS; + return; + } + + // status can only be APPROVED or REJECTED due to input validation + if (!$this->update($this->_post['id'], $this->_post['status'], $this->_post['msg'])) + { + trigger_error('AdminGuideResponse - write to db failed for guide #'.$this->_post['id'], E_USER_ERROR); + $this->result = self::ERR_WRITE_DB; + return; + } + + if ($this->_post['status'] == GuideMgr::STATUS_APPROVED) + Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]); + + $this->result = self::ERR_NONE; + } + + private function update(int $id, int $status, ?string $msg = null) : bool + { + if ($status == GuideMgr::STATUS_APPROVED) // set display rev to latest + $ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d, `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', $status, Type::GUIDE, $id, User::$id, time(), $id); + else + $ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id); + + if (!$ok) + return false; + + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status); + if ($msg) + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg); + + return true; + } +} + +?> diff --git a/endpoints/admin/guides.php b/endpoints/admin/guides.php new file mode 100644 index 00000000..16aa460b --- /dev/null +++ b/endpoints/admin/guides.php @@ -0,0 +1,46 @@ + Content > Guides Awaiting Approval + + protected function generate() : void + { + $this->h1 = 'Pending Guides'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); + if ($pending->error) + $data = []; + else + { + $data = $pending->getListviewData(); + $latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ?_articles WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs()); + foreach ($latest as $id => $rev) + $data[$id]['rev'] = $rev; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => array_values($data), + 'hiddenCols' => ['patch', 'comments', 'views', 'rating'], + 'extraCols' => '$_' + ), GuideList::$brickFile, 'guideAdminCol')); + } +} + +?> diff --git a/endpoints/edit/image.php b/endpoints/edit/image.php new file mode 100644 index 00000000..587d5a5e --- /dev/null +++ b/endpoints/edit/image.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'guide' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]] + ); + + /* + success: bool + id: image enumerator + type: 3 ? png : jpg + name: old filename + error: errString + */ + protected function generate() : void + { + if (!$this->assertGET('qqfile', 'guide')) + { + $this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]); + return; + } + + if (!User::canWriteGuide()) + { + $this->result = Util::toJSON(['success' => false, 'error' => Lang::main('genericError')]); + return; + } + + $this->result = GuideMgr::handleUpload(); + + if (isset($this->result['success'])) + $this->result += ['name' => $this->_get['qqfile']]; + + $this->result = Util::toJSON($this->result); + } +} + +?> diff --git a/endpoints/get-description/get-description.php b/endpoints/get-description/get-description.php new file mode 100644 index 00000000..139c6949 --- /dev/null +++ b/endpoints/get-description/get-description.php @@ -0,0 +1,35 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + public function __construct(string $param) + { + if ($param) // should be empty + $this->generate404(); + + parent::__construct($param); + } + + protected function generate() : void + { + if (!User::canWriteGuide()) + return; + + $this->result = GuideMgr::createDescription($this->_post['description']); + } +} + +?> diff --git a/endpoints/guide/changelog.php b/endpoints/guide/changelog.php new file mode 100644 index 00000000..685dfad3 --- /dev/null +++ b/endpoints/guide/changelog.php @@ -0,0 +1,104 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + // main container should be tagged:
        + + if (!$this->assertGET('id')) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + $guide = new GuideList(array(['id', $this->_get['id']])); + if ($guide->error) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + if (!$guide->canBeViewed() && !$guide->userCanView()) + $this->forward('?guides='.$guide->getField('category')); + + $this->h1 = lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); + if (!$this->h1) + $this->h1 = $guide->getField('name'); + + $this->gPageInfo += ['name' => $guide->getField('name')]; + + + $this->breadcrumb[] = $guide->getField('category'); + + + parent::generate(); + + /* - NYI (see "&& false") + $this->addScript([SC_JS_STRING, + + <<= parseInt(e.value)); + }); + + }; + + radios.each(function (i, e) { + e.onchange = limit.bind(this, e.name, parseInt(e.value)); + + if (i < 2 && e.name == "b") // first pair + $(e).trigger("click"); + else if (e.value == 0 && e.name == "a") // last pair + $(e).trigger("click"); + }); + }); + JS + ]); + */ + + $buff = '
          '; + $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) && false ? ($rev !== null ? '' : '') : ''; + + $logEntries = DB::Aowow()->select('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->_get['id']); + foreach ($logEntries as $log) + { + if ($log['status'] != GuideMgr::STATUS_NONE) + $buff .= '
        • '.$inp($log['rev']).''.Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).''.Util::formatTimeDiff($log['date'])."
        • \n"; + else if ($log['msg']) + $buff .= '
        • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
        • \n"; + else + $buff .= '
        • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
        • \n"; + } + + // append creation + $buff .= '
        • '.$inp(0).''.Lang::guide('clCreated').''.Util::formatTimeDiff($guide->getField('date'))."
        • \n
        \n"; + + if (User::isInGroup(U_GROUP_STAFF) && false) + $buff .= ''; + + $this->extraHTML = $buff; + } +} + +?> diff --git a/endpoints/guide/edit.php b/endpoints/guide/edit.php new file mode 100644 index 00000000..b2897167 --- /dev/null +++ b/endpoints/guide/edit.php @@ -0,0 +1,214 @@ + span { display: block; height: 22px; } + #upload-result { display: inline-block; text-align: right; } + #upload-progress { display: inline-block; margin-right: 8px; } + + CSS] + ); + protected array $expectedPOST = array( + 'save' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], // saved for more editing + 'submit' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], // submitted for review + 'title' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'name' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'description' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkDescription'] ], + 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom'] ], + 'category' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 9] ], + 'specId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => -1, 'max_value' => 2, 'default' => -1]], + 'classId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 11, 'default' => 0]] + ); + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_VALIDATE_INT], + 'rev' => ['filter' => FILTER_VALIDATE_INT] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + if (!User::canWriteGuide()) + $this->generateError(); + + if (!is_int($this->_get['id'])) // edit existing guide + return; + + $this->typeId = $this->_get['id']; // just to display sensible not-found msg + $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_guides WHERE `id` = ?d AND `status` <> ?d { AND `userId` = ?d }', $this->typeId, GuideMgr::STATUS_ARCHIVED, User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : User::$id); + if (!$status && $this->typeId) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + else if (!$this->typeId) + return; + + // just so we don't have to access GuideMgr from template + $this->isDraft = $status == GuideMgr::STATUS_DRAFT; + $this->editStatus = $status; + $this->editRev = DB::Aowow()->selectCell('SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC', Type::GUIDE, $this->typeId); + } + + protected function generate() : void + { + if ($this->_post['save'] || $this->_post['submit']) + { + if (!$this->saveGuide()) + $this->error = Lang::main('intError'); + else if ($this->_get['id'] === 0) + $this->forward('?guide=edit&id='.$this->typeId); + } + + $guide = new GuideList(array(['id', $this->typeId])); + + $this->h1 = Lang::guide('editTitle'); + array_unshift($this->title, $this->h1.Lang::main('colon').$guide->getField('title'), Lang::game('guides')); + + Lang::sort('guide', 'category'); + + // init required template vars + $this->editCategory = $this->_post['category'] ?? $guide->getField('category'); + $this->editTitle = $this->_post['title'] ?? $guide->getField('title'); + $this->editName = $this->_post['name'] ?? $guide->getField('name'); + $this->editDescription = $this->_post['description'] ?? $guide->getField('description'); + $this->editText = $this->_post['body'] ?? $guide->getArticle(); + $this->editClassId = $this->_post['classId'] ?? $guide->getField('classId'); + $this->editSpecId = $this->_post['specId'] ?? $guide->getField('specId'); + $this->editLocale = $this->_post['locale'] ?? Locale::tryFrom($guide->getField('locale')); + $this->editStatus = $this->editStatus ?: $guide->getField('status'); + $this->editStatusColor = GuideMgr::STATUS_COLORS[$this->editStatus]; + + $this->extendGlobalData($guide->getJSGlobals()); + + parent::generate(); + } + + private function saveGuide() : bool + { + // test requiered fields set + if (!$this->assertPOST('title', 'name', 'body', 'locale', 'category')) + { + trigger_error('GuideEditResponse::saveGuide - received malformed request', E_USER_ERROR); + return false; + } + + // test required fields context + if (!$this->_post['locale']->validate()) + return false; + + // sanitize: spec / class + if ($this->_post['category'] == 1) // Classes + { + if ($this->_post['classId'] && !ChrClass::tryFrom($this->_post['classId'])) + $this->_post['classId'] = 0; + + if ($this->_post['specId'] > -1 && !$this->_post['classId']) + $this->_post['specId'] = -1; + } + else + { + $this->_post['classId'] = 0; + $this->_post['specId'] = -1; + } + + $guideData = array( + 'category' => $this->_post['category'], + 'classId' => $this->_post['classId'], + 'specId' => $this->_post['specId'], + 'title' => $this->_post['title'], + 'name' => $this->_post['name'], + 'description' => $this->_post['description'] ?: GuideMgr::createDescription($this->_post['body']), + 'locale' => $this->_post['locale']->value, + 'roles' => User::$groups, + 'status' => $this->_post['submit'] ? GuideMgr::STATUS_REVIEW : GuideMgr::STATUS_DRAFT, + 'date' => time() + ); + + // new guide > reload editor + if ($this->_get['id'] === 0) + { + $guideData += ['userId' => User::$id]; + if (!($this->typeId = (int)DB::Aowow()->query('INSERT INTO ?_guides (?#) VALUES (?a)', array_keys($guideData), array_values($guideData)))) + { + trigger_error('GuideEditResponse::saveGuide - failed to save guide to db', E_USER_ERROR); + return false; + } + } + // existing guide > :shrug: + else if (DB::Aowow()->query('UPDATE ?_guides SET ?a WHERE `id` = ?d', $guideData, $this->typeId)) + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?d, ?)', $this->typeId, $this->editRev, time(), User::$id, $this->_post['changelog']); + else + { + trigger_error('GuideEditResponse::saveGuide - failed to update guide in db', E_USER_ERROR); + return false; + } + + // insert Article + $articleId = DB::Aowow()->query( + 'INSERT INTO ?_articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (?d, ?d, ?d, ?d, ?d, ?)', + Type::GUIDE, + $this->typeId, + $this->_post['locale']->value, + ++$this->editRev, + User::$groups & U_GROUP_STAFF ? User::$groups : User::$groups | U_GROUP_BLOGGER, + $this->_post['body'] + ); + + if (!is_int($articleId)) + { + if ($this->_get['id'] === 0) + DB::Aowow()->query('DELETE FROM ?_guides WHERE `id` = ?d', $this->typeId); + + trigger_error('GuideEditResponse::saveGuide - failed to save article to db', E_USER_ERROR); + return false; + } + + if ($this->_post['submit'] && $this->editStatus != GuideMgr::STATUS_REVIEW) + DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $this->typeId, time(), User::$id, GuideMgr::STATUS_REVIEW); + + $this->editStatus = $guideData['status']; + + return true; + } + + protected static function checkDescription(string $str) : string + { + // run checkTextBlob and also replace \n => \s and \s+ => \s + $str = preg_replace(parent::PATTERN_TEXT_BLOB, '', $str); + + $str = strtr($str, ["\n" => ' ', "\r" => ' ']); + + return preg_replace('/\s+/', ' ', trim($str)); + } +} + +?> diff --git a/endpoints/guide/guide.php b/endpoints/guide/guide.php new file mode 100644 index 00000000..90de7eb8 --- /dev/null +++ b/endpoints/guide/guide.php @@ -0,0 +1,250 @@ + ['filter' => FILTER_VALIDATE_INT], + 'rev' => ['filter' => FILTER_VALIDATE_INT] + ); + + public int $type = Type::GUIDE; + public int $typeId = 0; + public int $guideStatus = 0; + public array $guideRating = []; + public ?int $guideRevision = null; + + private GuideList $subject; + + public function __construct(string $nameOrId) + { + parent::__construct($nameOrId); + + /**********************/ + /* get mode + guideId */ + /**********************/ + + if (Util::checkNumeric($nameOrId, NUM_CAST_INT)) + $this->typeId = $nameOrId; + else if (preg_match(GuideMgr::VALID_URL, $nameOrId)) + { + if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($nameOrId))) + { + $this->typeId = intVal($id); + $this->articleUrl = Util::lower($nameOrId); + } + } + + $this->contribute = Type::getClassAttrib($this->type, 'contribute') ?? CONTRIBUTE_NONE; + } + + protected function generate() : void + { + $this->subject = new GuideList(array(['id', $this->typeId])); + if ($this->subject->error) + $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); + + if (!$this->subject->canBeViewed() && !$this->subject->userCanView()) + $this->forward('?guides='.$this->subject->getField('category')); + + $this->guideStatus = $this->subject->getField('status'); + if ($this->guideStatus != GuideMgr::STATUS_APPROVED && $this->guideStatus != GuideMgr::STATUS_ARCHIVED) + { + $this->cacheType = CACHE_TYPE_NONE; + $this->contribute = CONTRIBUTE_NONE; + } + + if ($this->articleUrl) + $this->guideRevision = $this->subject->getField('rev'); + else if ($this->subject->userCanView()) + $this->guideRevision = $this->_get['rev'] ?? $this->subject->getField('latest'); + else + $this->subject->getField('rev'); + + $this->h1 = $this->subject->getField('name'); + + $this->gPageInfo += array( + 'name' => $this->h1, + 'author' => $this->subject->getField('author') + ); + + + /*************/ + /* Menu Path */ + /*************/ + + if ($x = $this->subject?->getField('category')) + $this->breadcrumb[] = $x; + + + /**************/ + /* Page Title */ + /**************/ + + array_unshift($this->title, $this->subject->getField('title'), Lang::game('guides')); + + + /***********/ + /* Infobox */ + /***********/ + + if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_QUICKFACTS)) + $this->generateInfobox(); + + // needs post-cache updating + if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_RATING)) + $this->guideRating = array( + $this->subject->getField('rating'), // avg rating + User::canUpvote() && User::canDownvote() ? 'true' : 'false', + $this->subject->getField('_self'), // my rating amt; 0 = no vote + $this->typeId // guide Id + ); + + + /****************/ + /* Main Content */ + /****************/ + + if ($this->subject->userCanView()) + $this->redButtons[BUTTON_GUIDE_EDIT] = User::canWriteGuide() && $this->guideStatus != GuideMgr::STATUS_ARCHIVED; + + $this->redButtons[BUTTON_GUIDE_LOG] = true; + $this->redButtons[BUTTON_GUIDE_REPORT] = $this->subject->canBeReported(); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); + + // the article text itself is added by PageTemplate::addArticle() + parent::generate(); + + $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); + if ($this->guideRating) + $this->result->registerDisplayHook('guideRating', [self::class, 'starsHook']); + } + + private function generateInfobox() : void + { + $infobox = []; + + if ($this->subject->getField('cuFlags') & CC_FLAG_STICKY) + $infobox[] = '[span class=guide-sticky]'.Lang::guide('sticky').'[/span]'; + + $infobox[] = Lang::guide('author').'[url=?user='.$this->subject->getField('author').']'.$this->subject->getField('author').'[/url]'; + + if ($this->subject->getField('category') == 1) + { + $c = $this->subject->getField('classId'); + $s = $this->subject->getField('specId'); + if ($c > 0) + { + $this->extendGlobalIds(Type::CHR_CLASS, $c); + $infobox[] = Util::ucFirst(Lang::game('class')).Lang::main('colon').'[class='.$c.']'; + } + if ($s > -1) + $infobox[] = Lang::guide('spec').'[icon class="c'.$c.' icontiny" name='.Game::$specIconStrings[$c][$s].']'.Lang::game('classSpecs', $c, $s).'[/icon]'; + } + + // $infobox[] = Lang::guide('patch').Lang::main('colon').'3.3.5'; // replace with date + $infobox[] = Lang::guide('added').'[tooltip name=added]'.date('l, G:i:s', $this->subject->getField('date')).'[/tooltip][span class=tip tooltip=added]'.date(Lang::main('dateFmtShort'), $this->subject->getField('date')).'[/span]'; + + if ($this->guideStatus == GuideMgr::STATUS_ARCHIVED) + $infobox[] = Lang::guide('status', GuideMgr::STATUS_ARCHIVED); + + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + + if ($this->guideStatus == GuideMgr::STATUS_REVIEW && User::isInGroup(U_GROUP_STAFF) && $this->_get['rev']) + { + $this->addScript([SC_JS_STRING, <<infobox->append('[h3 style="text-align:center"]Admin[/h3]'); + $this->infobox->append('[div style="text-align:center"][url=# id="btn-accept" class=icon-tick]Approve[/url][url=# style="margin-left:20px" id="btn-reject" class=icon-delete]Reject[/url][/div]'); + } + } + + public static function infoboxHook(Template\PageTemplate &$pt, ?InfoboxMarkup &$infobox) : void + { + if ($pt->guideStatus != GuideMgr::STATUS_APPROVED) + return; + + // increment and display views + DB::Aowow()->query('UPDATE ?_guides SET `views` = `views` + 1 WHERE `id` = ?d', $pt->typeId); + + $nViews = DB::Aowow()->selectCell('SELECT `views` FROM ?_guides WHERE `id` = ?d', $pt->typeId); + + $infobox->addItem(Lang::guide('views').'[n5='.$nViews.']'); + + // should we have a rating item in the lv? + if (!$pt->guideRating) + return; + + $rating = GuideMgr::getRatings([$pt->typeId]); + if ($rating[$pt->typeId]['nvotes'] < 5) + $infobox->addItem(Lang::guide('rating').Lang::guide('noVotes')); + else + $infobox->addItem(Lang::guide('rating').Lang::guide('votes', [round($rating[$pt->typeId]['rating'], 1), $rating[$pt->typeId]['nvotes']])); + } + + public static function starsHook(Template\PageTemplate &$pt, ?array &$guideRating) : void + { + if ($pt->guideStatus != GuideMgr::STATUS_APPROVED) + return; + + $rating = GuideMgr::getRatings([$pt->typeId]); + $guideRating = array( + $rating[$pt->typeId]['rating'], + User::canUpvote() && User::canDownvote() ? 'true' : 'false', + $rating[$pt->typeId]['_self'] ?? 0, + $pt->typeId + ); + } +} + +?> diff --git a/endpoints/guide/guide_power.php b/endpoints/guide/guide_power.php new file mode 100644 index 00000000..99fd4dba --- /dev/null +++ b/endpoints/guide/guide_power.php @@ -0,0 +1,59 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] + ); + + private string $url = ''; + + public function __construct(string $idOrName) + { + parent::__construct($idOrName); + + // temp locale + if ($this->_get['domain']) + Lang::load($this->_get['domain']); + + if (Util::checkNumeric($idOrName, NUM_CAST_INT)) + $this->typeId = $idOrName; + else if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($idOrName))) + { + $this->typeId = intVal($id); + $this->url = Util::lower($idOrName); + } + } + + protected function generate() : void + { + $opts = []; + if ($this->typeId) + if (!($guide = new GuideList(array(['id', $this->typeId])))->error) + $opts = array( + 'name' => $guide->getField('name', true), + 'tooltip' => $guide->renderTooltip() + ); + + if (!$opts) + $this->cacheType = CACHE_TYPE_NONE; + + $this->result = new Tooltip(self::POWER_TEMPLATE, $this->url ?: $this->typeId, $opts); + } +} + +?> diff --git a/endpoints/guide/new.php b/endpoints/guide/new.php new file mode 100644 index 00000000..95de5c4d --- /dev/null +++ b/endpoints/guide/new.php @@ -0,0 +1,66 @@ + span { display: block; height: 22px; } + #upload-result { display: inline-block; text-align: right; } + #upload-progress { display: inline-block; margin-right: 8px; } + + CSS] + ); + + public function __construct(string $param) + { + parent::__construct($param); + + if (!User::canWriteGuide()) + $this->generateError(); + } + + protected function generate() : void + { + $this->h1 = Lang::guide('newTitle'); + + array_unshift($this->title, $this->h1, Lang::game('guides')); + + Lang::sort('guide', 'category'); + + // update required template vars + $this->editLocale = Lang::getLocale(); + + parent::generate(); + } +} + +?> diff --git a/endpoints/guide/vote.php b/endpoints/guide/vote.php new file mode 100644 index 00000000..ca7ee945 --- /dev/null +++ b/endpoints/guide/vote.php @@ -0,0 +1,50 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 5]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'rating')) + { + trigger_error('GuideVoteResponse - malformed request received', E_USER_ERROR); + $this->generate404(); + } + + if (!User::canUpvote() || !User::canDownvote()) // same logic as comments? + $this->generate403(); + + // by id, not own, published + $points = $votes = 0; + if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ?_guides WHERE `id` = ?d AND (`status` = ?d OR `rev` > 0)', $this->_post['id'], GuideMgr::STATUS_APPROVED)) + { + // apparently you are allowed to vote on your own guide + if ($g['cuFlags'] & GUIDE_CU_NO_RATING) + $this->generate403(); + + if (!$this->_post['rating']) + DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_GUIDE, $this->_post['id'], User::$id); + else + DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']); + + [$points, $votes] = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1" FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_GUIDE, $this->_post['id']); + } + + $this->result = Util::toJSON($votes ? ['rating' => $points / $votes, 'nvotes' => $votes] : ['rating' => 0, 'nvotes' => 0]); + } +} + +?> diff --git a/endpoints/guides/guides.php b/endpoints/guides/guides.php new file mode 100644 index 00000000..9c0db0ee --- /dev/null +++ b/endpoints/guides/guides.php @@ -0,0 +1,73 @@ +getCategoryFromUrl($pageParam); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::game('guides')); + + + if ($this->category) + $this->breadcrumb[] = $this->category[0]; + + + array_unshift($this->title, $this->h1); + if ($this->category) + array_unshift($this->title, Lang::guide('category', $this->category[0])); + + + $conditions = array( + ['locale', Lang::getLocale()->value], + ['status', GuideMgr::STATUS_ARCHIVED, '!'], // never archived guides + [ + 'OR', + ['status', GuideMgr::STATUS_APPROVED], // currently approved + ['rev', 0, '>'] // has previously approved revision + ] + ); + if ($this->category) + $conditions[] = ['category', $this->category[0]]; + + $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; + + $guides = new GuideList($conditions); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'name' => Util::ucFirst(Lang::game('guides')), + 'hiddenCols' => ['patch'], // pointless: display date instead + 'extraCols' => ['$Listview.extraCols.date'] // ok + ), GuideList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/my-guides/my-guides.php b/endpoints/my-guides/my-guides.php new file mode 100644 index 00000000..288d87aa --- /dev/null +++ b/endpoints/my-guides/my-guides.php @@ -0,0 +1,52 @@ +generateError(); + } + + protected function generate() : void + { + $this->h1 = Util::ucFirst(Lang::guide('myGuides')); + + array_unshift($this->title, $this->h1); + + $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; + + $guides = new GuideList(array(['userId', User::$id])); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $guides->getListviewData(), + 'name' => Util::ucFirst(Lang::game('guides')), + 'hiddenCols' => ['patch', 'author'], + 'visibleCols' => ['status'], + 'extraCols' => ['$Listview.extraCols.date'] + ), GuideList::$brickFile)); + + parent::generate(); + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php index 7f4fee64..b3e8f7b8 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -253,7 +253,7 @@ class UserBaseResponse extends TemplateResponse } // My Guides - $guides = new GuideList(['status', [GUIDE_STATUS_APPROVED, GUIDE_STATUS_ARCHIVED]], ['userId', $this->user['id']]); + $guides = new GuideList(['status', [GuideMgr::STATUS_APPROVED, GuideMgr::STATUS_ARCHIVED]], ['userId', $this->user['id']]); if (!$guides->error) { $this->lvTabs->addListviewTab(new Listview(array( diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php index 899c607e..1b2b6f95 100644 --- a/includes/ajaxHandler/admin.class.php +++ b/includes/ajaxHandler/admin.class.php @@ -7,7 +7,7 @@ if (!defined('AOWOW_REVISION')) class AjaxAdmin extends AjaxHandler { - protected $validParams = ['siteconfig', 'weight-presets', 'spawn-override', 'guide', 'comment']; + protected $validParams = ['siteconfig', 'weight-presets', 'spawn-override', 'comment']; protected $_get = array( 'action' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ], 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], @@ -64,13 +64,6 @@ class AjaxAdmin extends AjaxHandler $this->handler = 'spawnPosFix'; } - else if ($this->params[0] == 'guide') - { - if (!User::isInGroup(U_GROUP_STAFF)) - return; - - $this->handler = 'guideManage'; - } else if ($this->params[0] == 'comment') { if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD)) @@ -207,57 +200,6 @@ class AjaxAdmin extends AjaxHandler return '-1'; } - protected function guideManage() : string - { - $update = function (int $id, int $status, ?string $msg = null) : bool - { - if (!DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id)) - return false; - - // set display rev to latest - if ($status == GUIDE_STATUS_APPROVED) - DB::Aowow()->query('UPDATE ?_guides SET `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', Type::GUIDE, $id, User::$id, time(), $id); - - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status); - if ($msg) - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg); - return true; - }; - - if (!$this->_post['id']) - trigger_error('AjaxHander::guideManage - malformed request: id: '.$this->_post['id'].', status: '.$this->_post['status']); - else - { - $guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']); - if (!$guide) - trigger_error('AjaxHander::guideManage - guide #'.$this->_post['id'].' not found'); - else - { - if ($this->_post['status'] == $guide['status']) - trigger_error('AjaxHander::guideManage - guide #'.$this->_post['id'].' already has status #'.$this->_post['status']); - else - { - if ($this->_post['status'] == GUIDE_STATUS_APPROVED) - { - if ($update($this->_post['id'], GUIDE_STATUS_APPROVED, $this->_post['msg'])) - { - Util::gainSiteReputation($guide['userId'], SITEREP_ACTION_ARTICLE, ['id' => $this->_post['id']]); - return '1'; - } - else - return '-2'; - } - else if ($this->_post['status'] == GUIDE_STATUS_REJECTED) - return $update($this->_post['id'], GUIDE_STATUS_REJECTED, $this->_post['msg']) ? '1' : '-2'; - else - trigger_error('AjaxHander::guideManage - unhandled status change request'); - } - } - } - - return '-1'; - } - protected function commentOutOfDate() : string { $ok = false; diff --git a/includes/ajaxHandler/edit.class.php b/includes/ajaxHandler/edit.class.php deleted file mode 100644 index 1275b7c4..00000000 --- a/includes/ajaxHandler/edit.class.php +++ /dev/null @@ -1,82 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine'], - 'guide' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$params) - return; - - if ($params[0] == 'image') - $this->handler = 'handleUpload'; - else if ($params[0] == 'article') // has it's own editor page - $this->handler = null; - } - - /* - success: bool - id: image enumerator - type: 3 ? png : jpg - name: old filename - error: errString - */ - protected function handleUpload() : string - { - if (!User::canWriteGuide() || $this->_get['guide'] != 1) - return Util::toJSON(['success' => false, 'error' => '']); - - require_once('includes/libs/qqFileUploader.class.php'); - - $targetPath = 'static/uploads/guide/images/'; - $tmpPath = 'static/uploads/temp/'; - $tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16); - - $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); - $result = $uploader->handleUpload($tmpPath, $tmpFile, true); - - if (isset($result['success'])) - { - $finfo = new \finfo(FILEINFO_MIME); - $mime = $finfo->file($tmpPath.$result['newFilename']); - if (preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) - { - $i = 1; // image index - if ($files = scandir($targetPath, SCANDIR_SORT_DESCENDING)) - if (rsort($files, SORT_NATURAL) && $files[0] != '.' && $files[0] != '..') - $i = explode('.', $files[0])[0] + 1; - - $targetFile = $i . ($m[1] == 'png' ? '.png' : '.jpg'); - - // move to final location - if (!rename($tmpPath.$result['newFilename'], $targetPath.$targetFile)) - return Util::toJSON(['error' => Lang::main('intError')]); - - // send success - return Util::toJSON(array( - 'success' => true, - 'id' => $i, - 'type' => $m[1] == 'png' ? 3 : 2, - 'name' => $this->_get['qqfile'] - )); - } - - return Util::toJSON(['error' => Lang::screenshot('error', 'unkFormat')]); - } - - return Util::toJSON($result); - } -} - -?> diff --git a/includes/ajaxHandler/getdescription.class.php b/includes/ajaxHandler/getdescription.class.php deleted file mode 100644 index cabf84d9..00000000 --- a/includes/ajaxHandler/getdescription.class.php +++ /dev/null @@ -1,37 +0,0 @@ - [FILTER_CALLBACK, ['options' => 'Aowow\AjaxHandler::checkTextBlob']] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$params || $params[0]) // should be empty - return; - - $this->handler = 'handleDescription'; - } - - protected function handleDescription() : string - { - $this->contentType = MIME_TYPE_TEXT; - - if (!User::canWriteGuide()) - return ''; - - $desc = Markup::stripTags($this->_post['description']); - - return Lang::trimTextClean($desc, 120); - } -} - -?> diff --git a/includes/ajaxHandler/guide.class.php b/includes/ajaxHandler/guide.class.php deleted file mode 100644 index eb425f15..00000000 --- a/includes/ajaxHandler/guide.class.php +++ /dev/null @@ -1,63 +0,0 @@ - [FILTER_SANITIZE_NUMBER_INT, null], - 'rating' => [FILTER_SANITIZE_NUMBER_INT, null] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params || count($this->params) != 1) - return; - - $this->contentType = MIME_TYPE_TEXT; - - // select handler - if ($this->params[0] == 'vote') - $this->handler = 'voteGuide'; - } - - protected function voteGuide() : string - { - if (!$this->_post['id'] || $this->_post['rating'] < 0 || $this->_post['rating'] > 5) - { - header('HTTP/1.0 404 Not Found', true, 404); - return ''; - } - else if (!User::canUpvote() || !User::canDownvote()) // same logic as comments? - { - header('HTTP/1.0 403 Forbidden', true, 403); - return ''; - } - // by id, not own, published - if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ?_guides WHERE `id` = ?d AND (`status` = ?d OR `rev` > 0)', $this->_post['id'], GUIDE_STATUS_APPROVED)) - { - if ($g['cuFlags'] & GUIDE_CU_NO_RATING || $g['userId'] == User::$id) - { - header('HTTP/1.0 403 Forbidden', true, 403); - return ''; - } - - if (!$this->_post['rating']) - DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_GUIDE, $this->_post['id'], User::$id); - else - DB::Aowow()->query('REPLACE INTO ?_user_ratings VALUES (?d, ?d, ?d, ?d)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']); - - $res = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS `t`, IFNULL(COUNT(*), 0) AS `n` FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_GUIDE, $this->_post['id']); - return Util::toJSON($res['n'] ? ['rating' => $res['t'] / $res['n'], 'nvotes' => $res['n']] : ['rating' => 0, 'nvotes' => 0]); - } - - return Util::toJSON(['rating' => 0, 'nvotes' => 0]); - } -} - -?> diff --git a/includes/components/guidemgr.class.php b/includes/components/guidemgr.class.php new file mode 100644 index 00000000..3b738404 --- /dev/null +++ b/includes/components/guidemgr.class.php @@ -0,0 +1,106 @@ + '#71D5FF', + self::STATUS_REVIEW => '#FFFF00', + self::STATUS_APPROVED => '#1EFF00', + self::STATUS_REJECTED => '#FF4040', + self::STATUS_ARCHIVED => '#FFD100' + ); + + private static array $ratingsStore = []; + private static ?int $imgUploadIdx = null; + + public static function createDescription(string $text) : string + { + return Lang::trimTextClean(Markup::stripTags($text), 120); + } + + public static function getRatings(array $guideIds) : array + { + if (!$guideIds) + return []; + + if (array_keys(self::$ratingsStore) == $guideIds) + return self::$ratingsStore; + + self::$ratingsStore = array_fill_keys($guideIds, ['nvotes' => 0, 'rating' => -1]); + + $ratings = DB::Aowow()->select('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1", IFNULL(MAX(IF(`userId` = ?d, `value`, 0)), 0) AS "2" FROM ?_user_ratings WHERE `type` = ?d AND `entry` IN (?a) GROUP BY `entry`', User::$id, RATING_GUIDE, $guideIds); + foreach ($ratings as $id => [$total, $count, $self]) + { + self::$ratingsStore[$id]['nvotes'] = (int)$count; + self::$ratingsStore[$id]['_self'] = (int)$self; + if ($count >= 5 ) + self::$ratingsStore[$id]['rating'] = $total / $count; + } + + return self::$ratingsStore; + } + + public static function handleUpload() : array + { + require_once('includes/libs/qqFileUploader.class.php'); + + $tmpFile = User::$username.'-'.Type::GUIDE.'-0-'.Util::createHash(16); + + $uploader = new \qqFileUploader(['jpg', 'jpeg', 'png'], 10 * 1024 * 1024); + $result = $uploader->handleUpload(self::IMG_TMP_DIR, $tmpFile, true); + + if (isset($result['error'])) + return $result; + + $mime = (new \finfo(FILEINFO_MIME))?->file(self::IMG_TMP_DIR . $result['newFilename']); + + if (!preg_match('/^image\/(png|jpe?g)/i', $mime, $m)) + return ['error' => Lang::screenshot('error', 'unkFormat')]; + + // find next empty image name (an int) + if (is_null(self::$imgUploadIdx)) + { + if ($files = scandir(self::IMG_DEST_DIR, SCANDIR_SORT_DESCENDING)) + if (rsort($files, SORT_NATURAL) && $files[0] != '.' && $files[0] != '..') + $i = explode('.', $files[0])[0] + 1; + + self::$imgUploadIdx = $i ?? 1; + } + + $targetFile = self::$imgUploadIdx . ($m[1] == 'png' ? '.png' : '.jpg'); + + // move to final location + if (!rename(self::IMG_TMP_DIR.$result['newFilename'], self::IMG_DEST_DIR.$targetFile)) + { + trigger_error('GuideMgr::handleUpload - failed to move file', E_USER_ERROR); + return ['error' => Lang::main('intError')]; + } + + return array( + 'success' => true, + 'id' => self::$imgUploadIdx, + 'type' => $m[1] == 'png' ? 3 : 2 + ); + } +} + +?> diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index c53c58b5..1923a192 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -27,7 +27,6 @@ class PageTemplate private array $pageData = []; // processed by display hooks // template data that needs further processing .. ! WARNING ! they will not get aut fetched from $context as they are already defined here - protected array $guideRating = []; private string $gStaticUrl; private string $gHost; private string $gServerTime; diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index 76fb8672..bef62121 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -71,13 +71,15 @@ trait TrGuideEditor public int $editClassId = 0; public int $editSpecId = 0; public int $editRev = 0; - public int $editStatus = GUIDE_STATUS_DRAFT; - public string $editStatusColor = GuideMgr::STATUS_COLORS[GUIDE_STATUS_DRAFT]; + public int $editStatus = GuideMgr::STATUS_DRAFT; + public string $editStatusColor = GuideMgr::STATUS_COLORS[GuideMgr::STATUS_DRAFT]; public string $editTitle = ''; public string $editName = ''; public string $editDescription = ''; public string $editText = ''; + public string $error = ''; public Locale $editLocale = Locale::EN; + public bool $isDraft = false; } class TemplateResponse extends BaseResponse diff --git a/includes/dbtypes/guide.class.php b/includes/dbtypes/guide.class.php index 5f262f55..492b47a0 100644 --- a/includes/dbtypes/guide.class.php +++ b/includes/dbtypes/guide.class.php @@ -10,14 +10,6 @@ class GuideList extends DBTypeList { use ListviewHelper; - public const /* array */ STATUS_COLORS = array( - GUIDE_STATUS_DRAFT => '#71D5FF', - GUIDE_STATUS_REVIEW => '#FFFF00', - GUIDE_STATUS_APPROVED => '#1EFF00', - GUIDE_STATUS_REJECTED => '#FF4040', - GUIDE_STATUS_ARCHIVED => '#FFD100' - ); - public static int $type = Type::GUIDE; public static string $brickFile = 'guide'; public static string $dataTable = '?_guides'; @@ -28,9 +20,10 @@ class GuideList extends DBTypeList protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ?_guides g'; protected array $queryOpts = array( - 'g' => [['a', 'c'], 'g' => 'g.`id`'], - 'a' => ['j' => ['?_account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], - 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'] + 'g' => [['a', 'c', 'ar'], 'g' => 'g.`id`'], + 'a' => ['j' => ['?_account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], + 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'], + 'ar' => ['j' => ['?_articles ar ON ar.`type` = 300 AND ar.`typeId` = g.`id`'], 's' => ', MAX(ar.`rev`) AS "latest"'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -40,23 +33,11 @@ class GuideList extends DBTypeList if ($this->error) return; - $ratings = DB::Aowow()->select('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS `t`, IFNULL(COUNT(*), 0) AS `n`, IFNULL(MAX(IF(`userId` = ?d, `value`, 0)), 0) AS `s` FROM ?_user_ratings WHERE `type` = ?d AND `entry` IN (?a)', User::$id, RATING_GUIDE, $this->getFoundIDs()); + $ratings = GuideMgr::getRatings($this->getFoundIDs()); // post processing foreach ($this->iterate() as $id => &$_curTpl) - { - if (isset($ratings[$id])) - { - $_curTpl['nvotes'] = $ratings[$id]['n']; - $_curTpl['rating'] = $ratings[$id]['n'] < 5 ? -1 : $ratings[$id]['t'] / $ratings[$id]['n']; - $_curTpl['_self'] = $ratings[$id]['s']; - } - else - { - $_curTpl['nvotes'] = 0; - $_curTpl['rating'] = -1; - } - } + $_curTpl = array_merge($_curTpl, $ratings[$id]); } public static function getName(int $id) : ?LocString @@ -133,13 +114,13 @@ class GuideList extends DBTypeList public function canBeViewed() : bool { // currently approved || has prev. approved version - return $this->getField('status') == GUIDE_STATUS_APPROVED || $this->getField('rev') > 0; + return $this->getField('status') == GuideMgr::STATUS_APPROVED || $this->getField('rev') > 0; } public function canBeReported() : bool { // not own guide && is not archived - return $this->getField('userId') != User::$id && $this->getField('status') != GUIDE_STATUS_ARCHIVED; + return $this->getField('userId') != User::$id && $this->getField('status') != GuideMgr::STATUS_ARCHIVED; } public function getJSGlobals(int $addMask = GLOBALINFO_ANY) : array diff --git a/includes/defines.php b/includes/defines.php index a9f440ef..1c423f5e 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -227,13 +227,6 @@ define('STR_ALLOW_SHORT', 0x4); define('RATING_COMMENT', 1); define('RATING_GUIDE', 2); -define('GUIDE_STATUS_NONE', 0); -define('GUIDE_STATUS_DRAFT', 1); -define('GUIDE_STATUS_REVIEW', 2); -define('GUIDE_STATUS_APPROVED', 3); -define('GUIDE_STATUS_REJECTED', 4); -define('GUIDE_STATUS_ARCHIVED', 5); - define('DEFAULT_ICON', 'inv_misc_questionmark'); define('MENU_IDX_ID', 0); // ID: A number or string; null makes the menu item a separator diff --git a/includes/libs/qqFileUploader.class.php b/includes/libs/qqFileUploader.class.php index 27b27ab8..dae92a58 100644 --- a/includes/libs/qqFileUploader.class.php +++ b/includes/libs/qqFileUploader.class.php @@ -132,7 +132,7 @@ class qqFileUploader private function toBytes(string $str) : int { - $val = trim($str); + $val = substr(trim($str), 0, -1); $last = strtolower(substr($str, -1, 1)); switch ($last) { diff --git a/includes/type.class.php b/includes/type.class.php index 189e7cea..822b31f1 100644 --- a/includes/type.class.php +++ b/includes/type.class.php @@ -111,7 +111,7 @@ abstract class Type self::CURRENCY => [CurrencyList::class, 'currency', 'g_gatheredcurrencies', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], self::SOUND => [SoundList::class, 'sound', 'g_sounds', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE], self::ICON => [IconList::class, 'icon', 'g_icons', self::FLAG_RANDOM_SEARCHABLE | self::FLAG_FILTRABLE | self::FLAG_DB_TYPE | self::FLAG_HAS_ICON], - self::GUIDE => [GuideList::class, 'guide', '', self::FLAG_NONE], + self::GUIDE => [GuideList::class, 'guide', '', self::FLAG_DB_TYPE], self::PROFILE => [ProfileList::class, 'profile', '', self::FLAG_FILTRABLE], // x - not known in javascript self::GUILD => [GuildList::class, 'guild', '', self::FLAG_FILTRABLE], // x self::ARENA_TEAM => [ArenaTeamList::class, 'arena-team', '', self::FLAG_FILTRABLE], // x diff --git a/includes/user.class.php b/includes/user.class.php index dc9fe853..46d11f6d 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -681,7 +681,7 @@ class User if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE)) return $result; - if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GUIDE_STATUS_ARCHIVED)) + if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GuideMgr::STATUS_ARCHIVED)) { // fix url array_walk($guides, fn(&$x) => $x['url'] = '?guide='.($x['url'] ?: $x['id'])); diff --git a/includes/utilities.php b/includes/utilities.php index 10d84995..b435c06b 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -75,8 +75,6 @@ abstract class Util public static $mapSelectorString = '%s (%d)'; - public static $guideratingString = " $(document).ready(function() {\n $('#guiderating').append(GetStars(%.10F, %s, %u, %u));\n });"; - public static $expansionString = [null, 'bc', 'wotlk']; public static $tcEncoding = '0zMcmVokRsaqbdrfwihuGINALpTjnyxtgevElBCDFHJKOPQSUWXYZ123456789'; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index c3e0007b..d934a107 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Meine Leitfäden", 'editTitle' => "Eigenen Leitfaden bearbeiten", 'newTitle' => "Leitfaden erstellen", - 'author' => "Autor", - 'spec' => "Spezialisierung", + 'author' => "Autor: ", + 'spec' => "Spezialisierung: ", 'sticky' => "Angeheftet", - 'views' => "Ansichten", + 'views' => "Ansichten: ", 'patch' => "Patch", - 'added' => "Hinzugefügt", - 'rating' => "Wertung", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Bewertungen) [span id=guiderating][/span]", + 'added' => "Hinzugefügt: ", + 'rating' => "Wertung: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Bewertungen) [span id=guiderating][/span]", 'noVotes' => "nicht genug Bewertungen [span id=guiderating][/span]", 'byAuthor' => "Von %s", 'notFound' => "Dieser Leitfaden existiert nicht.", 'clTitle' => 'Änderungsprotokoll für "%2$s"', - 'clStatusSet' => 'Status gesetzt auf %s', - 'clCreated' => 'Erstellt', + 'clStatusSet' => 'Status gesetzt auf %s: ', + 'clCreated' => 'Erstellt: ', 'clMinorEdit' => 'Kleinere Bearbeitung', 'editor' => array( 'fullTitle' => 'Ganze Überschrift', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Name', 'nameTip' => 'Dies sollte ein einfacher und klarer Name für den Leitfaden sein, der an Orten wie Menüs und Leitfadenlisten verwendet werden kann.', 'description' => 'Beschreibung', - 'descriptionTip' => 'Beschreibung, die für Suchmaschinen verwendet wird.<br /><br />Wenn leer, wird es automatisch generiert.', + 'descriptionTip' => 'Beschreibung, die für Suchmaschinen verwendet wird.

        Wenn leer, wird es automatisch generiert.', // 'commentEmail' => 'Emailbenachrichtigung', // 'commentEmailTip' => 'Soll der Autor darüber benachrichtigt werden, dass Nutzer diesen Guide kommentieren?', 'changelog' => 'Änderungsprotokoll für diese Änderung', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Sehen Sie, wie Ihr Leitfaden aussehen wird', 'images' => 'Bilder', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Ihr Leitfaden ist im "Entwurfs"-Status und Sie sind der einzige der ihn sehen kann. Bearbeiten Sie ihn so lange Sie wollen und wenn Sie fertig sind reichen Sie ihn zur Überprüfung ein.', - GUIDE_STATUS_REVIEW => 'Ihr Leitfaden wird überprüft.', - GUIDE_STATUS_APPROVED => 'Ihr Leitfaden wurde veröffentlicht.', - GUIDE_STATUS_REJECTED => 'Ihr Leitfaden wurde abgewiesen. Nachdem die Mängel behoben wurde kann er erneut zur Überprüfung eingereicht werden.', - GUIDE_STATUS_ARCHIVED => 'Ihr Leitfaden ist veraltet und wurde archiviert. Er wird nicht mehr in der Übersicht gelistet und ist kann nicht mehr bearbeitet werden.]', + GuideMgr::STATUS_DRAFT => 'Ihr Leitfaden ist im "Entwurfs"-Status und Sie sind der einzige der ihn sehen kann. Bearbeiten Sie ihn so lange Sie wollen und wenn Sie fertig sind reichen Sie ihn zur Überprüfung ein.', + GuideMgr::STATUS_REVIEW => 'Ihr Leitfaden wird überprüft.', + GuideMgr::STATUS_APPROVED => 'Ihr Leitfaden wurde veröffentlicht.', + GuideMgr::STATUS_REJECTED => 'Ihr Leitfaden wurde abgewiesen. Nachdem die Mängel behoben wurde kann er erneut zur Überprüfung eingereicht werden.', + GuideMgr::STATUS_ARCHIVED => 'Ihr Leitfaden ist veraltet und wurde archiviert. Er wird nicht mehr in der Übersicht gelistet und ist kann nicht mehr bearbeitet werden.]', ) ), 'category' => array( diff --git a/localization/locale_enus.php b/localization/locale_enus.php index e4a7fe76..bc8d7ff0 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "My Guides", 'editTitle' => "Edit your Guide", 'newTitle' => "Create New Guide", - 'author' => "Author", - 'spec' => "Specialization", + 'author' => "Author: ", + 'spec' => "Specialization: ", 'sticky' => "Sticky Status", - 'views' => "Views", + 'views' => "Views: ", 'patch' => "Patch", - 'added' => "Added", - 'rating' => "Rating", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] votes) [span id=guiderating][/span]", + 'added' => "Added: ", + 'rating' => "Rating: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] votes) [span id=guiderating][/span]", 'noVotes' => "not enough votes [span id=guiderating][/span]", 'byAuthor' => "By %s", 'notFound' => "This guide doesn't exist.", 'clTitle' => 'Changelog For "%2$s"', - 'clStatusSet' => 'Status set to %s', - 'clCreated' => 'Created', + 'clStatusSet' => 'Status set to %s: ', + 'clCreated' => 'Created: ', 'clMinorEdit' => 'Minor Edit', 'editor' => array( 'fullTitle' => 'Full Title', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Name', 'nameTip' => 'This should be a simple and clear name of what the guide is, for use in places like menus and guide lists.', 'description' => 'Description', - 'descriptionTip' => 'Description that will be used for search engines.<br><br>If left empty, it will be generated automatically.', + 'descriptionTip' => "Description that will be used for search engines.

        If left empty, it will be generated automatically.", // 'commentEmail' => 'Comment Emails', // 'commentEmailTip' => 'Should the author get emailed whenever a user comments on this guide?', 'changelog' => 'Changelog For This Edit', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'See how your guide will look', 'images' => 'Images', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Your guide is in "Draft" status and you are the only one able to see it. Keep editing it as long as you like, and when you feel it's ready submit it for review.', - GUIDE_STATUS_REVIEW => 'Your guide is being reviewed.', - GUIDE_STATUS_APPROVED => 'Your guide has been published.', - GUIDE_STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', - GUIDE_STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + GuideMgr::STATUS_DRAFT => 'Your guide is in "Draft" status and you are the only one able to see it. Keep editing it as long as you like, and when you feel it's ready submit it for review.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', ) ), 'category' => array( diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 95a75707..1b8ac56b 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Mis Guías", 'editTitle' => "Editar tu Guía", 'newTitle' => "Crear Nueva Guía", - 'author' => "Autor", - 'spec' => "Especialización", + 'author' => "Autor: ", + 'spec' => "Especialización: ", 'sticky' => "Estado Fijo", - 'views' => "Visto", + 'views' => "Visto: ", 'patch' => "Parche", - 'added' => "Añadido", - 'rating' => "Valoración", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votos) [span id=guiderating][/span]", + 'added' => "Añadido: ", + 'rating' => "Valoración: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votos) [span id=guiderating][/span]", 'noVotes' => "necesita más votaciones [span id=guiderating][/span]", 'byAuthor' => "Por %s", 'notFound' => "Este/a guía no existe.", 'clTitle' => 'Historial de cambios para "%2$s"', - 'clStatusSet' => 'Estado cambiado a %s', - 'clCreated' => 'Creado', + 'clStatusSet' => 'Estado cambiado a %s: ', + 'clCreated' => 'Creado: ', 'clMinorEdit' => 'Modificación menor', 'editor' => array( 'fullTitle' => 'Título completo', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Nombre', 'nameTip' => 'Este debe ser un nombre simple y claro de lo que es la guía, para usar en menús y listas de guías.', 'description' => 'Descripción', - 'descriptionTip' => 'Descripción utilizada para los motores de búsqueda.<br /><br />Si se deja vacío, se generará automáticamente.', + 'descriptionTip' => 'Descripción utilizada para los motores de búsqueda.

        Si se deja vacío, se generará automáticamente.', // 'commentEmail' => 'Enviar comentarios por email', // 'commentEmailTip' => '¿El autor debería ser notificado por correo cuando un usuario escriba comentarios en esta guía?', 'changelog' => 'Historial de cambios para esta modificación', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Mira el aspecto de tu guía.', 'images' => 'Imágenes', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Tu guía está en estado "borrador" y solo tú puedes verla. Tienes todo el tiempo del mundo para editarla y, cuando creas que ya está lista, envíala para su revisión.', - GUIDE_STATUS_REVIEW => 'Tu guía está siendo revisada.', - GUIDE_STATUS_APPROVED => 'Tu guía ha sido publicada.', - GUIDE_STATUS_REJECTED => 'Tu guía ha sido rechazada. Una vez que se hayan corregido las deficiencias, puedes volver a enviarla para revisión.', - GUIDE_STATUS_ARCHIVED => 'Tu guía está desactualizada y ha sido archivada. Ya no aparecerá en la lista y no se puede editar.', + GuideMgr::STATUS_DRAFT => 'Tu guía está en estado "borrador" y solo tú puedes verla. Tienes todo el tiempo del mundo para editarla y, cuando creas que ya está lista, envíala para su revisión.', + GuideMgr::STATUS_REVIEW => 'Tu guía está siendo revisada.', + GuideMgr::STATUS_APPROVED => 'Tu guía ha sido publicada.', + GuideMgr::STATUS_REJECTED => 'Tu guía ha sido rechazada. Una vez que se hayan corregido las deficiencias, puedes volver a enviarla para revisión.', + GuideMgr::STATUS_ARCHIVED => 'Tu guía está desactualizada y ha sido archivada. Ya no aparecerá en la lista y no se puede editar.', ) ), 'category' => array( diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 7123dde1..8d94d2c5 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Mes guides", 'editTitle' => "Editez votre Guide", 'newTitle' => "Créer un nouveau Guide", - 'author' => "Auteur", - 'spec' => "Spécialisation", + 'author' => "Auteur : ", + 'spec' => "Spécialisation : ", 'sticky' => "Statut coller", - 'views' => "Vues", + 'views' => "Vues : ", 'patch' => "Patch", - 'added' => "Ajouté", - 'rating' => "Note", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votes) [span id=guiderating][/span]", + 'added' => "Ajouté : ", + 'rating' => "Note : ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] Votes) [span id=guiderating][/span]", 'noVotes' => "pas assez de votes [span id=guiderating][/span]", 'byAuthor' => "Par %s", 'notFound' => "Ce guide n'existe pas.", 'clTitle' => 'Journal des changements pour "%2$s"', - 'clStatusSet' => 'Statut défini comme %s', - 'clCreated' => 'Créé', + 'clStatusSet' => 'Statut défini comme %s : ', + 'clCreated' => 'Créé : ', 'clMinorEdit' => 'Modification mineure', 'editor' => array( 'fullTitle' => 'Titre complet', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'Nom', 'nameTip' => 'Ceci devrait être un nom clair et concis de ce en quoi consiste le guide, qui sera affiché dans les menus et listes de guides.', 'description' => 'Description', - 'descriptionTip' => 'Description qui sera utilisée par les moteurs de recherche.<br /><br />S'il est laissé vide, le résumé sera généré automatiquement.', + 'descriptionTip' => 'Description qui sera utilisée par les moteurs de recherche.

        S'il est laissé vide, le résumé sera généré automatiquement.', // 'commentEmail' => 'Recevoir les commentaires par courriel', // 'commentEmailTip' => 'L'auteur doit-il recevoir un courriel chaque fois qu'un utilisateur commente ce guide ?', 'changelog' => 'Journal des changements pour cette modification', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'Ayez un aperçu de votre guide', 'images' => 'Images', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'Votre guide est en statut "Brouillon" et vous êtes le seul à pouvoir le lire. Continuez de l'écrire comme vous le voulez, et quand vous sentez qu'il est prêt, soumettez-le pour approbation.', - GUIDE_STATUS_REVIEW => 'Your guide is being reviewed.', - GUIDE_STATUS_APPROVED => 'Your guide has been published.', - GUIDE_STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', - GUIDE_STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + GuideMgr::STATUS_DRAFT => 'Votre guide est en statut "Brouillon" et vous êtes le seul à pouvoir le lire. Continuez de l'écrire comme vous le voulez, et quand vous sentez qu'il est prêt, soumettez-le pour approbation.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', ) ), 'category' => array( diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 6e0de9a1..7304a86d 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "Мои руководÑтва", 'editTitle' => "Редактировать руководÑтво", 'newTitle' => "ÐапиÑать новое руководÑтво", - 'author' => "Ðвтор", - 'spec' => "Спек", + 'author' => "Ðвтор: ", + 'spec' => "Спек: ", 'sticky' => "Закрепленный", - 'views' => "ПроÑмотры", + 'views' => "ПроÑмотры: ", 'patch' => "Обновление", - 'added' => "Добавлено", - 'rating' => "Рейтинг", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] проголоÑовало) [span id=guiderating][/span]", + 'added' => "Добавлено: ", + 'rating' => "Рейтинг: ", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] проголоÑовало) [span id=guiderating][/span]", 'noVotes' => "недоÑтаточно голоÑов [span id=guiderating][/span]", 'byAuthor' => "От %s", 'notFound' => "Такого руководÑтво не ÑущеÑтвует.", 'clTitle' => 'ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹ «%2$s»', - 'clStatusSet' => 'ПриÑвоен ÑÑ‚Ð°Ñ‚ÑƒÑ Â«%s»', - 'clCreated' => 'Создано', + 'clStatusSet' => 'ПриÑвоен ÑÑ‚Ð°Ñ‚ÑƒÑ Â«%s»: ', + 'clCreated' => 'Создано: ', 'clMinorEdit' => 'Ðебольшое изменение', 'editor' => array( 'fullTitle' => 'Полный заголовок', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'ИмÑ', 'nameTip' => 'Укажите краткое и понÑтное название руководÑтва. Оно будет иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÑŒÑ Ð² меню и перечнÑÑ… руководÑтв.', 'description' => 'ОпиÑание', - 'descriptionTip' => 'ОпиÑание Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñковых ÑиÑтем.<br><br>ЕÑли поле будет оÑтавлено пуÑтым, то Ñайт Ñгенерирует опиÑание автоматичеÑки.', + 'descriptionTip' => 'ОпиÑание Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñковых ÑиÑтем.

        ЕÑли поле будет оÑтавлено пуÑтым, то Ñайт Ñгенерирует опиÑание автоматичеÑки.', // 'commentEmail' => 'E-mail уведомлениÑ', // 'commentEmailTip' => 'Должен ли автор руководÑтва получать e-mail оповещениÑ, когда к руководÑтву оÑтавлÑÑŽÑ‚ комментарий?', 'changelog' => 'ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹, внеÑенных Ñтой правкой', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => 'ПоÑмотрите, как будет выглÑдеть руководÑтво', 'images' => 'Images', 'statusTip' => array( - GUIDE_STATUS_DRAFT => 'РуководÑтво Ñохранено как "Черновик" — видеть его можете только вы. Правьте руководÑтво так долго, как Ñочтете нужным, а когда решите, что оно готово — отправьте на одобрение.', - GUIDE_STATUS_REVIEW => 'Your guide is being reviewed.', - GUIDE_STATUS_APPROVED => 'Your guide has been published.', - GUIDE_STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', - GUIDE_STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', + GuideMgr::STATUS_DRAFT => 'РуководÑтво Ñохранено как "Черновик" — видеть его можете только вы. Правьте руководÑтво так долго, как Ñочтете нужным, а когда решите, что оно готово — отправьте на одобрение.', + GuideMgr::STATUS_REVIEW => 'Your guide is being reviewed.', + GuideMgr::STATUS_APPROVED => 'Your guide has been published.', + GuideMgr::STATUS_REJECTED => 'Your guide has been rejected. After it\'s shortcomings have been remedied you may resubmit it for review.', + GuideMgr::STATUS_ARCHIVED => 'Your guide is outdated and has been archived. Is will no longer be listed and can\'t be edited.', ) ), 'category' => array( diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 42e090cb..2ebf767e 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -167,20 +167,20 @@ $lang = array( 'myGuides' => "我的指å—", 'editTitle' => "编辑你的指å—", 'newTitle' => "创建新指å—", - 'author' => "作者", - 'spec' => "专精", + 'author' => "作者:", + 'spec' => "专精:", 'sticky' => "置顶状æ€", - 'views' => "æµè§ˆé‡", + 'views' => "æµè§ˆé‡ï¼š", 'patch' => "è¡¥ä¸", - 'added' => "已添加", - 'rating' => "评分", - 'votes' => "[span id=guiderating-value]%d[/span]/5 ([span id=guiderating-votes][n5=%d][/span] 投票)[span id=guiderating][/span]", + 'added' => "已添加:", + 'rating' => "评分:", + 'votes' => "[span id=guiderating-value]%.2g[/span]/5 ([span id=guiderating-votes][n5=%d][/span] 投票)[span id=guiderating][/span]", 'noVotes' => "投票数é‡ä¸è¶³ [span id=guiderating][/span]", 'byAuthor' => "æ¥è‡ª %s", 'notFound' => "该指å—ä¸å­˜åœ¨ã€‚", 'clTitle' => '修改日志 "%2$s"', - 'clStatusSet' => '状æ€å·²è®¾ç½®ä¸º %s', - 'clCreated' => '已创建', + 'clStatusSet' => '状æ€å·²è®¾ç½®ä¸º %s:', + 'clCreated' => '已创建:', 'clMinorEdit' => 'å°ä¿®æ”¹', 'editor' => array( 'fullTitle' => '完整标题', @@ -188,7 +188,7 @@ $lang = array( 'name' => 'åç§°', 'nameTip' => 'è¿™åº”è¯¥æ˜¯ä¸€ä¸ªç®€å•æ˜Žäº†çš„æŒ‡å—å称,用于èœå•和指å—列表', 'description' => 'æè¿°', - 'descriptionTip' => 'æè¿°å°†ç”¨äºŽè¯´æ˜Žç‰‡æ®µ<br /><br />如果ä¸å¡«ï¼Œåˆ™è‡ªåŠ¨ç”Ÿæˆã€‚', + 'descriptionTip' => 'æè¿°å°†ç”¨äºŽè¯´æ˜Žç‰‡æ®µ

        如果ä¸å¡«ï¼Œåˆ™è‡ªåŠ¨ç”Ÿæˆã€‚', // 'commentEmail' => '评论电å­é‚®ä»¶', // 'commentEmailTip' => '当用户对此指å—å‘è¡¨è¯„è®ºæ—¶ï¼Œä½œè€…æ˜¯å¦æ”¶åˆ°ç”µå­é‚®ä»¶é€šçŸ¥ï¼Ÿ', 'changelog' => '当å‰ç¼–辑的修改日志', @@ -203,11 +203,11 @@ $lang = array( 'testGuide' => '自我æµè§ˆä½ çš„æŒ‡å—', 'images' => '图片', 'statusTip' => array( - GUIDE_STATUS_DRAFT => '你的指å—ç›®å‰æ˜¯è‰ç¨¿çжæ€ï¼Œåªæœ‰ä½ è‡ªå·±å¯è§ã€‚试ç€è¾“入更多的文字,当你觉得å¯ä»¥äº†çš„æ—¶å€™å°±æäº¤é€å®¡å§ã€‚', - GUIDE_STATUS_REVIEW => 'ä½ çš„æŒ‡å—æ­£åœ¨å®¡æ ¸ä¸­', - GUIDE_STATUS_APPROVED => '你的指å—å·²å‘布', - GUIDE_STATUS_REJECTED => '你的指å—已被拒ç»ã€‚在修正问题åŽï¼Œä½ å¯ä»¥é‡æ–°æäº¤å®¡æ ¸ã€‚', - GUIDE_STATUS_ARCHIVED => '你的指å—已过时,并已归档。它将ä¸å†åˆ—出,也无法编辑。', + GuideMgr::STATUS_DRAFT => '你的指å—ç›®å‰æ˜¯è‰ç¨¿çжæ€ï¼Œåªæœ‰ä½ è‡ªå·±å¯è§ã€‚试ç€è¾“入更多的文字,当你觉得å¯ä»¥äº†çš„æ—¶å€™å°±æäº¤é€å®¡å§ã€‚', + GuideMgr::STATUS_REVIEW => 'ä½ çš„æŒ‡å—æ­£åœ¨å®¡æ ¸ä¸­', + GuideMgr::STATUS_APPROVED => '你的指å—å·²å‘布', + GuideMgr::STATUS_REJECTED => '你的指å—已被拒ç»ã€‚在修正问题åŽï¼Œä½ å¯ä»¥é‡æ–°æäº¤å®¡æ ¸ã€‚', + GuideMgr::STATUS_ARCHIVED => '你的指å—已过时,并已归档。它将ä¸å†åˆ—出,也无法编辑。', ) ), 'category' => array( diff --git a/pages/admin.php b/pages/admin.php index a69209a0..6709c008 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -225,7 +225,7 @@ class AdminPage extends GenericPage private function handleGuideApprove() : void { - $pending = new GuideList([['status', GUIDE_STATUS_REVIEW]]); + $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); if ($pending->error) $data = []; else diff --git a/pages/guide.php b/pages/guide.php deleted file mode 100644 index a371694f..00000000 --- a/pages/guide.php +++ /dev/null @@ -1,549 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'rev' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'] - ); - - protected /* array */ $_post = array( - 'save' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], - 'submit' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkEmptySet'], - 'title' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'name' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'description' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GuidePage::checkDescription'], - 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'], - 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextBlob'], - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'category' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'specId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'classId' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'] - ); - - public function __construct($pageCall, $pageParam) - { - $guide = explode( "&", $pageParam, 2); - - parent::__construct($pageCall, $pageParam); - - if (isset($guide[1]) && preg_match(self::VALID_URL, $guide[1])) - $this->extra = $guide[1]; - - - /**********************/ - /* get mode + guideId */ - /**********************/ - - if (Util::checkNumeric($guide[0], NUM_CAST_INT)) - $this->typeId = $guide[0]; - else if (preg_match(self::VALID_URL, $guide[0])) - { - switch ($guide[0]) - { - case 'changelog': - if (!$this->_get['id']) - break; - - $this->show = self::SHOW_CHANGELOG; - $this->tpl = 'text-page-generic'; - $this->article = false; // do not include article from db - - // main container should be tagged:
        - // why is this here: is there a mediawiki like diff function for staff? - $this->addScript([SC_CSS_STRING, 'li input[type="radio"] {margin:0}']); - - $this->typeId = $this->_get['id']; // just to display sensible not-found msg - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `id` = ?d', $this->typeId)) - $this->typeId = intVal($id); - - break; - case 'new': - if (User::canWriteGuide()) - { - $this->show = self::SHOW_NEW; - $this->guideRevision = null; - - $this->initNew(); - return; // do not create new GuideList - } - break; - case 'edit': - if (User::canWriteGuide()) - { - if (!$this->initEdit()) - $this->notFound(Lang::game('guide'), Lang::guide('notFound')); - - $this->show = self::SHOW_EDITOR; - } - break; - default: - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($guide[0]))) - { - $this->typeId = intVal($id); - $this->guideRevision = null; - $this->articleUrl = Util::lower($guide[0]); - } - } - } - - - /*********************/ - /* load actual guide */ - /*********************/ - - $this->subject = new GuideList(array(['id', $this->typeId])); - if ($this->subject->error) - $this->notFound(Lang::game('guide'), Lang::guide('notFound')); - - if (!$this->subject->canBeViewed() && !$this->subject->userCanView()) - header('Location: ?guides='.$this->subject->getField('category'), true, 302); - - if ($this->show == self::SHOW_GUIDE && $this->_get['rev'] !== null && !$this->articleUrl && $this->subject->userCanView()) - $this->guideRevision = $this->_get['rev']; - else if ($this->show == self::SHOW_GUIDE && !$this->articleUrl) - $this->guideRevision = $this->subject->getField('rev'); - else - $this->guideRevision = null; - - if (!$this->name) - $this->name = $this->subject->getField('name'); - } - - protected function generateContent() : void - { - match ($this->show) - { - self::SHOW_NEW => $this->displayNew(), - self::SHOW_EDITOR => $this->displayEditor(), - self::SHOW_GUIDE => $this->displayGuide(), - self::SHOW_CHANGELOG => $this->displayChangelog(), - default => trigger_error('GuidePage::generateContent - what content!?') - }; - } - - private function displayNew() : void - { - // init required template vars - $this->editorFields = array( - 'locale' => Lang::getLocale()->value, - 'status' => GUIDE_STATUS_DRAFT - ); - } - - private function displayEditor() : void - { - // can't check in init as subject is unknown - if ($this->subject->getField('status') == GUIDE_STATUS_ARCHIVED) - $this->notFound(Lang::game('guide'), Lang::guide('notFound')); - - $status = GUIDE_STATUS_NONE; - $rev = DB::Aowow()->selectCell('SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1', Type::GUIDE, $this->typeId); - $curStatus = DB::Aowow()->selectCell('SELECT `status` FROM ?_guides WHERE `id` = ?d ', $this->typeId); - if ($rev === null) - $rev = 0; - - if ($this->save) - { - $rev++; - - // insert Article - DB::Aowow()->query('INSERT INTO ?_articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (?d, ?d, ?d, ?d, ?d, ?)', - Type::GUIDE, $this->typeId, $this->_post['locale'], $rev, User::$groups & U_GROUP_STAFF ? User::$groups : User::$groups | U_GROUP_BLOGGER, $this->_post['body']); - - // link to Guide - $guideData = array( - 'category' => $this->_post['category'], - 'classId' => $this->_post['classId'], - 'specId' => $this->_post['specId'], - 'title' => $this->_post['title'], - 'name' => $this->_post['name'], - 'description' => $this->_post['description'] ?: Lang::trimTextClean(Markup::stripTags($this->_post['body']), 120), - 'locale' => $this->_post['locale'], - 'roles' => User::$groups, - 'status' => GUIDE_STATUS_DRAFT - ); - - DB::Aowow()->query('UPDATE ?_guides SET ?a WHERE `id` = ?d', $guideData, $this->typeId); - - // new guide -> reload editor - if ($this->_get['id'] === 0) - header('Location: ?guide=edit&id='.$this->typeId, true, 302); - else - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?d, ?)', $this->typeId, $rev, time(), User::$id, $this->_post['changelog']); - - if ($this->_post['submit']) - { - $status = GUIDE_STATUS_REVIEW; - if ($curStatus != GUIDE_STATUS_REVIEW) - { - DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', GUIDE_STATUS_REVIEW, $this->typeId); - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $this->typeId, time(), User::$id, GUIDE_STATUS_REVIEW); - } - } - } - - // init required template vars - $this->editorFields = array( - 'category' => $this->_post['category'] ?? $this->subject->getField('category'), - 'title' => $this->_post['title'] ?? $this->subject->getField('title'), - 'name' => $this->_post['name'] ?? $this->subject->getField('name'), - 'description' => $this->_post['description'] ?? $this->subject->getField('description'), - 'text' => $this->_post['body'] ?? $this->subject->getArticle(), - 'status' => $status ?: $this->subject->getField('status'), - 'classId' => $this->_post['classId'] ?? $this->subject->getField('classId'), - 'specId' => $this->_post['specId'] ?? $this->subject->getField('specId'), - 'locale' => $this->_post['locale'] ?? $this->subject->getField('locale'), - 'rev' => $rev - ); - - $this->extendGlobalData($this->subject->getJSGlobals()); - } - - private function displayGuide() : void - { - if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_QUICKFACTS)) - { - $qf = []; - if ($this->subject->getField('cuFlags') & CC_FLAG_STICKY) - $qf[] = '[span class=guide-sticky]'.Lang::guide('sticky').'[/span]'; - - $qf[] = Lang::guide('author').Lang::main('colon').'[url=?user='.$this->subject->getField('author').']'.$this->subject->getField('author').'[/url]'; - - if ($this->subject->getField('category') == 1) - { - $c = $this->subject->getField('classId'); - $s = $this->subject->getField('specId'); - if ($c > 0) - { - $this->extendGlobalIds(Type::CHR_CLASS, $c); - $qf[] = Util::ucFirst(Lang::game('class')).Lang::main('colon').'[class='.$c.']'; - } - if ($s > -1) - $qf[] = Lang::guide('spec').Lang::main('colon').'[icon class="c'.$c.' icontiny" name='.Game::$specIconStrings[$c][$s].']'.Lang::game('classSpecs', $c, $s).'[/icon]'; - } - - // $qf[] = Lang::guide('patch').Lang::main('colon').'3.3.5'; // replace with date - $qf[] = Lang::guide('added').Lang::main('colon').'[tooltip name=added]'.date('l, G:i:s', $this->subject->getField('date')).'[/tooltip][span class=tip tooltip=added]'.date(Lang::main('dateFmtShort'), $this->subject->getField('date')).'[/span]'; - - switch ($this->subject->getField('status')) - { - case GUIDE_STATUS_APPROVED: - $qf[] = Lang::guide('views').Lang::main('colon').'[n5='.$this->subject->getField('views').']'; - - if (!($this->subject->getField('cuFlags') & GUIDE_CU_NO_RATING)) - { - $this->guideRating = array( - $this->subject->getField('rating'), // avg rating - User::canUpvote() && User::canDownvote() ? 'true' : 'false', - $this->subject->getField('_self'), // my rating amt; 0 = no vote - $this->typeId // guide Id - ); - - if ($this->subject->getField('nvotes') < 5) - $qf[] = Lang::guide('rating').Lang::main('colon').Lang::guide('noVotes'); - else - $qf[] = Lang::guide('rating').Lang::main('colon').Lang::guide('votes', [round($this->subject->getField('rating'), 1), $this->subject->getField('nvotes')]); - } - break; - case GUIDE_STATUS_ARCHIVED: - $qf[] = Lang::guide('status', GUIDE_STATUS_ARCHIVED); - break; - } - - $qf = '[ul][li]'.implode('[/li][li]', $qf).'[/li][/ul]'; - - if ($this->subject->getField('status') == GUIDE_STATUS_REVIEW && User::isInGroup(U_GROUP_STAFF) && $this->_get['rev']) - { - $this->addScript([SC_JS_STRING, ' - DomContentLoaded.addEvent(function() { - let send = function (status) - { - let message = ""; - let id = $WH.g_getGets().guide; - if (status == 4) // rejected - { - while (message === "") - message = prompt("Please provide your reasoning."); - - if (message === null) - return false; - } - - $.ajax({cache: false, url: "?admin=guide", type: "POST", - error: function() { - alert("Operation failed."); - }, - success: function(json) { - if (json != 1) - alert("Operation failed."); - else - window.location.href = "?admin=guides"; - }, - data: { id: id, status: status, msg: message } - }) - - return true; - }; - - $WH.ge("btn-accept").onclick = send.bind(null, 3); - $WH.ge("btn-reject").onclick = send.bind(null, 4); - }); - ']); - - $qf .= '[h3 style="text-align:center"]Admin[/h3]'; - - $qf .= '[div style="text-align:center"][url=# id="btn-accept" class=icon-tick]Approve[/url][url=# style="margin-left:20px" id="btn-reject" class=icon-delete]Reject[/url][/div]'; - } - } - - $this->redButtons[BUTTON_GUIDE_LOG] = true; - $this->redButtons[BUTTON_GUIDE_REPORT] = $this->subject->canBeReported(); - - $this->infobox = $qf ?? ''; - $this->author = $this->subject->getField('author'); // add to g_pageInfo in GenericPage:prepareContent() - - if ($this->subject->userCanView()) - $this->redButtons[BUTTON_GUIDE_EDIT] = User::canWriteGuide() && $this->subject->getField('status') != GUIDE_STATUS_ARCHIVED; - - // the article text itself is added by GenericPage::addArticle() - } - - private function displayChangelog() : void - { - $this->addScript([SC_JS_STRING, ' - $(document).ready(function() { - var radios = $("input[type=radio]"); - function limit(col, val) { - radios.each(function(i, e) { - if (col == e.name) - return; - - if (col == "b") - e.disabled = (val <= parseInt(e.value)); - else if (col == "a") - e.disabled = (val >= parseInt(e.value)); - }); - - }; - - radios.each(function (i, e) { - e.onchange = limit.bind(this, e.name, parseInt(e.value)); - - if (i < 2 && e.name == "b") // first pair - $(e).trigger("click"); - else if (e.value == 0 && e.name == "a") // last pair - $(e).trigger("click"); - }); - }); - ']); - - $buff = '
          '; - $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) ? ($rev !== null ? '' : '') : ''; - - $logEntries = DB::Aowow()->select('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->typeId); - foreach ($logEntries as $log) - { - if ($log['status'] != GUIDE_STATUS_NONE) - $buff .= '
        • '.$inp($log['rev']).Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).Lang::main('colon').''.Util::formatTimeDiff($log['date'])."
        • \n"; - else if ($log['msg']) - $buff .= '
        • '.$inp($log['rev']).Util::formatTimeDiff($log['date']).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
        • \n"; - else - $buff .= '
        • '.$inp($log['rev']).Util::formatTimeDiff($log['date']).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
        • \n"; - } - - // append creation - $buff .= '
        • '.$inp(0).''.Lang::guide('clCreated').Lang::main('colon').''.Util::formatTimeDiff($this->subject->getField('date'))."
        • \n
        \n"; - - - if (User::isInGroup(U_GROUP_STAFF)) - $buff .= ''; - - $this->name = lang::guide('clTitle', [$this->typeId, $this->subject->getField('title')]); - $this->extraHTML = $buff; - } - - private function initNew() : void - { - $this->addScript( - [SC_JS_FILE, 'js/article-description.js'], - [SC_JS_FILE, 'js/article-editing.js'], - [SC_JS_FILE, 'js/guide-editing.js'], - [SC_JS_FILE, 'js/fileuploader.js'], - [SC_JS_FILE, 'js/toolbar.js'], - [SC_JS_FILE, 'js/AdjacentPreview.js'], - [SC_CSS_FILE, 'css/article-editing.css'], - [SC_CSS_FILE, 'css/fileuploader.css'], - [SC_CSS_FILE, 'css/guide-edit.css'], - [SC_CSS_FILE, 'css/AdjacentPreview.css'], - - [SC_CSS_STRING, '#upload-result input[type=text] { padding: 0px 2px; font-size: 12px; }'], - [SC_CSS_STRING, '#upload-result > span { display:block; height: 22px; }'], - [SC_CSS_STRING, '#upload-result { display: inline-block; text-align:right; }'], - [SC_CSS_STRING, '#upload-progress { display: inline-block; margin-right:8px; }'] - ); - - $this->articleUrl = 'new'; - $this->tpl = 'guide-edit'; - $this->name = Lang::guide('newTitle'); - - Lang::sort('guide', 'category'); - - $this->typeId = 0; // signals 'edit' to create new guide - } - - private function initEdit() : bool - { - $this->addScript( - [SC_JS_FILE, 'js/article-description.js'], - [SC_JS_FILE, 'js/article-editing.js'], - [SC_JS_FILE, 'js/guide-editing.js'], - [SC_JS_FILE, 'js/fileuploader.js'], - [SC_JS_FILE, 'js/toolbar.js'], - [SC_JS_FILE, 'js/AdjacentPreview.js'], - [SC_CSS_FILE, 'css/article-editing.css'], - [SC_CSS_FILE, 'css/fileuploader.css'], - [SC_CSS_FILE, 'css/guide-edit.css'], - [SC_CSS_FILE, 'css/AdjacentPreview.css'], - - [SC_CSS_STRING, '#upload-result input[type=text] { padding: 0px 2px; font-size: 12px; }'], - [SC_CSS_STRING, '#upload-result > span { display:block; height: 22px; }'], - [SC_CSS_STRING, '#upload-result { display: inline-block; text-align:right; }'], - [SC_CSS_STRING, '#upload-progress { display: inline-block; margin-right:8px; }'] - ); - - $this->articleUrl = 'edit'; - $this->tpl = 'guide-edit'; - $this->name = Lang::guide('editTitle'); - $this->save = $this->_post['save'] || $this->_post['submit']; - - // reject inconsistent guide data - if ($this->save) - { - // req: set data - if (!$this->_post['title'] || !$this->_post['name'] || !$this->_post['body'] || $this->_post['locale'] === null) - return false; - - // req: valid data - if (!in_array($this->_post['category'], $this->validCats) || !(Cfg::get('LOCALES') & (1 << $this->_post['locale']))) - return false; - - // sanitize: spec / class - if ($this->_post['category'] == 1) // Classes - { - if ($this->_post['classId'] && !ChrClass::tryFrom($this->_post['classId'])) - $this->_post['classId'] = 0; - - if (!in_array($this->_post['specId'], [-1, 0, 1, 2])) - $this->_post['specId'] = -1; - if ($this->_post['specId'] > -1 && !$this->_post['classId']) - $this->_post['specId'] = -1; - } - else - { - $this->_post['classId'] = 0; - $this->_post['specId'] = -1; - } - } - - if ($this->_get['id']) // edit existing guide - { - $this->typeId = $this->_get['id']; // just to display sensible not-found msg - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `id` = ?d AND `status` <> ?d {AND `userId` = ?d}', $this->typeId, GUIDE_STATUS_ARCHIVED, User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : User::$id)) - $this->typeId = intVal($id); - } - else if ($this->_get['id'] === 0) // create new guide and load in editor - $this->typeId = DB::Aowow()->query('INSERT INTO ?_guides (`userId`, `date`, `status`) VALUES (?d, ?d, ?d)', User::$id, time(), GUIDE_STATUS_DRAFT); - - return $this->typeId > 0; - } - - protected function editorFields(string $field, bool $asInt = false) : string|int - { - return $this->editorFields[$field] ?? ($asInt ? 0 : ''); - } - - protected function generateTooltip() - { - $power = new \StdClass(); - if (!$this->subject->error) - { - $power->{'name_'.Lang::getLocale()->json()} = strip_tags($this->name); - $power->{'tooltip_'.Lang::getLocale()->json()} = $this->subject->renderTooltip(); - } - - return sprintf($this->powerTpl, Util::toJSON($this->articleUrl ?: $this->typeId), Lang::getLocale()->value, Util::toJSON($power, JSON_AOWOW_POWER)); - } - - protected function generatePath() : void - { - if ($x = $this->subject?->getField('category')) - $this->path[] = $x; - } - - protected function generateTitle() : void - { - if ($this->show == self::SHOW_EDITOR) - array_unshift($this->title, Lang::guide('editTitle').Lang::main('colon').$this->subject->getField('title'), Lang::game('guides')); - if ($this->show == self::SHOW_NEW) - array_unshift($this->title, Lang::guide('newTitle'), Lang::game('guides')); - else - array_unshift($this->title, $this->subject->getField('title'), Lang::game('guides')); - } - - protected function postCache() : void - { - // increment views of published guide; ignore caching - if ($this->subject?->getField('status') == GUIDE_STATUS_APPROVED) - DB::Aowow()->query('UPDATE ?_guides SET `views` = `views` + 1 WHERE `id` = ?d', $this->typeId); - } - - protected static function checkDescription(string $str) : string - { - // run checkTextBlob and also replace \n => \s and \s+ => \s - $str = preg_replace(parent::PATTERN_TEXT_BLOB, '', $str); - - $str = strtr($str, ["\n" => ' ', "\r" => ' ']); - - return preg_replace('/\s+/', ' ', trim($str)); - } -} - -?> diff --git a/pages/guides.php b/pages/guides.php deleted file mode 100644 index a2194a6c..00000000 --- a/pages/guides.php +++ /dev/null @@ -1,102 +0,0 @@ -getCategoryFromUrl($pageParam); - - parent::__construct($pageCall, $pageParam); - - if ($pageCall == 'my-guides') - { - if (!User::isLoggedIn()) - $this->error(); - - $this->name = Util::ucFirst(Lang::guide('myGuides')); - $this->myGuides = true; - } - else - $this->name = Util::ucFirst(Lang::game('guides')); - } - - protected function generateContent() - { - $hCols = ['patch']; // pointless: display date instead - $vCols = []; - $xCols = ['$Listview.extraCols.date']; // ok - - if ($this->myGuides) - { - $conditions = [['userId', User::$id]]; - $hCols[] = 'author'; - $vCols[] = 'status'; - } - else - { - $conditions = array( - ['locale', Lang::getLocale()->value], - ['status', GUIDE_STATUS_ARCHIVED, '!'], // never archived guides - [ - 'OR', - ['status', GUIDE_STATUS_APPROVED], // currently approved - ['rev', 0, '>'] // has previously approved revision - ] - ); - if (isset($this->category[0])) - $conditions[] = ['category', $this->category]; - } - - $data = []; - $guides = new GuideList($conditions); - if (!$guides->error) - $data = array_values($guides->getListviewData()); - - $tabData = array( - 'data' => $data, - 'name' => Util::ucFirst(Lang::game('guides')), - 'hiddenCols' => $hCols, - 'visibleCols' => $vCols, - 'extraCols' => $xCols - ); - - $this->lvTabs[] = [GuideList::$brickFile, $tabData]; - - $this->redButtons = [BUTTON_GUIDE_NEW => User::canWriteGuide()]; - } - - protected function generateTitle() - { - array_unshift($this->title, $this->name); - if (isset($this->category[0])) - array_unshift($this->title, Lang::guide('category', $this->category[0])); - - } - - protected function generatePath() - { - if (isset($this->category[0])) - $this->path[] = $this->category[0]; - } -} - -?> diff --git a/static/js/global.js b/static/js/global.js index 453f644e..3c8741aa 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -22987,17 +22987,14 @@ function g_modifyUrl(url, params, opt) { } function g_enhanceTextarea (ta, opt) { - if (!(ta instanceof jQuery)) { + if (!(ta instanceof jQuery)) ta = $(ta); - } - if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") { + if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") return; - } - if (typeof opt != "object") { + if (typeof opt != "object") opt = {}; - } var canResize = (function(el) { if (!el.dynamicResizeOption) @@ -23016,63 +23013,66 @@ function g_enhanceTextarea (ta, opt) { var wrapper = $("
        ", { "class": "enhanced-textarea-wrapper" }).insertBefore(ta).append(ta); if (!opt.hasOwnProperty("color")) - wrapper.addClass("enhanced-textarea-dark") + wrapper.addClass("enhanced-textarea-dark"); else if (opt.color) - wrapper.addClass("enhanced-textarea-" + opt.color) + wrapper.addClass("enhanced-textarea-" + opt.color); if (!opt.hasOwnProperty("dynamicSizing") || opt.dynamicSizing || opt.dynamicResizeOption) { var expander = $("
        ", { "class": "enhanced-textarea-expander" }).prependTo(wrapper); - var n = function(E, D, F) { - if (!F()) + var dynamicResize = function(textarea, exactHeight, canResizeFn) { + if (!canResizeFn()) return; - // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
        ") + "
        ").height() + (D ? 14 : 34) + "px") - E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()) + "
        ").height() + (D ? 14 : 34) + "px") + // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
        ") + "
        ").height() + (D ? 14 : 34) + "px"); + textarea.css("height", textarea.siblings(".enhanced-textarea-expander").html($WH.htmlentities(textarea.val()) + "
        ").height() + (exactHeight ? 14 : 34) + "px"); }; - ta.bind("keydown keyup change", n.bind(this, ta, opt.exactLineHeights, canResize)); - n(ta, opt.exactLineHeights, canResize); - var setWidth = function(D) { - D.css("width", D.parent().width() + "px") - }; + ta.bind("keydown keyup change", dynamicResize.bind(this, ta, opt.exactLineHeights, canResize)); + dynamicResize(ta, opt.exactLineHeights, canResize); + + var setWidth = function(el) { el.css("width", el.parent().width() + "px"); }; + setWidth(expander); setTimeout(setWidth.bind(null, expander), 1); - if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) { - wrapper.addClass("enhanced-textarea-dynamic-sizing") - } - } - if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) { - wrapper.addClass("enhanced-textarea-focus-changes") + + if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) + wrapper.addClass("enhanced-textarea-dynamic-sizing"); } + + if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) + wrapper.addClass("enhanced-textarea-focus-changes"); + if (opt.markup) { - var w = $("
        ", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); - var y = $("
        ", { "class": "enhanced-textarea-markup" }).appendTo(w); - var z = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(y); - var k = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(y); + var _markupMenu = $("
        ", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); + var _segments = $("
        ", { "class": "enhanced-textarea-markup" }).appendTo(_markupMenu); + var _toolbar = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _menu = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); if (opt.markup == "inline") - ar_AddInlineToolbar(ta.get(0), z.get(0), k.get(0)); + ar_AddInlineToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); else - ar_AddToolbar(ta.get(0), z.get(0), k.get(0)); + ar_AddToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); if (opt.dynamicResizeOption) { - var t = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(y); - var C = $("
        new configuration
        ' . $head . $rows . '
        '); + } + } + + private function buildRow(string $key, string $value, int $flags, ?string $default, string $comment) : string + { + $buff = ''; + $info = explode(' - ', $comment); + $key = $flags & Cfg::FLAG_PHP ? strtolower($key) : strtoupper($key); + + // name + if (!empty($info[0])) + $buff .= ''.sprintf(Util::$dfnString, $info[0], $key).''; + else + $buff .= ''.$key.''; + + // value + if ($flags & Cfg::FLAG_TYPE_BOOL) + $buff .= '
        '; + else if ($flags & Cfg::FLAG_OPT_LIST && !empty($info[1])) + { + $buff .= ''; + } + else if ($flags & Cfg::FLAG_BITMASK && !empty($info[1])) + { + $buff .= '
        '; + foreach (explode(', ', $info[1]) as $option) + { + [$idx, $name] = explode(':', $option); + $buff .= ''; + } + $buff .= '
        '; + } + else + $buff .= ''; + + // actions + $buff .= ''; + + $buff .= ''; + + if ($default) + $buff .= '|'; + else + $buff .= '|'; + + if (!($flags & Cfg::FLAG_PERSISTENT)) + $buff .= '|'; + + $buff .= ''; + + return $buff; + } +} + +?> diff --git a/endpoints/admin/siteconfig_add.php b/endpoints/admin/siteconfig_add.php new file mode 100644 index 00000000..a99e77a2 --- /dev/null +++ b/endpoints/admin/siteconfig_add.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertGET('key', 'val')) + { + trigger_error('AdminSiteconfigActionAddResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $key = trim($this->_get['key']); + $val = trim(urldecode($this->_get['val'])); + + $this->result = Cfg::add($key, $val); + } +} + +?> diff --git a/endpoints/admin/siteconfig_remove.php b/endpoints/admin/siteconfig_remove.php new file mode 100644 index 00000000..cef906d0 --- /dev/null +++ b/endpoints/admin/siteconfig_remove.php @@ -0,0 +1,30 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]] + ); + + protected function generate() : void + { + if (!$this->assertGET('key')) + { + trigger_error('AdminSiteconfigActionRemoveResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $this->result = Cfg::delete($this->_get['key']); + } +} + +?> diff --git a/endpoints/admin/siteconfig_update.php b/endpoints/admin/siteconfig_update.php new file mode 100644 index 00000000..5afe0bec --- /dev/null +++ b/endpoints/admin/siteconfig_update.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'val' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertGET('key', 'val')) + { + trigger_error('AdminSiteconfigActionUpdateResponse - malformed request received', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $key = trim($this->_get['key']); + $val = trim(urldecode($this->_get['val'])); + + $this->result = Cfg::set($key, $val); + } +} + +?> diff --git a/includes/cfg.class.php b/includes/cfg.class.php index 2ddffd10..138e541d 100644 --- a/includes/cfg.class.php +++ b/includes/cfg.class.php @@ -7,8 +7,8 @@ if (!defined('AOWOW_REVISION')) class Cfg { - public const PATTERN_CONF_KEY = '/[a-z0-9_\.\-]/i'; - public const PATTERN_INV_CONF_KEY = '/[^a-z0-9_\.\-]/i'; + public const PATTERN_CONF_KEY_CHAR = '/[a-z0-9_\.\-]/i'; + public const PATTERN_CONF_KEY_FULL = '/^[a-z0-9_\.\-]+$/i'; public const PATTERN_INVALID_CHARS = '/\p{C}/ui'; // config flags @@ -116,7 +116,7 @@ class Cfg $key = strtolower($key); - if (preg_match(self::PATTERN_INV_CONF_KEY, $key)) + if (!preg_match(self::PATTERN_CONF_KEY_FULL, $key)) return 'invalid chars in option name: [a-z 0-9 _ . -] are allowed'; if (isset(self::$store[$key])) @@ -129,7 +129,7 @@ class Cfg return 'this configuration option cannot be set'; $flags = self::FLAG_TYPE_STRING | self::FLAG_PHP; - if (!DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, ?d, ?d)', $key, $value, self::CAT_MISCELLANEOUS, $flags)) + if (!is_int(DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, ?d, ?d)', $key, $value, self::CAT_MISCELLANEOUS, $flags))) return 'internal error'; self::$store[$key] = [$value, $flags, self::CAT_MISCELLANEOUS, null, null]; @@ -349,7 +349,7 @@ class Cfg } if ($flags & self::FLAG_TYPE_BOOL) - $value = (bool)$value; + $value = $value ? 1 : 0; return ''; } @@ -384,7 +384,17 @@ class Cfg trigger_error($msg, E_USER_ERROR); } - private static function locales(/*int|string*/ $value, ?string &$msg = '') : bool + private static function useSSL() : bool + { + return (($_SERVER['HTTPS'] ?? 'off') != 'off') || (self::$store['force_ssl'][self::IDX_VALUE] ?? 0); + } + + + /***************************/ + /* onSet/onLoad validators */ + /***************************/ + + private static function locales(int|string $value, ?string &$msg = '') : bool { if (!CLI) return true; @@ -397,7 +407,7 @@ class Cfg return false; } - private static function acc_auth_mode(/*int|string*/ $value, ?string &$msg = '') : bool + private static function acc_auth_mode(int|string $value, ?string &$msg = '') : bool { if ($value == 1 && !extension_loaded('gmp')) { @@ -408,7 +418,7 @@ class Cfg return true; } - private static function profiler_enable(/*int|string*/ $value, ?string &$msg = '') : bool + private static function profiler_enable(int|string $value, ?string &$msg = '') : bool { if ($value != 1) return true; @@ -416,7 +426,7 @@ class Cfg return Profiler::queueStart($msg); } - private static function static_host(/*int|string*/ $value, ?string &$msg = '') : bool + private static function static_host(int|string $value, ?string &$msg = '') : bool { self::$store['static_url'] = array( // points js to images & scripts (self::useSSL() ? 'https://' : 'http://').$value, @@ -429,7 +439,7 @@ class Cfg return true; } - private static function site_host(/*int|string*/ $value, ?string &$msg = '') : bool + private static function site_host(int|string $value, ?string &$msg = '') : bool { self::$store['host_url'] = array( // points js to executable files (self::useSSL() ? 'https://' : 'http://').$value, @@ -442,9 +452,15 @@ class Cfg return true; } - private static function useSSL() : bool + private static function cache_mode(int|string $value, ?string &$msg = '') : bool { - return (($_SERVER['HTTPS'] ?? 'off') != 'off') || (self::$store['force_ssl'][self::IDX_VALUE] ?? 0); + if ($value & 0x2 && !class_exists('\Memcached')) + { + $msg .= 'PHP extension Memcached is not enabled.'; + return false; + } + + return true; } private static function screenshot_min_size(int|string $value, ?string &$msg = '') : bool diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 2be3beb7..3bc02c3b 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -217,6 +217,12 @@ trait TrCache private function memcached() : ?\Memcached { + if (!class_exists('\Memcached')) + { + trigger_error('Memcached is enabled by us but not in php!', E_USER_ERROR); + return null; + } + if (!$this->memcached && (Cfg::get('CACHE_MODE') & CACHE_MODE_MEMCACHED)) { $this->memcached = new \Memcached(); diff --git a/setup/tools/clisetup/siteconfig.us.php b/setup/tools/clisetup/siteconfig.us.php index 17dcf0c8..405061b7 100644 --- a/setup/tools/clisetup/siteconfig.us.php +++ b/setup/tools/clisetup/siteconfig.us.php @@ -114,7 +114,7 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); } - if (CLI::read(['idx' => ['', false, false, Cfg::PATTERN_CONF_KEY]], $uiIndex) && $uiIndex && $uiIndex['idx'] !== '') + if (CLI::read(['idx' => ['', false, false, Cfg::PATTERN_CONF_KEY_CHAR]], $uiIndex) && $uiIndex && $uiIndex['idx'] !== '') { $idx = array_search(strtolower($uiIndex['idx']), $cfgList); if ($idx === false) @@ -147,7 +147,7 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); $setting = array( - 'key' => ['option name', false, false, Cfg::PATTERN_CONF_KEY], + 'key' => ['option name', false, false, Cfg::PATTERN_CONF_KEY_CHAR], 'val' => ['value'] ); if (CLI::read($setting, $uiSetting) && $uiSetting) @@ -443,7 +443,13 @@ CLISetup::registerUtility(new class extends UtilityScript private function testCase(&$protocol, &$host, $testFile, &$status) : bool { - $res = get_headers($protocol.$host.$testFile, true); + // https://stackoverflow.com/questions/14279095/allow-self-signed-certificates-for-https-wrapper + $ctx = stream_context_create(array( + 'ssl' => ['verify_peer' => false, + 'allow_self_signed' => true] + )); + + $res = get_headers($protocol.$host.$testFile, true, $ctx); if (!preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) return false; diff --git a/setup/updates/1758578400_10.sql b/setup/updates/1758578400_10.sql new file mode 100644 index 00000000..5e7cd68d --- /dev/null +++ b/setup/updates/1758578400_10.sql @@ -0,0 +1,2 @@ +-- set on_set_fn check +UPDATE `aowow_config` SET `flags` = `flags` | 1024 WHERE `key` = 'cache_mode'; diff --git a/template/pages/admin/siteconfig.tpl.php b/template/pages/admin/siteconfig.tpl.php index ed8a9238..70324333 100644 --- a/template/pages/admin/siteconfig.tpl.php +++ b/template/pages/admin/siteconfig.tpl.php @@ -1,6 +1,8 @@ - +brick('header'); ?> + $this->brick('header'); +?> \n\n"; + + parent::generate(); + } +} + +?> diff --git a/endpoints/admin/weight-presets_save.php b/endpoints/admin/weight-presets_save.php new file mode 100644 index 00000000..50038a54 --- /dev/null +++ b/endpoints/admin/weight-presets_save.php @@ -0,0 +1,74 @@ + ['filter' => FILTER_VALIDATE_INT ], + '__icon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Cfg::PATTERN_CONF_KEY_FULL]], + 'scale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkScale'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', '__icon', 'scale')) + { + trigger_error('AdminWeightpresetsActionSaveResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + // save to db + DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $this->_post['id']); + DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']); + + foreach (explode(',', $this->_post['scale']) as $s) + { + [$k, $v] = explode(':', $s); + + if (!in_array($k, Util::$weightScales) || $v < 1) + continue; + + if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null) + { + trigger_error('AdminWeightpresetsActionSaveResponse - failed to write to database', E_USER_ERROR); + $this->result = self::ERR_WRITE_DB; + return; + } + } + + // write dataset + exec('php aowow --build=weightPresets', $out); + foreach ($out as $o) + if (strstr($o, 'ERR')) + { + trigger_error('AdminWeightpresetsActionSaveResponse - failed to write dataset' . $o, E_USER_ERROR); + $this->result = self::ERR_WRITE_FILE; + return; + } + + // all done + $this->result = self::ERR_NONE; + } + + protected static function checkScale(string $val) : string + { + if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) + return $val; + + return ''; + } +} + +?> diff --git a/includes/ajaxHandler/admin.class.php b/includes/ajaxHandler/admin.class.php deleted file mode 100644 index 1b2b6f95..00000000 --- a/includes/ajaxHandler/admin.class.php +++ /dev/null @@ -1,258 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], - 'key' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ], - 'all' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkEmptySet' ], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkUser' ], - 'val' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'guid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'area' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'floor' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ] - ); - protected $_post = array( - 'alt' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'], - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'scale' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkScale' ], - '__icon' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxAdmin::checkKey' ], - 'status' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt' ], - 'msg' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob'] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params) - return; - - if ($this->params[0] == 'siteconfig' && $this->_get['action']) - { - if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) - return; - - if ($this->_get['action'] == 'add') - $this->handler = 'confAdd'; - else if ($this->_get['action'] == 'remove') - $this->handler = 'confRemove'; - else if ($this->_get['action'] == 'update') - $this->handler = 'confUpdate'; - } - else if ($this->params[0] == 'weight-presets' && $this->_get['action']) - { - if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_BUREAU)) - return; - - if ($this->_get['action'] == 'save') - $this->handler = 'wtSave'; - } - else if ($this->params[0] == 'spawn-override') - { - if (!User::isInGroup(U_GROUP_MODERATOR)) - return; - - $this->handler = 'spawnPosFix'; - } - else if ($this->params[0] == 'comment') - { - if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD)) - return; - - $this->handler = 'commentOutOfDate'; - } - } - - protected function confAdd() : string - { - $key = trim($this->_get['key']); - $val = trim(urldecode($this->_get['val'])); - - return Cfg::add($key, $val); - } - - protected function confRemove() : string - { - if (!$this->reqGET('key')) - return 'invalid configuration option given'; - - return Cfg::delete($this->_get['key']); - } - - protected function confUpdate() : string - { - $key = trim($this->_get['key']); - $val = trim(urldecode($this->_get['val'])); - - return Cfg::set($key, $val); - } - - protected function wtSave() : string - { - if (!$this->reqPOST('id', '__icon')) - return '3'; - - // save to db - DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE id = ?d', $this->_post['id']); - DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']); - - foreach (explode(',', $this->_post['scale']) as $s) - { - [$k, $v] = explode(':', $s); - - if (!in_array($k, Util::$weightScales) || $v < 1) - continue; - - if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null) - return '1'; - } - - // write dataset - exec('php aowow --build=weightPresets', $out); - foreach ($out as $o) - if (strstr($o, 'ERR')) - return '2'; - - // all done - return '0'; - } - - protected function spawnPosFix() : string - { - if (!$this->reqGET('type', 'guid', 'area', 'floor')) - return '-4'; - - $guid = $this->_get['guid']; - $type = $this->_get['type']; - $area = $this->_get['area']; - $floor = $this->_get['floor']; - - if (!in_array($type, [Type::NPC, Type::OBJECT, Type::SOUND, Type::AREATRIGGER, Type::ZONE])) - return '-3'; - - DB::Aowow()->query('REPLACE INTO ?_spawns_override VALUES (?d, ?d, ?d, ?d, ?d)', $type, $guid, $area, $floor, AOWOW_REVISION); - - if ($wPos = WorldPosition::getForGUID($type, $guid)) - { - if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $wPos[$guid]['posX'], $wPos[$guid]['posY'], $area, $floor)) - { - $updGUIDs = [$guid]; - $newPos = array( - 'posX' => $point[0]['posX'], - 'posY' => $point[0]['posY'], - 'areaId' => $point[0]['areaId'], - 'floor' => $point[0]['floor'] - ); - - // if creature try for waypoints - if ($type == Type::NPC) - { - $jobs = array( - 'SELECT -w.id AS `entry`, w.point AS `pointId`, w.position_x AS `posX`, w.position_y AS `posY` FROM creature_addon ca JOIN waypoint_data w ON w.id = ca.path_id WHERE ca.guid = ?d AND ca.path_id <> 0', - 'SELECT `entry`, `pointId`, `location_x` AS `posX`, `location_y` AS `posY` FROM `script_waypoint` WHERE `entry` = ?d', - 'SELECT `entry`, `pointId`, `position_x` AS `posX`, `position_y` AS `posY` FROM `waypoints` WHERE `entry` = ?d' - ); - - foreach ($jobs as $idx => $job) - { - if ($swp = DB::World()->select($job, $idx ? $wPos[$guid]['id'] : $guid)) - { - foreach ($swp as $w) - { - if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor)) - { - $p = array( - 'posX' => $point[0]['posX'], - 'posY' => $point[0]['posY'], - 'areaId' => $point[0]['areaId'], - 'floor' => $point[0]['floor'] - ); - - DB::Aowow()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']); - } - } - } - } - - // also move linked vehicle accessories (on the very same position) - $updGUIDs = array_merge($updGUIDs, DB::Aowow()->selectCol('SELECT s2.guid FROM ?_spawns s1 JOIN ?_spawns s2 ON s1.posX = s2.posX AND s1.posY = s2.posY AND - s1.areaId = s2.areaId AND s1.floor = s2.floor AND s2.guid < 0 WHERE s1.guid = ?d', $guid)); - } - - DB::Aowow()->query('UPDATE ?_spawns SET ?a WHERE `type` = ?d AND `guid` IN (?a)', $newPos, $type, $updGUIDs); - - return '1'; - } - - return '-2'; - } - - return '-1'; - } - - protected function commentOutOfDate() : string - { - $ok = false; - switch ($this->_post['status']) - { - case 0: // up to date - if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id'])) - if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) - $rep->close(Report::STATUS_CLOSED_WONTFIX); - break; - case 1: // outdated, mark as deleted and clear other flags (sticky + outdated) - if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = ?d, `deleteUserId` = ?d, `deleteDate` = ?d WHERE `id` = ?d', CC_FLAG_DELETED, User::$id, time(), $this->_post['id'])) - if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) - $rep->close(Report::STATUS_CLOSED_SOLVED); - break; - default: - trigger_error('AjaxHandler::comentOutOfDate - called with invalid status'); - } - - return $ok ? '1' : '0'; - } - - - /***************************/ - /* additional input filter */ - /***************************/ - - protected static function checkKey(string $val) : string - { - // expecting string - if (preg_match(Cfg::PATTERN_INV_CONF_KEY, $val)) - return ''; - - return strtolower($val); - } - - protected static function checkUser($val) : string - { - $n = Util::lower(trim(urldecode($val))); - - if (User::isValidName($n)) - return $n; - - return ''; - } - - protected static function checkScale($val) : string - { - if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) - return $val; - - return ''; - } -} - -?> diff --git a/pages/admin.php b/pages/admin.php deleted file mode 100644 index 6709c008..00000000 --- a/pages/admin.php +++ /dev/null @@ -1,322 +0,0 @@ - ['filter' => FILTER_UNSAFE_RAW], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkInt'], - 'user' => ['filter' => FILTER_CALLBACK, 'options' => 'urldecode'] - ); - - private $generator = ''; - - public function __construct($pageCall, $pageParam) - { - switch ($pageParam) - { - case 'phpinfo': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV; - $this->generator = 'handlePhpInfo'; - $this->tpl = 'list-page-generic'; - - array_push($this->path, 2, 21); - $this->name = 'PHP Information'; - break; - case 'siteconfig': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV; - $this->generator = 'handleConfig'; - $this->tpl = 'admin/siteconfig'; - - array_push($this->path, 2, 18); - $this->name = 'Site Configuration'; - break; - case 'weight-presets': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV | U_GROUP_BUREAU; - $this->generator = 'handleWeightPresets'; - $this->tpl = 'admin/weight-presets'; - - array_push($this->path, 2, 16); - $this->name = 'Weight Presets'; - break; - case 'guides': - $this->reqUGroup = U_GROUP_STAFF; - $this->generator = 'handleGuideApprove'; - $this->tpl = 'list-page-generic'; - - array_push($this->path, 1, 25); - $this->name = 'Pending Guides'; - break; - case 'out-of-date': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_MOD; - $this->generator = 'handleOutOfDate'; - $this->tpl = 'list-page-generic'; - - array_push($this->path, 1, 23); - $this->name = 'Out of Date Comments'; - break; - case 'reports': - $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_EDITOR | U_GROUP_MOD | U_GROUP_LOCALIZER | U_GROUP_SCREENSHOT | U_GROUP_VIDEO; - $this->generator = 'handleReports'; - $this->tpl = 'admin/reports'; - - array_push($this->path, 5); - $this->name = 'Reports'; - break; - default: // error out through unset template - } - - parent::__construct($pageCall, $pageParam); - } - - protected function generateContent() : void - { - if (!$this->generator || function_exists($this->generator)) - return; - - $this->{$this->generator}(); - } - - private function handleConfig() : void - { - $this->addScript( - [SC_CSS_STRING, '.grid input[type=\'text\'], .grid input[type=\'number\'] { width:250px; text-align:left; }'], - [SC_CSS_STRING, '.grid input[type=\'button\'] { width:65px; padding:2px; }'], - [SC_CSS_STRING, '.grid a.tip { margin:0px 5px; opacity:0.8; }'], - [SC_CSS_STRING, '.grid a.tip:hover { opacity:1; }'], - [SC_CSS_STRING, '.grid tr { height:30px; }'], - [SC_CSS_STRING, '.grid .disabled { opacity:0.4 !important; }'], - [SC_CSS_STRING, '.grid .status { position:absolute; right:5px; }'] - ); - - $head = 'KeyValueOptions'; - foreach (Cfg::$categories as $idx => $catName) - { - $rows = ''; - foreach (Cfg::forCategory($idx) as $key => [$value, $flags, , $default, $comment]) - $rows .= $this->configAddRow($key, $value, $flags, $default, $comment); - - if ($idx == Cfg::CAT_MISCELLANEOUS) - $rows .= 'new configuration'; - - if (!$rows) - continue; - - $this->lvTabs[] = [null, array( - 'data' => '' . $head . $rows . '
        ', - 'name' => $catName, - 'id' => Profiler::urlize($catName) - )]; - } - } - - private function handlePhpInfo() : void - { - $this->addScript([ - SC_CSS_STRING, "\npre {margin: 0px; font-family: monospace;}\n" . - "td, th { border: 1px solid #000000; vertical-align: baseline;}\n" . - ".p {text-align: left;}\n" . - ".e {background-color: #ccccff; font-weight: bold; color: #000000;}\n" . - ".h {background-color: #9999cc; font-weight: bold; color: #000000;}\n" . - ".v {background-color: #cccccc; color: #000000;}\n" . - ".vr {background-color: #cccccc; text-align: right; color: #000000;}\n" - ]); - - $bits = [INFO_GENERAL, INFO_CONFIGURATION, INFO_ENVIRONMENT, INFO_MODULES]; - $names = ['General', '', '', 'Module']; - foreach ($bits as $i => $b) - { - ob_start(); - phpinfo($b); - $buff = ob_get_contents(); - ob_end_clean(); - - $buff = explode('
        ', $buff)[1]; - $buff = explode('
        ', $buff); - array_pop($buff); // remove last from stack - $buff = implode('
        ', $buff); // sew it together - - if (strpos($buff, '

        ')) - $buff = explode('

        ', $buff)[1]; - - if (strpos($buff, '

        ')) - { - $parts = explode('

        ', $buff); - foreach ($parts as $p) - { - if (!preg_match('/\w/i', $p)) - continue; - - $p = explode('

        ', $p); - - $body = substr($p[1], 0, -7); // remove trailing "
        \n" - $name = $names[$i] ? $names[$i].': ' : ''; - if (preg_match('/]*>([\w\s\d]+)<\/a>/i', $p[0], $m)) - $name .= $m[1]; - else - $name .= $p[0]; - - $this->lvTabs[] = [null, array( - 'data' => $body, - 'id' => strtolower(strtr($name, [' ' => ''])), - 'name' => $name - )]; - } - } - else - { - $this->lvTabs[] = [null, array( - 'data' => $buff, - 'id' => strtolower($names[$i]), - 'name' => $names[$i] - )]; - } - } - } - - private function handleWeightPresets() : void - { - $this->addScript( - [SC_JS_FILE, 'js/filters.js'], - [SC_CSS_STRING, '.wt-edit {display:inline-block; vertical-align:top; width:350px;}'] - ); - - $head = $body = ''; - - $scales = DB::Aowow()->select('SELECT `class` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `name`, `icon` FROM ?_account_weightscales WHERE `userId` = 0 ORDER BY `class`, `orderIdx` ASC'); - $weights = DB::Aowow()->selectCol('SELECT awd.`id` AS ARRAY_KEY, awd.`field` AS ARRAY_KEY2, awd.`val` FROM ?_account_weightscale_data awd JOIN ?_account_weightscales ad ON awd.`id` = ad.`id` WHERE ad.`userId` = 0'); - foreach ($scales as $cl => $data) - { - $ul = ''; - foreach ($data as $id => $s) - { - $weights[$id]['__icon'] = $s['icon']; - $ul .= '[url=# onclick="loadScale.bind(this, '.$id.')();"]'.$s['name'].'[/url][br]'; - } - - $head .= '[td=header]'.Lang::game('cl', $cl).'[/td]'; - $body .= '[td valign=top]'.$ul.'[/td]'; - } - - $this->extraText = '[table class=grid][tr]'.$head.'[/tr][tr]'.$body.'[/tr][/table]'; - - $this->extraHTML = '\n\n"; - } - - private function handleGuideApprove() : void - { - $pending = new GuideList([['status', GuideMgr::STATUS_REVIEW]]); - if ($pending->error) - $data = []; - else - { - $data = $pending->getListviewData(); - $latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ?_articles WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs()); - foreach ($latest as $id => $rev) - $data[$id]['rev'] = $rev; - } - - $this->lvTabs[] = [GuideList::$brickFile, array( - 'data' => array_values($data), - 'hiddenCols' => ['patch', 'comments', 'views', 'rating'], - 'extraCols' => '$_' - ), 'guideAdminCol']; - } - - private function handleOutOfDate() : void - { - $data = CommunityContent::getCommentPreviews(['flags' => CC_FLAG_OUTDATED]); - - $this->lvTabs[] = ['commentpreview', array( - 'data' => $data, - 'extraCols' => '$_' - ), 'commentAdminCol']; - } - - private function handleReports() : void - { - // todo: handle reports listing - // - } - - private function configAddRow($key, $value, $flags, $default, $comment) - { - $buff = ''; - $info = explode(' - ', $comment); - $key = $flags & Cfg::FLAG_PHP ? strtolower($key) : strtoupper($key); - - // name - if (!empty($info[0])) - $buff .= ''.sprintf(Util::$dfnString, $info[0], $key).''; - else - $buff .= ''.$key.''; - - // value - if ($flags & Cfg::FLAG_TYPE_BOOL) - $buff .= '
        '; - else if ($flags & Cfg::FLAG_OPT_LIST && !empty($info[1])) - { - $buff .= ''; - } - else if ($flags & Cfg::FLAG_BITMASK && !empty($info[1])) - { - $buff .= '
        '; - foreach (explode(', ', $info[1]) as $option) - { - [$idx, $name] = explode(':', $option); - $buff .= ''; - } - $buff .= '
        '; - } - else - $buff .= ''; - - // actions - $buff .= ''; - - $buff .= ''; - - if (isset($default)) - $buff .= '|'; - else - $buff .= '|'; - - if (!($flags & Cfg::FLAG_PERSISTENT)) - $buff .= '|'; - - $buff .= ''; - - return $buff; - } - - protected function generateTitle() {} - protected function generatePath() {} -} - -?> diff --git a/template/pages/admin/reports.tpl.php b/template/pages/admin/reports.tpl.php index 1731d07c..d35577e2 100644 --- a/template/pages/admin/reports.tpl.php +++ b/template/pages/admin/reports.tpl.php @@ -1,6 +1,8 @@ - +brick('header'); ?> + $this->brick('header'); +?> @@ -15,39 +17,15 @@ $this->brick('announcement'); $this->brick('pageTemplate'); ?>
        -

        name;?>

        +

        h1;?>

        brick('article'); + $this->brick('markup', ['markup' => $this->article]); - if (isset($this->extraText)): + $this->brick('markup', ['markup' => $this->extraText]); + + echo $this->extraHTML ?? ''; ?> -
        - - -
        -extraHTML)): - echo $this->extraHTML; - endif; -?> -

        Edit

        -
        -
        Icon
        -
        -
        -
        -
        Scale
        -
        -
        -
        diff --git a/template/pages/admin/weight-presets.tpl.php b/template/pages/admin/weight-presets.tpl.php index e383307d..68a25ea1 100644 --- a/template/pages/admin/weight-presets.tpl.php +++ b/template/pages/admin/weight-presets.tpl.php @@ -1,6 +1,8 @@ - +brick('header'); ?> + $this->brick('header'); +?> + $this->brick('markup', ['markup' => $this->extraText]); -
        -extraHTML)): - echo $this->extraHTML; - endif; + echo $this->extraHTML ?? ''; ?>

        Edit

        From 155bf1e4a37ac6e0f35d6ed16e633250f0a5efef Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:33:03 +0200 Subject: [PATCH 0981/1249] Template/Update (Part 46 - I) * account management rework: Base * create proper account settings page - modelviewer preferences - show ids in lists - announcement purge - public description * fix broken FKs between aowow_user_ratings and aowow_account --- endpoints/account/account.php | 168 +++++++ endpoints/account/signin.php | 4 +- .../account/update-community-settings.php | 48 ++ endpoints/account/update-general-settings.php | 60 +++ .../response/baseresponse.class.php | 4 +- includes/defines.php | 12 +- includes/user.class.php | 34 +- includes/utilities.php | 11 +- localization/locale_dede.php | 131 ++++- localization/locale_enus.php | 133 ++++- localization/locale_eses.php | 129 ++++- localization/locale_frfr.php | 131 ++++- localization/locale_ruru.php | 149 ++++-- localization/locale_zhcn.php | 133 ++++- pages/account.php | 465 ------------------ setup/updates/1758578400_11.sql | 3 + setup/updates/1758578400_12.sql | 11 + static/css/aowow.css | 11 +- static/js/account.js | 462 +++++++++++++++++ static/js/locale_dede.js | 1 + static/js/locale_enus.js | 1 + static/js/locale_eses.js | 1 + static/js/locale_frfr.js | 1 + static/js/locale_ruru.js | 1 + static/js/locale_zhcn.js | 1 + template/bricks/inputbox-form-signin.tpl.php | 2 +- template/pages/acc-dashboard.tpl.php | 141 ------ template/pages/account.tpl.php | 291 +++++++++++ 28 files changed, 1735 insertions(+), 804 deletions(-) create mode 100644 endpoints/account/account.php create mode 100644 endpoints/account/update-community-settings.php create mode 100644 endpoints/account/update-general-settings.php delete mode 100644 pages/account.php create mode 100644 setup/updates/1758578400_11.sql create mode 100644 setup/updates/1758578400_12.sql create mode 100644 static/js/account.js delete mode 100644 template/pages/acc-dashboard.tpl.php create mode 100644 template/pages/account.tpl.php diff --git a/endpoints/account/account.php b/endpoints/account/account.php new file mode 100644 index 00000000..cd57c720 --- /dev/null +++ b/endpoints/account/account.php @@ -0,0 +1,168 @@ +forwardToSignIn('account'); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('settings')); + + $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon` FROM ?_account WHERE `id` = ?d', User::$id); + + Lang::sort('game', 'ra'); + + parent::generate(); + + + /*************/ + /* Ban Popup */ + /*************/ + + $b = DB::Aowow()->select( + 'SELECT ab.`end` AS "0", ab.`reason` AS "1", a.`username` AS "2" + FROM ?_account_banned ab + LEFT JOIN ?_account a ON a.`id` = ab.`staffId` + WHERE ab.`userId` = ?d AND ab.`typeMask` & ?d AND (ab.`end` = 0 OR ab.`end` > UNIX_TIMESTAMP())', + User::$id, ACC_BAN_TEMP | ACC_BAN_PERM + ); + + $this->bans = $b ?: null; + + + /*******************/ + /* Status Messages */ + /*******************/ + + if (isset($_SESSION['msg'])) + { + [$var, $status, $msg] = $_SESSION['msg']; + if (property_exists($this, $var.'Message')) + $this->{$var.'Message'} = [$status, $msg]; + else + trigger_error('AccountBaseResponse::generate - unknown var in $_SESSION msg: '.$var, E_USER_WARNING); + + unset($_SESSION['msg']); + } + + + /*************/ + /* Form Data */ + /*************/ + + /* GENERAL */ + + // Modelviewer + if ($_ = DB::Aowow()->selectCell('SELECT `data` FROM ?_account_cookies WHERE `name` = ? AND `userId` = ?d', 'default_3dmodel', User::$id)) + [$this->modelrace, $this->modelgender] = explode(',', $_); + + // Lists + $this->idsInLists = $user['debug'] ? 1 : 0; + + /* PERSONAL */ + + // Email address + $this->curEmail = $user['email'] ?? ''; + + // Username + $this->curName = User::$username; + + // todo localize date format; store time + // $this->renameCD = date('F j, o', time() + 7 * DAY); + + /* COMMUNITY */ + + // Public Description + $this->description = ['body' => $user['description']]; + + // Forum Signature + // $this->signature = ['body' => $user['signature']]; + + // Avatar + $this->wowicon = $user['wowicon']; + $this->avMode = $user['avatar']; + + // status [reviewing, ok, rejected]? (only 2: rejected processed in js) + if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d AND `status` > 0', User::$id))) + { + array_walk($cuAvatars, function (&$x) { + $x['when'] *= 1000; // uploaded timestamp expected as msec for some reason + $x['caption'] = $x['name']; // only used for getVisibleText, duplicates name? + $x['type'] = 1; // always 1 ?, Dialog-popup doesn't work without it + }); + + foreach ($cuAvatars as $a) + if ($a['status'] != 2) + $this->customicons[$a['id']] = $a['name']; + + // TODO - replace with array_find in PHP 8.4 + if ($x = array_filter($cuAvatars, fn($x) => $x['current'] > 0 )) + $this->customicon = array_pop($x)['id']; + } + + /* PREMIUM */ + + $this->premium = User::isPremium(); + + if (!$this->premium) + return; + + // Avatar Manager + $this->avatarManager = new Listview([ + 'template' => 'avatar', + 'id' => 'avatar', + 'name' => '$LANG.tab_avatars', + 'parent' => 'avatar-manage', + 'hideNav' => 1 | 2, // top | bottom + 'data' => $cuAvatars ?? [] + ]); + + // Premium Border Selector + // ??? + } +} + +?> diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index b0fe5a6e..dce60520 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -62,7 +62,7 @@ class AccountSigninResponse extends TemplateResponse $this->forward($this->getNext(true)); $this->inputbox = ['inputbox-form-signin', array( - 'head' => Lang::account('doSignIn'), + 'head' => Lang::account('inputbox', 'head', 'signin'), 'action' => '?account=signin&next='.$this->getNext(), 'error' => $message, 'username' => $username, @@ -90,7 +90,7 @@ class AccountSigninResponse extends TemplateResponse // AUTH_BANNED => Lang::account('accBanned'); // ToDo: should this return an error? the actual account functionality should be blocked elsewhere AUTH_WRONGUSER => Lang::account('userNotFound'), AUTH_WRONGPASS => Lang::account('wrongPass'), - AUTH_IPBANNED => Lang::account('loginExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), + AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), AUTH_INTERNAL_ERR => Lang::main('intError'), default => Lang::main('intError') }; diff --git a/endpoints/account/update-community-settings.php b/endpoints/account/update-community-settings.php new file mode 100644 index 00000000..a09921f8 --- /dev/null +++ b/endpoints/account/update-community-settings.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($message = $this->updateSettings()) + $_SESSION['msg'] = ['community', $this->success, $message]; + } + + protected function updateSettings() + { + if (is_null($this->_post['desc'])) // assertPOST tests for empty string which is valid here + return Lang::main('genericError'); + + // description - 0 modified rows is still success + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `description` = ? WHERE `id` = ?d', $this->_post['desc'], User::$id))) + return Lang::main('genericError'); + + $this->success = true; + return Lang::account('updateMessage', 'community'); + } +} + +?> diff --git a/endpoints/account/update-general-settings.php b/endpoints/account/update-general-settings.php new file mode 100644 index 00000000..6e56ce3a --- /dev/null +++ b/endpoints/account/update-general-settings.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 11]], + 'modelgender' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['default' => 0, 'min_range' => 1, 'max_range' => 2] ], + 'idsInLists' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox'] ] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($message = $this->updateGeneral()) + $_SESSION['msg'] = ['general', $this->success, $message]; + } + + private function updateGeneral() : string + { + if (!$this->assertPOST('modelrace', 'modelgender')) + return Lang::main('genericError'); + + if ($this->_post['modelrace'] && !ChrRace::tryFrom($this->_post['modelrace'])) + return Lang::main('genericError'); + + // js handles this as cookie, so saved as cookie; Q - also save in ?_account table? + if (!DB::Aowow()->query('REPLACE INTO ?_account_cookies (`userId`, `name`, `data`) VALUES (?d, ?, ?)', User::$id, 'default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'])) + return Lang::main('genericError'); + + if (!setcookie('default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'], 0, '/')) + return Lang::main('intError'); + + // int > number of edited rows > no changes is still success + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `debug` = ?d WHERE `id` = ?d', $this->_post['idsInLists'] ? 1 : 0, User::$id))) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('updateMessage', 'general'); + } +} + +?> diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 3bc02c3b..986fa509 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -19,7 +19,7 @@ trait TrRecoveryHelper // check if already processing if ($_ = DB::Aowow()->selectCell('SELECT `statusTimer` - UNIX_TIMESTAMP() FROM ?_account WHERE `email` = ? AND `status` > ?d AND `statusTimer` > UNIX_TIMESTAMP()', $email, ACC_STATUS_NEW)) - return sprintf(Lang::account('isRecovering'), Util::formatTime($_ * 1000)); + return Lang::account('inputbox', 'error', 'isRecovering', [Util::formatTime($_ * 1000)]); // create new token and write to db $token = Util::createHash(); @@ -28,7 +28,7 @@ trait TrRecoveryHelper // send recovery mail if (!Util::sendMail($email, $mailTemplate, [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return sprintf(Lang::main('intError2'), 'send mail'); + return Lang::main('intError2', ['send mail']); return ''; } diff --git a/includes/defines.php b/includes/defines.php index 1c423f5e..29900e2e 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -62,10 +62,14 @@ define('DB_AUTH', 2); define('DB_CHARACTERS', 3); // Account Status -define('ACC_STATUS_OK', 0); // nothing special +define('ACC_STATUS_NONE', 0); // nothing special define('ACC_STATUS_NEW', 1); // just created, awaiting confirmation define('ACC_STATUS_RECOVER_USER', 2); // currently recovering username define('ACC_STATUS_RECOVER_PASS', 3); // currently recovering password +define('ACC_STATUS_CHANGE_EMAIL', 4); // currently changing contact email +define('ACC_STATUS_CHANGE_PASS', 5); // currently changing password +define('ACC_STATUS_CHANGE_USERNAME', 6); // currently changing username +define('ACC_STATUS_DELETED', 7); // is deleted - only a stub remains // Session Status define('SESSION_ACTIVE', 1); @@ -84,6 +88,12 @@ define('ACC_BAN_VIDEO', 0x0040); // cannot suggest vi define('ACC_BAN_GUIDE', 0x0080); // cannot write a guide define('ACC_BAN_FORUM', 0x0100); // cannot post on forums [not used here] +define('IP_BAN_TYPE_LOGIN_ATTEMPT', 0); +define('IP_BAN_TYPE_REGISTRATION_ATTEMPT', 1); +define('IP_BAN_TYPE_EMAIL_RECOVERY', 2); +define('IP_BAN_TYPE_PASSWORD_RECOVERY', 3); +define('IP_BAN_TYPE_USERNAME_RECOVERY', 4); + // Site Reputation/Privileges define('SITEREP_ACTION_REGISTER', 1); // Registered account define('SITEREP_ACTION_DAILYVISIT', 2); // Daily visit diff --git a/includes/user.class.php b/includes/user.class.php index 46d11f6d..5fca60fe 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -48,7 +48,7 @@ class User return false; // check IP bans - if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = 0', self::$ip)) + if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT)) { if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) return false; @@ -62,7 +62,7 @@ class User $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id()); $userData = DB::Aowow()->selectRow( - 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email` + 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` @@ -97,10 +97,10 @@ class User self::$preferedLoc = $loc; // reset expired account statuses - if ($userData['statusTimer'] < time() && $userData['status'] > ACC_STATUS_NEW) + if ($userData['statusTimer'] && $userData['statusTimer'] < time() && $userData['status'] != ACC_STATUS_NEW) { - DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = ?d', ACC_STATUS_OK, User::$id); - $userData['status'] = ACC_STATUS_OK; + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = ?d', ACC_STATUS_NONE, User::$id); + $userData['status'] = ACC_STATUS_NONE; } @@ -117,7 +117,7 @@ class User self::$dailyVotes = $userData['dailyVotes']; self::$excludeGroups = $userData['excludeGroups']; self::$status = $userData['status']; - // self::$debug = $userData['debug']; // TBD + self::$debug = $userData['debug']; self::$email = $userData['email']; if (Cfg::get('PROFILER_ENABLE')) @@ -251,9 +251,9 @@ class User return AUTH_INTERNAL_ERR; // handle login try limitation - $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); + $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); if (!$ipBan || !$ipBan['active']) // no entry exists or time expired; set count to 1 - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, 0, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK')); + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK')); else // entry already exists; increment count DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip); @@ -279,7 +279,7 @@ class User return AUTH_WRONGPASS; // successfull auth; clear bans for this IP - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = 0 AND `ip` = ?', self::$ip); + DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP)) return AUTH_BANNED; @@ -362,7 +362,7 @@ class User $name, $_SERVER["REMOTE_ADDR"] ?? '', self::$preferedLoc->value, - ACC_STATUS_OK, + ACC_STATUS_NONE, $userGroup >= U_GROUP_NONE ? $userGroup : U_GROUP_NONE ); @@ -497,7 +497,7 @@ class User public static function isRecovering() : bool { - return self::$status == ACC_STATUS_RECOVER_USER || self::$status == ACC_STATUS_RECOVER_PASS; + return self::$status != ACC_STATUS_NONE && self::$status != ACC_STATUS_NEW; } @@ -565,21 +565,13 @@ class User $gUser['characters'] = self::getCharacters(); $gUser['excludegroups'] = self::$excludeGroups; - if (Cfg::get('DEBUG') && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_TESTER)) + if (self::$debug) $gUser['debug'] = true; // csv id-list output option on listviews if (self::getPremiumBorder()) $gUser['settings'] = ['premiumborder' => 1]; else - $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied - - if (self::isPremium()) - $gUser['premium'] = 1; - - if (self::getPremiumBorder()) - $gUser['settings'] = ['premiumborder' => 1]; - else - $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied + $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ? if (self::isPremium()) $gUser['premium'] = 1; diff --git a/includes/utilities.php b/includes/utilities.php index b435c06b..61084146 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1212,12 +1212,15 @@ abstract class Util $body = Util::defStatic($body); + if ($expiration) + { + $vars += array_fill(0, 9, null); // vsprintf requires all unused indizes to also be set... + $vars[9] = Util::formatTime($expiration * 1000); + } + if ($vars) $body = vsprintf($body, $vars); - if ($expiration) - $body .= "\n\n".Lang::account('tokenExpires', [Util::formatTime($expiration * 1000)])."\n"; - $subject = Cfg::get('NAME_SHORT').Lang::main('colon') . $subject; $header = 'From: ' . Cfg::get('CONTACT_EMAIL') . "\n" . 'Reply-To: ' . Cfg::get('CONTACT_EMAIL') . "\n" . @@ -1225,7 +1228,7 @@ abstract class Util if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) { - Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_DEV | U_GROUP_ADMIN, LOG_LEVEL_INFO); + Util::addNote("Redirected from Util::sendMail:\n\nTo: " . $email . "\n\nSubject: " . $subject . "\n\n" . $body, U_GROUP_NONE, LOG_LEVEL_INFO); return true; } diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 5c2867c5..71e46780 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Anzahl an SQL-Queries", 'timeSQL' => "Zeit für SQL-Queries", 'noJScript' => 'Diese Seite macht ausgiebigen Gebrauch von JavaScript.
        Bitte aktiviert JavaScript in Eurem Browser.', - 'userProfiles' => "Deine Charaktere", + // 'userProfiles' => "Deine Charaktere", 'pageNotFound' => "Dies %s existiert nicht.", 'gender' => "Geschlecht", 'sex' => [null, "Mann", "Frau"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Seite: ", 'related' => "Weiterführende Informationen", 'contribute' => "Beitragen", - // 'replyingTo' => "Antwort zu einem Kommentar von", + // 'replyingTo' => "Antwort zu einem Kommentar von", 'submit' => "Absenden", + 'save' => 'Speichern', 'cancel' => "Abbrechen", 'rewards' => "Belohnungen", 'gains' => "Belohnungen", - 'login' => "Login", + // 'login' => "Login", 'forum' => "Forum", 'siteRep' => "Ruf: ", 'yourRepHistory'=> "Dein Ruf-Verlauf", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", + 'dateFmtUntil' => "j. F Y", 'timeAgo' => 'vor %s', 'nfSeparators' => ['.', ','], @@ -900,7 +902,6 @@ $lang = array( "Screenshot-Verwalter", "Video-Verwalter", "API-Partner", "Ausstehend" ), // signIn - 'doSignIn' => "Mit Eurem Konto anmelden", 'signIn' => "Anmelden", 'user' => "Benutzername", 'pass' => "Kennwort", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Benutzername", 'forgotPass' => "Kennwort", 'accCreate' => 'Noch kein Konto? Jetzt eins erstellen!', - 'resendMail' => "Bestätigungsmail erneut senden", - 'resendHint' => "Wenn Sie sich registriert haben, aber keine Bestätigungs-E-Mail erhalten haben, geben Sie Ihre E-Mail-Adresse unten ein und senden Sie das Formular ab. (Bitte überprüfen Sie Ihre Spam- oder Papierkorb-Ordner, um sicherzustellen, dass die E-Mail nicht versehentlich an der falschen Stelle abgelegt wurde!)", // recovery - 'recoverUser' => "Benutzernamenanfrage", - 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", - 'newPass' => "Neues Kennwort", - 'tokenExpires' => "Das Token wird in %s verfallen.", + 'newPass' => "Neues Kennwort:", + 'confNewPass' => "Neues Kennwort bestätigen:", + 'passResetHint' => 'Wenn ihr euer Kennwort nicht mehr wisst, könnt ihr es auf dieser Seite zurücksetzen.', + // 'tokenExpires' => "Das Token wird in %s verfallen.", // creation - 'register' => "Registrierung: Schritt %s von 2", - 'passConfirm' => "Kennwort bestätigen", + 'passConfirm' => "Kennwort bestätigen:", // dashboard 'ipAddress' => "IP-Adresse: ", 'lastIP' => "Letzte bekannte IP: ", - // 'myAccount' => "Mein Account", - // 'editAccount' => "Benutze die folgenden Formulare um deine Account-Informationen zu aktualisieren", - // 'viewPubDesc' => 'Die Beschreibung in deinem öffentlichen Profil ansehen', + // 'myAccount' => "Mein Account", + // 'editAccount' => "Benutze die folgenden Formulare um deine Account-Informationen zu aktualisieren", + // 'viewPubDesc' => 'Die Beschreibung in deinem öffentlichen Profil ansehen', // bans 'accBanned' => "Dieses Konto wurde geschlossen", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "Diese E-Mail-Adresse ist ungültig.", // message_emailnotvalid - 'emailNotFound' => "Die E-Mail-Adresse, die Ihr eingegeben habt, ist mit keinem Konto verbunden.

        Falls Ihr die E-Mail-Adresse vergessen habt, mit der Ihr Euer Konto erstellt habt, kontaktiert Ihr bitte CFG_CONTACT_EMAIL für Hilfestellung.", - 'createAccSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euer Konto zu erstellen.", - 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euren Benutzernamen zu erhalten.", - 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt den Anweisungen um euer Kennwort zurückzusetzen.", - 'accActivated' => 'Euer Konto wurde soeben aktiviert.
        Ihr könnt euch nun anmelden', 'userNotFound' => "Ein Konto mit diesem Namen existiert nicht.", 'wrongPass' => "Dieses Kennwort ist ungültig.", - // 'accInactive' => "Dieses Konto wurde bisher nicht aktiviert.", - 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", - 'signupExceeded'=> "Die maximale Anzahl an Regustrierungen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + // 'accInactive' => "Dieses Konto wurde bisher nicht aktiviert.", 'errNameLength' => "Euer Benutzername muss mindestens 4 Zeichen lang sein.", // message_usernamemin 'errNameChars' => "Euer Benutzername kann nur aus Buchstaben und Zahlen bestehen.", // message_usernamenotvalid 'errPassLength' => "Euer Kennwort muss mindestens 6 Zeichen lang sein.", // message_passwordmin 'passMismatch' => "Die eingegebenen Kennworte stimmen nicht überein.", 'nameInUse' => "Es existiert bereits ein Konto mit diesem Namen.", 'mailInUse' => "Diese E-Mail-Adresse ist bereits mit einem Konto verbunden.", - 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", 'passCheckFail' => "Die Kennwörter stimmen nicht überein.", // message_passwordsdonotmatch - 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden." // message_newpassdifferent + 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent + 'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent + + // settings + 'settings' => "Kontoeinstellungen", + 'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.", + 'tabGeneral' => "Allgemein", + 'tabPersonal' => "Persönliches", + 'tabCommunity' => "Community", + 'tabPremium' => "Premium", + 'preferences' => "Voreinstellungen", + 'modelviewer' => "Modellviewer", + 'mvNote' => "Vorgegebenes Charaktermodell:", + 'lists' => "Listen", + 'listsNote' => "Zeigt IDs in unterstützten Listen", + 'announcements' => "Bekanntmachungen", + 'annNote' => "Entfernt die Daten von Bekanntmachungen, die du geschlossen hast, damit sie wieder sichtbar werden.", + 'purge' => "Löschen", + 'curPass' => "Derzeitiges Kennwort:", + 'globalLogout' => "Von allen Browsern/Geräten abmelden", + 'curEmail' => "Momentane E-Mail-Adresse:", + 'newEmail' => "Neue E-Mail-Adresse:", + 'userPage' => "Benutzerseite", + 'publicDesc' => "Öffentliche Beschreibung", + 'publicDescNote'=> 'Erzähl uns etwas über dich und deine WoW-Charaktere. Alles, was du hier eingibst, erscheint auf deiner Benutzerseite.', + 'forums' => "Foren", + 'signature' => "Signatur", + 'signatureNote' => "Deine Signatur erscheint unter all deinen Forenbeiträgen.", + 'usernameNote' => "Nutzernamen können nur einmal alle %s geändert werden und müssen 4-16 Zeichen lang sein. Sonderzeichen sind nicht erlaubt.", + 'curName' => "Aktueller Nutzername:", + 'newName' => "Neuer Nutzername:", + 'accDelete' => "Konto löschen", + 'accDeleteNote' => 'Wenn du dein Konto und alle persönlichen Daten vollständig löschen möchtest, dann geh zu unserer Kontolöschung.', + 'avatar' => "Avatar", + 'avatarNote' => "Dein Avatar wird neben all deinen Forenbeiträgen angezeigt.", + 'avWowIcon' => "World of Warcraft-Icon", + 'avWowIconNote' => 'z.B. INV_Axe_54
        Tipp: Um den Namen eines Symbols herauszufinden, doppelklickt einfach auf das große Symbol, während ihr auf einer Gegenstands- oder Zauberseite seid. Kopiert den Text anschließend und fügt ihn oben ein.', + 'avIconName' => "Symbolname:", + 'none' => "Keins", + 'preview' => "Vorschau", + 'custom' => "Benutzerdefiniert", + 'premiumStatus' => "Premium Status", + 'status' => "Status", + 'active' => "Activ", + 'inactive' => "Inaktiv", + 'activeCD' => "Ihr müsst bis zum %s warten um euren Nutzernamen erneut zu ändern.", + 'updateMessage' => array( + 'general' => "Deine Einstellungen wurden aktualisiert.", + 'community' => "Eure öffentliche Beschreibung und Forensignatur wurden erfolgreich aktualisiert.", + 'personal' => "Eine Bestätigungsnachricht wurde an %s versandt.", + 'username' => 'Nutzername von %1$s zu %2$s geändert.', + 'avNotFound' => "Symbol nicht gefunden.", + 'avSuccess' => "Euer Avatar wurde erfolgreich aktualisiert.", + 'avNoChange' => "Es wurden keine Änderungen durchgeführt.", + 'av1stUser' => "Glückwunsch! Ihr habt eine einzigartige Auswahl getroffen! /jubeln", + 'avNthUser' => "Zur Eurer Information, Euer Symbol wird bereits von %d anderen Benutzer(n) benutzt." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Erfolg", + 'error' => "Hoppla!", + 'register' => "Registrierung: Schritt %s von 2", + 'recoverUser' => "Benutzernamenanfrage", + 'recoverPass' => "Kennwort zurücksetzen: Schritt %s von 2", + 'resendMail' => "Bestätigungsmail erneut senden", + 'signin' => "Mit Eurem Konto anmelden" + ), + 'message' => array( + 'accActivated' => 'Euer Konto wurde soeben aktiviert.
        Ihr könnt euch nun anmelden', + 'resendMail' => "Wenn Sie sich registriert haben, aber keine Bestätigungs-E-Mail erhalten haben, geben Sie Ihre E-Mail-Adresse unten ein und senden Sie das Formular ab. (Bitte überprüfen Sie Ihre Spam- oder Papierkorb-Ordner, um sicherzustellen, dass die E-Mail nicht versehentlich an der falschen Stelle abgelegt wurde!)", + 'mailChangeOk' => "Ihre E-Mail-Adresse wurde erfolgreich geändert.", + 'mailRevertOk' => "Ihre Anfrage zur Änderung der E-Mail-Adresse wurde storniert/zurückgesetzt.", + 'passChangeOk' => "Ihr Kennwort wurde erfolgreich geändert.", + 'deleteAccSent' => "Eine E-Mail mit einem Bestätigungslink wurde an %s gesendet.", + 'deleteOk' => "Ihr Konto wurde erfolgreich entfernt. Wir hoffen, Sie bald wiederzusehen!

        Sie können dieses Fenster jetzt schließen.", + 'createAccSent' => 'Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um Euer Konto zu erstellen.

        Falls du keine Bestätigungsnachricht erhalten hast klicke hier um eine neue zu senden.
        ', + 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euren Benutzernamen zu erhalten.", + 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euer Kennwort zurückzusetzen.", + ), + 'error' => array( + 'mailTokenUsed' => 'Dieser Schlüssel zur Änderung der E-Mail-Adresse wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'passTokenUsed' => 'Dieser Schlüssel zur Änderung des Kennworts wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'passTokenLost' => "Kein Token wurde bereitgestellt. Wenn Sie in einer E-Mail einen Link zum Zurücksetzen des Kennworts erhalten haben, kopieren Sie die gesamte URL (einschließlich des Tokens am Ende) in die Adressleiste Ihres Browsers.", + 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", + 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + 'signupExceeded' => "Die maximale Anzahl an Registrierungen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", + // 'emailNotFound' => "Die E-Mail-Adresse, die Ihr eingegeben habt, ist mit keinem Konto verbunden.

        Falls Ihr die E-Mail-Adresse vergessen habt, mit der Ihr Euer Konto erstellt habt, kontaktiert Ihr bitte CFG_CONTACT_EMAIL für Hilfestellung.", + 'emailNotFound' => "Diese E-Mail-Adresse wurde in unserem System nicht gefunden.", + ) + ) ), 'user' => array( 'notFound' => "Der Benutzer \"%s\" wurde nicht gefunden!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "%d. Stockwerk" ), 'privileges' => array( - 'main' => "Auf unserer Seite könnt Ihr Ruf erringen. Hauptsächlich erringt man Ruf dadurch, dass Eure Kommentare positiv bewertet werden.

        Das heißt, Euer Ruf hängt in gewissem Maße davon ab, wie sehr Ihr der Community beiträgt.

        Mit dem Sammeln von Ruf verdient Ihr Euch auch das Vertrauen der Gemeinschaft ein, und Ihr erhält Privilegien. Unten könnt Ihr eine vollständige Liste einsehen.", + 'main' => "Auf unserer Seite könnt Ihr Ruf erringen. Hauptsächlich erringt man Ruf dadurch, dass Eure Kommentare positiv bewertet werden.

        Das heißt, Euer Ruf hängt in gewissem Maße davon ab, wie sehr Ihr der Community beiträgt.

        Mit dem Sammeln von Ruf verdient Ihr Euch auch das Vertrauen der Gemeinschaft ein, und Ihr erhält Privilegien. Unten könnt Ihr eine vollständige Liste einsehen.", 'privilege' => "Privileg", 'privileges' => "Privilegien", 'requiredRep' => "Benötigter Ruf", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index edc82692..812c3606 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Number of SQL queries", 'timeSQL' => "Time of SQL queries", 'noJScript' => 'This site makes extensive use of JavaScript.
        Please enable JavaScript in your browser.', - 'userProfiles' => "My Profiles", + // 'userProfiles' => "My Profiles", 'pageNotFound' => "This %s doesn't exist.", 'gender' => "Gender", 'sex' => [null, "Male", "Female"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Side: ", 'related' => "Related", 'contribute' => "Contribute", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "Submit", + 'save' => 'Save', 'cancel' => "Cancel", 'rewards' => "Rewards", 'gains' => "Gains", - 'login' => "Login", + // 'login' => "Login", 'forum' => "Forum", 'siteRep' => "Reputation: ", 'yourRepHistory'=> "Your Reputation History", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", + 'dateFmtUntil' => "F j, Y", 'timeAgo' => "%s ago", 'nfSeparators' => [',', '.'], @@ -900,7 +902,6 @@ $lang = array( "Screenshot manager", "Video manager", "API partner", "Pending" ), // signIn - 'doSignIn' => "Log in to your Account", 'signIn' => "Log In", 'user' => "Username", 'pass' => "Password", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Username", 'forgotPass' => "Password", 'accCreate' => 'Don\'t have an account? Create one now!', - 'resendMail' => "Re-Send Verification Email", - 'resendHint' => "If you registered but did not receive a verification email, enter your email address below and submit the form. (Please be sure to check your spam or trash folders to make sure the email didn't accidentally get put in the wrong place!)", // recovery - 'recoverUser' => "Username Request", - 'recoverPass' => "Password Reset: Step %s of 2", - 'newPass' => "New Password", - 'tokenExpires' => "This token expires in %s.", + 'newPass' => "New Password:", + 'confNewPass' => "Confirm new password:", + 'passResetHint' => 'If you don\'t know your password, visit the password reset page to reset it.', + // 'tokenExpires' => "This token expires in %s.", // previously appended to all emails, now it's part of the mail template // creation - 'register' => "Registration - Step %s of 2", - 'passConfirm' => "Confirm password", + 'passConfirm' => "Confirm password:", // dashboard 'ipAddress' => "IP address: ", 'lastIP' => "last used IP: ", - // 'myAccount' => "My Account", - // 'editAccount' => "Simply use the forms below to update your account information", - // 'viewPubDesc' => 'View your Public Description in your Profile Page', + // 'myAccount' => "My Account", + // 'editAccount' => "Simply use the forms below to update your account information.", + // 'viewPubDesc' => 'View your Public Description in your Profile Page', // bans 'accBanned' => "This account was closed", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "That email address is not valid.", // message_emailnotvalid - 'emailNotFound' => "The email address you entered is not associated with any account.

        If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", - 'createAccSent' => "An email was sent to %s. Simply follow the instructions to create your account.", - 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", - 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password.", - 'accActivated' => 'Your account has been activated.
        Proceed to sign in', 'userNotFound' => "The username you entered does not exists.", 'wrongPass' => "That password is not vaild.", - // 'accInactive' => "That account has not yet been confirmed active.", - 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", - 'signupExceeded'=> "The maximum number of signups from this IP has been exceeded. Please try again in %s.", + // 'accInactive' => "That account has not yet been confirmed active.", 'errNameLength' => "Your username must be at least 4 characters long.", // message_usernamemin 'errNameChars' => "Your username can only contain letters and numbers.", // message_usernamenotvalid 'errPassLength' => "Your password must be at least 6 characters long.", // message_passwordmin 'passMismatch' => "The passwords you entered do not match.", - 'nameInUse' => "That username is already taken.", + 'nameInUse' => "This username is already in use.", 'mailInUse' => "That email is already registered to an account.", - 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Passwords do not match.", // message_passwordsdonotmatch - 'newPassDiff' => "Your new password must be different than your previous one." // message_newpassdifferent + 'newPassDiff' => "Your new password must be different than your previous one.", // message_newpassdifferent + 'newMailDiff' => "Your new email address must be different than your previous one.", // message_newemaildifferent + + // settings + 'settings' => "Account Settings", + 'settingsNote' => "Simply use the forms below to update your account information.", + 'tabGeneral' => "General", + 'tabPersonal' => "Personal", + 'tabCommunity' => "Community", + 'tabPremium' => "Premium", + 'preferences' => "Preferences", + 'modelviewer' => "Model Viewer", + 'mvNote' => "Default character model:", + 'lists' => "Lists", + 'listsNote' => "Show IDs in supported lists", + 'announcements' => "Announcements", + 'annNote' => "Removes data related to announcements you have closed so that they may be viewed again.", + 'purge' => "Purge", + 'curPass' => "Current password:", + 'globalLogout' => "Log me out of all other browsers/devices", + 'curEmail' => "Current email address:", + 'newEmail' => "New email address:", + 'userPage' => "User Page", + 'publicDesc' => "Public Description", + 'publicDescNote'=> 'Tell us more about yourself and your WoW characters. Whatever you type here will appear on your user page.', + 'forums' => "Forums", + 'signature' => "Signature", + 'signatureNote' => "Your signature will appear beneath all of your posts in the forums.", + 'usernameNote' => "Usernames can only be changed once every %s and must be between 4-16 characters. No special characters are permitted.", + 'curName' => "Current Username:", + 'newName' => "New Username:", + 'accDelete' => "Delete Account", + 'accDeleteNote' => "If you'd like to completely delete your account and all its personal information, visit our account deletion page.", + 'avatar' => "Avatar", + 'avatarNote' => "Your avatar will appear next to all of your posts in the forums.", + 'avWowIcon' => "Icon from World of Warcraft", + 'avWowIconNote' => 'e.g. INV_Axe_54
        Tip: To find the name of an icon, simply double-click the big icon while
        browsing an item or spell page. Then copy and paste it above.', + 'avIconName' => "Icon name:", + 'none' => "None", + 'preview' => "Preview", + 'custom' => "Custom", + 'premiumStatus' => "Premium Status", + 'status' => "Status", + 'active' => "Active", + 'inactive' => "Inactive", + 'activeCD' => "You must wait until %s to change your username again.", + 'updateMessage' => array( + 'general' => "Updated your preferences.", + 'community' => "Your public description and forum signature have been updated successfully.", + 'personal' => "A confirmation email was sent to %s.", + 'username' => 'Username changed from %1$s to %2$s.', + 'avNotFound' => "Icon not found.", + 'avSuccess' => "Your avatar has been updated successfully.", + 'avNoChange' => "No changes were made.", + 'av1stUser' => "Congratulations for picking one that is unique! /cheer", + 'avNthUser' => "FYI, your icon is also used by %d other user(s)." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Success", + 'error' => "Oops!", + 'register' => "Registration - Step %s of 2", + 'recoverUser' => "Username Request", + 'recoverPass' => "Password Reset: Step %s of 2", + 'resendMail' => "Re-Send Verification Email", + 'signin' => "Log in to your Account" + ), + 'message' => array( + 'accActivated' => 'Your account has been activated.
        Proceed to sign in', + 'resendMail' => "If you registered but did not receive a verification email, enter your email address below and submit the form. (Please be sure to check your spam or trash folders to make sure the email didn't accidentally get put in the wrong place!)", + 'mailChangeOk' => "Your email address has been changed successfully.", + 'mailRevertOk' => "Your email change request has been cancelled/reverted.", + 'passChangeOk' => "Your password has been changed successfully.", + 'deleteAccSent' => "An email has been sent to %s with confirmation link attached.", + 'deleteOk' => "Your account has been successfully removed. We hope to see you again soon!

        You may now close this window.", + 'createAccSent' => 'An email was sent to %s. Simply follow the instructions to create your account.

        If you don\'t receive the verification email, click here to send another one.
        ', + 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", + 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password." + ), + 'error' => array( + 'mailTokenUsed' => 'Either that email change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', + 'passTokenUsed' => 'Either that password change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', + 'passTokenLost' => "No token was provided. If you received a reset password link in an email, please copy and paste the entire URL (including the token at the end) into your browser's location bar.", + 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", + 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", + 'signupExceeded' => "The maximum number of signups from this IP has been exceeded. Please try again in %s.", + // 'emailNotFound' => "The email address you entered is not associated with any account.

        If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", + 'emailNotFound' => "That email address wasn't found in our system." + ) + ) ), 'user' => array( 'notFound' => "User \"%s\" not found!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "Level %d" ), 'privileges' => array( - 'main' => "Here on our Site you can generate reputation. The main way to generate it is to get your comments upvotes.

        So, reputation is a rough measure of how much you contributed to the community.

        As you amass reputation you earn the community's trust and you will be granted with additional privileges. You can find a full list below.", + 'main' => "Here on our Site you can generate reputation. The main way to generate it is to get your comments upvotes.

        So, reputation is a rough measure of how much you contributed to the community.

        As you amass reputation you earn the community's trust and you will be granted with additional privileges. You can find a full list below.", 'privilege' => "Privilege", 'privileges' => "Privileges", 'requiredRep' => "Reputation Required", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 979a603b..385861b1 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Número de consultas de SQL", 'timeSQL' => "El tiempo para las consultas de SQL", 'noJScript' => 'Este sitio hace uso intenso de JavaScript.
        Por favor habilita JavaScript en tu navegador.', - 'userProfiles' => "Tus personajes", + // 'userProfiles' => "Tus personajes", 'pageNotFound' => "Este %s no existe.", 'gender' => "Género", 'sex' => [null, "Hombre", "Mujer"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Lado: ", 'related' => "Información relacionada", 'contribute' => "Contribuir", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "Enviar", + 'save' => 'Guardar', 'cancel' => "Cancelar", 'rewards' => "Recompensas", 'gains' => "Ganancias", - 'login' => "Ingresar", + // 'login' => "Ingresar", 'forum' => "Foro", 'siteRep' => "Reputación: ", 'yourRepHistory'=> "Tu Historial de Reputación", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d/m/Y", 'dateFmtLong' => "d/m/Y \a \l\a\s g:i A", + 'dateFmtUntil' => "j \d\\e F \d\\e Y", 'timeAgo' => 'hace %s', 'nfSeparators' => ['.', ','], @@ -900,7 +902,6 @@ $lang = array( "Gestor de Capturas de pantalla","Gestor de vídeos", "Partner de API", "Pendiente" ), // signIn - 'doSignIn' => "Iniciar sesión con tu cuenta", 'signIn' => "Iniciar sesión", 'user' => "Nombre de usuario", 'pass' => "Contraseña", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Nombre de usuario", 'forgotPass' => "Contraseña", 'accCreate ' => '¿No tienes una cuenta? ¡Crea una ahora!', - 'resendMail' => "Reenviar correo de verificación", - 'resendHint' => "Si te has registrado pero no recibiste un correo de verificación, introduce tu dirección de correo más abajo y completa el formulario. (¡Por favor, asegúrate de comprobar tus directorios de correo no deseado o papelera por si el correo acabara en el lugar equivocado!)", // recovery - 'recoverUser' => "Pedir nombre de usuario", - 'recoverPass' => "Reiniciar contraseña: Paso %s de 2", - 'newPass' => "Nueva Contraseña", - 'tokenExpires' => "Este token expira en %s", + 'newPass' => "Nueva Contraseña:", + 'confNewPass' => "Confirmar contraseña nueva:", + 'passResetHint' => 'Si no sabes tu contraseña, visita la página de restablecimiento de contraseña para restablecerla.', + // 'tokenExpires' => "Este token expira en %s", // creation - 'register' => "Inscripción: Paso %s de 2", - 'passConfirm' => "Confirmar contraseña", + 'passConfirm' => "Confirmar contraseña:", // dashboard 'ipAddress' => "Dirección IP: ", 'lastIP' => "Última IP usada: ", - // 'myAccount' => "Mi cuenta", - // 'editAccount' => "Use el formulario siguienta para actualizar la información de la cuenta.", - // 'viewPubDesc' => 'Mira tu descripción pública en tu Página de perfil', + // 'myAccount' => "Mi cuenta", + // 'editAccount' => "Use el formulario siguienta para actualizar la información de la cuenta.", + // 'viewPubDesc' => 'Mira tu descripción pública en tu Página de perfil', // bans 'accBanned' => "Esta cuenta fue cerrada.", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "Esa dirección de correo electrónico no es válida.", // message_emailnotvalid - 'emailNotFound' => "El correo electrónico que ingresaste no está asociado con ninguna cuenta.

        Si olvistaste el correo electronico con el que registraste la cuenta, escribe a CFG_CONTACT_EMAIL para asistencia.", - 'createAccSent' => "Un correo fue enviado a %s. Siga las instrucciones para crear su cuenta.", - 'recovUserSent' => "Un correo fue enviado a %s. Siga las instrucciones para recuperar su nombre de usuario.", - 'recovPassSent' => "Un correo fue enviado a %s. Siga las instrucciones para reiniciar su contraseña.", - 'accActivated' => 'Su cuenta ha sido activada.
        Ingrese a para ingresar', 'userNotFound' => "El usuario que ha ingresado no existe", 'wrongPass' => "La contraseña no es valida.", - // 'accInactive' => "That account has not yet been confirmed active.", - 'loginExceeded' => "Ha excedido la cantidad de inicios de sesion con esta IP. Por favor intente en %s", - 'signupExceeded'=> "Ha excedido la cantidad de creaciones de cuentas con esta IP. Por favor intente en %s.", + // 'accInactive' => "That account has not yet been confirmed active.", 'errNameLength' => "Tu nombre de usuario tiene que tener por lo menos cuatro caracteres.", // message_usernamemin 'errNameChars' => "Tu nombre de usuario solo puede contener números y letras.", // message_usernamenotvalid 'errPassLength' => "Tu contraseña tiene que tener por lo menos seis caracteres.", // message_passwordmin 'passMismatch' => "La contraseña que ingresó no concuerdan.", 'nameInUse' => "El nombre de usuario ya se encuentra utilzado", 'mailInUse' => "El correo electrónico ya se encuentra registrado a una cuenta", - 'isRecovering' => "Esta cuenta ya se encuentra en proceso de recuperación. Siga las intrucciones en su correo o espere %s para que el token expire ", 'passCheckFail' => "Las contraseñas no son iguales.", // message_passwordsdonotmatch - 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior." // message_newpassdifferent + 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior.",// message_newpassdifferent + 'newMailDiff' => "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", // message_newemaildifferent + + // settings + 'settings' => "Mi cuenta", + 'settingsNote' => "Simplemente usa el siguiente formulario para actualizar la información de tu cuenta.", + 'tabGeneral' => "General", + 'tabPersonal' => "Personal", + 'tabCommunity' => "Comunidad", + 'tabPremium' => "Premium", + 'preferences' => "Preferencias", + 'modelviewer' => "Visualizador de modelos", + 'mvNote' => "Modelo de personaje por defecto:", + 'lists' => "Listas", + 'listsNote' => "Mostrar IDs en listas soportadas", + 'announcements' => "Anuncios", + 'annNote' => "Elimina datos relacionados con anuncios que hayas cerrado para que puedan ser vistos de nuevo.", + 'purge' => "Purgar", + 'curPass' => "Contraseña actual:", + 'globalLogout' => "Cerrar sesión en todos los otros navegadores/dispositivos", + 'curEmail' => "Dirección de correo electrónico actual", + 'newEmail' => "Dirección de correo electrónico nueva", + 'userPage' => "Página de usuario", + 'publicDesc' => "Descripción pública", + 'publicDescNote'=> 'Dinos más sobre ti y tus personajes de WoW. Lo que escribas aquí aparecerá en tu página de usuario.', + 'forums' => "Foros", + 'signature' => "Firma", + 'signatureNote' => "Tu firma aparecerá debajo de todos tus mensajes en los foros.", + 'usernameNote' => "Los nombres de usuario solo pueden cambiarse una vez cada %s y deben tener entre 4 y 16 caracteres. No se permiten caracteres especiales.", + 'curName' => "Nombre de Usuario Actual:", + 'newName' => "Nuevo Nombre de Usuario:", + 'accDelete' => "Eliminar Cuenta", + 'accDeleteNote' => 'Si quieres eliminar completamente tu cuenta y toda tu información personal, visita nuestra página de eliminación de cuenta.', + 'avatar' => "Avatar", + 'avatarNote' => "Tu avatar aparecerá al lado de todos tus mensajes en los foros.", + 'avWowIcon' => "Ãcono de World of Warcraft", + 'avWowIconNote' => 'ej. INV_Axe_54
        Sugerencia: Para encontrar el nombre de un icono, simplemente haz doble-clic en el icono grande mientras estás viendo una página de un objeto o un hechizo. Después cópialo arriba.', + 'avIconName' => "Nombre de ícono:", + 'none' => "Ninguno", + 'preview' => "Visualizar", + 'custom' => "Personalizado", + 'premiumStatus' => "Suscripción Premium", + 'status' => "Estado", + 'active' => "Activo", + 'inactive' => "Inactivo", + 'activeCD' => "Debes esperar hasta %s para cambiar tu nombre de usuario nuevamente.", + 'updateMessage' => array( + 'general' => "Preferencias actualizadas.", + 'community' => "Tu descripción pública y tu firma en el foro se han actualizado correctamente.", + 'personal' => "Se envió un correo electrónico de confirmación a %s.", + 'username' => 'Nombre de usuario cambiado de %1$s a %2$s.', + 'avNotFound' => "No se encontró este avatar.", + 'avSuccess' => "Tu avatar ha sido actualizado correctamente.", + 'avNoChange' => "No se hicieron cambios.", + 'av1stUser' => "¡Felicidades, tienes un avatar único! /hurra", + 'avNthUser' => "Para tu información, tu avatar también está siendo usado por %d otros usuarios." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Éxito", + 'error' => "¡Ups!", + 'register' => "Inscripción: Paso %s de 2", + 'recoverUser' => "Solicitar nombre de usuario", + 'recoverPass' => "Restablecer contraseña: Paso %s de 2", + 'resendMail' => "Reenviar correo de verificación", + 'signin' => "Iniciar sesión con tu cuenta" + ), + 'message' => array( + 'accActivated' => 'Su cuenta ha sido activada.
        Ingrese a para ingresar', + 'resendMail' => "Si te has registrado pero no recibiste un correo de verificación, introduce tu dirección de correo más abajo y completa el formulario. (¡Por favor, asegúrate de comprobar tus directorios de correo no deseado o papelera por si el correo acabara en el lugar equivocado!)", + 'mailChangeOk' => "Tu dirección de correo electrónico ha sido cambiada correctamente.", + 'mailRevertOk' => "Tu solicitud de cambio de correo electrónico ha sido cancelada/revertida.", + 'passChangeOk' => "Tu contraseña ha sido cambiada correctamente.", + 'deleteAccSent' => "Se ha enviado un correo electrónico a %s con el enlace de confirmación adjunto.", + 'deleteOk' => "Tu cuenta ha sido eliminada correctamente. ¡Esperamos verte de nuevo pronto!

        Ahora puedes cerrar esta ventana.", + 'createAccSent' => 'Un correo fue enviado a %s. Sigue las instrucciones para crear tu cuenta.

        Si no recibes el correo de verificación, haz clic aquí para enviar otro.', + 'recovUserSent' => "Un correo fue enviado a %s. Sigue las instrucciones para recuperar tu nombre de usuario.", + 'recovPassSent' => "Un correo fue enviado a %s. Sigue las instrucciones para restablecer tu contraseña." + ), + 'error' => array( + 'mailTokenUsed' => 'Ese código de cambio de correo electrónico ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', + 'passTokenUsed' => 'Ese código de cambio de contraseña ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', + 'passTokenLost' => "No se recibió ningún código de petición. Si recibiste un enlace para restablecer tu contraseña por correo, por favor copia y pega la dirección completa (incluyendo el código del final) en la barra de dirección de tu navegador.", + 'isRecovering' => "Esta cuenta ya se encuentra en proceso de recuperación. Sigue las instrucciones en tu correo o espera %s para que el token expire.", + 'loginExceeded' => "Has excedido la cantidad de inicios de sesión con esta IP. Por favor intenta en %s.", + 'signupExceeded' => "Has excedido la cantidad de creaciones de cuentas con esta IP. Por favor intenta en %s.", + // 'emailNotFound' => "El correo electrónico que ingresaste no está asociado con ninguna cuenta.

        Si olvistaste el correo electronico con el que registraste la cuenta, escribe a CFG_CONTACT_EMAIL para asistencia.", + 'emailNotFound' => "Esa dirección de correo electrónico no fue encontrada en nuestro sistema." + ) + ) ), 'user' => array( 'notFound' => "¡No se encontró el usuario \"%s\"!", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 9f9acf20..1974541e 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "Nombre de requêtes SQL", 'timeSQL' => "Temps d'exécution des requêtes SQL", 'noJScript' => "Ce site requiert JavaScript pour fonctionner.
        Veuillez activer JavaScript dans votre navigateur.", - 'userProfiles' => "Vos personnages", // translate.google :x + // 'userProfiles' => "Vos personnages", // translate.google :x 'pageNotFound' => "Ce %s n'existe pas.", 'gender' => "Genre", 'sex' => [null, "Homme", "Femme"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Coté : ", 'related' => "Informations connexes", 'contribute' => "Contribuer", - // 'replyingTo' => "En réponse au commentaire de", + // 'replyingTo' => "En réponse au commentaire de", 'submit' => "Soumettre", + 'save' => 'Sauver', 'cancel' => "Annuler", 'rewards' => "Récompenses", 'gains' => "Gains", - 'login' => "[Login]", + // 'login' => "[Login]", 'forum' => "Forum", 'siteRep' => "Réputation : ", 'yourRepHistory'=> "Votre historique de réputation", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ' : ', 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d à g:i A", + 'dateFmtUntil' => "j F Y", 'timeAgo' => 'il y a %s', 'nfSeparators' => [' ', ','], @@ -900,7 +902,6 @@ $lang = array( "Gestionnaire de capture d'écran","Gestionnaire de vidéos", "Partenaire API", "En attente" ), // signIn - 'doSignIn' => "Connexion à votre compte", 'signIn' => "Connexion", 'user' => "Nom d'utilisateur", 'pass' => "Mot de passe", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "Nom d'utilisateur", 'forgotPass' => "Mot de passe", 'accCreate' => 'Vous n\'avez pas encore de compte ? Créez-en un maintenant !', - 'resendMail' => "Renvoyer le courriel de vérification", - 'resendHint' => "Si vous vous êtes enregistré mais n'avez pas reçu de courriel de vérification, entrez votre adresse électronique ci-dessous et validez le formulaire. (Assurez-vous de vérifier vos dossiers de courrier indésirable et votre corbeille pour vous assurer que le courriel ne s'y soit pas perdu !)", // recovery - 'recoverUser' => "Demande de nom d'utilisateur", - 'recoverPass' => "Changement de mot de passe : Étape %s de 2", - 'newPass' => "Nouveau mot de passe", - 'tokenExpires' => "This token expires in %s.", + 'newPass' => "Nouveau mot de passe :", + 'confNewPass' => "Confirm new password:", + 'passResetHint' => 'Si vous ne connaissez pas votre mot de passe, rendez-vous sur la page de réinitialisation du mot de passe pour le réinitialiser.', + // 'tokenExpires' => "This token expires in %s.", // creation - 'register' => "Enregistrement : Étape %s de 2", - 'passConfirm' => "Confirmez", + 'passConfirm' => "Confirmez :", // dashboard 'ipAddress' => "Addresse IP : ", 'lastIP' => "Dernière IP utilisée : ", - // 'myAccount' => "Mon compte", - // 'editAccount' => "Utilisez les formulaires ci-dessous pour mettre à jour vos informations.", - // 'viewPubDesc' => 'Voyez vos informations publiques dans votre Profile Page', + // 'myAccount' => "Mon compte", + // 'editAccount' => "Utilisez les formulaires ci-dessous pour mettre à jour vos informations.", + // 'viewPubDesc' => 'Voyez vos informations publiques dans votre Profile Page', // bans 'accBanned' => "Ce compte a été fermé.", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "Cette adresse courriel est invalide.", // message_emailnotvalid - 'emailNotFound' => "L'address email que vous avez entrée n'est pas associée à un compte.

        Si vous avez oublié l'address email avec laquelle vous avez enregistré votre compteCFG_CONTACT_EMAIL pour obtenir de l'aide.", - 'createAccSent' => "Un email a été envoyé à %s. Suivez les instructions pour créer votre compte.", - 'recovUserSent' => "Un email a été envoyé à %s. Suivez les instructions pour récupérer votre nom d'utilisateur.", - 'recovPassSent' => "Un email a été envoyé à %s. Suivez les instructions pour réinitialiser votre mot de passe.", - 'accActivated' => 'Votre compte a été activé.
        Vous pouvez maintenant vous connecter', 'userNotFound' => "Le nom d'utilisateur que vous avez saisi n'éxiste pas.", 'wrongPass' => "Ce mot de passe est invalide.", - // 'accInactive' => "Ce compte n'a pas encore été activé.", - 'loginExceeded' => "Le nombre maximum de connections depuis cette IP a été dépassé. Essayez de nouevau dans %s.", - 'signupExceeded'=> "Le nombre maximum d'inscriptions depuis cette IP a été dépassé. Essayez de nouveau dans %s.", + // 'accInactive' => "Ce compte n'a pas encore été activé.", 'errNameLength' => "Votre nom d'utilisateur doit faire au moins 4 caractères de long.", // message_usernamemin 'errNameChars' => "Votre nom d'utilisateur doit contenir seulement des lettres et des chiffres.", // message_usernamenotvalid 'errPassLength' => "Votre mot de passe doit faire au moins 6 caractères de long.", // message_passwordmin 'passMismatch' => "Les mots de passe que vous avez saisis ne correspondent pas.", 'nameInUse' => "Ce nom d'utilisateur est déjà utilisé.", 'mailInUse' => "Cette addresse email est déjà liée à un compte.", - 'isRecovering' => "Ce compte est déjà en train d'être récupéré. Suivez les instruction dans l'email reçu ou attendez %s pour que le token expire.", 'passCheckFail' => "Les mots de passe ne correspondent pas.", // message_passwordsdonotmatch - 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien." // message_newpassdifferent + 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien.", // message_newpassdifferent + 'newMailDiff' => "Votre nouvelle adresse courriel doit être différente de l'ancienne.", // message_newemaildifferent + + // settings + 'settings' => "Mon compte", + 'settingsNote' => "Veuillez utiliser les formulaires ci-dessous pour apporter des changements.", + 'tabGeneral' => "Général", + 'tabPersonal' => "Personnel", + 'tabCommunity' => "Communauté", + 'tabPremium' => "Premium", + 'preferences' => "Préférences", + 'modelviewer' => "Visionneuse 3D", + 'mvNote' => "Modèle de personnage par défaut :", + 'lists' => "Listes", + 'listsNote' => "Afficher les IDs dans les listes supportées", + 'announcements' => "Annonces", + 'annNote' => "Supprimer les données relatives aux annonces que vous avez fermées pour qu'elles puissent être vues à nouveau.", + 'purge' => "Effacer", + 'curPass' => "Mot de passe actuel :", + 'globalLogout' => "Me déconnecter de tous les autres navigateurs/appareils", + 'curEmail' => "Adresse courriel actuelle :", + 'newEmail' => "Nouvelle adresse e-mail :", + 'userPage' => "Page d'utilisateur", + 'publicDesc' => "Description publique", + 'publicDescNote'=> 'Dites-nous en un peu plus sur vous et vos persos de WoW. Tout ce que vous écrivez ici apparaîtra dans votre page d\'utilisateur.', + 'forums' => "Forum", + 'signature' => "Signature", + 'signatureNote' => "Votre signature apparaîtra en dessous de chacun de vos messages dans le forum.", + 'usernameNote' => "Les noms d'utilisateur ne peuvent être changés qu'une fois tous les %s et doivent comporter entre 4 et 16 caractères. Aucun caractère spécial n'est autorisé.", + 'curName' => "Nom d'utilisateur actuel :", + 'newName' => "Nouveau nom d'utilisateur :", + 'accDelete' => "Supprimer le compte", + 'accDeleteNote' => 'Si vous voulez complètement supprimer votre compte et toutes ses informations personnelles, visitez notre page de suppression de compte.', + 'avatar' => "Avatar", + 'avatarNote' => "Votre avatar apparaîtra à côté de chacun de vos messages dans le forum.", + 'avWowIcon' => "Icône de World of Warcraft ", + 'avWowIconNote' => 'ex. INV_Axe_54
        Astuce : Pour trouver le nom d\'une icône, vous n\'avez qu\'à double-cliquer sur la grosse icône lorsque vous naviguez sur une page d\'objet ou de sort. Ensuite copiez-collez le nom ci-dessous.', + 'avIconName' => "Nom de l'icône :", + 'none' => "Aucun", + 'preview' => "Aperçu", + 'custom' => "Personnalisé", + 'premiumStatus' => "Souscription Premium", + 'status' => "Statut", + 'active' => "Actives", + 'inactive' => "Inactives", + 'activeCD' => "Vous devez attendre jusqu'à %s pour changer à nouveau votre nom d'utilisateur.", + 'updateMessage' => array( + 'general' => "Vos préférences ont été mises à jour.", + 'community' => "Votre description publique et votre signature de forum ont été actualisées correctement.", + 'personal' => "Un courriel de confirmation a été envoyé à %s.", + 'username' => 'Nom d\'utilisateur changé de %1$s à %2$s.', + 'avNotFound' => "Icône non trouvée.", + 'avSuccess' => "Votre avatar a été mis à jour avec succès.", + 'avNoChange' => "Aucun changement à été fait.", + 'av1stUser' => "Félicitations pour en avoir choisir un qui est unique !", + 'avNthUser' => "Au passage, votre icône est également utilisée par %d autre(s) utilisateur(s)." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "Succès", + 'error' => "Oups.", + 'register' => "Enregistrement : Étape %s de 2", + 'recoverUser' => "Demande de nom d'utilisateur", + 'recoverPass' => "Changement de mot de passe : Étape %s de 2", + 'resendMail' => "Renvoyer le courriel de vérification", + 'signin' => "Connexion à votre compte" + ), + 'message' => array( + 'accActivated' => 'Votre compte a été activé.
        Vous pouvez maintenant vous connecter', + 'resendMail' => "Si vous vous êtes enregistré mais n'avez pas reçu de courriel de vérification, entrez votre adresse électronique ci-dessous et validez le formulaire. (Assurez-vous de vérifier vos dossiers de courrier indésirable et votre corbeille pour vous assurer que le courriel ne s'y soit pas perdu !)", + 'mailChangeOk' => "Votre adresse courriel a été changée avec succès.", + 'mailRevertOk' => "Votre demande de changement d'adresse courriel a été annulée/révoquée.", + 'passChangeOk' => "Votre mot de passe a été changé avec succès.", + 'deleteAccSent' => "Un courriel a été envoyé à %s avec le lien de confirmation.", + 'deleteOk' => "Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt !

        Vous pouvez maintenant fermer cette fenêtre.", + 'createAccSent' => 'Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu\'il contient pour créer votre compte.

        Si vous ne recevez pas l\'email de vérification, cliquez ici pour en envoyer un autre.
        ', + 'recovUserSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour récupérer votre nom d'utilisateur.", + 'recovPassSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour réinitialiser votre mot de passe." + ), + 'error' => array( + 'mailTokenUsed' => "Cette clé de changement d'adresse courriel a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", + 'passTokenUsed' => "Cette clé de changement de mot de passe a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", + 'passTokenLost' => "Aucun jeton n'a été fourni. Si vous avez reçu un lien de réinitialisation du mot de passe dans un courriel, merci de copier et coller l'URL entière (y compris le jeton à la fin) dans la barre d'adresse de votre navigateur.", + 'isRecovering' => "Ce compte est déjà en train d'être récupéré. Suivez les instruction dans l'email reçu ou attendez %s pour que le token expire.", + 'loginExceeded' => "Le nombre maximum de connections depuis cette IP a été dépassé. Essayez de nouevau dans %s.", + 'signupExceeded' => "Le nombre maximum d'inscriptions depuis cette IP a été dépassé. Essayez de nouveau dans %s.", + // 'emailNotFound' => "L'address email que vous avez entrée n'est pas associée à un compte.

        Si vous avez oublié l'address email avec laquelle vous avez enregistré votre compteCFG_CONTACT_EMAIL pour obtenir de l'aide.", + 'emailNotFound' => "Cette adresse électronique n'a pas été trouvée dans notre système." + ) + ) ), 'user' => array( 'notFound' => "Utilisateur \"%s\" non trouvé!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "Plancher %d" ), 'privileges' => array( - 'main' => "Sur AoWoW, vous pouvez accumuler de la réputation. Le principal moyen d'en accumuler est d'avoir un score élevé pour vos commentaires.

        Ainsi, la réputation est une vision sommaire de vos contributions à la communauté.

        En amassant de la réputation, vous gagnez le respect de la communauté et vous obtiendrez certains privilèges. Vous pouvez en trouver la liste complète ci-dessous.", + 'main' => "Sur AoWoW, vous pouvez accumuler de la réputation. Le principal moyen d'en accumuler est d'avoir un score élevé pour vos commentaires.

        Ainsi, la réputation est une vision sommaire de vos contributions à la communauté.

        En amassant de la réputation, vous gagnez le respect de la communauté et vous obtiendrez certains privilèges. Vous pouvez en trouver la liste complète ci-dessous.", 'privilege' => "Privilège", 'privileges' => "Privilèges", 'requiredRep' => "Réputation Requise", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index f6d74b02..64a925d9 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "КоличеÑтво SQL запроÑов", 'timeSQL' => "Ð’Ñ€ÐµÐ¼Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ SQL запроÑов", 'noJScript' => 'Данный Ñайт активно иÑпользует технологию JavaScript.
        ПожалуйÑта, Включите JavaScript в вашем браузере.', - 'userProfiles' => "Ваши перÑонажи", // translate.google :x + // 'userProfiles' => "Ваши перÑонажи", // translate.google :x 'pageNotFound' => "Такое %s не ÑущеÑтвует.", 'gender' => "Пол", 'sex' => [null, "Мужчина", "Женщина"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "Сторона: ", 'related' => "Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ", 'contribute' => "Добавить", - // 'replyingTo' => "Ответ на комментарий от", + // 'replyingTo' => "Ответ на комментарий от", 'submit' => "Отправить", + 'save' => 'Сохранить', 'cancel' => "Отмена", 'rewards' => "Ðаграды", 'gains' => "БонуÑ", - 'login' => "[Login]", + // 'login' => "[Login]", 'forum' => "Форум", 'siteRep' => "РепутациÑ: ", 'yourRepHistory'=> "ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð²Ð°ÑˆÐµÐ¹ репутации", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ": ", 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d в g:i A", + 'dateFmtUntil' => "j F Y г.", 'timeAgo' => '%s назад', 'nfSeparators' => [' ', ','], @@ -900,7 +902,6 @@ $lang = array( "Менеджер изображений", "Менеджер видео", "API партнер", "Ожидающее" ), // signIn - 'doSignIn' => "Войти в вашу учетную запиÑÑŒ", 'signIn' => "Вход", 'user' => "Логин", 'pass' => "Пароль", @@ -909,55 +910,133 @@ $lang = array( 'forgotUser' => "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", 'forgotPass' => "Пароль", 'accCreate' => 'У Ð²Ð°Ñ ÐµÑ‰Ðµ нет учетной запиÑи? ЗарегиÑтрируйтеÑÑŒ прÑмо ÑейчаÑ!', - 'resendMail' => "Вновь выÑлать верификационное пиÑьмо", - 'resendHint' => "ЕÑли вы зарегиÑтрировалиÑÑŒ, но не получили проверочного пиÑьма, пожалуйÑта, введите ваш email Ð°Ð´Ñ€ÐµÑ Ð½Ð¸Ð¶Ðµ и подтвердите отправку формы. (ПожалуйÑта, удоÑтоверьтеÑÑŒ, что Ð’Ñ‹ проверили папку Ñо Ñпамом и/или корзину Вашего почтового ÑервиÑа)", // recovery - 'recoverUser' => "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¸Ð¼ÐµÐ½Ð¸ пользователÑ", - 'recoverPass' => "Ð¡Ð±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ: Шаг %s из 2", - 'newPass' => "New Password", - 'tokenExpires' => "This token expires in %s.", + 'newPass' => "Ðовый пароль:", + 'confNewPass' => "Подтвердите новый пароль:", + 'passResetHint' => 'ЕÑли вы не знаете пароль от Ñвоей учетной запиÑи, пожалуйÑта, поÑетите Ñтраницу ÑброÑа паролÑ.', + // 'tokenExpires' => "This token expires in %s.", // creation - 'register' => "РегиÑтрациÑ: Шаг %s из 2", - 'passConfirm' => "Повторите пароль", + 'passConfirm' => "Повторите пароль:", // dashboard - 'ipAddress' => "[IP-Adress]: ", - 'lastIP' => "[last used IP]: ", - // 'myAccount' => "[My Account]", - // 'editAccount' => "[Simply use the forms below to update your account information]", - // 'viewPubDesc' => '[View your Public Description in your Profile Page]', + 'ipAddress' => "IP-Adress: ", + 'lastIP' => "last used IP: ", + // 'myAccount' => "My Account", + // 'editAccount' => "ИÑпользуйте нижеприведённую форму, чтобы обновить информацию о вашей учетной запиÑи.", + // 'viewPubDesc' => 'View your Public Description in your Profile Page', // bans - 'accBanned' => "[This Account was closed]", - 'bannedBy' => "[Banned by]: ", - 'reason' => "[Reason]: ", - 'ends' => "[Ends on]: ", - 'permanent' => "[The ban is permanent]", - 'noReason' => "[No reason was given.]", + 'accBanned' => "This Account was closed", + 'bannedBy' => "Banned by: ", + 'reason' => "Reason: ", + 'ends' => "Ends on: ", + 'permanent' => "The ban is permanent", + 'noReason' => "No reason was given.", // form-text 'emailInvalid' => "ÐедопуÑтимый Ð°Ð´Ñ€ÐµÑ email.", // message_emailnotvalid - 'emailNotFound' => "The email address you entered is not associated with any account.

        If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", - 'createAccSent' => "An email was sent to %s. Simply follow the instructions to create your account.", - 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", - 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password.", - 'accActivated' => 'Your account has been activated.
        Proceed to sign in', 'userNotFound' => "The username you entered does not exists.", 'wrongPass' => "That password is not vaild.", - // 'accInactive' => "That account has not yet been confirmed active.", - 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", - 'signupExceeded'=> "The maximum number of signups from this IP has been exceeded. Please try again in %s.", + // 'accInactive' => "That account has not yet been confirmed active.", 'errNameLength' => "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð½Ðµ должно быть короче 4 Ñимволов.", // message_usernamemin 'errNameChars' => "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¼Ð¾Ð¶ÐµÑ‚ Ñодержать только буквы и цифры.", // message_usernamenotvalid 'errPassLength' => "Ваш пароль должен ÑоÑтоÑть минимум из 6 знаков.", // message_passwordmin 'passMismatch' => "The passwords you entered do not match.", - 'nameInUse' => "That username is already taken.", + 'nameInUse' => "That username is already in use.", 'mailInUse' => "That email is already registered to an account.", - 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Пароли не Ñовпадают.", // message_passwordsdonotmatch - 'newPassDiff' => "Прежний и новый пароли не должны Ñовпадать." // message_newpassdifferent + 'newPassDiff' => "Прежний и новый пароли не должны Ñовпадать.", // message_newpassdifferent + 'newMailDiff' => "Прежний и новый e-mail адреÑа не должны Ñовпадать.", // message_newemaildifferent + + // settings + 'settings' => "Параметры учетной запиÑи", + 'settingsNote' => "ИÑпользуйте нижеприведённую форму, чтобы обновить информацию о вашей учетной запиÑи.", + 'tabGeneral' => "Общее", + 'tabPersonal' => "ПерÑональное", + 'tabCommunity' => "СообщеÑтво", + 'tabPremium' => "Premium", + 'preferences' => "ПредпочтениÑ", + 'modelviewer' => "3D-проÑмотр", + 'mvNote' => "Модель перÑонажа по умолчанию:", + 'lists' => "СпиÑки", + 'listsNote' => "Показывать ID в поддерживаемых ÑпиÑках", + 'announcements' => "ОбъÑвлениÑ", + 'annNote' => "УдалÑет данные о закрытых объÑвлениÑÑ…, поÑле чего вы Ñможете их увидеть Ñнова.", + 'purge' => "СброÑить", + 'curPass' => "Текущий пароль:", + 'globalLogout' => "Выйти на вÑех уÑтройÑтвах и/или браузерах ", + 'curEmail' => "Текущий Ð°Ð´Ñ€ÐµÑ email:", + 'newEmail' => "Ðовый Ð°Ð´Ñ€ÐµÑ email:", + 'userPage' => "Профиль пользователÑ", + 'publicDesc' => "ОпиÑание", + 'publicDescNote'=> 'РаÑÑкажите нам о Ñебе и ваших перÑонажах из World of Warcraft. Ð’Ñе, что вы напишите, будет отображатьÑÑ Ð½Ð° Ñтраница пользователÑ.', + 'forums' => "Форум", + 'signature' => "ПодпиÑÑŒ", + 'signatureNote' => "Этой подпиÑью будут ÑопровождатьÑÑ Ð²Ñе ÑообщениÑ, опубликованные вами на форумах Ñайта.", + 'usernameNote' => "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð´Ð¾Ð»Ð¶Ð½Ð¾ включать не менее 4 и не более 16 Ñимволов, и может быть изменено один раз в течение %s. Специальные Ñимволы не допуÑкаютÑÑ.", + 'curName' => "Текущее Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", + 'newName' => "Ðовое Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", + 'accDelete' => "Удалить учетную запиÑÑŒ", + 'accDeleteNote' => 'ЕÑли вы хотите удалить Ñвою учетную запиÑÑŒ и вÑе, ÑвÑзанные Ñ Ð½ÐµÐ¹ перÑональные данные, перейдите на Ñтраницу ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ð¾Ð¹ запиÑи.', + 'avatar' => "Ðватар", + 'avatarNote' => "Ðватар будет Ñопровождать вÑе ÑообщениÑ, опубликованные вами на форумах.", + 'avWowIcon' => "Значок из World of Warcraft", + 'avWowIconNote' => 'например, INV_Axe_54
        Совет: Чтобы найти название значка, дважды щелкните большом значке, когда вы Ñмотрите Ñтраницу Ñ Ð¾Ð¿Ð¸Ñанием предмета или заклинаниÑ. Затем вÑтавьте Ñту Ñтроку в документ.', + 'avIconName' => "Ðазвание иконки:", + 'none' => "Ðет", + 'preview' => "Предварительный проÑмотр", + 'custom' => "Свой", + 'premiumStatus' => "Premium подпиÑка", + 'status' => "СтатуÑ", + 'active' => "Ðктивно", + 'inactive' => "Ðеактивно", + 'activeCD' => "Ð’Ñ‹ должны подождать до %s, чтобы Ñнова изменить Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ.", + 'updateMessage' => array( + 'general' => "ÐŸÑ€ÐµÐ´Ð¿Ð¾Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ñ‹.", + 'community' => "ОпиÑание и подпиÑÑŒ уÑпешно обновлены.", + 'personal' => "ПиÑьмо Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸ÐµÐ¼ было отправлено на %s.", + 'username' => 'Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¾ Ñ %1$s на %2$s.', + 'avNotFound' => "Иконка не найдена.", + 'avSuccess' => "Ðватар уÑпешно обновлен. ПоздравлÑем ВаÑ!", + 'avNoChange' => "Ðе произошло никаких изменений.", + 'av1stUser' => "Ðватар, выбранный Вами, уникален! /ура", + 'avNthUser' => "Примите во внимание, что такой значок уже иÑпользуетÑÑ %d пользователÑми." + ), + 'inputbox' => array( + 'head' => array( + 'success' => "УÑпешно", + 'error' => "УпÑ!", + 'register' => "РегиÑтрациÑ: Шаг %s из 2", + 'recoverUser' => "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¸Ð¼ÐµÐ½Ð¸ пользователÑ", + 'recoverPass' => "Ð¡Ð±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ: Шаг %s из 2", + 'resendMail' => "Вновь выÑлать верификационное пиÑьмо", + 'signin' => "Войти в вашу учетную запиÑÑŒ" + ), + 'message' => array( + 'accActivated' => 'Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была активирована.
        Перейдите к входу', + 'resendMail' => "ЕÑли вы зарегиÑтрировалиÑÑŒ, но не получили проверочного пиÑьма, пожалуйÑта, введите ваш email Ð°Ð´Ñ€ÐµÑ Ð½Ð¸Ð¶Ðµ и подтвердите отправку формы. (ПожалуйÑта, удоÑтоверьтеÑÑŒ, что Ð’Ñ‹ проверили папку Ñо Ñпамом и/или корзину Вашего почтового ÑервиÑа)", + 'mailChangeOk' => "Ваш Ð°Ð´Ñ€ÐµÑ Ñлектронной почты был уÑпешно изменен.", + 'mailRevertOk' => "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° изменение адреÑа Ñлектронной почты был отменен/отозван.", + 'passChangeOk' => "Ваш пароль был уÑпешно изменен.", + 'deleteAccSent' => "ПиÑьмо Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸ÐµÐ¼ было отправлено на %s.", + 'deleteOk' => "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была уÑпешно удалена. ÐадеемÑÑ ÑƒÐ²Ð¸Ð´ÐµÑ‚ÑŒ Ð²Ð°Ñ Ñнова!

        Теперь вы можете закрыть Ñто окно.", + 'createAccSent' => 'ПиÑьмо Ñ Ð¸Ð½ÑтрукциÑми Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи было отправлено на Ð°Ð´Ñ€ÐµÑ %s/b>. Следуйте инÑтрукциÑм, Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð»Ð¶ÐµÐ½Ð¸Ñ Ñ€ÐµÐ³Ð¸Ñтрации.

        ЕÑли вы не получили пиÑьмо Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ, нажмите здеÑÑŒ, чтобы отправить его повторно.
        ', + 'recovUserSent' => "ПиÑьмо Ñ Ð¸Ð½ÑтрукциÑми Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи было отправлено на Ð°Ð´Ñ€ÐµÑ %s/b>. ПроÑто Ñледуйте инÑтрукциÑм Ð´Ð»Ñ Ð²Ð¾ÑÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ пользователÑ.", + 'recovPassSent' => "ПиÑьмо Ñ Ð¸Ð½ÑтрукциÑми Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи было отправлено на Ð°Ð´Ñ€ÐµÑ %s/b>. ПроÑто Ñледуйте инÑтрукциÑм Ð´Ð»Ñ ÑброÑа паролÑ." + ), + 'error' => array( + 'mailTokenUsed' => 'Этот ключ Ð´Ð»Ñ Ñмены email уже был иÑпользован или недейÑтвителен. ПоÑетите вашу Ñтраницу наÑтроек учетной запиÑи, чтобы попробовать Ñнова.', + 'passTokenUsed' => 'Этот ключ Ð´Ð»Ñ Ñмены Ð¿Ð°Ñ€Ð¾Ð»Ñ ÑƒÐ¶Ðµ был иÑпользован или недейÑтвителен. ПоÑетите вашу Ñтраницу наÑтроек учетной запиÑи, чтобы попробовать Ñнова.', + 'passTokenLost' => "Ключ не был получен. ЕÑли вы ÑброÑили пароль по ÑÑылке из пиÑьма, отправленного на email, пожалуйÑта, Ñкопируйте URL целиком и вÑтавьте в адреÑную Ñтроку (Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ ÐºÐ»ÑŽÑ‡, указанный в конце ÑÑылки).", + 'isRecovering' => "Эта ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ уже воÑÑтанавливаетÑÑ. Следуйте инÑтрукциÑм в пиÑьме или дождитеÑÑŒ иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ Ñрока дейÑÑ‚Ð²Ð¸Ñ Ñ‚Ð¾ÐºÐµÐ½Ð° через %s.", + 'loginExceeded' => "ДоÑтигнуто макÑимальное количеÑтво попыток входа Ñ Ñтого IP. ПожалуйÑта, попробуйте Ñнова через %s.", + 'signupExceeded' => "ДоÑтигнуто макÑимальное количеÑтво региÑтраций Ñ Ñтого IP. ПожалуйÑта, попробуйте Ñнова через %s.", + // 'emailNotFound' => "The email address you entered is not associated with any account.

        If you forgot the email you registered your account with email CFG_CONTACT_EMAIL for assistance.", + 'emailNotFound' => "Этот Ð°Ð´Ñ€ÐµÑ Ñлектронной почты не найден в нашей ÑиÑтеме." + ) + ) ), 'user' => array( 'notFound' => "Пользователь \"%s\" не найден!", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "Уровень %d" ), 'privileges' => array( - 'main' => "ЗдеÑÑŒ на AoWoW вы можете зарабатывать репутацию. ОÑновной иÑточник Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñ€ÐµÐ¿ÑƒÑ‚Ð°Ñ†Ð¸Ð¸ — увеличение рейтинга ваших комментариев другими пользователÑми.

        Ð ÐµÐ¿ÑƒÑ‚Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¸Ð¼ÐµÑ€Ð½Ð¾ измерÑет количеÑтво вашего вклада в ÑообщеÑтво.

        По мере того, как вы зарабатываете репутацию, вы получаете доверие ÑообщеÑтва и оÑобые привилегии. Полный ÑпиÑок привилегий раÑположен ниже.", + 'main' => "ЗдеÑÑŒ на AoWoW вы можете зарабатывать репутацию. ОÑновной иÑточник Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñ€ÐµÐ¿ÑƒÑ‚Ð°Ñ†Ð¸Ð¸ — увеличение рейтинга ваших комментариев другими пользователÑми.

        Ð ÐµÐ¿ÑƒÑ‚Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¸Ð¼ÐµÑ€Ð½Ð¾ измерÑет количеÑтво вашего вклада в ÑообщеÑтво.

        По мере того, как вы зарабатываете репутацию, вы получаете доверие ÑообщеÑтва и оÑобые привилегии. Полный ÑпиÑок привилегий раÑположен ниже.", 'privilege' => "ПривилегиÑ", 'privileges' => "Привилегии", 'requiredRep' => "Ðеобходима репутациÑ", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 0bfd5a87..4d710049 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -28,7 +28,7 @@ $lang = array( 'numSQL' => "æ•°æ®åº“查询次数", 'timeSQL' => "æ•°æ®åº“查询时间", 'noJScript' => '本站点基于JavaScript。
        请在你的æµè§ˆå™¨é‡Œå¯ç”¨JavaScript。', - 'userProfiles' => "我的简介", + // 'userProfiles' => "我的简介", 'pageNotFound' => "%sä¸å­˜åœ¨ã€‚", 'gender' => "性别", 'sex' => [null, "男性", "女性"], @@ -40,12 +40,13 @@ $lang = array( 'side' => "阵è¥ï¼š", 'related' => "相关", 'contribute' => "贡献", - // 'replyingTo' => "The answer to a comment from", + // 'replyingTo' => "The answer to a comment from", 'submit' => "æäº¤", + 'save' => 'ä¿å­˜', 'cancel' => "å–æ¶ˆ", 'rewards' => "奖励", 'gains' => "获得", - 'login' => "登录", + // 'login' => "登录", 'forum' => "论å›", 'siteRep' => "站点声望:", 'yourRepHistory'=> "您的声望历å²", @@ -132,6 +133,7 @@ $lang = array( 'colon' => ':', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", + 'dateFmtUntil' => "Yå¹´n月jæ—¥", 'timeAgo' => '%s之å‰', 'nfSeparators' => [',', '.'], @@ -892,7 +894,7 @@ $lang = array( ), 'account' => array( 'title' => "æ•°æ®åº“è´¦å·", - 'email' => "电å­é‚®ç®±åœ°å€", + 'email' => "邮箱地å€", 'continue' => "ç»§ç»­", 'groups' => array( -1 => "æ— ", "测试员", "管ç†å‘˜", "编辑器", "管ç†å‘˜", "官僚", @@ -900,7 +902,6 @@ $lang = array( "截å±ç®¡ç†å™¨", "视频管ç†å‘˜", "API伙伴", "等待中" ), // signIn - 'doSignIn' => "登录你的数æ®åº“è´¦å·", 'signIn' => "登录", 'user' => "用户å", 'pass' => "密ç ", @@ -909,25 +910,22 @@ $lang = array( 'forgotUser' => "用户å", 'forgotPass' => "密ç ", 'accCreate' => '没有账å·ï¼ŸçŽ°åœ¨åˆ›å»ºä¸€ä¸ªï¼', - 'resendMail' => "釿–°å‘é€éªŒè¯é‚®ä»¶", - 'resendHint' => "[If you registered but did not receive a verification email, enter your email address below and submit the form. (Please be sure to check your spam or trash folders to make sure the email didn't accidentally get put in the wrong place!)]", // recovery - 'recoverUser' => "用户å需求", - 'recoverPass' => "密ç é‡ç½®ï¼šæ­¥éª¤ %s / 2", - 'newPass' => "新密ç ", - 'tokenExpires' => "此令牌将在%s过期。", + 'newPass' => "新密ç ï¼š", + 'confNewPass' => "确认密ç ï¼š", + 'passResetHint' => '如果您忘记了当å‰å¯†ç ï¼Œè¯·è®¿é—® 密ç é‡ç½®é¡µé¢ 进行é‡ç½®ã€‚', + // 'tokenExpires' => "此令牌将在%s过期。", // creation - 'register' => "注册 - 步骤 %s / 2", - 'passConfirm' => "确认密ç ", + 'passConfirm' => "确认密ç ï¼š", // dashboard 'ipAddress' => "IP地å€ï¼š", 'lastIP' => "上次使用IP地å€ï¼š", - // 'myAccount' => "我的账å·", - // 'editAccount' => "åªéœ€ä½¿ç”¨ä»¥ä¸‹è¡¨æ ¼å°±èƒ½æ›´æ–°ä½ çš„叿ˆ·ä¿¡æ¯", - // 'viewPubDesc' => 'åœ¨ä½ çš„ç®€ä»‹é¡µé¢æŸ¥çœ‹ä½ å…¬å…±æè¿°', + // 'myAccount' => "我的账å·", + // 'editAccount' => "åªéœ€ä½¿ç”¨ä»¥ä¸‹è¡¨æ ¼å°±èƒ½æ›´æ–°ä½ çš„叿ˆ·ä¿¡æ¯", + // 'viewPubDesc' => 'åœ¨ä½ çš„ç®€ä»‹é¡µé¢æŸ¥çœ‹ä½ å…¬å…±æè¿°', // bans 'accBanned' => "这个账å·å·²è¢«å…³é—­", @@ -939,25 +937,106 @@ $lang = array( // form-text 'emailInvalid' => "该电å­é‚®ä»¶åœ°å€æ— æ•ˆã€‚", // message_emailnotvalid - 'emailNotFound' => "你输入的电å­é‚®ä»¶åœ°å€ä¸Žä»»ä½•叿ˆ·ä¸å…³è”。

        如果您忘记了使用哪个电å­é‚®ä»¶æ³¨å†Œäº†æ‚¨çš„叿ˆ·ï¼Œè¯·å‘é€ç”µå­é‚®ä»¶è‡³CFG_CONTACT_EMAIL寻求帮助。", - 'createAccSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªéœ€æŒ‰ç…§è¯´æ˜Žåˆ›å»ºä½ çš„叿ˆ·ã€‚", - 'recovUserSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªéœ€æŒ‰ç…§è¯´æ˜Žæ¢å¤ä½ çš„用户å。", - 'recovPassSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªéœ€æŒ‰ç…§è¯´æ˜Žé‡ç½®ä½ çš„密ç ã€‚", - 'accActivated' => 'ä½ çš„å¸æˆ·å·²è¢«æ¿€æ´»ã€‚
        继续登录', 'userNotFound' => "输入的用户åä¸å­˜åœ¨ã€‚", 'wrongPass' => "å¯†ç æ— æ•ˆã€‚", - // 'accInactive' => "è¯¥å¸æˆ·å°šæœªç¡®è®¤æ¿€æ´»ã€‚", - 'loginExceeded' => "这个IP最大登录次数已超过。请在%såŽå†æ¬¡å°è¯•。", - 'signupExceeded'=> "这个IP最大注册次数已超过。请在%såŽå†æ¬¡å°è¯•。", + // 'accInactive' => "è¯¥å¸æˆ·å°šæœªç¡®è®¤æ¿€æ´»ã€‚", 'errNameLength' => "你的用户å必须至少4个字符长度。", // message_usernamemin 'errNameChars' => "你的用户ååªèƒ½åŒ…å«å­—æ¯å’Œæ•°å­—。", // message_usernamenotvalid 'errPassLength' => "你的密ç å¿…须至少6个字符长度。", // message_passwordmin 'passMismatch' => "你输入的密ç ä¸åŒ¹é…。", 'nameInUse' => "用户å已被å ç”¨ã€‚", 'mailInUse' => "该电å­é‚®ä»¶å·²æ³¨å†Œåˆ°ä¸€ä¸ªå¸æˆ·ã€‚", - 'isRecovering' => "æ­¤å¸æˆ·å·²æ¢å¤ã€‚按照电å­é‚®ä»¶ä¸­çš„说明或等待%såŽä»¤ç‰Œè¿‡æœŸã€‚", 'passCheckFail' => "密ç ä¸åŒ¹é…。", // message_passwordsdonotmatch - 'newPassDiff' => "你的新密ç å¿…须与以å‰çš„密ç ä¸åŒã€‚" // message_newpassdifferent + 'newPassDiff' => "你的新密ç å¿…须与以å‰çš„密ç ä¸åŒã€‚", // message_newpassdifferent + 'newMailDiff' => "您的新邮箱地å€å¿…é¡»ä¸åŒäºŽæ—§åœ°å€ã€‚", // message_newemaildifferent + + // settings + 'settings' => "è´¦å·è®¾ç½®", + 'settingsNote' => "使用下列表格就能å‡çº§æ‚¨çš„è´¦å·ä¿¡æ¯ã€‚", + 'tabGeneral' => "常规", + 'tabPersonal' => "个人", + 'tabCommunity' => "社区", + 'tabPremium' => "高级会员", + 'preferences' => "å好", + 'modelviewer' => "模型查看器", + 'mvNote' => "默认角色模型:", + 'lists' => "清å•", + 'listsNote' => "在支æŒçš„æ¸…å•中显示ID", + 'announcements' => "公告", + 'annNote' => "清空您已关闭的公告数æ®ï¼Œä»¥ä¾¿æ—¥åŽå†æ¬¡æµè§ˆã€‚", + 'purge' => "清除", + 'curPass' => "当å‰å¯†ç ï¼š", + 'globalLogout' => "从所有其他æµè§ˆå™¨/设备中登出当å‰è´¦æˆ·", + 'curEmail' => "当å‰é‚®ç®±åœ°å€ï¼š", + 'newEmail' => "新邮箱地å€ï¼š", + 'userPage' => "用户页", + 'publicDesc' => "公开æè¿°", + 'publicDescNote'=> '跟我们说说您自己和您的 WoW 角色å§ã€‚您输入的信æ¯ä¼šæ˜¾ç¤ºåœ¨æ‚¨çš„ 用户页 上。', + 'forums' => "论å›", + 'signature' => "ç­¾å", + 'signatureNote' => "ç­¾åæ˜¾ç¤ºåœ¨è®ºå›å‘帖的下方。", + 'usernameNote' => "ç”¨æˆ·åæ¯%såªèƒ½æ›´æ”¹ä¸€æ¬¡ï¼Œé•¿åº¦éœ€ä¸º4-16个字符,ä¸å…许特殊字符。", + 'curName' => "当å‰ç”¨æˆ·å:", + 'newName' => "新用户å:", + 'accDelete' => "删除账户", + 'accDeleteNote' => 'å¦‚æžœæ‚¨æƒ³å½»åº•åˆ é™¤æ‚¨çš„è´¦æˆ·ä»¥åŠæ‰€æœ‰ä¸ªäººä¿¡æ¯ï¼Œè¯·è®¿é—®æˆ‘们的 账户删除页é¢ã€‚', + 'avatar' => "人物", + 'avatarNote' => "您的头åƒå°†æ˜¾ç¤ºåœ¨æ‚¨æ‰€æœ‰è®ºå›å¸–å­çš„æ—è¾¹ã€‚", + 'avWowIcon' => "魔兽世界图标", + 'avWowIconNote' => '如INV_Axe_54
        å°å»ºè®®ï¼šè¦æ‰¾åˆ°å›¾æ ‡çš„å字,åªè¦åœ¨æµè§ˆå›¾æ ‡ 或 spell 页颿—¶ åŒå‡»å¤§å›¾æ ‡ï¼ŒæŽ¥ç€å¤åˆ¶ç²˜è´´åˆ°ä¸Šé¢ã€‚', + 'avIconName' => "图标å:", + 'none' => "æ— ", + 'preview' => "预览", + 'custom' => "自定义", + 'premiumStatus' => "高级会员订阅", + 'status' => "状æ€", + 'active' => "激活", + 'inactive' => "未激活", + 'activeCD' => "您必须等到%såŽæ‰èƒ½å†æ¬¡æ›´æ”¹ç”¨æˆ·å。", + 'updateMessage' => array( + 'general' => "已更新您的å好设置。", + 'community' => "å·²æˆåŠŸæ›´æ–°æ‚¨çš„å…¬å¼€æè¿°ä¸Žè®ºå›ç­¾å。", + 'personal' => "确认邮件已å‘é€åˆ° %s。", + 'username' => '用户å已从 %1$s 更改为 %2$s。', + 'avNotFound' => "图标未找到", + 'avSuccess' => "æ‚¨çš„å¤´åƒæ›´æ–°æˆåŠŸã€‚", + 'avNoChange' => "没有åšè¿‡æ”¹å˜â€‹", + 'av1stUser' => "æ­å–œé€‰åˆ°äº†æœ€ç‹¬ç‰¹çš„é‚£ä¸€ä¸ªï¼ /å¹²æ¯", + 'avNthUser' => "​æç¤ºï¼Œæ‚¨çš„图标也被%d其他用户使用。" + ), + 'inputbox' => array( + 'head' => array( + 'success' => "æˆåŠŸ", + 'error' => "哦嚯ï¼", + 'register' => "注册 - 步骤 %s / 2", + 'recoverUser' => "用户å需求", + 'recoverPass' => "密ç é‡ç½®ï¼šæ­¥éª¤ %s / 2", + 'resendMail' => "釿–°å‘é€éªŒè¯é‚®ä»¶", + 'signin' => "登录你的数æ®åº“è´¦å·" + ), + 'message' => array( + 'accActivated' => 'ä½ çš„å¸æˆ·å·²è¢«æ¿€æ´»ã€‚
        继续登录', + 'resendMail' => "如果您已注册但未收到验è¯é‚®ä»¶ï¼Œè¯·åœ¨ä¸‹æ–¹è¾“入您的邮箱地å€å¹¶æäº¤è¡¨å•。(请务必检查您的垃圾邮件或回收站文件夹,以确ä¿é‚®ä»¶æ²¡æœ‰è¢«è¯¯æ”¾åˆ°é”™è¯¯çš„ä½ç½®ï¼ï¼‰", + 'mailChangeOk' => "您的邮箱地å€å·²æˆåŠŸæ›´æ”¹ã€‚", + 'mailRevertOk' => "æ‚¨çš„é‚®ç®±æ›´æ”¹è¯·æ±‚å·²è¢«å–æ¶ˆ/撤销。", + 'passChangeOk' => "您的密ç å·²æˆåŠŸæ›´æ”¹ã€‚", + 'deleteAccSent' => "å·²å‘ %s å‘é€äº†ä¸€å°å¸¦æœ‰ç¡®è®¤é“¾æŽ¥çš„邮件。", + 'deleteOk' => "您的账户已æˆåŠŸåˆ é™¤ã€‚å¸Œæœ›ä¸ä¹…åŽèƒ½å†æ¬¡è§åˆ°æ‚¨ï¼

        您现在å¯ä»¥å…³é—­æ­¤çª—å£ã€‚", + 'createAccSent' => '电å­é‚®ä»¶å‘é€åˆ°%s。åªè¯·æŒ‰ç…§è¯´æ˜Žåˆ›å»ºæ‚¨çš„账户。

        如果您没有收到验è¯é‚®ä»¶ï¼Œç‚¹å‡»è¿™é‡Œé‡æ–°å‘é€ã€‚', + 'recovUserSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªè¯·æŒ‰ç…§è¯´æ˜Žæ¢å¤æ‚¨çš„用户å。", + 'recovPassSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªè¯·æŒ‰ç…§è¯´æ˜Žé‡ç½®æ‚¨çš„密ç ã€‚" + ), + 'error' => array( + 'mailTokenUsed' => 'è¯¥é‚®ç®±æ›´æ”¹å¯†é’¥å·²è¢«ä½¿ç”¨ï¼Œæˆ–ä¸æ˜¯æœ‰æ•ˆå¯†é’¥ã€‚请访问您的账户设置页é¢é‡æ–°å°è¯•。', + 'passTokenUsed' => 'è¯¥å¯†ç æ›´æ”¹å¯†é’¥å·²è¢«ä½¿ç”¨ï¼Œæˆ–䏿˜¯æœ‰æ•ˆå¯†é’¥ã€‚请访问您的账户设置页é¢é‡æ–°å°è¯•。', + 'passTokenLost' => "未æä¾›ä»¤ç‰Œã€‚如果您在邮件中收到é‡ç½®å¯†ç é“¾æŽ¥ï¼Œè¯·å°†æ•´ä¸ªç½‘å€ï¼ˆåŒ…括最åŽçš„令牌)å¤åˆ¶å¹¶ç²˜è´´åˆ°æµè§ˆå™¨åœ°å€æ ä¸­ã€‚", + 'isRecovering' => "æ­¤å¸æˆ·å·²æ¢å¤ã€‚按照电å­é‚®ä»¶ä¸­çš„说明或等待%såŽä»¤ç‰Œè¿‡æœŸã€‚", + 'loginExceeded' => "这个IP最大登录次数已超过。请在%såŽå†æ¬¡å°è¯•。", + 'signupExceeded' => "这个IP最大注册次数已超过。请在%såŽå†æ¬¡å°è¯•。", + // 'emailNotFound' => "你输入的电å­é‚®ä»¶åœ°å€ä¸Žä»»ä½•叿ˆ·ä¸å…³è”。

        如果您忘记了使用哪个电å­é‚®ä»¶æ³¨å†Œäº†æ‚¨çš„叿ˆ·ï¼Œè¯·å‘é€ç”µå­é‚®ä»¶è‡³CFG_CONTACT_EMAIL寻求帮助。", + 'emailNotFound' => "未在我们的系统中找到该电å­é‚®ä»¶åœ°å€ã€‚" + ) + ) ), 'user' => array( 'notFound' => "用户 \"%s\" 未找到", @@ -1241,7 +1320,7 @@ $lang = array( 'floorN' => "[Level %d]" ), 'privileges' => array( - 'main' => "在我们的网站上,你å¯ä»¥é€šè¿‡ 声望. æ¥èŽ·å–特æƒã€‚获å–声望的主è¦é€”径是获得评论的赞åŒã€‚

        因此,声望是衡é‡ä½ å¯¹ç¤¾åŒºçš„贡献程度的一个大致指标。

        éšç€å£°æœ›çš„积累,你将获得社区的信任,并被赋予é¢å¤–的特æƒã€‚以下是完整的特æƒåˆ—表。", + 'main' => "在我们的网站上,你å¯ä»¥é€šè¿‡ 声望. æ¥èŽ·å–特æƒã€‚获å–声望的主è¦é€”径是获得评论的赞åŒã€‚

        因此,声望是衡é‡ä½ å¯¹ç¤¾åŒºçš„贡献程度的一个大致指标。

        éšç€å£°æœ›çš„积累,你将获得社区的信任,并被赋予é¢å¤–的特æƒã€‚以下是完整的特æƒåˆ—表。", 'privilege' => "特æƒ", 'privileges' => "特æƒ", 'requiredRep' => "需è¦å£°æœ›", diff --git a/pages/account.php b/pages/account.php deleted file mode 100644 index 2b39a866..00000000 --- a/pages/account.php +++ /dev/null @@ -1,465 +0,0 @@ - [false], - 'forgotpassword' => [false], - 'forgotusername' => [false] - ); - - protected $user = ''; - protected $error = ''; - protected $next = ''; - - protected $lvTabs = []; - protected $banned = []; - - protected $_get = array( - 'token' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'next' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - ); - - protected $_post = array( - 'username' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\GenericPage::checkTextLine'], - 'token' => ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW], - 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AccountPage::rememberCallback'], - 'email' => ['filter' => FILTER_SANITIZE_EMAIL] - ); - - public function __construct($pageCall, $pageParam) - { - if ($pageParam) - $this->category = [$pageParam]; - - parent::__construct($pageCall, $pageParam); - - if ($pageParam) - { - // requires auth && not authed - if ($this->validCats[$pageParam][0] && !User::isLoggedIn()) - $this->forwardToSignIn('account='.$pageParam); - // doesn't require auth && authed - else if (!$this->validCats[$pageParam][0] && User::isLoggedIn()) - header('Location: ?account', true, 302); // goto dashboard - } - } - - protected static function rememberCallback($val) - { - return $val == 'yes' ? $val : null; - } - - protected function generateContent() - { - if (!$this->category) - { - $this->createDashboard(); - return; - } - - switch ($this->category[0]) - { - case 'forgotpassword': - if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) - { - if (Cfg::get('ACC_EXT_RECOVER_URL')) - header('Location: '.Cfg::get('ACC_EXT_RECOVER_URL'), true, 302); - else - $this->error(); - } - - $this->tpl = 'acc-recover'; - $this->resetPass = false; - - if ($this->createRecoverPass($nStep)) // location-header after final step - header('Location: ?account=signin', true, 302); - - $this->head = sprintf(Lang::account('recoverPass'), $nStep); - break; - case 'forgotusername': - if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) - { - if (Cfg::get('ACC_EXT_RECOVER_URL')) - header('Location: '.Cfg::get('ACC_EXT_RECOVER_URL'), true, 302); - else - $this->error(); - } - - $this->tpl = 'acc-recover'; - $this->resetPass = false; - - if ($this->_post['email']) - { - if (!Util::isValidEmail($this->_post['email'])) - $this->error = Lang::account('emailInvalid'); - else if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE email = ?', $this->_post['email'])) - $this->error = Lang::account('emailNotFound'); - else if ($err = $this->doRecoverUser()) - $this->error = $err; - else - $this->text = sprintf(Lang::account('recovUserSent'). $this->_post['email']); - } - - $this->head = Lang::account('recoverUser'); - break; - case 'signup': - if (!Cfg::get('ACC_ALLOW_REGISTER')) - $this->error(); - - if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) - { - if (Cfg::get('ACC_EXT_CREATE_URL')) - header('Location: '.Cfg::get('ACC_EXT_CREATE_URL'), true, 302); - else - $this->error(); - } - - $this->tpl = 'acc-signUp'; - $nStep = 1; - if ($this->_post['username'] || $this->_post['password'] || $this->_post['c_password'] || $this->_post['email']) - { - if ($err = $this->doSignUp()) - $this->error = $err; - else - { - $nStep = 1.5; - $this->text = sprintf(Lang::account('createAccSent'), $this->_post['email']); - } - } - else if ($this->_get['token'] && ($newId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE status = ?d AND token = ?', ACC_STATUS_NEW, $this->_get['token']))) - { - $nStep = 2; - DB::Aowow()->query('UPDATE ?_account SET status = ?d, statusTimer = 0, token = 0, userGroups = ?d WHERE token = ?', ACC_STATUS_OK, U_GROUP_NONE, $this->_get['token']); - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, ?d + 1, UNIX_TIMESTAMP() + ?d)', User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); - - $this->text = sprintf(Lang::account('accActivated'), $this->_get['token']); - } - else - $this->next = $this->getNext(); - - $this->head = sprintf(Lang::account('register'), $nStep); - break; - default: - header('Location: '.$this->getNext(true), true, 302); - break; - } - } - - protected function generateTitle() - { - $this->title = [Lang::account('title')]; - } - - protected function generatePath() { } - - private function createDashboard() - { - if (!User::isLoggedIn()) - $this->forwardToSignIn('account'); - - $user = DB::Aowow()->selectRow('SELECT * FROM ?_account WHERE `id` = ?d', User::$id); - $bans = DB::Aowow()->select('SELECT ab.*, a.`username`, ab.`id` AS ARRAY_KEY FROM ?_account_banned ab LEFT JOIN ?_account a ON a.`id` = ab.`staffId` WHERE ab.`userId` = ?d', User::$id); - - /***********/ - /* Infobox */ - /***********/ - - $infobox = []; - $infobox[] = Lang::user('joinDate'). Lang::main('colon').'[tooltip name=joinDate]'. date('l, G:i:s', $user['joinDate']). '[/tooltip][span class=tip tooltip=joinDate]'. date(Lang::main('dateFmtShort'), $user['joinDate']). '[/span]'; - $infobox[] = Lang::user('lastLogin').Lang::main('colon').'[tooltip name=lastLogin]'.date('l, G:i:s', $user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.date(Lang::main('dateFmtShort'), $user['prevLogin']).'[/span]'; - $infobox[] = Lang::account('lastIP').Lang::main('colon').$user['prevIP']; - $infobox[] = Lang::account('email'). Lang::main('colon').$user['email']; - - $groups = []; - foreach (Lang::account('groups') as $idx => $key) - if ($idx >= 0 && $user['userGroups'] & (1 << $idx)) - $groups[] = (!fMod(count($groups) + 1, 3) ? '[br]' : null).Lang::account('groups', $idx); - $infobox[] = Lang::user('userGroups').Lang::main('colon').($groups ? implode(', ', $groups) : Lang::account('groups', -1)); - $infobox[] = Util::ucFirst(Lang::main('siteRep')).Lang::main('colon').User::getReputation(); - - - $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; - - /*************/ - /* Ban Popup */ - /*************/ - - foreach ($bans as $b) - { - if (!($b['typeMask'] & (ACC_BAN_TEMP | ACC_BAN_PERM)) || ($b['end'] && $b['end'] <= time())) - continue; - - $this->banned = array( - 'by' => [$b['staffId'], $b['username']], - 'end' => $b['end'], - 'reason' => $b['reason'] - ); - - break; // one is enough - } - - /************/ - /* Listview */ - /************/ - - $this->forceTabs = true; - - // Reputation changelog (params only for comment-events) - if ($repData = DB::Aowow()->select('SELECT action, amount, date AS \'when\', IF(action IN (3, 4, 5), sourceA, 0) AS param FROM ?_account_reputation WHERE userId = ?d', User::$id)) - { - foreach ($repData as &$r) - $r['when'] = date(Util::$dateFormatInternal, $r['when']); - - $this->lvTabs[] = ['reputationhistory', ['data' => $repData]]; - } - - // comments - if ($_ = CommunityContent::getCommentPreviews(['user' => User::$id, 'comments' => true])) - { - // needs foundCount for params - // _totalCount: 377, - // note: $WH.sprintf(LANG.lvnote_usercomments, 377), - - $this->lvTabs[] = ['commentpreview', array( - 'data' => $_, - 'hiddenCols' => ['author'], - 'onBeforeCreate' => '$Listview.funcBox.beforeUserComments' - )]; - } - - // replies - if ($_ = CommunityContent::getCommentPreviews(['user' => User::$id, 'replies' => true])) - { - // needs commentid (parentComment) for data - // needs foundCount for params - // _totalCount: 377, - // note: $WH.sprintf(LANG.lvnote_usercomments, 377), - - $this->lvTabs[] = ['replypreview', array( - 'data' => $_, - 'hiddenCols' => ['author'] - )]; - } - -/* -
        - - - -
        - - -*/ - // claimed characters - // profiles - // own screenshots - // own videos - // own comments (preview) - // articles guides..? - - - // cpmsg change pass messaeg class:failure|success, msg:blabla - } - - private function createRecoverPass(&$step) - { - $step = 1; - - if ($this->_post['email']) // step 1 - { - if (!Util::isValidEmail($this->_post['email'])) - $this->error = Lang::account('emailInvalid'); - else if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE email = ?', $this->_post['email'])) - $this->error = Lang::account('emailNotFound'); - else if ($err = $this->doRecoverPass()) - $this->error = $err; - else - { - $step = 1.5; - $this->text = sprintf(Lang::account('recovPassSent'), $this->_post['email']); - } - } - else if ($this->_get['token']) // step 2 - { - $step = 2; - $this->resetPass = true; - $this->token = $this->_get['token']; - } - else if ($this->_post['token'] && $this->_post['email'] && $this->_post['password'] && $this->_post['c_password']) - { - $step = 2; - $this->resetPass = true; - $this->token = $this->_post['token']; // insecure source .. that sucks; but whats the worst that could happen .. this account cannot be recovered for some minutes - - if ($err = $this->doResetPass()) - $this->error = $err; - else - return true; - } - - return false; - } - - private function doSignUp() - { - // check username - if (!User::isValidName($this->_post['username'], $e)) - return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'); - - // check password - if (!User::isValidPass($this->_post['password'], $e)) - return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); - - if ($this->_post['password'] != $this->_post['c_password']) - return Lang::account('passMismatch'); - - // check email - if (!Util::isValidEmail($this->_post['email'])) - return Lang::account('emailInvalid'); - - // check ip - if (!User::$ip) - return Lang::main('intError'); - - // limit account creation - $ip = DB::Aowow()->selectRow('SELECT `ip`, `count`, `unbanDate` FROM ?_account_bannedips WHERE `type` = 1 AND `ip` = ?', User::$ip); - if ($ip && $ip['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ip['unbanDate'] >= time()) - { - DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip); - return sprintf(Lang::account('signupExceeded'), Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)); - } - - // username taken - if ($_ = DB::Aowow()->SelectCell('SELECT `username` FROM ?_account WHERE (`username` = ? OR `email` = ?) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $this->_post['username'], $this->_post['email'], ACC_STATUS_NEW, ACC_STATUS_NEW)) - return $_ == $this->_post['username'] ? Lang::account('nameInUse') : Lang::account('mailInUse'); - - // create.. - $token = Util::createHash(); - $ok = DB::Aowow()->query('REPLACE INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', - $this->_post['username'], - User::hashCrypt($this->_post['password']), - $this->_post['username'], - $this->_post['email'], - User::$ip, - Lang::getLocale()->value, - U_GROUP_PENDING, - ACC_STATUS_NEW, - Cfg::get('ACC_CREATE_SAVE_DECAY'), - $token - ); - if (!$ok) - return Lang::main('intError'); - - if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return Lang::main('intError2', ['send mail']); - - if ($id = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE token = ?', $token)) - Util::gainSiteReputation($id, SITEREP_ACTION_REGISTER); - - // success:: update ip-bans - if (!$ip || $ip['unbanDate'] < time()) - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (ip, type, count, unbanDate) VALUES (?, 1, 1, UNIX_TIMESTAMP() + ?d)', User::$ip, Cfg::get('ACC_FAILED_AUTH_BLOCK')); - else - DB::Aowow()->query('UPDATE ?_account_bannedips SET count = count + 1, unbanDate = UNIX_TIMESTAMP() + ?d WHERE ip = ? AND type = 1', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip); - } - - private function doRecoverPass() - { - if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token)) - return $_; - - // send recovery mail - if (!Util::sendMail($this->_post['email'], 'reset-password', [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return Lang::main('intError2', ['send mail']); - } - - private function doResetPass() - { - if ($this->_post['password'] != $this->_post['c_password']) - return Lang::account('passCheckFail'); - - if (!Util::isValidEmail($this->_post['email'])) - return Lang::account('emailInvalid'); - - $userData = DB::Aowow()->selectRow('SELECT `id, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', - $this->_post['token'], - $this->_post['email'], - ACC_STATUS_RECOVER_PASS - ); - if (!$userData) - return Lang::account('emailNotFound'); // assume they didn't meddle with the token - - if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash'])) - return Lang::account('newPassDiff'); - - if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_OK, $userData['id'])) - return Lang::main('intError'); - } - - private function doRecoverUser() - { - if ($_ = $this->initRecovery(ACC_STATUS_RECOVER_USER, Cfg::get('ACC_RECOVERY_DECAY'), $token)) - return $_; - - if (!Util::sendMail($this->_post['email'], 'recover-user', [$token], Cfg::get('ACC_RECOVERY_DECAY'))) - return Lang::main('intError2', ['send mail']); - } - - private function initRecovery($type, $delay, &$token) - { - if (!$type) - return Lang::main('intError'); - - // check if already processing - if ($_ = DB::Aowow()->selectCell('SELECT statusTimer - UNIX_TIMESTAMP() FROM ?_account WHERE email = ? AND status <> ?d AND statusTimer > UNIX_TIMESTAMP()', $this->_post['email'], ACC_STATUS_OK)) - return sprintf(Lang::account('isRecovering'), Util::formatTime($_ * 1000)); - - // create new token and write to db - $token = Util::createHash(); - if (!DB::Aowow()->query('UPDATE ?_account SET token = ?, status = ?d, statusTimer = UNIX_TIMESTAMP() + ?d WHERE email = ?', $token, $type, $delay, $this->_post['email'])) - return Lang::main('intError'); - } - - private function getNext($forHeader = false) - { - $next = $forHeader ? '.' : ''; - if ($this->_get['next']) - $next = $this->_get['next']; - else if (isset($_SERVER['HTTP_REFERER']) && strstr($_SERVER['HTTP_REFERER'], '?')) - $next = explode('?', $_SERVER['HTTP_REFERER'])[1]; - - if ($forHeader && !$next) - $next = '.'; - - return ($forHeader && $next != '.' ? '?' : '').$next; - } -} - -?> diff --git a/setup/updates/1758578400_11.sql b/setup/updates/1758578400_11.sql new file mode 100644 index 00000000..e525afa0 --- /dev/null +++ b/setup/updates/1758578400_11.sql @@ -0,0 +1,3 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `debug` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'show ids in lists user option' AFTER `userGroups`, + MODIFY COLUMN `description` text NOT NULL DEFAULT ''; diff --git a/setup/updates/1758578400_12.sql b/setup/updates/1758578400_12.sql new file mode 100644 index 00000000..ad48339e --- /dev/null +++ b/setup/updates/1758578400_12.sql @@ -0,0 +1,11 @@ +ALTER TABLE `aowow_user_ratings` + DROP KEY `FK_acc_co_rate_user`, + DROP FOREIGN KEY `FK_userId`, + DROP PRIMARY KEY; + +ALTER TABLE `aowow_user_ratings` MODIFY `userId` int unsigned NULL; + +ALTER TABLE `aowow_user_ratings` + ADD UNIQUE KEY (`type`,`entry`,`userId`), + ADD KEY `FK_acc_co_rate_user` (`userId`), + ADD CONSTRAINT FK_userId FOREIGN KEY (`userId`) REFERENCES aowow_account(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/static/css/aowow.css b/static/css/aowow.css index e78c690e..9d9f653b 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -502,6 +502,15 @@ a.premium-user-badge { border-radius: 6px; } +/* aowow - imported for account page */ +.box { + padding: 15px; + background: #282828; + border-radius: 6px; + display: table; + margin: 10px 0; +} + .reputation-negative-amount span { /* The whole text has the class; the number is spanned */ color: red; font-weight: bold; @@ -3202,7 +3211,7 @@ td.screenshot-cell:hover img { .text h1 { color: white; - font-size: 19px; + font-size: 19px !important; font-weight: normal; border-bottom: 1px solid #505050; padding: 0 0 5px 0; diff --git a/static/js/account.js b/static/js/account.js new file mode 100644 index 00000000..49a1cfed --- /dev/null +++ b/static/js/account.js @@ -0,0 +1,462 @@ +function pm() { + var pass1 = $('#newpass').val(); + var pass2 = $('#confirmpass').val(); + var + bracket = '', + buff = ''; + + if (pass1 != '' && $WH.trim(pass1).length < 6) + buff = '' + LANG.message_passwordmin + ''; + + if (pass1 != '' && pass2 != '') { + if (buff != '') + buff += '
        '; + + if (pass1 == pass2) + buff += '' + LANG.myaccount_passmatch + ''; + else + buff += '' + LANG.myaccount_passdontmatch + ''; + } + + if (buff != '') + bracket = '}'; + + $WH.ge('pm1').innerHTML = bracket; + $WH.ge('pm2').innerHTML = buff; +} + +function spd(form) { + var desc = form.elements.desc; + if (desc.value.length == 0) + return true; + + if (desc.value.length < 10) { + alert(LANG.message_descriptiontooshort); + return false; + } + + var charLimit = Listview.funcBox.coGetCharLimit(2); + if (desc.value.length > charLimit) + if (!confirm($WH.sprintf(LANG.confirm_descriptiontoolong, charLimit, desc.value.substring(charLimit - 30, charLimit)))) + return false; + + return true; +} + +function sfs(form) { + var sig = form.elements.sig; + sig.value = $WH.trim(sig.value); + if (sig.value.length == 0) + return true; + + var charLimit = Listview.funcBox.coGetCharLimit(4); + if (sig.value.length > charLimit) + if (!confirm($WH.sprintf(LANG.confirm_signaturetoolong, charLimit, sig.value.substring(charLimit - 30, charLimit)))) + return false; + + var nLines; + if ((nLines = sig.value.indexOf("\n")) != -1 && (nLines = sig.value.indexOf("\n", nLines + 1)) != -1 && (nLines = sig.value.indexOf("\n", nLines + 1)) != -1) + if (!confirm($WH.sprintf(LANG.confirm_signaturetoomanylines, 3))) + return false; + + return true; +} + +$(document).ready(function () { + $('form#change-password').submit(function () { + var curPass = $('input[name=currentPassword]'); + var newPass = $('input[name=newPassword]'); + var checkPass = $('input[name=confirmPassword]'); + + if (!curPass.val() && !newPass.val() && !checkPass.val()) { + alert(LANG.message_enteremailorpass); + return false; + } + + if (newPass.val() || checkPass.val()) { + if (!curPass.val()) { + alert(LANG.message_enterpassword); + curPass[0].focus(); + return false; + } + + if ($WH.trim(newPass.val()).length < 6) { + alert(LANG.message_passwordmin); + newPass[0].focus(); + return false; + } + + if ($WH.trim(newPass.val()) === $WH.trim(curPass.val())) { + alert(LANG.message_newpassdifferent); + newPass[0].focus(); + return false; + } + + if (newPass.val() !== checkPass.val()) { + alert(LANG.message_passwordsdonotmatch); + newPass[0].focus(); + return false; + } + } + + return true; + }); + + $('form#change-email').submit(function () { + var curMail = $('input[name=current-email]'); + var newMail = $('input[name=newemail]'); + + if (!newMail.val()) { + alert(LANG.message_enteremailorpass); + return false; + } + + if (newMail.val()) { + if (newMail.val() == curMail.val()) { + alert(LANG.message_newemaildifferent); + newMail[0].focus(); + return false; + } + + if (!g_isEmailValid(newMail.val())) { + alert(LANG.message_emailnotvalid); + newMail[0].focus(); + return false; + } + } + + return true; + }); + + $('form#change-username').submit(function () { + var curName = $('input[name=current-username]'); + var newName = $('input[name=newUsername]'); + + if (!newName.val()) { + alert(LANG.message_enterusername); + newName[0].focus(); + return false; + } + if ($WH.trim(newName.val()).length < 4) { + alert(LANG.message_usernamemin); + newName[0].focus(); + return false; + } + if (!g_isUsernameValid(newName.val())) { + alert(LANG.message_usernamenotvalid); + newName[0].focus(); + return false; + } + if (newName.val() == curName.val()) { + alert(LANG.message_newnamedifferent); + newName[0].focus(); + return false; + } + }); +}); + +function fa_validateForm(form) { + if (form.elements.avatar[2].checked && form.elements.customicon.selectedIndex == 0) { + form.action = '?upload=image-crop'; + form.enctype = 'multipart/form-data'; + } + else { + form.action = '?account=forum-avatar'; + form.enctype = 'application/x-www-form-urlencoded'; + } + + return true; +} + +function faChange(mode) { + $WH.ge('avaSel1').style.display = (mode == 1 ? '': 'none'); + $WH.ge('avaSel2').style.display = (mode == 2 ? '': 'none'); +} + +function spawi() { + var inp = $WH.ge('wowicon'); + inp.value = $WH.trim(inp.value); + + var preview = $WH.ge('avaPre1'); + while (preview.firstChild) + $WH.de(preview.firstChild); + + $WH.ae(preview, Icon.createUser(1, inp.value, 2, null, ((g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER))); +} + +function spawj() { + var avSelect = $WH.ge('customicon'); + var preview = $WH.ge('avaPre2'); + while (preview.firstChild) + $WH.de(preview.firstChild); + + if (avSelect.selectedIndex != 0) { + $WH.ge('iconbrowse').style.display = 'none'; + iconId = avSelect.options[avSelect.selectedIndex].value; + $WH.ae(preview, Icon.createUser(2, iconId, 2, null, ((g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER))); + preview.style.display = ''; + } + else { + preview.style.display = 'none'; + $WH.ge('iconbrowse').style.display = ''; + } +} + +var imageDetailDialog = new Dialog(); +Listview.templates.avatar = { + sort: [4], + nItemsPerPage: -1, + mode: 1, + poundable: 0, + columns: [{ + id: 'name', + name: LANG.name, + type: 'text', + value: 'name', + align: 'left', + compute: function (data, td, tr) { + tr.onclick = imageDetailDialog.show.bind(null, 'imageupload', { + data: data, + onSubmit: this.template.updateImageInfo.bind(this, data) + }); + var avIcon = Icon.createUser(2, data.id, 0, null, (g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER); + avIcon.style.cssFloat = avIcon.style.styleFloat = 'left'; + td.style.position = 'relative'; + $WH.ae(td, avIcon); + $WH.ae(td, $WH.ce('span', { style: { paddingLeft: '7px', lineHeight: '1.8em' }, innerHTML: data.name })); + if (data.current) { + $WH.ae(td, $WH.ce('span', { + style: { + fontStyle: 'italic', + cssFloat: 'right', + styleFloat: 'right', + marginTop: '3px' + }, + className: 'small', + innerHTML: 'Current' + })); + } + }, + getVisibleText: function (a) { + return a.caption; + } + }, + { + id: 'size', + name: 'Size', + type: 'number', + value: 'size', + width: '125px', + compute: function (a, b) { + return Listview.funcBox.coFormatFileSize(a.size) + } + }, + { + id: 'status', + name: 'Status', + type: 'text', + value: 'status', + width: '100px', + compute: function (a, b) { + if (a.status == 2) + $WH.ae(b, $WH.ce('span', { className: 'q10', innerHTML: 'Rejected' })) + else + return 'Ready'; + } + }, + { + id: 'when', + name: 'When', + type: 'date', + value: 'when', + width: '150px', + compute: function (b, d) { + var c = $WH.ce('span'); + var a = new Date(b.when); + g_formatDate(c, (g_serverTime - a) / 1000, a); + $WH.ae(d, c) + } + }], + onBeforeCreate: function () { + for (i in this.data) + this.data[i].pos = i; + }, + createCbControls: function (e, d) { + if (!d && this.data.length < 15) + return; + + var c = $WH.ce('input'), + b = $WH.ce('input'), + a = $WH.ce('input'); + + c.type = b.type = a.type = 'button'; + + c.value = 'Delete'; + b.value = 'Set as avatar'; + a.value = 'Upload new one'; + + c.onclick = this.template.deleteFiles.bind(this); + b.onclick = this.template.useAvatar.bind(this); + a.onclick = this.template.jumpToUpload.bind(this); + + $WH.ae(e, b); + $WH.ae(e, c); + $WH.ae(e, a); + }, + updateImageInfo: function (b, a) { + if (b.name != a.name) { + $.post('?account=rename-icon', { + id: a.id, + name: a.name + }); + this.setRow(a); + } + }, + deleteFiles: function () { + var rows = this.getCheckedRows(); + if (!rows.length) + return; + + var ids = '', + first = true; + $WH.array_walk(rows, function (x) { + if (first) + first = false; + else + ids += ','; + + ids += x.id; + }); + + var _ = confirm('Are you sure you want to delete these icons?'); + if (_ == false) + return; + + $.post('?account=delete-icon', { id: ids }); + + this.deleteRows(rows); + this.resetCheckedRows(); + this.refreshRows(); + }, + useAvatar: function () { + var rows = this.getCheckedRows(); + if (!rows.length) + return; + + if (rows.length > 1) { + alert('Please select only 1 image to use as your avatar.'); + return; + } + + var row = rows[0]; + $WH.array_walk(this.data, function (x) { + x.current = 0; + x.__tr = null + }); + row.current = 1; + + new Ajax('?account=forum-avatar&avatar=2&customicon=' + row.id); + this.refreshRows() + }, + jumpToUpload: function () { + // aowow - community is not on idx:2 for extAuth cases + // _.show(2); + _.show(_.tabs.findIndex((x) => x.id == 'community')); + location.href = '?account#community'; + + var a = $WH.ac(document.fa); + window.scrollTo(0, a.y); + + document.fa.avatar[2].click(); + document.fa.customicon.selectedIndex = 0; + + spawj(); + }, + onNoData: function (lv) { + var sp = $WH.ce('span'); + var a = $WH.ce('a'); + + a.onclick = this.template.jumpToUpload.bind(this); + a.href = 'javascript:;'; + $WH.ae(a, $WH.ct('Upload')); + + $WH.ae(sp, $WH.ct("You havn't uploaded any custom avatars yet. ")); + $WH.ae(sp, a); + $WH.ae(sp, $WH.ct(' one now!')); + + $WH.ae(lv, sp); + } +}; + +Dialog.templates.imageupload = { + title: LANG.dialog_imagedetails, + // aowow - adapted to existing css - buttons: [['check', LANG.ok], ['x', LANG.cancel]], + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + fields: [ + { + id: 'id', + type: 'hidden', + label: ' ', + size: 30, + required: 0, + compute: function (field, value, form, td, tr) { + var div = $WH.ce('div'); + div.style.position = 'relative'; + + var div2 = $WH.ce('div'); + div2.style.position = 'relative'; + + var img = $WH.ce('img'); + switch (this.data.type) { + case 1: + img = Icon.createUser(2, null, 2, null, (g_user.roles & U_GROUP_PREMIUM) ? g_user.settings.premiumborder : Icon.STANDARD_BORDER); + break; + } + + $WH.ae(div2, img); + this.icon = img; + + $WH.ae(div, field); + $WH.ae(div, div2); + + $WH.ae(td, div); + } + }, + { + id: 'name', + type: 'text', + label: LANG.dialog_imagename, + size: 20, + required: 1, + submitOnEnter: 1, + validate: function (newValue, data) { + if (newValue.match(/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/)) + return true; + else { + alert(LANG.message_invalidname); + return false; + } + } + }, + ], + onBeforeShow: function () { + switch (this.data.type) { + case 1: + this.template.width = 300; + break; + } + }, + onShow: function (form) { + switch (this.data.type) { + case 1: + var url = g_staticUrl + '/uploads/avatars/' + this.data.id + '.jpg'; + Icon.setTexture(this.icon, 2, url); + break; + } + setTimeout(function () { + var inp = form.elements.name; + inp.focus(); + inp.select(); + }, 1); + } +}; diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 32d67761..055c8497 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -2965,6 +2965,7 @@ var LANG = { message_invalidname: "Bildname ist ungültig. Muss alphanumerisch sein, maximal 20 Zeichen haben und mit einem Buchstaben anfangen.", message_newemaildifferent: "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", message_newpassdifferent: "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", + message_newnamedifferent: "Euer neuer Benutzername muss sich von eurem alten Benutzernamen unterscheiden.", message_noscreenshot: "Wählt bitte den Screenshot aus, den Ihr hochladen möchtet.", message_novideo: "Bitte gebt gültige Videoinformationen ein.", message_nothingtoviewin3d: "Es wurden keine Gegenstände ausgewählt, die in 3D angezeigt werden können.", diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 4aacd372..417ee9ab 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -3014,6 +3014,7 @@ var LANG = { message_invalidname: "Image name is invalid. Must be alphanumeric, 20 characters max, and start with a letter.", message_newemaildifferent: "Your new email address must be different than your previous one.", message_newpassdifferent: "Your new password must be different than your previous one.", + message_newnamedifferent: "Your new username must be different than your previous one.", message_noscreenshot: "Please select the screenshot to upload.", message_novideo: "Please enter valid video information.", message_nothingtoviewin3d: "No items were selected that can be viewed in 3D.", diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index 6cbf3d68..ca03d740 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -2965,6 +2965,7 @@ var LANG = { message_invalidname: "El nombre de la imagen es inválido. Debe ser alfanumérico con un máx de 20 caracteres y debe empezar por una letra.", message_newemaildifferent: "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", message_newpassdifferent: "Su nueva contraseña tiene que ser diferente a Su contraseña anterior.", + message_newnamedifferent: "Su nuevo nombre de usuario tiene que ser diferente a su nombre de usuario anterior.", message_noscreenshot: "Por favor seleccione la captura de pantalla para subir.", message_novideo: "Por favor, introduce información válida del vídeo.", message_nothingtoviewin3d: "No se han seleccionado objetos que se puedan ver en 3D.", diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 747196f1..04be890c 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -2966,6 +2966,7 @@ var LANG = { message_invalidname: "Le nom de l'image est invalide. Doit être alphanumérique, 20 caractères maximum et doit commencer par une lettre.", message_newemaildifferent: "Votre nouvelle adresse courriel doit être différente de l'ancienne.", message_newpassdifferent: "Votre nouveau mot de passe doit être différent de l'ancien.", + message_newnamedifferent: "Votre nouveau nom d'utilisateur doit être différent de l'ancien.", message_noscreenshot: "Veuillez sélectionner la capture d'écran à envoyer.", message_novideo: "Veuillez entrer des informations valide pour le vidéo.", message_nothingtoviewin3d: "Aucun objets qui ont été sélectionnés ne peuvent être vus en 3D.", diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 0211ba2c..117fd9cc 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -2966,6 +2966,7 @@ var LANG = { message_invalidname: "Ðазвание Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð½ÐµÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾. Должно Ñодержать только латинÑкие буквы и цифры, начинатьÑÑ Ñ Ð±ÑƒÐºÐ²Ñ‹, и быть не более 20 Ñимволов в длину.", message_newemaildifferent: "Прежний и новый e-mail адреÑа не должны Ñовпадать.", message_newpassdifferent: "Прежний и новый пароли не должны Ñовпадать.", + message_newnamedifferent: "Прежнее и новое Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð½Ðµ должны Ñовпадать.", message_noscreenshot: "Выберите изображение Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸.", message_novideo: "Введите корректную информацию о видео.", message_nothingtoviewin3d: "Ð’Ñ‹ не выбрали предметы, которые можно проÑмотреть в 3D.", diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 186bb754..055d844c 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -3013,6 +3013,7 @@ var LANG = { message_invalidname: "å›¾ç‰‡åæ— æ•ˆã€‚必须使用字æ¯å’Œæ•°å­—,最多20个字符,以字æ¯å¼€å¤´ã€‚", message_newemaildifferent: "您的新邮箱地å€å¿…é¡»ä¸åŒäºŽæ—§åœ°å€ã€‚", message_newpassdifferent: "您的新密ç å¿…é¡»ä¸åŒäºŽæ—§å¯†ç ã€‚", + message_newnamedifferent: "您的新用户åå¿…é¡»ä¸åŒäºŽæ—§ç”¨æˆ·å。", message_noscreenshot: "请选择è¦ä¸Šä¼ çš„æˆªå±ã€‚", message_novideo: "请输入有效的视频信æ¯ã€‚", message_nothingtoviewin3d: "没有选中å¯ä»¥3Dæµè§ˆçš„物å“。", diff --git a/template/bricks/inputbox-form-signin.tpl.php b/template/bricks/inputbox-form-signin.tpl.php index 76876e57..a917c7a5 100644 --- a/template/bricks/inputbox-form-signin.tpl.php +++ b/template/bricks/inputbox-form-signin.tpl.php @@ -52,7 +52,7 @@
        -
        | |
        +
        | |
        diff --git a/template/pages/acc-dashboard.tpl.php b/template/pages/acc-dashboard.tpl.php deleted file mode 100644 index a90c91fc..00000000 --- a/template/pages/acc-dashboard.tpl.php +++ /dev/null @@ -1,141 +0,0 @@ - - -brick('header'); ?> - -
        -
        -
        - -brick('announcement'); - - $this->brick('pageTemplate'); - - $this->brick('infobox'); -?> - - - -
        -

        -banned): -?> -
        -

        -
          -
        • '.Lang::account('bannedBy').''.Lang::main('colon').''.$b['by'][1].''; ?>
        • -
        • '.Lang::account('ends').''.Lang::main('colon').($b['end'] ? date(Lang::main('dateFmtLong'), $b['end']) : Lang::account('permanent')); ?>
        • -
        • '.Lang::account('reason').''.Lang::main('colon').''.($b['reason'] ?: Lang::account('noReason')).''; ?>
        • -
        -
        - - - - - -

        {$lang.publicDesc}

        -
        {$lang.Your_description_has_been_updated_successfully}.
        - -
        - {$lang.viewPublicDesc|sprintf:$user.name}. -
        -
        - -
        - -
        -{* CLAIM CHARACTERS *} -

        [Select Character]

        -{strip} - -{if $user.chars} - - {foreach from=$user.chars item=c} - - - - - {/foreach} -
        - {if $c.this} - {$c.name} - {else} - {$c.name} - {/if} -   - {if $c.guild} - <{$c.guild|escape:"html"}> - {/if} -  â€” {$c.text} -
        -{else} - [no characters on ths account] -{/if} -
        - -{/strip} -{* CHANGE PASSWORD / EMAIL / DISPLAYNAME / AVATAR * } -

        {$lang.Change_password}

        -
        - -{if isset($cpmsg)} -
        {$cpmsg.msg}
        -{/if} - - - - - -
        {$lang.Current_password}{$lang.colon}
        {$lang.New_password}{$lang.colon}
        {$lang.Confirm_new_password}{$lang.colon}
        -
        - - -
        -
        - -
        - -*/ -?> - -brick('lvTabs'); ?> - -
        -
        -
        - -brick('footer'); ?> diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php new file mode 100644 index 00000000..ca64947c --- /dev/null +++ b/template/pages/account.tpl.php @@ -0,0 +1,291 @@ +brick('header'); +?> +
        +
        +
        + +brick('announcement'); + + $this->brick('pageTemplate'); +?> + +
        +

        +bans): + foreach ($this->bans as $b): + [$end, $reason, $name] = $b; +?> +
        +

        +
          +
        • '.Lang::account('bannedBy').''.($name ? ''.$name.'' : '<System>');?>
        • +
        • '.Lang::account('ends').''.($end ? date(Lang::main('dateFmtLong'), $end) : Lang::account('permanent'));?>
        • +
        • '.Lang::account('reason').''.''.($reason ?: Lang::account('noReason')).'';?>
        • +
        +
        + +
        + + + + +
        +
        + +
        + +
        +
        + + + +cfg('ACC_AUTH_MODE') == AUTH_MODE_SELF): +?> + + + + + + + +
        +
        + + + + +
        +
        +
        + +brick('footer'); ?> From f16479b50cc8fe453ef3dc29ae61c90937f2ecc3 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:43:07 +0200 Subject: [PATCH 0982/1249] Template/Update (Part 46 - II) * account management rework: Signup functionality --- endpoints/account/activate.php | 73 ++++++++ endpoints/account/signin.php | 61 ++++--- endpoints/account/signup.php | 163 ++++++++++++++++++ .../inputbox-form-signup.tpl.php} | 39 +---- 4 files changed, 279 insertions(+), 57 deletions(-) create mode 100644 endpoints/account/activate.php create mode 100644 endpoints/account/signup.php rename template/{pages/acc-signUp.tpl.php => bricks/inputbox-form-signup.tpl.php} (79%) diff --git a/endpoints/account/activate.php b/endpoints/account/activate.php new file mode 100644 index 00000000..d437d75c --- /dev/null +++ b/endpoints/account/activate.php @@ -0,0 +1,73 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + public function __construct() + { + parent::__construct(); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $msg = $this->activate(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'register', [2]), 'message' => $msg]]; + else + { + $_SESSION['error']['activate'] = $msg; + $this->forward('?account=resend'); + } + + parent::generate(); + } + + private function activate() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + if (DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` IN (?a) AND `token` = ?', [ACC_STATUS_NONE, ACC_STATUS_NEW], $this->_get['key'])) + { + // don't remove the token yet. It's needed on signin page. + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `userGroups` = ?d WHERE `token` = ?', ACC_STATUS_NONE, U_GROUP_NONE, $this->_get['key']); + + // fully apply block for further registration attempts from this ip + DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d + 1, UNIX_TIMESTAMP() + ?d)', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'accActivated', [$this->_get['key']]); + } + + // grace period expired and other user claimed name + return Lang::main('intError'); + } +} + +?> diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index dce60520..c4a0329e 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -19,6 +19,7 @@ class AccountSigninResponse extends TemplateResponse use TrGetNext; protected string $template = 'text-page-generic'; + protected string $pageName = 'signin'; protected array $expectedPOST = array( 'username' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateLogin'] ], @@ -26,8 +27,8 @@ class AccountSigninResponse extends TemplateResponse 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe'] ] ); protected array $expectedGET = array( - 'token' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{32}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] ); private bool $success = false; @@ -43,46 +44,54 @@ class AccountSigninResponse extends TemplateResponse protected function generate() : void { - $username = ''; - $message = ''; + $username = + $error = ''; + $rememberMe = !!$this->_post['remember_me']; $this->title = [Lang::account('title')]; - if ($this->_get['token']) + // coming from user recovery or creation, prefill username + if ($this->_get['key']) { - // coming from username recovery, prefill username - if ($_ = DB::Aowow()->selectCell('SELECT `login` FROM ?_account WHERE `status` IN (?a) AND `token` = ? AND `statusTimer` > UNIX_TIMESTAMP()', [ACC_STATUS_RECOVER_USER, ACC_STATUS_OK], $this->_get['token'])) - $username = $_; + if ($userData = DB::Aowow()->selectRow('SELECT a.`login` AS "0", IF(s.`expires`, 0, 1) AS "1" FROM ?_account a LEFT JOIN ?_account_sessions s ON a.`id` = s.`userId` AND a.`token` = s.`sessionId` WHERE a.`status` IN (?a) AND a.`token` = ?', + [ACC_STATUS_RECOVER_USER, ACC_STATUS_NONE], $this->_get['key'])) + [$username, $rememberMe] = $userData; } - $message = $this->doSignIn(); - if (!$this->success) - User::destroy(); - else + if ($this->doSignIn($error)) $this->forward($this->getNext(true)); + if ($error) + User::destroy(); + $this->inputbox = ['inputbox-form-signin', array( 'head' => Lang::account('inputbox', 'head', 'signin'), 'action' => '?account=signin&next='.$this->getNext(), - 'error' => $message, + 'error' => $error, 'username' => $username, - 'rememberMe' => !!$this->_post['remember_me'], + 'rememberMe' => $rememberMe, 'hasRecovery' => Cfg::get('ACC_EXT_RECOVER_URL') || Cfg::get('ACC_AUTH_MODE') == AUTH_MODE_SELF, )]; parent::generate(); } - private function doSignIn() : string + private function doSignIn(string &$error) : bool { if (is_null($this->_post['username']) && is_null($this->_post['password'])) - return ''; + return false; if (!$this->assertPOST('username')) - return Lang::account('userNotFound'); + { + $error = Lang::account('userNotFound'); + return false; + } if (!$this->assertPOST('password')) - return Lang::account('wrongPass'); + { + $error = Lang::account('wrongPass'); + return false; + } $error = match (User::authenticate($this->_post['username'], $this->_post['password'])) { @@ -95,10 +104,7 @@ class AccountSigninResponse extends TemplateResponse default => Lang::main('intError') }; - if (!$error) - $this->success = true; - - return $error; + return !$error; } private function onAuthSuccess() : string @@ -109,14 +115,11 @@ class AccountSigninResponse extends TemplateResponse return Lang::main('intError'); } - $email = filter_var($this->_post['username'], FILTER_VALIDATE_EMAIL); - // reset account status, update expiration - $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE { `email` = ? } { `login` = ? }', + $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE `id` = ?d', User::$ip, User::$ip, User::$ip, ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW, - $email ?: DBSIMPLE_SKIP, - !$email ? $this->_post['username'] : DBSIMPLE_SKIP + User::$id // available after successful User:authenticate ); if (!is_int($ok)) // num updated fields or null on fail @@ -125,6 +128,10 @@ class AccountSigninResponse extends TemplateResponse return Lang::main('intError'); } + // DELETE temp session + if ($this->_get['key']) + DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `sessionId` = ?', $this->_get['key']); + session_regenerate_id(true); // user status changed => regenerate id // create new session entry diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php new file mode 100644 index 00000000..f8a819e1 --- /dev/null +++ b/endpoints/account/signup.php @@ -0,0 +1,163 @@ + ['filter' => FILTER_SANITIZE_SPECIAL_CHARS, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'remember_me' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkRememberMe']] + ); + + protected array $expectedGET = array( + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct() + { + // if the user is logged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + // redirect to external registration page, if set + if (Cfg::get('ACC_EXT_CREATE_URL')) + $this->forward(Cfg::get('ACC_EXT_CREATE_URL')); + + parent::__construct(); + + // registration not enabled on self + if (!Cfg::get('ACC_ALLOW_REGISTER')) + $this->generateError(); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + // step 1 - no params > signup form + // step 2 - any param > status box + // step 3 - on ?account=activate + + $message = $this->doSignUp(); + + if ($this->success) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]) + )]; + } + else + { + $this->inputbox = ['inputbox-form-signup', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1]), + 'error' => $message, + 'action' => '?account=signup&next='.$this->getNext(), + 'username' => $this->_post['username'] ?? '', + 'email' => $this->_post['email'] ?? '', + 'rememberMe' => !!$this->_post['remember_me'], + )]; + } + + parent::generate(); + } + + private function doSignUp() : string + { + // no input yet. show clean form + if (!$this->assertPOST('username', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + // check username + if (!Util::validateUsername($this->_post['username'], $e)) + return Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'); + + // check password + if (!Util::validatePassword($this->_post['password'], $e)) + return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + + if ($this->_post['password'] !== $this->_post['c_password']) + return Lang::account('passMismatch'); + + // check ip + if (!User::$ip) + return Lang::main('intError'); + + // limit account creation + if (DB::Aowow()->selectRow('SELECT 1 FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ? AND `count` >= ?d AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'))) + { + DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = ?d', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT); + return Lang::account('inputbox', 'error', 'signupExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); + } + + // username / email taken + if ($inUseData = DB::Aowow()->SelectRow('SELECT `id`, `username`, `status` = ?d AND `statusTimer` < UNIX_TIMESTAMP() AS "expired" FROM ?_account WHERE (LOWER(`username`) = LOWER(?) OR LOWER(`email`) = LOWER(?))', ACC_STATUS_NEW, $this->_post['username'], $this->_post['email'])) + { + if ($inUseData['expired']) + DB::Aowow()->query('DELETE FROM ?_account WHERE `id` = ?d', $inUseData['id']); + else + return Util::lower($inUseData['username']) == Util::lower($this->_post['username']) ? Lang::account('nameInUse') : Lang::account('mailInUse'); + } + + // create.. + $token = Util::createHash(); + $userId = DB::Aowow()->query('INSERT INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', + $this->_post['username'], + User::hashCrypt($this->_post['password']), + $this->_post['username'], + $this->_post['email'], + User::$ip, + Lang::getLocale()->value, + U_GROUP_PENDING, + ACC_STATUS_NEW, + Cfg::get('ACC_CREATE_SAVE_DECAY'), + $token + ); + + if (!$userId) + return Lang::main('intError'); + + // create session tied to the token to store remember_me status + DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)', + $userId, $token, time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE); + + if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_CREATE_SAVE_DECAY'))) + return Lang::main('intError2', ['send mail']); + + // success: update ip-bans + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + Util::gainSiteReputation($userId, SITEREP_ACTION_REGISTER); + + $this->success = true; + return ''; + } +} + +?> diff --git a/template/pages/acc-signUp.tpl.php b/template/bricks/inputbox-form-signup.tpl.php similarity index 79% rename from template/pages/acc-signUp.tpl.php rename to template/bricks/inputbox-form-signup.tpl.php index 6b439795..20724f66 100644 --- a/template/pages/acc-signUp.tpl.php +++ b/template/bricks/inputbox-form-signup.tpl.php @@ -1,23 +1,10 @@ - - -brick('header'); ?> - -
        -
        -
        brick('announcement'); + namespace Aowow\Template; - $this->brick('pageTemplate'); + use \Aowow\Lang; ?>
        -text)): ?> -
        -

        head; ?>

        -
        -
        text; ?>
        -
        - + -
        +
        -

        head; ?>

        -
        error; ?>
        +

        +
        - + @@ -103,9 +88,9 @@ - + - + '); + + if(edit) + row.addClass('comment-reply-row').addClass('reply-edit-row'); + + row.html('' + + ''); + + /* Set up the various variables for the controls we just created */ + Body = row.find('.comment-form textarea'); + AddButton = row.find('.comment-form input[type=submit]'); + TextCounter = row.find('.comment-form span.text-counter'); + Form = row.find('.comment-form form'); + AjaxLoader = row.find('.comment-form .ajax-loader'); + FormContainer = row.find('.comment-form'); + + /* Intercept submits */ + Form.submit(function () { Submit(); return false; }); + + UpdateTextCounter(); + + /* This is kinda a mess.. Every browser seems to implement keyup, keydown and keypress differently. + * - keyup: We need to use keyup to update the text counter for the simple reason we want to update it only when the user stops typing. + * - keydown: We need to use keydown to detect the ESC key because it's the only one that works in all browsers for ESC + * - keypress: We need to use keypress to detect Enter because it's the only one that 1) Works 2) Allows us to prevent a new line from being entered in the textarea + * I find it very funny that in each scenario there is only one of the 3 that works, and that that one is always different from the others. + */ + + Body.keyup(function (e) { UpdateTextCounter(); }); + Body.keydown(function (e) { if (e.keyCode == 27) { Close(); return false; } }); // ESC + Body.keypress(function (e) { if (e.keyCode == 13) { Submit(); return false; } }); // ENTER + + if(edit) + { + post.after(row); + post.hide(); + Form.find('textarea').text(comment.replies[post.attr('data-idx')].body); + } + else + CommentsTable.append(row); + + DialogTableRowContainer = row; + Form.find('textarea').focus(); + } + + function Open() + { + if (!Initialized) + Initialize(); + + Active = true; + + if(!edit) + { + AddCommentLink.hide(); + post.find('.comment-replies').show(); + FormContainer.show(); + FormContainer.find('textarea').focus(); + } + } + + function Close() + { + Active = false; + + if(edit) + { + if(DialogTableRowContainer) + DialogTableRowContainer.remove(); + post.show(); + return; + } + + AddCommentLink.show(); + FormContainer.hide(); + + if (CommentsCount == 0) + post.find('.comment-replies').hide(); + } + + function Submit() + { + if (!Active || Submitting) + return; + + if (Body.val().length < MIN_LENGTH || Body.val().length > MAX_LENGTH) + { + /* Flash the char counter to attract the attention of the user. */ + if (!Flashing) + { + Flashing = true; + TextCounter.animate({ opacity: '0.0' }, 150); + TextCounter.animate({ opacity: '1.0' }, 150, null, function() { Flashing = false; }); + } + + return false; + } + + SetSubmitState(); + $.ajax({ + type: 'POST', + url: edit ? '?comment=edit-reply' : '?comment=add-reply', + data: { commentId: comment.id, replyId: (edit ? post.attr('data-replyid') : 0), body: Body.val() }, + success: function (newReplies) { OnSubmitSuccess(newReplies); }, + dataType: 'json', + error: function (jqXHR) { OnSubmitFailure(jqXHR.responseText); } + }); + return true; + } + + function SetSubmitState() + { + Submitting = true; + AjaxLoader.show(); + AddButton.attr('disabled', 'disabled'); + FormContainer.find('.message-box').remove(); + } + + function ClearSubmitState() + { + Submitting = false; + AjaxLoader.hide(); + AddButton.removeAttr('disabled'); + } + + function OnSubmitSuccess(newReplies) + { + comment.replies = newReplies; + Listview.templates.comment.updateReplies(comment); + } + + function OnSubmitFailure(error) + { + ClearSubmitState(); + MessageBox(FormContainer, error); + } + + function UpdateTextCounter() + { + var text = '(error)'; + var cssClass = 'q0'; + var chars = Body.val().replace(/(\s+)/g, ' ').replace(/^\s*/, '').replace(/\s*$/, '').length; + var charsLeft = MAX_LENGTH - chars; + + if (chars == 0) + text = $WH.sprintf(LANG.replylength1_format, MIN_LENGTH); + else if (chars < MIN_LENGTH) + text = $WH.sprintf(LANG.replylength2_format, MIN_LENGTH - chars); + else + { + text = $WH.sprintf(charsLeft == 1 ? LANG.replylength4_format : LANG.replylength3_format, charsLeft); + + if (charsLeft < 120) + cssClass = 'q10'; + else if (charsLeft < 240) + cssClass = 'q5'; + else if (charsLeft < 360) + cssClass = 'q11'; + } + + TextCounter.html(text).attr('class', cssClass); + } +} + +function SetupShowMoreComments(post, comment) +{ + var ShowMoreCommentsLink = post.find('.show-more-replies'); + var CommentCell = post.find('.comment-replies'); + + ShowMoreCommentsLink.click(function () { ShowMoreComments(); }); + + function ShowMoreComments() + { + /* Replace link with ajax loader */ + ShowMoreCommentsLink.hide(); + CommentCell.append(CreateAjaxLoader()); + + $.ajax({ + type: 'GET', + url: '?comment=show-replies', + data: { id: comment.id }, + success: function (replies) { comment.replies = replies; Listview.templates.comment.updateReplies(comment); }, + dataType: 'json', + error: function () { OnFetchFail(); } + }); + } + + function OnFetchFail() + { + ShowMoreCommentsLink.show(); + CommentCell.find('.ajax-loader').remove(); + + MessageBox(CommentCell, "There was an error fetching the comments. Try refreshing the page."); + } +} + +function SetupRepliesControls(post, comment) +{ + var CommentId = post.attr('data-replyid'); + var VoteUpControl = post.find('.reply-upvote'); + var VoteDownControl = post.find('.reply-downvote'); + var FlagControl = post.find('.reply-report'); + var CommentScoreText = post.find('.reply-rating'); + var CommentActions = post.find('.reply-controls'); + var DeleteButton = post.find('.reply-delete'); + var EditButton = post.find('.reply-edit'); + var Voting = false; + var Deleting = false; + // aowow - detach functionality is custom + var Detaching = false; + var DetachButton = post.find('.reply-detach'); + var Container = comment.repliesCell; + + EditButton.click(function() { + SetupAddEditComment(post, comment, true); + }); + + FlagControl.click(function () + { + if (Voting || !confirm(LANG.replyreportwarning_tip)) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=flag-reply', + data: { id: CommentId }, + success: function () { OnFlagSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + VoteUpControl.click(function () + { + if (VoteUpControl.attr('data-hasvoted') == 'true' || VoteUpControl.attr('data-canvote') != 'true' || Voting) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=upvote-reply', + data: { id: CommentId }, + success: function () { OnVoteSuccessful(1); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + VoteDownControl.click(function () + { + if (VoteDownControl.attr('data-hasvoted') == 'true' || VoteDownControl.attr('data-canvote') != 'true' || Voting) + return; + + Voting = true; + $.ajax({ + type: 'POST', + url: '?comment=downvote-reply', + data: { id: CommentId }, + success: function () { OnVoteSuccessful(-1); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + DetachButton.click(function () + { + if (Detaching) { + MessageBox(CommentActions, LANG.message_cantdetachcomment); + return; + } + + if (!confirm(LANG.confirm_detachcomment)) { + return; + } + + Detaching = true; + $.ajax({ + type: 'POST', + url: '?comment=detach-reply', + data: { id: CommentId }, + success: function () { OnDetachSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + DeleteButton.click(function () + { + if (Deleting) + return; + + if (!confirm(LANG.deletereplyconfirmation_tip)) + return; + + Deleting = true; + $.ajax({ + type: 'POST', + url: '?comment=delete-reply', + data: { id: CommentId }, + success: function () { OnDeleteSuccessful(); }, + error: function (jqXHR) { OnError(jqXHR.responseText); } + }); + }); + + function OnVoteSuccessful(ratingChange) + { + var rating = parseInt(CommentScoreText.text()); + + rating += ratingChange; + + CommentScoreText.text(rating); + + if(ratingChange > 0) + VoteUpControl.attr('data-hasvoted', 'true'); + else + VoteDownControl.attr('data-hasvoted', 'true'); + + VoteUpControl.attr('data-canvote', 'false'); + VoteDownControl.attr('data-canvote', 'false'); + + if(ratingChange > 0) + FlagControl.remove(); + Voting = false; + } + + function OnFlagSuccessful() + { + Voting = false; + FlagControl.remove(); + } + + function OnDetachSuccessful() + { + post.remove(); + MessageBox(Container, LANG.message_commentdetached); + Detaching = false; + } + + function OnDeleteSuccessful() + { + post.remove(); + Deleting = false; + } + + function OnError(text) + { + Voting = false; + Deleting = false; + Detaching = false; + + if (!text) + text = LANG.genericerror; + + MessageBox(CommentActions, text); + } +} + +/* +Global comment-related functions +*/ + +function co_addYourComment() +{ + tabsContribute.focus(0); + var ta = $WH.gE(document.forms['addcomment'], 'textarea')[0]; + ta.focus(); +} + +function co_validateForm(f) +{ + var ta = $WH.gE(f, 'textarea')[0]; + + // prevent locale comments on guide pages + var locale = Locale.getId(); + // aowow - disabled + // if(locale != LOCALE_ENUS && $(f).attr('action') && ($(f).attr('action').replace(/^.*type=([0-9]*).*$/i, '$1')) == 100) + if (false) + { + alert(LANG.message_cantpostlcomment_tip); + return false; + } + + if (g_user.permissions & 1) { + return true; + } + + if (Listview.funcBox.coValidate(ta)) { + return true; + } + + return false; +} + +// Display a warning if a user attempts to leave the page and he has started writing a message +$(document).ready(function() +{ + g_setupChangeWarning($("form[name=addcomment]"), [$("textarea[name=commentbody]")], LANG.message_startedpost); +}); diff --git a/setup/tools/filegen/templates/global.js/conditionList.js b/setup/tools/filegen/templates/global.js/conditionList.js new file mode 100644 index 00000000..c788c24c --- /dev/null +++ b/setup/tools/filegen/templates/global.js/conditionList.js @@ -0,0 +1,329 @@ +/* aowow - custom: TrinityCore Conditions */ +var ConditionList = new function() { + var self = this, + _conditions = null; + + self.createCell = function(conditions) + { + if (!conditions) + return null; + + _conditions = conditions; + + return _createCell(); + }; + + self.createTab = function(conditions) + { + if (!conditions) + return null; + + _conditions = conditions; + + return _createTab(); + }; + + function _makeList(mask, src, tpl) + { + var arr = Listview.funcBox.assocBinFlags(mask, src).sort(), + buff = ''; + + for (var i = 0, len = arr.length; i < len; ++i) + { + if (len > 1 && i == len - 1) + buff += LANG.or; + else if (i > 0) + buff += LANG.comma; + + buff += $WH.sprintf(tpl, arr[i], src[arr[i]]); + } + + return buff; + } + + function _parseEntry(entry, targets, target) + { + var str = '', + negate = false, + strIdx = 0, + param = []; + + [strIdx, ...param] = entry; + + negate = strIdx < 0; + strIdx = Math.abs(strIdx); + + if (!g_conditions[strIdx]) + return 'unknown condition index #' + strIdx; + + switch (strIdx) + { + case 5: + var standings = {}; + for (let i in g_reputation_standings) + standings[i * 1 + 1] = g_reputation_standings[i]; + + param[1] = _makeList(entry[2], standings, '$2'); + break; + + case 6: + if (entry[1] == 1) + param[0] = $WH.sprintf('[span class=icon-alliance]$1[/span]', g_sides[1]); + else if (entry[1] == 2) + param[0] = $WH.sprintf('[span class=icon-horde]$1[/span]', g_sides[2]); + else + param[0] = $WH.sprintf('[span class=icon-alliance]$1[/span]$2[span class=icon-horde]$3[/span]', g_sides[1], LANG.or, g_sides[2]); + break; + + case 10: + param[0] = g_drunk_states[entry[1]] ?? 'UNK DRUNK STATE'; + break; + + case 13: + param[2] = g_instance_info[entry[3]] ?? 'UNK INSTANCE INFO'; + break; + + case 15: + param[0] = _makeList(entry[1], g_chr_classes, '[class=$1]'); + break; + + case 16: + param[0] = _makeList(entry[1], g_chr_races, '[race=$1]'); + break; + + case 20: + if (entry[1] == 0) + param[0] = $WH.sprintf('[span class=icon-$1]$2[/span]', g_file_genders[0], LANG.male); + else if (entry[1] == 1) + param[0] = $WH.sprintf('[span class=icon-$1]$2[/span]', g_file_genders[1], LANG.female); + else + param[0] = g_npc_types[10]; // not specified + break; + + case 21: + var states = {}; + for (let i in g_unit_states) + states[i * 1 + 1] = g_unit_states[i]; + + param[0] = _makeList(entry[1], states, '$2'); + break; + + case 22: + if (entry[2]) + param[0] = '[zone=' + entry[2] + ']'; + else + param[0] = g_zone_categories[entry[1]] ?? 'UNK ZONE'; + break; + + case 24: + param[0] = g_npc_types[entry[1]] ?? 'UNK NPC TYPE'; + break; + + case 26: + var idx = 0, buff = []; + while (entry[1] >= (1 << idx)) { + if (!(entry[1] & (1 << idx++))) + continue; + + buff.push(idx); + } + param[0] = buff ? buff.join(LANG.comma) : ''; + break; + + case 27: + case 37: + case 38: + param[1] = g_operators[entry[2]]; + break; + + case 31: + if (entry[2] && entry[1] == 3) + param[0] = '[npc=' + entry[2] + ']'; + else if (entry[2] && entry[1] == 5) + param[0] = '[object=' + entry[2] + ']'; + else + param[0] = g_world_object_types[entry[1]] ?? 'UNK TYPEID'; + break; + + case 32: + var objectTypes = {}; + for (let i in g_world_object_types) + objectTypes[i * 1 + 1] = g_world_object_types[i]; + + param[0] = _makeList(entry[1], objectTypes, '$2'); + break; + + case 33: + param[0] = targets[entry[1]]; + param[1] = g_relation_types[entry[2]] ?? 'UNK RELATION'; + param[2] = targets[target]; + break; + + case 34: + param[0] = targets[entry[1]]; + + var standings = {}; + for (let i in g_reputation_standings) + standings[i * 1 + 1] = g_reputation_standings[i]; + param[1] = _makeList(entry[2], standings, '$2'); + break; + + case 35: + param[0] = targets[entry[1]]; + param[2] = g_operators[entry[3]]; + break; + + case 42: + if (!entry[1]) + param[0] = g_stand_states[entry[2]] ?? 'UNK STAND_STATE'; + else if (entry[1] == 1) + param[0] = g_stand_states[entry[2] ? 1 : 0]; + else + param[0] = ''; + break; + + case 47: + var quest_states = {}; + for (let i in g_quest_states) + quest_states[i * 1 + 1] = g_quest_states[i]; + + param[1] = _makeList(entry[2], quest_states, '$2'); + break; + } + + str = g_conditions[strIdx]; + + // fill in params + str = $WH.sprintfa(str, param[0], param[1], param[2]); + + // resolve NegativeCondition + str = str.replace(/\$N([^:]*):([^;]*);/g, '$' + (negate > 0 ? 2 : 1)); + + // resolve vars + return str.replace(/\$C(\d+)([^:]*):([^;]*);/g, (_, i, y, n) => (i > 0 ? y : n)); + } + + function _createTab() + { + var buff = ''; + + // tabs for conditionsTypes + for (g in _conditions) + { + if (!g_condition_sources[g]) + continue; + + let k = 0; + for (h in _conditions[g]) + { + var srcGroup, srcEntry, srcId, target, + targets, desc, + nGroups = Object.keys(_conditions[g][h]).length, + curGroup = 1; + + [srcGroup, srcEntry, srcId, target] = h.split(':').map((x) => parseInt(x)); + [targets, desc] = g_condition_sources[g]; + + // resolve targeting + let src = desc.replace(/\$T([^:]*):([^;]*);/, (_, t1, t2) => (target ? t2 : t1).replace('%', targets[target])); + let rand = $WH.rs(); + + buff += '[h3][toggler' + (k ? '=hidden' : '') + ' id=' + rand + ']' + $WH.sprintfa(src, srcGroup, srcEntry, srcId) + '[/toggler][/h3][div' + (k++ ? '=hidden' : '') + ' id=' + rand + ']'; + + if (nGroups > 1) + { + buff += LANG.note_condition_group + '[br][br]'; + buff += '[table class=grid]'; + } + + // table for elseGroups + for (i in _conditions[g][h]) + { + var group = _conditions[g][h][i], + nEntries = Object.keys(_conditions[g][h][i]).length; + + if (nGroups <= 1 && nEntries > 1) + buff += '[div style="padding-left:15px"]' + LANG.note_condition + '[/div]'; + if (nGroups > 1) + buff += '[tr][td width=70px valign=middle align=center]' + LANG.group + ' ' + (curGroup++) + LANG.colon + '[/td][td]'; + + // individual conditions + buff += '[ol]'; + for (j in group) + buff += '[li]' + _parseEntry(group[j], targets, target) + '[/li]'; + buff += '[/ol]'; + + if (nGroups > 1) + buff += '[/td][/tr]'; + } + + if (nGroups > 1) + buff += '[/tr][/table]'; + + buff += '[/div]'; + } + } + + return buff; + } + + function _createCell() + { + var rows = []; + + // tabs for conditionsTypes + for (let g in _conditions) + { + if (!g_condition_sources[g]) + continue; + + for (let h in _conditions[g]) + { + var target, targets, + + [, , , target] = h.split(':').map((x) => parseInt(x)); + [targets, ] = g_condition_sources[g]; + + let nElseGroups = Object.keys(_conditions[g][h]).length + + // table for elseGroups + for (let i in _conditions[g][h]) + { + let subGroup = [], + group = _conditions[g][h][i], + nEntries = Object.keys(_conditions[g][h][i]).length + buff = ''; + + if (nElseGroups > 1) + { + let rand = $WH.rs(); + buff += '[toggler' + (i > 0 ? '=hidden' : '') + ' id=cell-' + rand + ']' + (i > 0 ? LANG.cnd_or : LANG.cnd_either) + '[/toggler][div' + (i > 0 ? '=hidden' : '') + ' id=cell-' + rand + ']'; + } + + // individual conditions + for (let j in group) + subGroup.push(_parseEntry(group[j], targets, target)); + + for (j in subGroup) + { + if (nEntries > 1 && j > 0 && j == subGroup.length - 1) + buff += LANG.and + '[br]'; + else if (nEntries > 1 && j > 0) + buff += ',[br]'; + + buff += subGroup[j]; + } + + if (nElseGroups > 1) + buff += '[/div]'; + + rows.push(buff); + } + } + } + + return rows.length > 1 ? rows.join('[br]') : rows[0]; + } + +} +/* end custom */ diff --git a/setup/tools/filegen/templates/global.js/contacttool.js b/setup/tools/filegen/templates/global.js/contacttool.js new file mode 100644 index 00000000..516bc733 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/contacttool.js @@ -0,0 +1,550 @@ +var ContactTool = new function() +{ + this.general = 0; + this.comment = 1; + this.post = 2; + this.screenshot = 3; + this.character = 4; + this.video = 5; + this.guide = 6; + + var _dialog; + + var contexts = { + 0: [ // general + [1, true], // General feedback + [2, true], // Bug report + [8, true], // Article misinformation + [3, true], // Typo/mistranslation + [4, true], // Advertise with us + [5, true], // Partnership opportunities + [6, true], // Press inquiry + [7, true] // Other + ], + 1: [ // comment + [15, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Advertising + [16, true], // Inaccurate + [17, true], // Out of date + [18, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Spam + [19, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [20, function(post) { return ((post.roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 2: [ // forum post + [30, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Advertising + [37, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0 && (post.roles & U_GROUP_MODERATOR) == 0 && g_users[post.user].avatar == 2); }], // Avatar + [31, true], // Inaccurate + [32, true], // Out of date + [33, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Spam + [34, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0 && post.op && !post.sticky); }], // Sticky request + [35, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [36, function(post) { return (g_users && g_users[post.user] && (g_users[post.user].roles & U_GROUP_MODERATOR) == 0);}] // Other + ], + 3: [ // screenshot + [45, true], // Inaccurate, + [46, true], // Out of date, + [47, function(screen) { return (g_users && g_users[screen.user] && (g_users[screen.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [48, function(screen) { return (g_users && g_users[screen.user] && (g_users[screen.user].roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 4: [ // character + [60, true], // Inaccurate completion data + [61, true] // Other + ], + 5: [ // video + [45, true], // Inaccurate, + [46, true], // Out of date, + [47, function(video) { return (g_users && g_users[video.user] && (g_users[video.user].roles & U_GROUP_MODERATOR) == 0); }], // Vulgar/inappropriate + [48, function(video) { return (g_users && g_users[video.user] && (g_users[video.user].roles & U_GROUP_MODERATOR) == 0); }] // Other + ], + 6: [ // Guide + [45, true], // Inaccurate, + [46, true], // Out of date, + [48, true] // Other + ] + }; + + var errors = { + 1: LANG.ct_resp_error1, + 2: LANG.ct_resp_error2, + 3: LANG.ct_resp_error3, + 7: LANG.ct_resp_error7 + }; + + var oldHash = null; + + this.displayError = function(field, message) + { + alert(message); + } + + this.onShow = function() + { + if (location.hash && location.hash != '#contact') + oldHash = location.hash; + if (this.data.mode == 0) + location.replace('#contact'); + } + + this.onHide = function() + { + if (oldHash && (oldHash.indexOf('screenshots:') == -1 || oldHash.indexOf('videos:') == -1)) + location.replace(oldHash); + else + location.replace('#.'); + } + + this.onSubmit = function(data, button, form) + { + if (data.submitting) + return false; + + for (var i = 0; i < form.elements.length; ++i) + form.elements[i].disabled = true; + + var params = [ + 'contact=1', + 'mode=' + $WH.urlencode(data.mode), + 'reason=' + $WH.urlencode(data.reason), + 'desc=' + $WH.urlencode(data.description), + 'ua=' + $WH.urlencode(navigator.userAgent), + 'appname=' + $WH.urlencode(navigator.appName), + 'page=' + $WH.urlencode(data.currenturl) + ]; + + if (data.mode == 0) // contact us + { + if (data.relatedurl) + params.push('relatedurl=' + $WH.urlencode(data.relatedurl)); + if (data.email) + params.push('email=' + $WH.urlencode(data.email)); + } + else if (data.mode == 1) // comment + params.push('id=' + $WH.urlencode(data.comment.id)); + else if (data.mode == 2) // forum post + params.push('id=' + $WH.urlencode(data.post.id)); + else if (data.mode == 3) // screenshot + params.push('id=' + $WH.urlencode(data.screenshot.id)); + else if (data.mode == 4) // character + params.push('id=' + $WH.urlencode(data.profile.source)); + else if (data.mode == 5) // video + params.push('id=' + $WH.urlencode(data.video.id)); + else if (data.mode == 6) // guide + params.push('id=' + $WH.urlencode(data.guide.id)); + + data.submitting = true; + var url = '?contactus'; + new Ajax(url, { + method: 'POST', + params: params.join('&'), + onSuccess: function(xhr, opt) { + var resp = xhr.responseText; + if (resp == 0) + { + if (g_user.name) + alert($WH.sprintf(LANG.ct_dialog_thanks_user, g_user.name)); + else + alert(LANG.ct_dialog_thanks); + + Lightbox.hide(); + } + else + { + if (errors[resp]) + alert(errors[resp]); + else + alert('Error: ' + resp); + } + }, + onFailure: function(xhr, opt) { + alert('Failure submitting contact request: ' + xhr.statusText); + }, + onComplete: function(xhr, opt) { + for (var i = 0; i < form.elements.length; ++i) + form.elements[i].disabled = false; + + data.submitting = false; + } + }); + return false; + } + + this.show = function(opt) + { + if (!opt) + opt = {}; + + var data = { mode: 0 }; + $WH.cO(data, opt); + data.reasons = contexts[data.mode]; + if (location.href.indexOf('#contact') != -1) + data.currenturl = location.href.substr(0, location.href.indexOf('#contact')); + else + data.currenturl = location.href; + + var form = 'contactus'; + if (data.mode != 0) + form = 'reportform'; + + if (!_dialog) + { + this.init(); + } + + _dialog.show(form, { + data: data, + onShow: this.onShow, + onHide: this.onHide, + onSubmit: this.onSubmit + }) + } + + this.checkPound = function() + { + if (location.hash && location.hash == '#contact') + { + ContactTool.show(); + } + } + + var dialog_contacttitle = LANG.ct_dialog_contactwowhead; + + this.init = function() + { + _dialog = new Dialog(); + + Dialog.templates.contactus = { + title: dialog_contacttitle, + width: 550, + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + + fields: [ + { + id: 'reason', + type: 'select', + label: LANG.ct_dialog_reason, + required: 1, + options: [], + compute: function(field, value, form, td) + { + $WH.ee(field); + + for (var i = 0; i < this.data.reasons.length; ++i) + { + var id = this.data.reasons[i][0]; + var check = this.data.reasons[i][1]; + var valid = false; + if (typeof check == 'function') + valid = check(this.extra); + else + valid = check; + + if (!valid) + continue; + + var o = $WH.ce('option'); + o.value = id; + if (value && value == id) + o.selected = true; + + $WH.ae(o, $WH.ct(g_contact_reasons[id])); + $WH.ae(field, o); + } + + field.onchange = function() + { + if (this.value == 1 || this.value == 2 || this.value == 3) + { + form.currenturl.parentNode.parentNode.style.display = ''; + form.relatedurl.parentNode.parentNode.style.display = ''; + } + else + { + form.currenturl.parentNode.parentNode.style.display = 'none'; + form.relatedurl.parentNode.parentNode.style.display = 'none'; + } + }.bind(field); + + td.style.width = '98%'; + }, + validate: function(newValue, data, form) + { + var error = ''; + if (!newValue || newValue.length == 0) + error = LANG.ct_dialog_error_reason; + + if (error == '') + return true; + + ContactTool.displayError(form.reason, error); + form.reason.focus(); + return false; + } + }, + { + id: 'currenturl', + type: 'text', + disabled: true, + label: LANG.ct_dialog_currenturl, + size: 40 + }, + { + id: 'relatedurl', + type: 'text', + label: LANG.ct_dialog_relatedurl, + caption: LANG.ct_dialog_optional, + size: 40, + validate: function(newValue, data, form) + { + var error = ''; + var urlRe = /^(http(s?)\:\/\/|\/)?([\w]+:\w+@)?([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))(:[\d]{1,5})?((\/?\w+\/)+|\/?)(\w+\.[\w]{3,4})?((\?\w+=\w+)?(&\w+=\w+)*)?/; + newValue = newValue.trim(); + if (newValue.length >= 250) + error = LANG.ct_dialog_error_relatedurl; + else if (newValue.length > 0 && !urlRe.test(newValue)) + error = LANG.ct_dialog_error_invalidurl; + + if (error == '') + return true; + + ContactTool.displayError(form.relatedurl, error); + form.relatedurl.focus(); + return false; + } + }, + { + id: 'email', + type: 'text', + label: LANG.ct_dialog_email, + caption: LANG.ct_dialog_email_caption, + compute: function(field, value, form, td, tr) + { + if (g_user.email) + { + this.data.email = g_user.email; + tr.style.display = 'none'; + } + else + { + var func = function() + { + $('#contact-emailwarn').css('display', g_isEmailValid($(form.email).val()) ? 'none' : ''); + Lightbox.reveal(); + }; + + $(field).keyup(func).blur(func); + } + }, + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length >= 100) + error = LANG.ct_dialog_error_emaillen; + else if (newValue.length > 0 && !g_isEmailValid(newValue)) + error = LANG.ct_dialog_error_email; + + if (error == '') + return true; + + ContactTool.displayError(form.email, error); + form.email.focus(); + return false; + } + }, + { + id: 'description', + type: 'textarea', + caption: LANG.ct_dialog_desc_caption, + width: '98%', + required: 1, + size: [10, 30], + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length == 0 || newValue.length > 10000) + error = LANG.ct_dialog_error_desc; + + if (error == '') + return true; + + ContactTool.displayError(form.description, error); + form.description.focus(); + return false; + } + }, + { + id: 'noemailwarning', + type: 'caption', + compute: function(field, value, form, td) + { + $(td).html('').css('white-space', 'normal').css('padding', '0 4px'); + } + } + ], + + onInit: function(form) + { + + }, + + onShow: function(form) + { + if (this.data.focus && form[this.data.focus]) + setTimeout(g_setCaretPosition.bind(null, form[this.data.focus], form[this.data.focus].value.length), 100); + else if (form['reason'] && !form.reason.value) + setTimeout($WH.bindfunc(form.reason.focus, form.reason), 10); + else if (form['relatedurl'] && !form.relatedurl.value) + setTimeout($WH.bindfunc(form.relatedurl.focus, form.relatedurl), 10); + else if (form['email'] && !form.email.value) + setTimeout($WH.bindfunc(form.email.focus, form.email), 10); + else if (form['description'] && !form.description.value) + setTimeout($WH.bindfunc(form.description.focus, form.description), 10); + + setTimeout(Lightbox.reveal, 250); + } + } + + Dialog.templates.reportform = { + title: LANG.ct_dialog_report, + width: 550, + // height: 360, + buttons: [['okay', LANG.ok], ['cancel', LANG.cancel]], + fields: [ + { + id: 'reason', + type: 'select', + label: LANG.ct_dialog_reason, + options: [], + compute: function(field, value, form, td) + { + switch (this.data.mode) + { + case 1: // comment + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportcomment, '' + this.data.comment.user + ''); + break; + case 2: // forum post + var rep = '' + this.data.post.user + ''; + if (this.data.post.op) + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reporttopic, rep); + else + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportpost, rep); + break; + case 3: // screenshot + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportscreen, '' + this.data.screenshot.user + ''); + break; + case 4: // character + $WH.ee(form.firstChild); + $WH.ae(form.firstChild, $WH.ct(LANG.ct_dialog_reportchar)); + break; + case 5: // video + form.firstChild.innerHTML = $WH.sprintf(LANG.ct_dialog_reportvideo, '' + this.data.video.user + ''); + break; + case 6: // guide + form.firstChild.innerHTML = 'Report guide'; + break; + } + form.firstChild.setAttribute('style', ''); + + $WH.ee(field); + + var extra; + if (this.data.mode == 1) + extra = this.data.comment; + else if (this.data.mode == 2) + extra = this.data.post; + else if (this.data.mode == 3) + extra = this.data.screenshot; + else if (this.data.mode == 4) + extra = this.data.profile; + else if (this.data.mode == 5) + extra = this.data.video; + else if (this.data.mode == 6) + extra = this.data.guide; + + $WH.ae(field, $WH.ce('option', { selected: (!value), value: -1 })); + + for (var i = 0; i < this.data.reasons.length; ++i) + { + var id = this.data.reasons[i][0]; + var check = this.data.reasons[i][1]; + var valid = false; + if (typeof check == 'function') + valid = check(extra); + else + valid = check; + + if (!valid) + continue; + + var o = $WH.ce('option'); + o.value = id; + if (value && value == id) + o.selected = true; + + $WH.ae(o, $WH.ct(g_contact_reasons[id])); + $WH.ae(field, o); + } + + td.style.width = '98%'; + }, + validate: function(newValue, data, form) + { + var error = ''; + if (!newValue || newValue == -1 || newValue.length == 0) + error = LANG.ct_dialog_error_reason; + + if (error == '') + return true; + + ContactTool.displayError(form.reason, error); + form.reason.focus(); + return false; + } + }, + { + id: 'description', + type: 'textarea', + caption: LANG.ct_dialog_desc_caption, + width: '98%', + required: 1, + size: [10, 30], + validate: function(newValue, data, form) + { + var error = ''; + newValue = newValue.trim(); + if (newValue.length == 0 || newValue.length > 10000) + error = LANG.ct_dialog_error_desc; + + if (error == '') + return true; + + ContactTool.displayError(form.description, error); + form.description.focus(); + return false; + } + } + ], + + onInit: function(form) + { + + }, + + onShow: function(form) + { + /* Work-around for IE7 */ + var reason = $(form).find("*[name=reason]")[0]; + var description = $(form).find("*[name=description]")[0]; + + if (this.data.focus && form[this.data.focus]) + setTimeout(g_setCaretPosition.bind(null, form[this.data.focus], form[this.data.focus].value.length), 100); + else if (!reason.value) + setTimeout($WH.bindfunc(reason.focus, reason), 10); + else if (!description.value) + setTimeout($WH.bindfunc(description.focus, description), 10); + } + } + } + + $(document).ready(this.checkPound); +}; diff --git a/setup/tools/filegen/templates/global.js/cookies.js b/setup/tools/filegen/templates/global.js/cookies.js new file mode 100644 index 00000000..3cce1d3e --- /dev/null +++ b/setup/tools/filegen/templates/global.js/cookies.js @@ -0,0 +1,37 @@ +// TODO: Create a "Cookies" object + +function g_cookiesEnabled() +{ + document.cookie = 'enabledTest'; + return (document.cookie.indexOf("enabledTest") != -1) ? true : false; +} + +function g_getWowheadCookie(name) +{ + if (g_user.id > 0) + { + return g_user.cookies[name]; // no point checking if it exists, as undefined tests as false anyways + } + else + { + return $WH.gc(name); // plus gc does the same thing.. + } +} + +function g_setWowheadCookie(name, data, browser) +{ + var temp = name.substr(0, 5) == 'temp_'; + if (!browser && g_user.id > 0 && !temp) { + new Ajax('?cookie=' + name + '&' + name + '=' + $WH.urlencode(data), { + method: 'get', + onSuccess: function(xhr) { + if (xhr.responseText == 0) + g_user.cookies[name] = data; + } + }); + } + else if (browser || g_user.id == 0) + { + $WH.sc(name, 14, data, null, location.hostname); + } +} diff --git a/setup/tools/filegen/templates/global.js/dialog.js b/setup/tools/filegen/templates/global.js/dialog.js new file mode 100644 index 00000000..e46fa3a4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/dialog.js @@ -0,0 +1,568 @@ +var Dialog = function() +{ +var + _self = this, + _template, + _onSubmit = null, + _templateName, + + _funcs = {}, + _data, + + _inited = false, + _form = $WH.ce('form'), + _elements = {}; + + _form.onsubmit = function() { + _processForm(); + return false + }; + + this.show = function(template, opt) + { + if (template) + { + _templateName = template; + _template = Dialog.templates[_templateName]; + + _self.template = _template; + } + else + return; + + if (_template.onInit && !_inited) + (_template.onInit.bind(_self, _form, opt))(); + + if (opt.onBeforeShow) + _funcs.onBeforeShow = opt.onBeforeShow.bind(_self, _form); + + if (_template.onBeforeShow) + _template.onBeforeShow = _template.onBeforeShow.bind(_self, _form); + + if (opt.onShow) + _funcs.onShow = opt.onShow.bind(_self, _form); + + if (_template.onShow) + _template.onShow = _template.onShow.bind(_self, _form); + + if (opt.onHide) + _funcs.onHide = opt.onHide.bind(_self, _form); + + if (_template.onHide) + _template.onHide = _template.onHide.bind(_self, _form); + + if (opt.onSubmit) + _funcs.onSubmit = opt.onSubmit; + + if (_template.onSubmit) + _onSubmit = _template.onSubmit.bind(_self, _form); + + if (opt.data) + { + _inited = false; + _data = {}; + $WH.cO(_data, opt.data); + } + + _self.data = _data; + + Lightbox.show('dialog-' + _templateName, { + onShow: _onShow, + onHide: _onHide + }); + } + + this.getValue = function(id) + { + return _getValue(id); + } + + this.setValue = function(id, value) + { + _setValue(id, value); + } + + this.getSelectedValue = function(id) + { + return _getSelectedValue(id); + } + + this.getCheckedValue = function(id) + { + return _getCheckedValue(id); + } + + function _onShow(dest, first) + { + if (first || !_inited) + _initForm(dest); + + if (_template.onBeforeShow) + _template.onBeforeShow(); + + if (_funcs.onBeforeShow) + _funcs.onBeforeShow(); + + Lightbox.setSize(_template.width, _template.height); + dest.className = 'dialog'; + + _updateForm(); + + if (_template.onShow) + _template.onShow(); + + if (_funcs.onShow) + _funcs.onShow(); + } + + function _initForm(dest) + { + $WH.ee(dest); + $WH.ee(_form); + + var container = $WH.ce('div'); + container.className = 'text'; + $WH.ae(dest, container); + + $WH.ae(container, _form); + + if (_template.title) + { + var h = $WH.ce('h1'); + $WH.ae(h, $WH.ct(_template.title)); + $WH.ae(_form, h); + } + + var t = $WH.ce('table'), + tb = $WH.ce('tbody'), + mergeCell = false; + + $WH.ae(t, tb); + $WH.ae(_form, t); + + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + element; + + if (!mergeCell) + { + tr = $WH.ce('tr'); + th = $WH.ce('th'); + td = $WH.ce('td'); + } + + field.__tr = tr; + + if (_data[field.id] == null) + _data[field.id] = (field.value ? field.value : ''); + + var options; + if (field.options) + { + options = []; + + if (field.optorder) + $WH.cO(options, field.optorder); + else + { + for (var j in field.options) + options.push(j); + } + + if (field.sort) + options.sort(function(a, b) { return field.sort * $WH.strcmp(field.options[a], field.options[b]); }); + } + + switch (field.type) + { + case 'caption': + + th.colSpan = 2; + th.style.textAlign = 'left'; + th.style.padding = 0; + + if (field.compute) + (field.compute.bind(_self, null, _data[field.id], _form, th, tr))(); + else if (field.label) + $WH.ae(th, $WH.ct(field.label)); + + $WH.ae(tr, th); + $WH.ae(tb, tr); + + continue; + break; + + case 'textarea': + + var f = element = $WH.ce('textarea'); + + f.name = field.id; + + if (field.disabled) + f.disabled = true; + + f.rows = field.size[0]; + f.cols = field.size[1]; + + td.colSpan = 2; + + if (field.label) + { + th.colSpan = 2; + th.style.textAlign = 'left'; + th.style.padding = 0; + td.style.padding = 0; + + $WH.ae(th, $WH.ct(field.label)); + $WH.ae(tr, th); + $WH.ae(tb, tr); + + tr = $WH.ce('tr'); + } + + $WH.ae(td, f); + + break; + + case 'select': + + var f = element = $WH.ce('select'); + + f.name = field.id; + + if (field.size) + f.size = field.size; + + if (field.disabled) + f.disabled = true; + + if (field.multiple) + f.multiple = true; + + for (var j = 0, len2 = options.length; j < len2; ++j) + { + var o = $WH.ce('option'); + + o.value = options[j]; + + $WH.ae(o, $WH.ct(field.options[options[j]])); + $WH.ae(f, o) + } + + $WH.ae(td, f); + + break; + + case 'dynamic': + + td.colSpan = 2; + td.style.textAlign = 'left'; + td.style.padding = 0; + + if (field.compute) + (field.compute.bind(_self, null, _data[field.id], _form, td, tr))(); + + $WH.ae(tr, td); + $WH.ae(tb, tr); + + element = td; + + break; + + case 'checkbox': + case 'radio': + + var k = 0; + element = []; + for (var j = 0, len2 = options.length; j < len2; ++j) + { + var + s = $WH.ce('span'), + f, + l, + uniqueId = 'sdfler46' + field.id + '-' + options[j]; + + if (j > 0 && !field.noInputBr) + $WH.ae(td, $WH.ce('br')); + + l = $WH.ce('label'); + l.setAttribute('for', uniqueId); + l.onmousedown = $WH.rf; + + f = $WH.ce('input', { name: field.id, value: options[j], id: uniqueId }); + f.setAttribute('type', field.type); + + if (field.disabled) + f.disabled = true; + + if (field.submitOnDblClick) + l.ondblclick = f.ondblclick = function(e) { _processForm(); }; + + if (field.compute) + (field.compute.bind(_self, f, _data[field.id], _form, td, tr))(); + + $WH.ae(l, f); + $WH.ae(l, $WH.ct(field.options[options[j]])); + $WH.ae(td, l); + + element.push(f); + } + + break; + + default: // Textbox + + var f = element = $WH.ce('input'); + + f.name = field.id; + + if (field.size) + f.size = field.size; + + if (field.disabled) + f.disabled = true; + + if (field.submitOnEnter) + { + f.onkeypress = function(e) { + e = $WH.$E(e); + if (e.keyCode == 13) + _processForm(); + }; + } + + f.setAttribute('type', field.type); + + $WH.ae(td, f); + + break; + } + + if (field.label) + { + if (field.type == 'textarea') + { + if (field.labelAlign) + td.style.textAlign = field.labelAlign; + + td.colSpan = 2; + } + else + { + if (field.labelAlign) + th.style.textAlign = field.labelAlign; + + $WH.ae(th, $WH.ct(field.label)); + $WH.ae(tr, th); + } + } + + if (field.placeholder) + f.placeholder = field.placeholder; + + if (field.type != 'checkbox' && field.type != 'radio') + { + if (field.width) + f.style.width = field.width; + + if (field.compute && field.type != 'caption' && field.type != 'dynamic') + (field.compute.bind(_self, f, _data[field.id], _form, td, tr))(); + } + + if (field.caption) + { + var s = $WH.ce('small'); + if (field.type != 'textarea') + s.style.paddingLeft = '2px'; + s.className = 'q0'; // commented in 5.0? + $WH.ae(s, $WH.ct(field.caption)); + $WH.ae(td, s); + } + + $WH.ae(tr, td); + $WH.ae(tb, tr); + + mergeCell = field.mergeCell; + + _elements[field.id] = element; + } + + for (var i = _template.buttons.length; i > 0; --i) + { + var + button = _template.buttons[i - 1], + a = $WH.ce('a'); + + a.onclick = _processForm.bind(a, button[0]); + a.className = 'dialog-' + button[0]; + a.href = 'javascript:;'; + $WH.ae(a, $WH.ct(button[1])); + $WH.ae(dest, a); + } + + var _ = $WH.ce('div'); + _.className = 'clear'; + $WH.ae(dest, _); + + _inited = true; + } + + function _updateForm() + { + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + f = _elements[field.id]; + + switch (field.type) + { + case 'caption': // Do nothing + break; + + case 'select': + for (var j = 0, len2 = f.options.length; j < len2; j++) + f.options[j].selected = (f.options[j].value == _data[field.id] || $WH.in_array(_data[field.id], f.options[j].value) != -1); + break; + + case 'checkbox': + case 'radio': + for (var j = 0, len2 = f.length; j < len2; j++) + f[j].checked = (f[j].value == _data[field.id] || $WH.in_array(_data[field.id], f[j].value) != -1); + break; + + default: + f.value = _data[field.id]; + break; + } + + if (field.update) + (field.update.bind(_self, null, _data[field.id], _form, f))(); + } + } + + function _onHide() + { + if (_template.onHide) + _template.onHide(); + + if (_funcs.onHide) + _funcs.onHide(); + } + + function _processForm(button) + { + // if (button == 'x') // aowow - button naming differs + if (button == 'cancel') // Special case + return Lightbox.hide(); + + for (var i = 0, len = _template.fields.length; i < len; ++i) + { + var + field = _template.fields[i], + newValue; + + switch (field.type) + { + case 'caption': // Do nothing + continue; + + case 'select': + newValue = _getSelectedValue(field.id); + break; + + case 'checkbox': + case 'radio': + newValue = _getCheckedValue(field.id); + break; + + case 'dynamic': + if (field.getValue) + { + newValue = field.getValue(field, _data, _form); + break; + } + default: + newValue = _getValue(field.id); + break; + } + + if (field.validate) + { + if (!field.validate(newValue, _data, _form)) + return; + } + + if (newValue && typeof newValue == 'string') + newValue = $WH.trim(newValue); + + _data[field.id] = newValue; + } + + _submitData(button); + } + + function _submitData(button) + { + var ret; + + if (_onSubmit) + ret = _onSubmit(_data, button, _form); + + if (_funcs.onSubmit) + ret = _funcs.onSubmit(_data, button, _form); + + if (ret === undefined || ret) + Lightbox.hide(); + + return false; + } + + function _getValue(id) + { + return _elements[id].value; + } + + function _setValue(id, value) + { + _elements[id].value = value; + } + + function _getSelectedValue(id) + { + var + result = [], + f = _elements[id]; + + for (var i = 0, len = f.options.length; i < len; i++) + { + if (f.options[i].selected) + result.push(parseInt(f.options[i].value) == f.options[i].value ? parseInt(f.options[i].value) : f.options[i].value); + } + + if (result.length == 1) + result = result[0]; + + return result; + } + + function _getCheckedValue(id) + { + var + result = [], + f = _elements[id]; + + for (var i = 0, len = f.length; i < len; i++) + { + if (f[i].checked) + result.push(parseInt(f[i].value) == f[i].value ? parseInt(f[i].value) : f[i].value); + } + + return result; + } +}; + +Dialog.templates = {}; +Dialog.extraFields = {}; diff --git a/setup/tools/filegen/templates/global.js/dom_manipulation.js b/setup/tools/filegen/templates/global.js/dom_manipulation.js new file mode 100644 index 00000000..787c45e9 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/dom_manipulation.js @@ -0,0 +1,252 @@ +/* +Global functions related to DOM manipulation, events & forms that jQuery doesn't already provide +*/ + +function g_addCss(css) +{ + var style = $WH.ce('style'); + style.type = 'text/css'; + + if (style.styleSheet) // ie + style.styleSheet.cssText = css; + else + $WH.ae(style, $WH.ct(css)); + + var head = $WH.gE(document, 'head')[0]; + $WH.ae(head, style); +} + +function g_setTextNodes(n, text) +{ + if (n.nodeType == 3) + n.nodeValue = text; + else + { + for (var i = 0; i < n.childNodes.length; ++i) + g_setTextNodes(n.childNodes[i], text); + } +} + +function g_setInnerHtml(n, text, nodeType) +{ + if (n.nodeName.toLowerCase() == nodeType) + n.innerHTML = text; + else + { + for (var i = 0; i < n.childNodes.length; ++i) + g_setInnerHtml(n.childNodes[i], text, nodeType); + } +} + +function g_getFirstTextContent(node) +{ + for (var i = 0; i < node.childNodes.length; ++i) + { + if (node.childNodes[i].nodeName == '#text') + return node.childNodes[i].nodeValue; + + var ret = g_getFirstTextContent(node.childNodes[i]); + if (ret) + return ret; + } + + return false; +} + +function g_getTextContent(el) +{ + var txt = ''; + for (var i = 0; i < el.childNodes.length; ++i) + { + if (el.childNodes[i].nodeValue) + txt += el.childNodes[i].nodeValue; + else if (el.childNodes[i].nodeName == 'BR') + txt += '\n'; + + txt += g_getTextContent(el.childNodes[i]); + } + + return txt; +} + +function g_toggleDisplay(el) +{ + el = $(el); + el.toggle(); + if (el.is(':visible')) + return true; + + return false; +} + +function g_enableScroll(enabled) +{ + if (!enabled) + { + $WH.aE(document, 'mousewheel', g_enableScroll.F); + $WH.aE(window, 'DOMMouseScroll', g_enableScroll.F); + } + else + { + $WH.dE(document, 'mousewheel', g_enableScroll.F); + $WH.dE(window, 'DOMMouseScroll', g_enableScroll.F); + } +} + +g_enableScroll.F = function(e) +{ + if (e.stopPropagation) + e.stopPropagation(); + if (e.preventDefault) + e.preventDefault(); + + e.returnValue = false; + e.cancelBubble = true; + + return false; +}; + +// from http://blog.josh420.com/archives/2007/10/setting-cursor-position-in-a-textbox-or-textarea-with-javascript.aspx +function g_setCaretPosition(elem, caretPos) +{ + if (!elem) + return; + + if (elem.createTextRange) + { + var range = elem.createTextRange(); + range.move('character', caretPos); + range.select(); + } + else if (elem.selectionStart != undefined) + { + elem.focus(); + elem.setSelectionRange(caretPos, caretPos); + } + else + elem.focus(); +} + +function g_insertTag(where, tagOpen, tagClose, repFunc) +{ + var n = $WH.ge(where); + + n.focus(); + if (n.selectionStart != null) + { + var s = n.selectionStart, + e = n.selectionEnd, + sL = n.scrollLeft, + sT = n.scrollTop; + + var selectedText = n.value.substring(s, e); + if (typeof repFunc == 'function') + selectedText = repFunc(selectedText); + + n.value = n.value.substr(0, s) + tagOpen + selectedText + tagClose + n.value.substr(e); + n.selectionStart = n.selectionEnd = e + tagOpen.length; + + n.scrollLeft = sL; + n.scrollTop = sT; + } + else if (document.selection && document.selection.createRange) + { + var range = document.selection.createRange(); + + if (range.parentElement() != n) + return; + + var selectedText = range.text; + if (typeof repFunc == 'function') + selectedText = repFunc(selectedText); + + range.text = tagOpen + selectedText + tagClose; +/* + range.moveEnd("character", -tagClose.length); + range.moveStart("character", range.text.length); + + range.select(); +*/ + } + + if (n.onkeyup) + n.onkeyup(); +} + +function g_onAfterTyping(input, func, delay) +{ + var timerId; + var ldsgksdgnlk623 = function() + { + if (timerId) + { + clearTimeout(timerId); + timerId = null; + } + timerId = setTimeout(func, delay); + }; + input.onkeyup = ldsgksdgnlk623; +} + +function g_onClick(el, func) +{ + var firstEvent = 0; + + function rightClk(n) + { + if (firstEvent) + { + if (firstEvent != n) + return; + } + else + firstEvent = n; + + func(true); + } + + el.onclick = function(e) + { + e = $WH.$E(e); + + if (e._button == 2) // middle click + return true; + + return false; + } + + el.oncontextmenu = function() + { + rightClk(1); + + return false; + } + + el.onmouseup = function(e) + { + e = $WH.$E(e); + + if (e._button == 3 || e.shiftKey || e.ctrlKey) // Right/Shift/Ctrl + { + rightClk(2); + } + else if (e._button == 1) // Left + { + func(false); + } + + return false; + } +} + +function g_isLeftClick(e) +{ + e = $WH.$E(e); + return (e && e._button == 1); +} + +function g_preventEmptyFormSubmission() // Used on the homepage and in the top bar +{ + if (!$.trim(this.elements[0].value)) + return false; +} diff --git a/setup/tools/filegen/templates/global.js/favorites.js b/setup/tools/filegen/templates/global.js/favorites.js new file mode 100644 index 00000000..2470302f --- /dev/null +++ b/setup/tools/filegen/templates/global.js/favorites.js @@ -0,0 +1,262 @@ +var Favorites = new function() +{ + var _type = null; + var _typeId = null; + var _favIcon = null; + + this.pageInit = function(h1, type, typeId) + { + if (typeof h1 == 'string') + { + if (!document.querySelector) + return; + + h1 = document.querySelector(h1); + } + + if (!h1 || typeof type != 'number' || typeof typeId != 'number') + return; + + _type = type; + _typeId = typeId; + + createIcon(h1); + } + + function initFavIcon() + { + var h1 = typeof g_pageInfo == 'object' && typeof g_pageInfo.type == 'number' && typeof g_pageInfo.typeId == 'number' ? document.querySelector('#main-contents h1') : null; + if (!h1) { + if (document.readyState !== 'complete') + setTimeout(initFavIcon, 9); + + return; + } + + _type = g_pageInfo.type; + _typeId = g_pageInfo.typeId; + + createIcon(h1); + } + + this.hasFavorites = function() + { + return !!g_favorites.length + } + + this.getMenu = function() + { + var favMenu = []; + var nGroups = 0; + var nEntries = 0; + + for (var i = 0, favGroup; favGroup = g_favorites[i]; i++) + { + if (!favGroup.entities.length) + continue; + + nGroups++; + var subMenu = []; + for (var j = 0, favEntry; favEntry = favGroup.entities[j]; j++) + { + subMenu.push([favEntry[0], favEntry[1], '?' + g_types[favGroup.id] + '=' + favEntry[0]]); + nEntries++ + } + + Menu.sort(subMenu); + favMenu.push([favGroup.id, LANG.types[favGroup.id][2], , subMenu]) + } + + Menu.sort(favMenu); + + // display short favorites as 1-dim list + if ((nGroups == 1 && nEntries <= 45) || (nGroups == 2 && nGroups + nEntries <= 30) || (nGroups > 2 && nGroups + nEntries <= 15)) + { + var list = []; + + for (var i = 0; subMenu = favMenu[i]; i++) + { + list.push([, subMenu[MENU_IDX_NAME]]); + + for (var j = 0, subEntry; subEntry = subMenu[MENU_IDX_SUB][j]; j++) + { + var listEntry = [subEntry[MENU_IDX_ID], subEntry[MENU_IDX_NAME], subEntry[MENU_IDX_URL]]; + + if (subEntry[MENU_IDX_OPT]) + listEntry[MENU_IDX_OPT] = subEntry[MENU_IDX_OPT]; + + list.push(listEntry); + } + } + + favMenu = list; + } + + return favMenu; + } + + this.refreshMenu = function() + { + var menuRoot = $('#toplinks-favorites'); + if (!menuRoot.length) + return; + + var favMenu = Favorites.getMenu(); + if (!favMenu.length) { + menuRoot.hide(); + return; + } + + Menu.add(menuRoot, favMenu); + menuRoot.show(); + } + + function createIcon(heading) + { + _favIcon = $('', { + 'class': 'fav-star', + mouseout: $WH.Tooltip.hide + }).appendTo(heading); + + if (g_user.id) + { + _favIcon.addClass('fav-star' + (isFaved(_type, _typeId) ? '-1' : '-0')).click((function(type, typeId, name) { + toggleEntry(type, typeId, name); + updateIcon(type, typeId); + $WH.Tooltip.hide(); + }).bind(null, _type, _typeId, heading.textContent.trim().replace(/(.+)<.*/, '$1'))); + + _favIcon.mouseover(function(event) { + var tt = this.className.match(/\bfav-star-0\b/) ? LANG.addtofavorites : LANG.removefromfavorites; + $WH.Tooltip.show(this, tt, false, false, 'q2'); + }); + + } + else + { + _favIcon.addClass('fav-star-0').click(function() { + location.href = "?account=signin"; + $WH.Tooltip.hide(); + }).mouseover(function(event) { + $WH.Tooltip.show(this, LANG.favorites_login + '
        ' + LANG.clicktologin + ''); + }); + } + } + + function updateIcon(type, typeId) + { + if (_favIcon) + { + var rmv = 'fav-star-0'; + var add = 'fav-star-1'; + if (!isFaved(type, typeId)) + { + rmv = 'fav-star-1'; + add = 'fav-star-0'; + } + + _favIcon.removeClass(rmv).addClass(add); + } + } + + function isFaved(type, typeId) + { + var idx = getIndex(type); + if (idx == -1) + return false; + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + if (j[0] == typeId) + return true; + + return false; + } + + function toggleEntry(type, typeId, name) + { + if (isFaved(type, typeId)) + removeEntry(type, typeId); + else + addEntry(type, typeId, name); + } + + function addEntry(type, typeId, name) + { + var idx = getIndex(type, true); + if (idx == -1) + { + /* $WH. */ console.error("Invalid type when adding entity to favorites! Type was:", type); + return; + } + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + { + if (j[0] == typeId) + { + alert(LANG.favorites_duplicate.replace('%s', LANG.types[type][1])); + return; + } + } + + sendUpdate('add', type, typeId); + g_favorites[idx].entities.push([typeId, name]); + Favorites.refreshMenu(); + } + + function removeEntry(type, typeId) + { + var idx = getIndex(type); + if (idx == -1) + return; + + for (var i = 0, j; j = g_favorites[idx].entities[i]; i++) + { + if (j[0] == typeId) + { + sendUpdate('remove', type, typeId); + g_favorites[idx].entities.splice(i, 1); + if (!g_favorites[idx].entities.length) + g_favorites.splice(idx, 1); + + Favorites.refreshMenu(); + return; + } + } + } + + function getIndex(type, createNew) + { + if (!LANG.types[type]) + return -1; + + for (var i = 0, j; j = g_favorites[i]; i++) + if (j.id == type) + return i; + + if (!createNew) + return -1; + + g_favorites.push({ id: type, entities: [] }); + + g_favorites.sort(function(a, b) { return $WH.strcmp(LANG.types[a.id], LANG.types[b.id]) }); + + for (i = 0; j = g_favorites[i]; i++) + if (j.id == type) + return i; + + return -1; + } + + function sendUpdate(method, type, typeId) + { + var data = { + id: typeId, + // sessionKey: g_user.sessionKey + }; + data[method] = type; + $.post('?account=favorites', data); + } + + if (document.querySelector && $WH.localStorage.isSupported()) + initFavIcon(); +}; diff --git a/setup/tools/filegen/templates/global.js/guide.js b/setup/tools/filegen/templates/global.js/guide.js new file mode 100644 index 00000000..254abab5 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/guide.js @@ -0,0 +1,368 @@ +var g_localTime = new Date(); + +/* This function is to get the stars for the vote control for the guides. */ + +function GetStars(stars, ratable, userRating, guideId) +{ + var STARS_MAX = 5; + var averageRating = stars; + + if (userRating) + stars = userRating; + + stars = Math.round(stars*2)/2; + var starsRounded = Math.round(stars); + var ret = $("").addClass('stars').addClass('max-' + STARS_MAX).addClass('stars-' + starsRounded); + + if (!g_user.id) + ratable = false; + + if (ratable) + ret.addClass('ratable'); + + if (userRating) + ret.addClass('rated'); + + /* This is kinda lame but oh well */ + var contents = ''; + + var wbr = '​'; + var tmp = stars; + for (var i = 1; i <= STARS_MAX; ++i) + { + if (tmp < 1 && tmp > 0) + contents += ''; + else + contents += ''; + --tmp; + + contents += '' + wbr + ''; + } + + for (var i = 1; i <= STARS_MAX; ++i) + contents += ''; + + contents += ''; + + ret.append(contents); + + if (ratable) + { + var starNumber = 0; + ret.find('i.clickable').each(function() { var starId = ++starNumber; $(this).click(function() { VoteGuide(guideId, averageRating, starId); }); }) + } + + if (userRating) + { + var clear = $("").addClass('clear').click(function() { VoteGuide(guideId, averageRating, 0); }); + ret.append(clear); + } + + if (stars >= 0) + ret.mouseover(function(event) {$WH.Tooltip.showAtCursor(event, 'Rating: ' + stars + ' / ' + STARS_MAX, 0, 0, 'q');}).mousemove(function(event) {$WH.Tooltip.cursorUpdate(event)}).mouseout(function() {$WH.Tooltip.hide()}); + + return ret; +} + +function VoteGuide(guideId, oldRating, newRating) +{ + // Update stars display + $('#guiderating').html(GetStars(oldRating, true, newRating, guideId)); + + // Vote + $.ajax({cache: false, url: '?guide=vote', type: 'POST', + error: function() { + $('#guiderating').html(GetStars(oldRating, true, 0, guideId)); + alert('Voting failed. Try again later.'); + }, + success: function(json) { + var data = eval('(' + json + ')'); + $('#guiderating-value').text(data.rating); + $('#guiderating-votes').text(GetN5(data.nvotes)); + }, + data: { id: guideId, rating: newRating } + }); +} + +/* g_enhanceTextarea and createOptionsMenuWidget are only ever used by the article/guide editor. Why are they in global.js? */ + +function g_enhanceTextarea (ta, opt) { + if (!(ta instanceof jQuery)) + ta = $(ta); + + if (ta.data("wh-enhanced") || ta.prop("tagName") != "TEXTAREA") + return; + + if (typeof opt != "object") + opt = {}; + + var canResize = (function(el) { + if (!el.dynamicResizeOption) + return true; + + if ($WH.localStorage.get("dynamic-textarea-resizing") === "true") + return true; + + if ($WH.localStorage.get("dynamic-textarea-resizing") === "false") + return false; + + return !el.hasOwnProperty("dynamicSizing") || el.dynamicSizing; + }).bind(null, opt); + + var height = ta.height() || 500; + var wrapper = $("
        ", { "class": "enhanced-textarea-wrapper" }).insertBefore(ta).append(ta); + + if (!opt.hasOwnProperty("color")) + wrapper.addClass("enhanced-textarea-dark"); + else if (opt.color) + wrapper.addClass("enhanced-textarea-" + opt.color); + + if (!opt.hasOwnProperty("dynamicSizing") || opt.dynamicSizing || opt.dynamicResizeOption) { + var expander = $("
        ", { "class": "enhanced-textarea-expander" }).prependTo(wrapper); + var dynamicResize = function(textarea, exactHeight, canResizeFn) { + if (!canResizeFn()) + return; + + // E.css("height", E.siblings(".enhanced-textarea-expander").html($WH.htmlentities(E.val()).replace(/\n/g, "
        ") + "
        ").height() + (D ? 14 : 34) + "px"); + textarea.css("height", textarea.siblings(".enhanced-textarea-expander").html($WH.htmlentities(textarea.val()) + "
        ").height() + (exactHeight ? 14 : 34) + "px"); + }; + + ta.bind("keydown keyup change", dynamicResize.bind(this, ta, opt.exactLineHeights, canResize)); + dynamicResize(ta, opt.exactLineHeights, canResize); + + var setWidth = function(el) { el.css("width", el.parent().width() + "px"); }; + + setWidth(expander); + setTimeout(setWidth.bind(null, expander), 1); + + if (!opt.dynamicResizeOption || (opt.dynamicResizeOption && canResize())) + wrapper.addClass("enhanced-textarea-dynamic-sizing"); + } + + if (!opt.hasOwnProperty("focusChanges") || opt.focusChanges) + wrapper.addClass("enhanced-textarea-focus-changes"); + + if (opt.markup) { + var _markupMenu = $("
        ", { "class": "enhanced-textarea-markup-wrapper" }).prependTo(wrapper); + var _segments = $("
        ", { "class": "enhanced-textarea-markup" }).appendTo(_markupMenu); + var _toolbar = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _menu = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + + if (opt.markup == "inline") + ar_AddInlineToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); + else + ar_AddToolbar(ta.get(0), _toolbar.get(0), _menu.get(0)); + + if (opt.dynamicResizeOption) { + var _dynResize = $("
        ", { "class": "enhanced-textarea-markup-segment" }).appendTo(_segments); + var _lblDynResize = $("
        />
        @@ -118,9 +103,3 @@ - -
        - - - -brick('footer'); ?> From 8fadce88ad0460a7df05912fdd99a552206572db Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:51:36 +0200 Subject: [PATCH 0983/1249] Template/Update (Part 46 - III) * account management rework: Recovery Options --- endpoints/account/forgot-password.php | 101 +++++++++++++ endpoints/account/forgot-username.php | 100 +++++++++++++ endpoints/account/resend-submit.php | 52 +++++++ endpoints/account/resend.php | 98 +++++++++++++ endpoints/account/reset-password.php | 121 ++++++++++++++++ template/bricks/inputbox-form-email.tpl.php | 46 ++++++ .../bricks/inputbox-form-password.tpl.php | 78 ++++++++++ template/bricks/inputbox-form-signin.tpl.php | 2 +- template/mails/reset-password_0.tpl | 2 +- template/mails/reset-password_2.tpl | 2 +- template/mails/reset-password_3.tpl | 2 +- template/mails/reset-password_4.tpl | 2 +- template/mails/reset-password_6.tpl | 2 +- template/mails/reset-password_8.tpl | 2 +- template/pages/acc-recover.tpl.php | 136 ------------------ 15 files changed, 603 insertions(+), 143 deletions(-) create mode 100644 endpoints/account/forgot-password.php create mode 100644 endpoints/account/forgot-username.php create mode 100644 endpoints/account/resend-submit.php create mode 100644 endpoints/account/resend.php create mode 100644 endpoints/account/reset-password.php create mode 100644 template/bricks/inputbox-form-email.tpl.php create mode 100644 template/bricks/inputbox-form-password.tpl.php delete mode 100644 template/pages/acc-recover.tpl.php diff --git a/endpoints/account/forgot-password.php b/endpoints/account/forgot-password.php new file mode 100644 index 00000000..4121f47f --- /dev/null +++ b/endpoints/account/forgot-password.php @@ -0,0 +1,101 @@ + display email form + * 2. submit email form > send mail with recovery link + * 3. click recovery link from mail > display password reset form + * 4. submit password reset form > update password + */ + +class AccountforgotpasswordResponse extends TemplateResponse +{ + use TrRecoveryHelper, TrGetNext; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'forgot-password'; + + protected array $expectedPOST = array( + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + // don't redirect logged in users + // you can be forgetful AND logged in + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + $msg = $this->processMailForm(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverPass', [1.5]), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'recoverPass', [1]), + 'error' => $msg, + 'action' => '?account=forgot-password&next='.$this->getNext(), + 'email' => $this->_post['email'] ?? '' + )]; + } + + private function processMailForm() : string + { + // no input yet. show clean email form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); + + // on cooldown pretend we dont know the email address + if ($timeout && $timeout > time()) + return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // pretend recovery started + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) + { + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]); + } + + // recovery actually started + if ($err = $this->startRecovery(ACC_STATUS_RECOVER_PASS, 'reset-password', $this->_post['email'])) + return $err; + + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'recovPassSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/forgot-username.php b/endpoints/account/forgot-username.php new file mode 100644 index 00000000..4a6245d4 --- /dev/null +++ b/endpoints/account/forgot-username.php @@ -0,0 +1,100 @@ + display email form + * 2. submit email form > send mail with recovery link + * ( 3. click recovery link from mail to go to signin page (so not on this page) ) + */ + +class AccountforgotusernameResponse extends TemplateResponse +{ + use TrRecoveryHelper; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'forgot-username'; + + protected array $expectedPOST = array( + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + // if the user is looged in goto account dashboard + if (User::isLoggedIn()) + $this->forward('?account'); + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + $msg = $this->processMailForm(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'recoverUser'), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'recoverUser'), + 'error' => $msg, + 'action' => '?account=forgot-username' + )]; + } + + private function processMailForm() : string + { + // no input yet. show empty form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); + + // on cooldown pretend we dont know the email address + if ($timeout && $timeout > time()) + return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // pretend recovery started + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) + { + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]); + } + + // recovery actually started + if ($err = $this->startRecovery(ACC_STATUS_RECOVER_USER, 'recover-user', $this->_post['email'])) + return $err; + + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'recovUserSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/resend-submit.php b/endpoints/account/resend-submit.php new file mode 100644 index 00000000..c45c0499 --- /dev/null +++ b/endpoints/account/resend-submit.php @@ -0,0 +1,52 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + public function __construct(string $pageParam) + { + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + $error = $message = ''; + + if ($this->assertPOST('email')) + $message = Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + else + $error = Lang::main('intError'); + + parent::generate(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'register', [1.5]), + 'message' => $message, + 'error' => $error + )]; + } +} + +?> diff --git a/endpoints/account/resend.php b/endpoints/account/resend.php new file mode 100644 index 00000000..234aea17 --- /dev/null +++ b/endpoints/account/resend.php @@ -0,0 +1,98 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + $this->title[] = Lang::account('title'); + + parent::generate(); + + // error from account=activate + if (isset($_SESSION['error']['activate'])) + { + $msg = $_SESSION['error']['activate']; + unset($_SESSION['error']['activate']); + } + else + $msg = $this->resend(); + + if ($this->success) + $this->inputbox = ['inputbox-status', ['head' => Lang::account('inputbox', 'head', 'resendMail'), 'message' => $msg]]; + else + $this->inputbox = ['inputbox-form-email', array( + 'head' => Lang::account('inputbox', 'head', 'resendMail'), + 'message' => Lang::account('inputbox', 'message', 'resendMail'), + 'error' => $msg, + 'action' => '?account=resend', + )]; + } + + private function resend() : string + { + // no input yet. show clean form + if (is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT')); + + // on cooldown pretend we dont know the email address + if ($timeout && $timeout > time()) + return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + + // check email and account status + if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ?_account WHERE `email` = ? AND `status` = ?d', $this->_post['email'], ACC_STATUS_NEW)) + { + if (!Util::sendMail($this->_post['email'], 'activate-account', [$token])) + return Lang::main('intError'); + + DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); + + $this->success = true; + return Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + } + + // pretend recovery started + // do not confirm or deny existence of email + $this->success = !Cfg::get('DEBUG'); + return Cfg::get('DEBUG') ? Lang::account('inputbox', 'error', 'emailNotFound') : Lang::account('inputbox', 'message', 'createAccSent', [$this->_post['email']]); + } +} + +?> diff --git a/endpoints/account/reset-password.php b/endpoints/account/reset-password.php new file mode 100644 index 00000000..44c39b0b --- /dev/null +++ b/endpoints/account/reset-password.php @@ -0,0 +1,121 @@ + display email form + * 2. submit email form > send mail with recovery link + * 3. click recovery link from mail > display password reset form + * 4. submit password reset form > update password + */ + +class AccountresetpasswordResponse extends TemplateResponse +{ + use TrRecoveryHelper, TrGetNext; + + protected string $template = 'text-page-generic'; + protected string $pageName = 'reset-password'; + + protected array $expectedGET = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + ); + protected array $expectedPOST = array( + 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], + 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW ], + 'password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'c_password' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + private bool $success = false; + + public function __construct() + { + $this->title[] = Lang::account('title'); + + parent::__construct(); + + // don't redirect logged in users + // you can be forgetful AND logged in + + if (Cfg::get('ACC_EXT_RECOVER_URL')) + $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); + + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + } + + protected function generate() : void + { + parent::generate(); + + $errMsg = ''; + if (!$this->assertGET('key') && !$this->assertPOST('key')) + $errMsg = Lang::account('inputbox', 'error', 'passTokenLost'); + else if ($this->_get['key'] && !DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `token` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS)) + $errMsg = Lang::account('inputbox', 'error', 'passTokenUsed'); + + if ($errMsg) + { + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', 'error'), + 'error' => $errMsg + )]; + + return; + } + + // step "2.5" + $errMsg = $this->doResetPass(); + if ($this->success) + $this->forward('?account=signin'); + + // step 2 + $this->inputbox = ['inputbox-form-password', array( + 'head' => Lang::account('inputbox', 'head', 'recoverPass', [2]), + 'token' => $this->_post['key'] ?? $this->_get['key'], + 'action' => '?account=reset-password&next=account=signin', + 'error' => $errMsg, + )]; + } + + private function doResetPass() : string + { + // no input yet. show clean form + if (!$this->assertPOST('key', 'password', 'c_password') && is_null($this->_post['email'])) + return ''; + + // truncated due to validation fail + if (!$this->_post['email']) + return Lang::account('emailInvalid'); + + if ($this->_post['password'] != $this->_post['c_password']) + return Lang::account('passCheckFail'); + + $userData = DB::Aowow()->selectRow('SELECT `id`, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', + $this->_post['key'], + $this->_post['email'], + ACC_STATUS_RECOVER_PASS + ); + if (!$userData) + return Lang::account('inputbox', 'error', 'emailNotFound'); + + if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash'])) + return Lang::account('newPassDiff'); + + if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_NONE, $userData['id'])) + return Lang::main('intError'); + + $this->success = true; + return ''; + } +} + +?> diff --git a/template/bricks/inputbox-form-email.tpl.php b/template/bricks/inputbox-form-email.tpl.php new file mode 100644 index 00000000..dfc7be5d --- /dev/null +++ b/template/bricks/inputbox-form-email.tpl.php @@ -0,0 +1,46 @@ + +
        + + + +
        +
        +

        +
        + + +
        + +
        + +
        + + +
        + +
        +
        + diff --git a/template/bricks/inputbox-form-password.tpl.php b/template/bricks/inputbox-form-password.tpl.php new file mode 100644 index 00000000..5f76c375 --- /dev/null +++ b/template/bricks/inputbox-form-password.tpl.php @@ -0,0 +1,78 @@ + +
        + + + +
        +
        +

        +
        + + + + + + + + + + + + + + + + + + +
        + + +
        +
        + + diff --git a/template/bricks/inputbox-form-signin.tpl.php b/template/bricks/inputbox-form-signin.tpl.php index a917c7a5..4fa30d83 100644 --- a/template/bricks/inputbox-form-signin.tpl.php +++ b/template/bricks/inputbox-form-signin.tpl.php @@ -52,7 +52,7 @@
        -
        | |
        +
        | |
        diff --git a/template/mails/reset-password_0.tpl b/template/mails/reset-password_0.tpl index d2e8bb11..7fe9762b 100644 --- a/template/mails/reset-password_0.tpl +++ b/template/mails/reset-password_0.tpl @@ -2,6 +2,6 @@ Password Reset Follow this link to reset your password. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s If you did not request this mail simply ignore it. diff --git a/template/mails/reset-password_2.tpl b/template/mails/reset-password_2.tpl index ba7fd163..97a3cd75 100644 --- a/template/mails/reset-password_2.tpl +++ b/template/mails/reset-password_2.tpl @@ -2,6 +2,6 @@ Réinitialisation du mot de passe Suivez ce lien pour réinitialiser votre mot de passe. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Si vous n'avez pas fait de demande de réinitialisation, ignorez cet e-mail. diff --git a/template/mails/reset-password_3.tpl b/template/mails/reset-password_3.tpl index d7cd3a00..0cf87890 100644 --- a/template/mails/reset-password_3.tpl +++ b/template/mails/reset-password_3.tpl @@ -2,6 +2,6 @@ Kennwortreset Folgt diesem Link um euer Kennwort zurückzusetzen. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Falls Ihr diese Mail nicht angefordert habt kann sie einfach ignoriert werden. diff --git a/template/mails/reset-password_4.tpl b/template/mails/reset-password_4.tpl index 80c54065..615da1fb 100644 --- a/template/mails/reset-password_4.tpl +++ b/template/mails/reset-password_4.tpl @@ -2,6 +2,6 @@ é‡ç½®å¯†ç  点击此链接以é‡ç½®æ‚¨çš„密ç ã€‚ -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s 如果您没有请求此邮件,请忽略它。 diff --git a/template/mails/reset-password_6.tpl b/template/mails/reset-password_6.tpl index 0560bfd2..83ad31fc 100644 --- a/template/mails/reset-password_6.tpl +++ b/template/mails/reset-password_6.tpl @@ -2,6 +2,6 @@ Reinicio de Contraseña Siga este enlace para reiniciar su contraseña. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s Si usted no solicitó este correo, por favor ignorelo. diff --git a/template/mails/reset-password_8.tpl b/template/mails/reset-password_8.tpl index e9b88d44..ca1317d8 100644 --- a/template/mails/reset-password_8.tpl +++ b/template/mails/reset-password_8.tpl @@ -2,6 +2,6 @@ Ð¡Ð±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ ÐŸÐµÑ€ÐµÐ¹Ð´Ð¸Ñ‚Ðµ по Ñтой ÑÑылке, чтобы ÑброÑить Ñвой пароль. -HOST_URL?account=forgotpassword&key=%s +HOST_URL?account=reset-password&key=%s ЕÑли вы не запрашивали Ñто пиÑьмо, проÑто проигнорируйте его. diff --git a/template/pages/acc-recover.tpl.php b/template/pages/acc-recover.tpl.php deleted file mode 100644 index 8b0edad8..00000000 --- a/template/pages/acc-recover.tpl.php +++ /dev/null @@ -1,136 +0,0 @@ - - -brick('header'); ?> - -
        -
        -
        -brick('announcement'); - - $this->brick('pageTemplate'); -?> -
        -text)): ?> -
        -

        head; ?>

        -
        -
        text; ?>
        -
        -resetPass): ?> - - -
        -
        -

        head; ?>

        -
        error; ?>
        - - - - - - - - - - - - - - - - - - - -
        - -
        -
        - - - - - -
        -
        -

        head; ?>

        -
        error; ?>
        - -
        - -
        - - -
        - -
        -
        - - -
        -
        -
        - -brick('footer'); ?> From 258ac19f0a7624de373d0e4ebc507fc81cd0c9d5 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:55:08 +0200 Subject: [PATCH 0984/1249] Template/Update (Part 46 - IV) * account management rework: Personal Settings functionality * email, password, username update * email updates now also mails the old address for confirmation --- README.md | 1 + endpoints/account/account.php | 14 ++-- endpoints/account/confirm-email-address.php | 62 +++++++++++++++ endpoints/account/confirm-password.php | 60 ++++++++++++++ endpoints/account/revert-email-address.php | 62 +++++++++++++++ endpoints/account/update-email.php | 80 +++++++++++++++++++ endpoints/account/update-password.php | 86 +++++++++++++++++++++ endpoints/account/update-username.php | 61 +++++++++++++++ includes/kernel.php | 2 +- localization/locale_dede.php | 2 +- localization/locale_enus.php | 2 +- localization/locale_eses.php | 2 +- localization/locale_frfr.php | 2 +- localization/locale_ruru.php | 2 +- localization/locale_zhcn.php | 2 +- setup/updates/1758578400_13.sql | 2 + setup/updates/1758578400_14.sql | 2 + template/mails/change-email_0.tpl | 11 +++ template/mails/change-email_2.tpl | 11 +++ template/mails/change-email_3.tpl | 11 +++ template/mails/change-email_4.tpl | 11 +++ template/mails/change-email_6.tpl | 11 +++ template/mails/change-email_8.tpl | 12 +++ template/mails/revert-email_0.tpl | 11 +++ template/mails/revert-email_2.tpl | 11 +++ template/mails/revert-email_3.tpl | 11 +++ template/mails/revert-email_4.tpl | 11 +++ template/mails/revert-email_6.tpl | 11 +++ template/mails/revert-email_8.tpl | 11 +++ template/mails/update-password_0.tpl | 10 +++ template/mails/update-password_2.tpl | 10 +++ template/mails/update-password_3.tpl | 10 +++ template/mails/update-password_4.tpl | 10 +++ template/mails/update-password_6.tpl | 10 +++ template/mails/update-password_8.tpl | 10 +++ template/pages/account.tpl.php | 6 +- 36 files changed, 628 insertions(+), 15 deletions(-) create mode 100644 endpoints/account/confirm-email-address.php create mode 100644 endpoints/account/confirm-password.php create mode 100644 endpoints/account/revert-email-address.php create mode 100644 endpoints/account/update-email.php create mode 100644 endpoints/account/update-password.php create mode 100644 endpoints/account/update-username.php create mode 100644 setup/updates/1758578400_13.sql create mode 100644 setup/updates/1758578400_14.sql create mode 100644 template/mails/change-email_0.tpl create mode 100644 template/mails/change-email_2.tpl create mode 100644 template/mails/change-email_3.tpl create mode 100644 template/mails/change-email_4.tpl create mode 100644 template/mails/change-email_6.tpl create mode 100644 template/mails/change-email_8.tpl create mode 100644 template/mails/revert-email_0.tpl create mode 100644 template/mails/revert-email_2.tpl create mode 100644 template/mails/revert-email_3.tpl create mode 100644 template/mails/revert-email_4.tpl create mode 100644 template/mails/revert-email_6.tpl create mode 100644 template/mails/revert-email_8.tpl create mode 100644 template/mails/update-password_0.tpl create mode 100644 template/mails/update-password_2.tpl create mode 100644 template/mails/update-password_3.tpl create mode 100644 template/mails/update-password_4.tpl create mode 100644 template/mails/update-password_6.tpl create mode 100644 template/mails/update-password_8.tpl diff --git a/README.md b/README.md index 44bea61d..cc69e630 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Also, this project is not meant to be used for commercial purposes of any kind! + [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php) + [Multibyte String](https://www.php.net/manual/en/book.mbstring.php) + [File Information](https://www.php.net/manual/en/book.fileinfo.php) + + [Internationalization](https://www.php.net/manual/en/book.intl.php) + [GNU Multiple Precision](https://www.php.net/manual/en/book.gmp.php) (When using TrinityCore as auth source) + MySQL ≥ 5.7.0 OR MariaDB ≥ 10.6.4 OR similar + [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101) (no other other providers are supported at this time) diff --git a/endpoints/account/account.php b/endpoints/account/account.php index cd57c720..e560a55e 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -28,6 +28,7 @@ class AccountBaseResponse extends TemplateResponse public string $curEmail = ''; public string $curName = ''; public string $renameCD = ''; + public string $activeCD = ''; public array $description = []; public array $signature = []; public int $avMode = 0; @@ -51,7 +52,7 @@ class AccountBaseResponse extends TemplateResponse { array_unshift($this->title, Lang::account('settings')); - $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon` FROM ?_account WHERE `id` = ?d', User::$id); + $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon`, `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id); Lang::sort('game', 'ra'); @@ -108,10 +109,13 @@ class AccountBaseResponse extends TemplateResponse $this->curEmail = $user['email'] ?? ''; // Username - $this->curName = User::$username; - - // todo localize date format; store time - // $this->renameCD = date('F j, o', time() + 7 * DAY); + $this->curName = User::$username; + $this->renameCD = Util::formatTime(Cfg::get('ACC_RENAME_DECAY') * 1000); + if ($user['renameCooldown'] > time()) + { + $locCode = implode('_', str_split(Lang::getLocale()->json(), 2)); // ._. + $this->activeCD = (new \IntlDateFormatter($locCode, pattern: Lang::main('dateFmtIntl')))->format($user['renameCooldown']); + } /* COMMUNITY */ diff --git a/endpoints/account/confirm-email-address.php b/endpoints/account/confirm-email-address.php new file mode 100644 index 00000000..eb11801c --- /dev/null +++ b/endpoints/account/confirm-email-address.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->change(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + // this should probably leave change info intact for revert + // todo - move personal settings changes to separate table + private function change() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time()) + return Lang::account('inputbox', 'error', 'mailTokenUsed'); + + // 0 changes == error + if (!DB::Aowow()->query('UPDATE ?_account SET `email` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('inputbox', 'message', 'mailChangeOk'); + } +} + +?> diff --git a/endpoints/account/confirm-password.php b/endpoints/account/confirm-password.php new file mode 100644 index 00000000..4eec91f0 --- /dev/null +++ b/endpoints/account/confirm-password.php @@ -0,0 +1,60 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->confirm(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + private function confirm() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_PASS || $acc['statusTimer'] < time()) + return Lang::account('inputbox', 'error', 'passTokenUsed'); + + // 0 changes == error + if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('inputbox', 'message', 'passChangeOk'); + } +} + +?> diff --git a/endpoints/account/revert-email-address.php b/endpoints/account/revert-email-address.php new file mode 100644 index 00000000..37475082 --- /dev/null +++ b/endpoints/account/revert-email-address.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + private bool $success = false; + + protected function generate() : void + { + parent::generate(); + + if (User::isBanned()) + return; + + $msg = $this->revert(); + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg, + )]; + } + + // this should probably take precedence over email-change + // todo - move personal settings changes to separate table + private function revert() : string + { + if (!$this->assertGET('key')) + return Lang::main('intError'); + + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time()) + return Lang::account('inputbox', 'error', 'mailTokenUsed'); + + // 0 changes == error + if (!DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + return Lang::main('intError'); + + $this->success = true; + return Lang::account('inputbox', 'message', 'mailRevertOk'); + } +} + +?> diff --git a/endpoints/account/update-email.php b/endpoints/account/update-email.php new file mode 100644 index 00000000..104e18a5 --- /dev/null +++ b/endpoints/account/update-email.php @@ -0,0 +1,80 @@ + ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updateMail()) + $_SESSION['msg'] = ['email', $this->success, $msg]; + } + + private function updateMail() : string + { + // no input yet + if (is_null($this->_post['newemail'])) + return Lang::main('intError'); + // truncated due to validation fail + if (!$this->_post['newemail']) + return Lang::account('emailInvalid'); + + if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ? AND `id` <> ?d', $this->_post['newemail'], User::$id)) + return Lang::account('mailInUse'); + + $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', User::$id); + if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL) + return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + + $oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); + if ($this->_post['newemail'] == $oldEmail) + return Lang::account('newMailDiff'); + + $token = Util::createHash(); + + // store new mail in updateValue field, exchange when confirmation mail gets confirmed + if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + $this->_post['newemail'], ACC_STATUS_CHANGE_EMAIL, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id)) + return Lang::main('intError'); + + if (!Util::sendMail($this->_post['newemail'], 'change-email', [$token, $this->_post['newemail']], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + if (!Util::sendMail($oldEmail, 'revert-email', [$token, $oldEmail], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + $this->success = true; + return Lang::account('updateMessage', 'personal', [$this->_post['newemail']]); + } +} + +?> diff --git a/endpoints/account/update-password.php b/endpoints/account/update-password.php new file mode 100644 index 00000000..cc4151d6 --- /dev/null +++ b/endpoints/account/update-password.php @@ -0,0 +1,86 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'newPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'confirmPassword' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], + 'globalLogout' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCheckbox']] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updatePassword()) + $_SESSION['msg'] = ['password', $this->success, $msg]; + } + + private function updatePassword() : string + { + if (!$this->assertPOST('currentPassword', 'newPassword', 'confirmPassword')) + return Lang::main('intError'); + + if (!Util::validatePassword($this->_post['newPassword'], $e)) + return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + + if ($this->_post['newPassword'] !== $this->_post['confirmPassword']) + return Lang::account('passMismatch'); + + $userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ?_account WHERE `id` = ?d', User::$id); + if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time()) + return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + + if (!User::verifyCrypt($this->_post['currentPassword'], $userData['passHash'])) + return Lang::account('wrongPass'); + + if (User::verifyCrypt($this->_post['newPassword'], $userData['passHash'])) + return Lang::account('newPassDiff'); + + $token = Util::createHash(); + + // store new hash in updateValue field, exchange when confirmation mail gets confirmed + if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + User::hashCrypt($this->_post['newPassword']), ACC_STATUS_CHANGE_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id)) + return Lang::main('intError'); + + $email = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); + if (!Util::sendMail($email, 'update-password', [$token, $email], Cfg::get('ACC_RECOVERY_DECAY'))) + return Lang::main('intError2', ['send mail']); + + // logout all other active sessions + if ($this->_post['globalLogout']) + DB::Aowow()->query('UPDATE ?_account_sessions SET `status` = ?d, `touched` = ?d WHERE `userId` = ?d AND `sessionId` <> ? AND `status` = ?d', SESSION_FORCED_LOGOUT, time(), User::$id, session_id(), SESSION_ACTIVE); + + $this->success = true; + return Lang::account('updateMessage', 'personal', [User::$email]); + } +} + +?> diff --git a/endpoints/account/update-username.php b/endpoints/account/update-username.php new file mode 100644 index 00000000..d301fb6a --- /dev/null +++ b/endpoints/account/update-username.php @@ -0,0 +1,61 @@ + ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] + ); + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + (new TemplateResponse())->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if (User::isBanned()) + return; + + if ($msg = $this->updateUsername()) + $_SESSION['msg'] = ['username', $this->success, $msg]; + } + + private function updateUsername() : string + { + if (!$this->assertPOST('newUsername')) + return Lang::main('intError'); + + if (DB::Aowow()->selectCell('SELECT `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id) > time()) + return Lang::main('intError'); // should have grabbed the error response.. + + // yes, including your current name. you don't want to change into your current name, right? + if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_post['newUsername'])) + return Lang::account('nameInUse'); + + DB::Aowow()->query('UPDATE ?_account SET `username` = ?, `renameCooldown` = ?d WHERE `id` = ?d', $this->_post['newUsername'], time() + Cfg::get('acc_rename_decay'), User::$id); + + $this->success = true; + return Lang::account('updateMessage', 'username', [User::$username, $this->_post['newUsername']]); + } +} + +?> diff --git a/includes/kernel.php b/includes/kernel.php index 4d444d5a..7145501d 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -13,7 +13,7 @@ define('CLI_HAS_E', CLI && // WIN10 and later u (!OS_WIN || (function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(STDOUT)))); -$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring', 'fileinfo'/*, 'gmp'*/]; +$reqExt = ['SimpleXML', 'gd', 'mysqli', 'mbstring', 'fileinfo', 'intl'/*, 'gmp'*/]; $badExt = []; $error = ''; if ($ext = array_filter($reqExt, fn($x) => !extension_loaded($x))) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 71e46780..23ff084b 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", - 'dateFmtUntil' => "j. F Y", + 'dateFmtIntl' => "d. MMMM y", 'timeAgo' => 'vor %s', 'nfSeparators' => ['.', ','], diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 812c3606..670ec5e9 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", - 'dateFmtUntil' => "F j, Y", + 'dateFmtIntl' => "MMMM d, y", 'timeAgo' => "%s ago", 'nfSeparators' => [',', '.'], diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 385861b1..9e5af257 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ': ', 'dateFmtShort' => "d/m/Y", 'dateFmtLong' => "d/m/Y \a \l\a\s g:i A", - 'dateFmtUntil' => "j \d\\e F \d\\e Y", + 'dateFmtIntl' => "d 'de' MMMM 'de' y", 'timeAgo' => 'hace %s', 'nfSeparators' => ['.', ','], diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 1974541e..cd982692 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ' : ', 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d à g:i A", - 'dateFmtUntil' => "j F Y", + 'dateFmtIntl' => "d MMMM y", 'timeAgo' => 'il y a %s', 'nfSeparators' => [' ', ','], diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 64a925d9..5f702169 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ": ", 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d в g:i A", - 'dateFmtUntil' => "j F Y г.", + 'dateFmtIntl' => "d MMMM y г.", 'timeAgo' => '%s назад', 'nfSeparators' => [' ', ','], diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 4d710049..e184e6dd 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -133,7 +133,7 @@ $lang = array( 'colon' => ':', 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", - 'dateFmtUntil' => "Yå¹´n月jæ—¥", + 'dateFmtIntl' => "yå¹´M月dæ—¥", 'timeAgo' => '%s之å‰', 'nfSeparators' => [',', '.'], diff --git a/setup/updates/1758578400_13.sql b/setup/updates/1758578400_13.sql new file mode 100644 index 00000000..c055f944 --- /dev/null +++ b/setup/updates/1758578400_13.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `renameCooldown` int unsigned NOT NULL DEFAULT 0 COMMENT 'timestamp when rename is available again' AFTER `updateValue`; diff --git a/setup/updates/1758578400_14.sql b/setup/updates/1758578400_14.sql new file mode 100644 index 00000000..3445704e --- /dev/null +++ b/setup/updates/1758578400_14.sql @@ -0,0 +1,2 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_rename_decay'; +INSERT INTO `aowow_config` VALUES ('acc_rename_decay', 30 * 24 * 60 * 60, '30 * 24 * 60 * 60', 3, 129, 'delay between username changes'); diff --git a/template/mails/change-email_0.tpl b/template/mails/change-email_0.tpl new file mode 100644 index 00000000..58a92f10 --- /dev/null +++ b/template/mails/change-email_0.tpl @@ -0,0 +1,11 @@ +# Created: 2025 +Email Change Confirm +Greetings, + +We received a request to change your account's email address. If you made this request, please follow the link below to confirm the change. + +HOST_URL?account=confirm-email-address&key=%1$s + +If you didn't request this change please feel free to disregard this email. If the link did not work or you have any further concerns about this, please contact CONTACT_EMAIL. The link will become invalid %10$s after this email was sent. + +The NAME_SHORT team diff --git a/template/mails/change-email_2.tpl b/template/mails/change-email_2.tpl new file mode 100644 index 00000000..c9743068 --- /dev/null +++ b/template/mails/change-email_2.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Demande de confirmation de changement d'adresse e-mail +Bonjour, + +Nous avons reçu une demande de modification de l'adresse e-mail associée à votre compte. Si vous êtes à l'origine de cette demande, veuillez suivre le lien ci-dessous pour confirmer le changement. + +HOST_URL?account=confirm-email-address&key=%1$s + +Si vous n'avez pas demandé ce changement, vous pouvez ignorer cet e-mail. Si le lien ne fonctionne pas ou si vous avez d'autres préoccupations à ce sujet, veuillez contacter CONTACT_EMAIL. Ce lien deviendra invalide %10$s après l'envoi de cet e-mail. + +L'équipe NAME_SHORT diff --git a/template/mails/change-email_3.tpl b/template/mails/change-email_3.tpl new file mode 100644 index 00000000..8abae027 --- /dev/null +++ b/template/mails/change-email_3.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Bestätigung der E-Mail-Änderung angefordert +Hallo, + +Wir haben eine Anfrage zur Änderung Ihrer E-Mail-Adresse erhalten. Wenn Sie diese Anfrage gestellt haben, folgen Sie bitte dem untenstehenden Link, um die Änderung zu bestätigen. + +HOST_URL?account=confirm-email-address&key=%1$s + +Falls Sie diese Änderung nicht angefordert haben, können Sie diese E-Mail ignorieren. Falls der Link nicht funktioniert oder Sie weitere Fragen haben, wenden Sie sich bitte an CONTACT_EMAIL. Der Link wird %10$s nach Versand dieser E-Mail ungültig. + +Das Team von NAME_SHORT diff --git a/template/mails/change-email_4.tpl b/template/mails/change-email_4.tpl new file mode 100644 index 00000000..9fbd9efa --- /dev/null +++ b/template/mails/change-email_4.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +确认更改电å­é‚®ä»¶åœ°å€ +您好, + +我们收到了一项更改您账户电å­é‚®ä»¶åœ°å€çš„请求。如果是您本人æ“作,请点击下方链接以确认更改。 + +HOST_URL?account=confirm-email-address&key=%1$s + +如果您未曾å‘起此更改,请忽略此邮件。如果链接无法使用或您对此有任何疑问,请è”ç³» CONTACT_EMAIL。此链接将在本邮件å‘é€åŽ %10$s 失效。 + +NAME_SHORT 团队敬上 diff --git a/template/mails/change-email_6.tpl b/template/mails/change-email_6.tpl new file mode 100644 index 00000000..16aabc7b --- /dev/null +++ b/template/mails/change-email_6.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Confirmación de cambio de correo electrónico +Saludos, + +Hemos recibido una solicitud para cambiar la dirección de correo electrónico de su cuenta. Si usted realizó esta solicitud, siga el enlace de abajo para confirmar el cambio. + +HOST_URL?account=confirm-email-address&key=%1$s + +Si usted no solicitó este cambio, puede ignorar este correo. Si el enlace no funciona o tiene alguna inquietud, por favor contacte a CONTACT_EMAIL. El enlace se invalidará %10$s después de que este correo haya sido enviado. + +El equipo de NAME_SHORT diff --git a/template/mails/change-email_8.tpl b/template/mails/change-email_8.tpl new file mode 100644 index 00000000..aaeeed88 --- /dev/null +++ b/template/mails/change-email_8.tpl @@ -0,0 +1,12 @@ +# GPTed from 2025 source +Подтверждение Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð°Ð´Ñ€ÐµÑа Ñлектронной почты +ЗдравÑтвуйте, + +Мы получили Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° изменение адреÑа Ñлектронной почты, ÑвÑзанного Ñ Ð²Ð°ÑˆÐ¸Ð¼ аккаунтом. ЕÑли вы отправили Ñтот запроÑ, пожалуйÑта, перейдите по ÑÑылке ниже Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ. + +HOST_URL?account=confirm-email-address&key=%1$s + +ЕÑли вы не запрашивали Ñто изменение, проÑто проигнорируйте Ñто пиÑьмо. ЕÑли ÑÑылка не работает или у Ð²Ð°Ñ ÐµÑть дополнительные вопроÑÑ‹, пожалуйÑта, ÑвÑжитеÑÑŒ Ñ CONTACT_EMAIL. СÑылка Ñтанет недейÑтвительной через %10$s поÑле отправки Ñтого пиÑьма. + +Команда NAME_SHORT +ПожалуйÑта, перейдите по ÑÑылке ниже, чтобы подтвердить ваш новый Ð°Ð´Ñ€ÐµÑ Ñлектронной почты. diff --git a/template/mails/revert-email_0.tpl b/template/mails/revert-email_0.tpl new file mode 100644 index 00000000..881c02f9 --- /dev/null +++ b/template/mails/revert-email_0.tpl @@ -0,0 +1,11 @@ +# Created: 2025 +Email Change Requested +Greetings, + +We received a request to change your account's email address. If you made this request, please follow the instructions in the confirmation email sent to the address indicated. If you didn't make such a request, please click the link below to prevent the email from being changed. + +HOST_URL?account=revert-email-address&key=%1$s + +If the link did not work or you have any further concerns about this, please contact CONTACT_EMAIL. This link will automatically become invalid %10$s from now. + +The NAME_SHORT team diff --git a/template/mails/revert-email_2.tpl b/template/mails/revert-email_2.tpl new file mode 100644 index 00000000..b9b37958 --- /dev/null +++ b/template/mails/revert-email_2.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Demande de modification d'adresse e-mail +Bonjour, + +Nous avons reçu une demande de modification de l'adresse e-mail associée à votre compte. Si vous êtes à l'origine de cette demande, veuillez suivre les instructions contenues dans l'e-mail de confirmation envoyé à l'adresse indiquée. Si vous n'êtes pas à l'origine de cette demande, veuillez cliquer sur le lien ci-dessous pour empêcher la modification de l'adresse e-mail. + +HOST_URL?account=revert-email-address&key=%1$s + +Si le lien ne fonctionne pas ou si vous avez d'autres préoccupations à ce sujet, veuillez contacter CONTACT_EMAIL. Ce lien deviendra automatiquement invalide dans %10$s. + +L'équipe NAME_SHORT diff --git a/template/mails/revert-email_3.tpl b/template/mails/revert-email_3.tpl new file mode 100644 index 00000000..4ccc7af9 --- /dev/null +++ b/template/mails/revert-email_3.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +E-Mail-Änderung angefordert +Hallo, + +Wir haben eine Anfrage zur Änderung Ihrer E-Mail-Adresse erhalten. Wenn Sie diese Anfrage gestellt haben, folgen Sie bitte den Anweisungen in der Bestätigungs-E-Mail, die an die angegebene Adresse gesendet wurde. Falls Sie diese Anfrage nicht gestellt haben, klicken Sie bitte auf den untenstehenden Link, um die Änderung der E-Mail-Adresse zu verhindern. + +HOST_URL?account=revert-email-address&key=%1$s + +Falls der Link nicht funktioniert oder Sie weitere Fragen haben, wenden Sie sich bitte an CONTACT_EMAIL. Dieser Link wird automatisch nach %%10$s ungültig. + +Ihr NAME_SHORT-Team diff --git a/template/mails/revert-email_4.tpl b/template/mails/revert-email_4.tpl new file mode 100644 index 00000000..ecc86e88 --- /dev/null +++ b/template/mails/revert-email_4.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +请求更改电å­é‚®ä»¶åœ°å€ +您好, + +我们收到了一项更改您账户电å­é‚®ä»¶åœ°å€çš„请求。如果是您本人æ“作,请按照å‘é€åˆ°æŒ‡å®šåœ°å€çš„确认邮件中的说明进行æ“ä½œã€‚å¦‚æžœä¸æ˜¯æ‚¨æœ¬äººæ“作,请点击下方链接以阻止电å­é‚®ä»¶åœ°å€çš„æ›´æ”¹ã€‚ + +HOST_URL?account=revert-email-address&key=%1$s + +如果链接无法使用或您对此有任何疑问,请è”ç³» CONTACT_EMAIL。此链接将在 %10$s åŽè‡ªåŠ¨å¤±æ•ˆã€‚ + +NAME_SHORT 团队敬上 diff --git a/template/mails/revert-email_6.tpl b/template/mails/revert-email_6.tpl new file mode 100644 index 00000000..2fbf927b --- /dev/null +++ b/template/mails/revert-email_6.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Solicitud de cambio de correo electrónico +Saludos, + +Hemos recibido una solicitud para cambiar la dirección de correo electrónico de su cuenta. Si usted realizó esta solicitud, siga las instrucciones en el correo de confirmación enviado a la dirección indicada. Si no realizó esta solicitud, haga clic en el enlace de abajo para evitar el cambio de correo electrónico. + +HOST_URL?account=revert-email-address&key=%1$s + +Si el enlace no funciona o tiene alguna inquietud, por favor contacte a CONTACT_EMAIL. Este enlace se invalidará automáticamente en %10$s. + +El equipo de NAME_SHORT diff --git a/template/mails/revert-email_8.tpl b/template/mails/revert-email_8.tpl new file mode 100644 index 00000000..b257ad2a --- /dev/null +++ b/template/mails/revert-email_8.tpl @@ -0,0 +1,11 @@ +# GPTed from 2025 source +Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° изменение адреÑа Ñлектронной почты +ЗдравÑтвуйте, + +Мы получили Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° изменение адреÑа Ñлектронной почты, ÑвÑзанного Ñ Ð²Ð°ÑˆÐ¸Ð¼ аккаунтом. ЕÑли вы отправили Ñтот запроÑ, пожалуйÑта, Ñледуйте инÑтрукциÑм в пиÑьме Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸ÐµÐ¼, отправленном на указанный адреÑ. ЕÑли вы не отправлÑли такой запроÑ, пожалуйÑта, перейдите по ÑÑылке ниже, чтобы предотвратить изменение адреÑа Ñлектронной почты. + +HOST_URL?account=revert-email-address&key=%1$s + +ЕÑли ÑÑылка не работает или у Ð²Ð°Ñ ÐµÑть дополнительные вопроÑÑ‹, пожалуйÑта, ÑвÑжитеÑÑŒ Ñ CONTACT_EMAIL. Эта ÑÑылка автоматичеÑки Ñтанет недейÑтвительной через %10$s. + +Команда NAME_SHORT diff --git a/template/mails/update-password_0.tpl b/template/mails/update-password_0.tpl new file mode 100644 index 00000000..d55404cb --- /dev/null +++ b/template/mails/update-password_0.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Password Confirmation +Hey! + +Please click the link below to confirm your new password. +HOST_URL?account=confirm-password&key=%1$s + +Let us know if you have any problems! + +The NAME_SHORT team diff --git a/template/mails/update-password_2.tpl b/template/mails/update-password_2.tpl new file mode 100644 index 00000000..e971a03a --- /dev/null +++ b/template/mails/update-password_2.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Confirmation du mot de passe +Bonjour ! + +Veuillez cliquer sur le lien ci-dessous pour confirmer votre nouveau mot de passe. +HOST_URL?account=confirm-password&key=%1$s + +Faites-nous savoir si vous rencontrez des problèmes ! + +L'équipe NAME_SHORT diff --git a/template/mails/update-password_3.tpl b/template/mails/update-password_3.tpl new file mode 100644 index 00000000..348a7abb --- /dev/null +++ b/template/mails/update-password_3.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Passwortbestätigung +Hallo! + +Bitte klicke auf den untenstehenden Link, um dein neues Passwort zu bestätigen. +HOST_URL?account=confirm-password&key=%1$s + +Lass uns wissen, falls du Probleme hast! + +Das NAME_SHORT Team diff --git a/template/mails/update-password_4.tpl b/template/mails/update-password_4.tpl new file mode 100644 index 00000000..affa20f4 --- /dev/null +++ b/template/mails/update-password_4.tpl @@ -0,0 +1,10 @@ +# Created by ChatGPT from May 2025 base; locale 0 +密ç ç¡®è®¤ +ä½ å¥½ï¼ + +请点击下é¢çš„链接以确认你的新密ç ã€‚ +HOST_URL?account=confirm-password&key=%1$s + +å¦‚æžœä½ æœ‰ä»»ä½•é—®é¢˜ï¼Œè¯·å‘Šè¯‰æˆ‘ä»¬ï¼ + +NAME_SHORT 团队敬上 diff --git a/template/mails/update-password_6.tpl b/template/mails/update-password_6.tpl new file mode 100644 index 00000000..b46dd849 --- /dev/null +++ b/template/mails/update-password_6.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Confirmación de contraseña +¡Hola! + +Por favor, haz clic en el siguiente enlace para confirmar tu nueva contraseña. +HOST_URL?account=confirm-password&key=%1$s + +¡Avísanos si tienes algún problema! + +El equipo de NAME_SHORT diff --git a/template/mails/update-password_8.tpl b/template/mails/update-password_8.tpl new file mode 100644 index 00000000..9266fa1d --- /dev/null +++ b/template/mails/update-password_8.tpl @@ -0,0 +1,10 @@ +# Created: May 2025 +Подтверждение Ð¿Ð°Ñ€Ð¾Ð»Ñ +ЗдравÑтвуйте! + +ПожалуйÑта, перейдите по ÑÑылке ниже, чтобы подтвердить ваш новый пароль. +HOST_URL?account=confirm-password&key=%1$s + +Сообщите нам, еÑли у Ð²Ð°Ñ Ð²Ð¾Ð·Ð½Ð¸ÐºÐ½ÑƒÑ‚ какие-либо проблемы! + +Команда NAME_SHORT diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php index ca64947c..fe9b076b 100644 --- a/template/pages/account.tpl.php +++ b/template/pages/account.tpl.php @@ -110,9 +110,9 @@ if ($this->bans):
        -
        -renameCD): ?> -

        renameCD]);?>
        +
        renameCD]);?>
        +activeCD): ?> +

        activeCD]);?>
        From 1d5539b3620ba6e2e07af923afc630c15a838460 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:46:57 +0200 Subject: [PATCH 0985/1249] Template/Update (Part 46 - V) * account management rework: Avatar functionality * show avatar at comments (beckported, because no forums) --- endpoints/account/account.php | 25 +++-- endpoints/account/delete-icon.php | 47 +++++++++ endpoints/account/forum-avatar.php | 108 +++++++++++++++++++++ endpoints/account/premium-border.php | 41 ++++++++ endpoints/account/rename-icon.php | 36 +++++++ endpoints/upload/image-complete.php | 88 +++++++++++++++++ endpoints/upload/image-crop.php | 84 ++++++++++++++++ endpoints/user/user.php | 7 +- includes/components/avatarmgr.class.php | 122 ++++++++++++++++++++++++ includes/dbtypes/user.class.php | 27 +++++- includes/user.class.php | 34 ++++--- localization/locale_dede.php | 13 +++ localization/locale_enus.php | 13 +++ localization/locale_eses.php | 13 +++ localization/locale_frfr.php | 13 +++ localization/locale_ruru.php | 13 +++ localization/locale_zhcn.php | 13 +++ setup/tools/clisetup/filegen.us.php | 1 + setup/updates/1758578400_15.sql | 3 + setup/updates/1758578400_16.sql | 2 + static/css/aowow.css | 3 +- static/js/global.js | 92 +++++++++++++++--- template/pages/account.tpl.php | 54 ++++++++--- template/pages/image-crop.tpl.php | 45 +++++++++ 24 files changed, 839 insertions(+), 58 deletions(-) create mode 100644 endpoints/account/delete-icon.php create mode 100644 endpoints/account/forum-avatar.php create mode 100644 endpoints/account/premium-border.php create mode 100644 endpoints/account/rename-icon.php create mode 100644 endpoints/upload/image-complete.php create mode 100644 endpoints/upload/image-crop.php create mode 100644 includes/components/avatarmgr.class.php create mode 100644 setup/updates/1758578400_15.sql create mode 100644 setup/updates/1758578400_16.sql create mode 100644 template/pages/image-crop.tpl.php diff --git a/endpoints/account/account.php b/endpoints/account/account.php index e560a55e..2512d508 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -14,12 +14,13 @@ class AccountBaseResponse extends TemplateResponse protected array $scripts = [[SC_JS_FILE, 'js/account.js']]; // display status of executed step (forwarding back to this page) - public ?array $generalMessage = null; - public ?array $emailMessage = null; - public ?array $usernameMessage = null; - public ?array $passwordMessage = null; - public ?array $communityMessage = null; - public ?array $avatarMessage = null; + public ?array $generalMessage = null; + public ?array $emailMessage = null; + public ?array $usernameMessage = null; + public ?array $passwordMessage = null; + public ?array $communityMessage = null; + public ?array $avatarMessage = null; + public ?array $premiumborderMessage = null; // form fields public int $modelrace = 0; @@ -36,6 +37,7 @@ class AccountBaseResponse extends TemplateResponse public int $customicon = 0; public array $customicons = []; public bool $premium = false; + public int $reputation = 0; public ?Listview $avatarManager = null; public ?array $bans; @@ -130,7 +132,7 @@ class AccountBaseResponse extends TemplateResponse $this->avMode = $user['avatar']; // status [reviewing, ok, rejected]? (only 2: rejected processed in js) - if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d AND `status` > 0', User::$id))) + if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d', User::$id))) { array_walk($cuAvatars, function (&$x) { $x['when'] *= 1000; // uploaded timestamp expected as msec for some reason @@ -139,7 +141,7 @@ class AccountBaseResponse extends TemplateResponse }); foreach ($cuAvatars as $a) - if ($a['status'] != 2) + if ($a['status'] != AvatarMgr::STATUS_REJECTED) $this->customicons[$a['id']] = $a['name']; // TODO - replace with array_find in PHP 8.4 @@ -154,6 +156,8 @@ class AccountBaseResponse extends TemplateResponse if (!$this->premium) return; + $this->reputation = User::getReputation(); + // Avatar Manager $this->avatarManager = new Listview([ 'template' => 'avatar', @@ -161,11 +165,12 @@ class AccountBaseResponse extends TemplateResponse 'name' => '$LANG.tab_avatars', 'parent' => 'avatar-manage', 'hideNav' => 1 | 2, // top | bottom - 'data' => $cuAvatars ?? [] + 'data' => $cuAvatars ?? [], + 'note' => Lang::account('avatarSlots', [count($this->customicons), Cfg::get('acc_max_avatar_uploads')]) ]); // Premium Border Selector - // ??? + // solved by js } } diff --git a/endpoints/account/delete-icon.php b/endpoints/account/delete-icon.php new file mode 100644 index 00000000..e70ea64a --- /dev/null +++ b/endpoints/account/delete-icon.php @@ -0,0 +1,47 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id')) + return; + + // non-int > error + $selected = DB::Aowow()->selectCell('SELECT `current` FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + if ($selected === null || $selected === false) + return; + + DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + + // if deleted avatar is also currently selected, unset + if ($selected) + DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + + $path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']); + if (!unlink($path)) + trigger_error('AccountDeleteiconResponse - failed to delete file: '.$path, E_USER_ERROR); + } +} + +?> diff --git a/endpoints/account/forum-avatar.php b/endpoints/account/forum-avatar.php new file mode 100644 index 00000000..a123d572 --- /dev/null +++ b/endpoints/account/forum-avatar.php @@ -0,0 +1,108 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2 ]], + 'wowicon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]], // file name can have \W chars: inv_misc_fork&knife, achievement_dungeon_drak'tharon_heroic + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + // called via ajax + protected array $expectedGET = array( + 'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 2, 'max_range' => 2]], + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + $msg = match ($this->_post['avatar'] ?? $this->_get['avatar']) + { + 0 => $this->unset(), // none + 1 => $this->fromIcon(), // wow icon + 2 => $this->fromUpload(!$this->_get['avatar']), // custom icon (premium feature) + default => Lang::main('genericError') + }; + + if ($msg) + $_SESSION['msg'] = ['avatar', $this->success, $msg]; + } + + private function unset() : string + { + $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } + + private function fromIcon() : string + { + if (!$this->assertPOST('wowicon')) + return Lang::main('intError'); + + $icon = strtolower(trim($this->_post['wowicon'])); + + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_icons WHERE `name` = ?', $icon)) + return Lang::account('updateMessage', 'avNotFound'); + + $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1, `wowicon` = ? WHERE `id` = ?d', strtolower($icon), User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + $msg = Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account WHERE `wowicon` = ?', $icon)) > 1) + $msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]); + else + $msg .= ' '.Lang::account('updateMessage', 'av1stUser'); + + return $msg; + } + + protected function fromUpload(bool $viaPOST) : string + { + if (!User::isPremium()) + return Lang::main('genericError'); + + if (($viaPOST && !$this->assertPOST('customicon')) || (!$viaPOST && !$this->assertGET('customicon'))) + return Lang::main('intError'); + + $customIcon = $this->_post['customicon'] ?? $this->_get['customicon']; + + $x = DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = IF(`id` = ?d, 1, 0) WHERE `userId` = ?d AND `status` <> ?d', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED); + if (!is_int($x)) + return Lang::main('genericError'); + + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `avatar` = 2 WHERE `id` = ?d', User::$id))) + return Lang::main('intError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } +} + +?> diff --git a/endpoints/account/premium-border.php b/endpoints/account/premium-border.php new file mode 100644 index 00000000..e1ee43d8 --- /dev/null +++ b/endpoints/account/premium-border.php @@ -0,0 +1,41 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 4]], + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if (!$this->assertPOST('avatarborder')) + return; + + $x = DB::Aowow()->query('UPDATE ?_account SET `avatarborder` = ?d WHERE `id` = ?d', $this->_post['avatarborder'], User::$id); + if (!is_int($x)) + $_SESSION['msg'] = ['premiumborder', false, Lang::main('genericError')]; + else if (!$x) + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avNoChange')]; + else + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avSuccess')]; + } +} + +?> diff --git a/endpoints/account/rename-icon.php b/endpoints/account/rename-icon.php new file mode 100644 index 00000000..46735d01 --- /dev/null +++ b/endpoints/account/rename-icon.php @@ -0,0 +1,36 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'name' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' =>'/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/']] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id', 'name')) + return; + + // regexp same as in account.js + DB::Aowow()->query('UPDATE ?_account_avatars SET `name` = ? WHERE `id` = ?d AND `userId` = ?d', trim($this->_post['name']), $this->_post['id'], User::$id); + } +} + +?> diff --git a/endpoints/upload/image-complete.php b/endpoints/upload/image-complete.php new file mode 100644 index 00000000..10f9ea3b --- /dev/null +++ b/endpoints/upload/image-complete.php @@ -0,0 +1,88 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCoords']], + ); + + public string $imgHash; + public int $newId; + + public function __construct(string $pageParam) + { + if (User::isBanned()) + $this->generate404(); + + parent::__construct($pageParam); + + if (!preg_match('/^upload=image-complete&(\d+)\.(\w{16})$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->newId, $this->imgHash] = $m; + + if (!$this->imgHash || !$this->newId) + $this->generate404(); + } + + protected function generate() : void + { + if (!$this->handleComplete()) + $_SESSION['msg'] = ['avatar', false, AvatarMgr::$error ?: Lang::main('intError')]; + } + + private function handleComplete() : bool + { + if (!$this->assertPOST('coords')) + return false; + + if (!AvatarMgr::init()) + return false; + + if (!AvatarMgr::loadFile(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')) + return false; + + if (!AvatarMgr::cropImg(...$this->_post['coords'])) + return false; + + if (!AvatarMgr::createAtlas($this->newId)) + return false; + + $fSize = filesize(sprintf(AvatarMgr::PATH_AVATARS, $this->newId)); + if (!$fSize) + return false; + + $newId = DB::Aowow()->query('INSERT INTO ?_account_avatars (`id`, `userId`, `name`, `when`, `size`) VALUES (?d, ?d, ?, ?d, ?d)', $this->newId, User::$id, 'Avatar '.$this->newId, time(), $fSize); + if (!is_int($newId)) + { + trigger_error('UploadImagecompleteResponse - avatar query failed', E_USER_ERROR); + return false; + } + + // delete temp files + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')); + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash)); + + return true; + } + + protected static function checkCoords(string $val) : ?array + { + if (preg_match('/^[01]\.[0-9]{3}(,[01]\.[0-9]{3}){3}$/', $val)) + return explode(',', $val); + + return null; + } +} + +?> diff --git a/endpoints/upload/image-crop.php b/endpoints/upload/image-crop.php new file mode 100644 index 00000000..ba046dd6 --- /dev/null +++ b/endpoints/upload/image-crop.php @@ -0,0 +1,84 @@ +generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if ($err = $this->handleUpload()) + { + $_SESSION['msg'] = ['avatar', false, $err]; + $this->forward('?account#community'); + } + + $this->h1 = Lang::account('avatarSubmit'); + + $fileBase = User::$username.'-avatar-'.$this->nextId.'-'.$this->imgHash; + $dimensions = AvatarMgr::calcImgDimensions(); + + $this->cropper = $dimensions + array( + 'url' => Cfg::get('STATIC_URL').'/uploads/temp/'.$fileBase.'.jpg', + 'parent' => 'av-container', + 'minCrop' => ICON_SIZE_LARGE, // optional; defaults to 150 - min selection size (a square) + 'type' => Type::NPC, // NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] + 'typeId' => 15384, // = arbitrary image upload + 'constraint' => [1, 1] // [xMult, yMult] - relative size to each other (here: be square) + ); + + parent::generate(); + } + + private function handleUpload() : string + { + if (!AvatarMgr::init()) + return Lang::main('intError'); + + if (!AvatarMgr::validateUpload()) + return AvatarMgr::$error; + + if (!AvatarMgr::loadUpload()) + return Lang::main('intError'); + + $n = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account_avatars WHERE `userId` = ?d', User::$id); + if ($n && $n > Cfg::get('ACC_MAX_AVATAR_UPLOADS')) + return Lang::main('intError'); + + // why is ++(); illegal syntax? WHO KNOWS!? + $this->nextId = (DB::Aowow()->selectCell('SELECT MAX(`id`) FROM ?_account_avatars') ?: 0) + 1; + + if (!AvatarMgr::tempSaveUpload(['avatar', $this->nextId], $this->imgHash)) + return Lang::main('intError'); + + return ''; + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php index b3e8f7b8..c89a6cf2 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -38,7 +38,7 @@ class UserBaseResponse extends TemplateResponse if (!$pageParam) $this->forwardToSignIn('user'); - if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) + if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`avatarborder`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) $this->user = $user; else $this->generateNotFound(Lang::user('notFound', [$pageParam])); @@ -115,12 +115,15 @@ class UserBaseResponse extends TemplateResponse default => '' }; + if (!($this->user['userGroups'] & U_GROUP_PREMIUM)) + $this->user['avatarborder'] = 2; + $this->userIcon = array( // JS: Icon.createUser() $this->user['avatar'], // avatar: 1(iconString), 2(customId) $avatarMore, // avatarMore: iconString or customId IconElement::SIZE_MEDIUM, // size: (always medium) null, // url: (always null) - User::isInGroup(U_GROUP_PREMIUM) ? 0 : 2, // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] + $this->user['avatarborder'], // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] false, // noBorder: always false '$Icon.getPrivilegeBorder('.$this->user['sumRep'].')' // reputationLevel: calculated in js from passed rep points ); diff --git a/includes/components/avatarmgr.class.php b/includes/components/avatarmgr.class.php new file mode 100644 index 00000000..4df080c8 --- /dev/null +++ b/includes/components/avatarmgr.class.php @@ -0,0 +1,122 @@ + self::MAX_W || $is[1] > self::MAX_H) + self::$error = Lang::account('selectAvatar'); + } + else + self::$error = Lang::account('selectAvatar'); + + if (!self::$error) + return true; + + self::$fileName = ''; + return false; + } + + /* create icon texture atlas + * ****************************** + * * LARGE * MEDIUM * + * * * * + * * * * + * * ************* + * * * SMOL * * + * * * * * + * * ********* * + * ****************************** + * + * as static/uploads/avatars/.jpg + */ + + public static function createAtlas(string $fileName) : bool + { + if (!self::$img) + return false; + + $sizes = [ICON_SIZE_LARGE, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL]; + + $dest = imagecreatetruecolor(ICON_SIZE_LARGE + ICON_SIZE_MEDIUM, ICON_SIZE_LARGE); + $srcW = imagesx(self::$img); + $srcH = imagesx(self::$img); + + $destX = $destY = 0; + foreach ($sizes as $idx => $dim) + { + imagecopyresampled($dest, self::$img, $destX, $destY, 0, 0, $dim, $dim, $srcW, $srcH); + + if ($idx % 2) + $destY += $dim; + else + $destX += $dim; + } + + if (!imagejpeg($dest, sprintf(self::PATH_AVATARS, $fileName), self::JPEG_QUALITY)) + return false; + + self::$img = null; + $dest = null; + return true; + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + // unsure yet how that's supposed to work + // for now pending uploads can be used right away +} + +?> diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php index 1d356760..44f4f83b 100644 --- a/includes/dbtypes/user.class.php +++ b/includes/dbtypes/user.class.php @@ -26,7 +26,7 @@ class UserList extends DBTypeList foreach ($this->iterate() as $userId => $__) { $data[$this->curTpl['username']] = array( - 'border' => 0, // border around avatar (rarityColors) + 'border' => $this->getPremiumborder(), 'roles' => $this->curTpl['userGroups'], 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), 'posts' => 0, // forum posts @@ -47,22 +47,39 @@ class UserList extends DBTypeList $data[$this->curTpl['username']]['avatarmore'] = $this->curTpl['wowicon']; break; case 2: - if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> 2', $userId)) + if ($this->isPremium()) { - $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; - $data[$this->curTpl['username']]['avatarmore'] = $av; + if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> ?d', $userId, AvatarMgr::STATUS_REJECTED)) + { + $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; + $data[$this->curTpl['username']]['avatarmore'] = $av; + } } break; } // more optional data // sig: markdown formated string (only used in forum?) - // border: seen as null|1|3 .. changes the border around the avatar (i suspect its meaning changed and got decoupled from premium-status with the introduction of patreon-status) } return [Type::USER => $data]; } + // seen as null|1|3 .. changes the border around the avatar (chosen from account > premium tab?) + // changed at the end of MoP. No longer a jsBool but index to Icon.premiumBorderClasses + private function getPremiumBorder() : int + { + if (!$this->isPremium() || !$this->curTpl['avatar']) + return 2; // 2 is "none" + + return $this->curTpl['avatarborder']; + } + + public function isPremium() : bool + { + return $this->curTpl['userGroups'] & U_GROUP_PREMIUM || $this->curTpl['reputation'] >= Cfg::get('REP_REQ_PREMIUM'); + } + public function getListviewData() : array { return []; } public function renderTooltip() : ?string { return null; } diff --git a/includes/user.class.php b/includes/user.class.php index 5fca60fe..ebf180af 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -24,6 +24,7 @@ class User private static int $reputation = 0; private static string $dataKey = ''; private static int $excludeGroups = 1; + private static int $avatarborder = 2; // 2 is default / reputation colored private static ?LocalProfileList $profiles = null; public static function init() @@ -62,7 +63,7 @@ class User $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id()); $userData = DB::Aowow()->selectRow( - 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug` + 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`, a.`avatar`, a.`avatarborder` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` @@ -119,6 +120,7 @@ class User self::$status = $userData['status']; self::$debug = $userData['debug']; self::$email = $userData['email']; + self::$avatarborder = $userData['avatarborder']; if (Cfg::get('PROFILER_ENABLE')) { @@ -129,6 +131,18 @@ class User self::$profiles = (new LocalProfileList($conditions)); } + // reset premium options + if (!self::isPremium()) + { + if ($userData['avatar'] == 2) + { + DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1 WHERE `id` = ?d', self::$id); + DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = 0 WHERE `userId` = ?d', self::$id); + } + + // avatar borders + // do not reset, it's just not sent to the browser + } // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) // - consecutive visits @@ -482,7 +496,7 @@ class User public static function isPremium() : bool { - return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM'); + return !self::isBanned() && (self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM')); } public static function isLoggedIn() : bool @@ -568,14 +582,14 @@ class User if (self::$debug) $gUser['debug'] = true; // csv id-list output option on listviews - if (self::getPremiumBorder()) - $gUser['settings'] = ['premiumborder' => 1]; + if (self::isPremium()) + { + $gUser['premium'] = 1; + $gUser['settings'] = ['premiumborder' => self::$avatarborder]; + } else $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ? - if (self::isPremium()) - $gUser['premium'] = 1; - if ($_ = self::getProfilerExclusions()) $gUser = array_merge($gUser, $_); @@ -717,12 +731,6 @@ class User return $data; } - - // not sure what to set .. user selected? - public static function getPremiumBorder() : bool - { - return self::isInGroup(U_GROUP_PREMIUM); - } } ?> diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 23ff084b..c1e1e179 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent 'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "Neuen Avatar hochladen", + 'goToManager' => "Zur Avatarverwaltung gehen", + 'manageAvatars' => "Avatare verwalten", + 'avatarSlots' => '%1$d / %2$d Avatarplätze belegt', + 'manageBorders' => "Premium Rahmen verwalten", + 'selectAvatar' => "Wählt einen Avatar zum hochladen.", + 'errTooSmall' => "Euer Avatar muss wenigstens %dpx groß sein.", + 'cropAvatar' => "Ihr könnt Euren Avatar zuschneiden.", + 'avatarSubmit' => "Avatar-Einsendung", + 'reminder' => "Erinnerung", + 'avatarCoC' => "Dass Benutzen von Bildern, die gegen die Regeln verstoßen kann zum Verlust Eures Premium-Status führen.", + // settings 'settings' => "Kontoeinstellungen", 'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 670ec5e9..9ec1ff02 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Your new password must be different than your previous one.", // message_newpassdifferent 'newMailDiff' => "Your new email address must be different than your previous one.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "Upload new Avatar", + 'goToManager' => "Go to Avatar Manager", + 'manageAvatars' => "Manage Avatars", + 'avatarSlots' => 'Using %1$d / %2$d avatar slots', + 'manageBorders' => "Manage Premium Borders", + 'selectAvatar' => "Please select the avatar to upload.", + 'errTooSmall' => "Your avatar must be at last %dpx in size.", + 'cropAvatar' => "You may crop your avatar.", + 'avatarSubmit' => "Avatar Submission", + 'reminder' => "Reminder", + 'avatarCoC' => "Using imagery violating out terms of service may result in revocation of your premium privileges.", + // settings 'settings' => "Account Settings", 'settingsNote' => "Simply use the forms below to update your account information.", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 9e5af257..e281220a 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior.",// message_newpassdifferent 'newMailDiff' => "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Mi cuenta", 'settingsNote' => "Simplemente usa el siguiente formulario para actualizar la información de tu cuenta.", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index cd982692..2d19e8cb 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien.", // message_newpassdifferent 'newMailDiff' => "Votre nouvelle adresse courriel doit être différente de l'ancienne.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Mon compte", 'settingsNote' => "Veuillez utiliser les formulaires ci-dessous pour apporter des changements.", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 5f702169..9238b977 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Прежний и новый пароли не должны Ñовпадать.", // message_newpassdifferent 'newMailDiff' => "Прежний и новый e-mail адреÑа не должны Ñовпадать.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Параметры учетной запиÑи", 'settingsNote' => "ИÑпользуйте нижеприведённую форму, чтобы обновить информацию о вашей учетной запиÑи.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index e184e6dd..c72151a8 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "你的新密ç å¿…须与以å‰çš„密ç ä¸åŒã€‚", // message_newpassdifferent 'newMailDiff' => "您的新邮箱地å€å¿…é¡»ä¸åŒäºŽæ—§åœ°å€ã€‚", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "è´¦å·è®¾ç½®", 'settingsNote' => "使用下列表格就能å‡çº§æ‚¨çš„è´¦å·ä¿¡æ¯ã€‚", diff --git a/setup/tools/clisetup/filegen.us.php b/setup/tools/clisetup/filegen.us.php index 0d7ac502..70188e42 100644 --- a/setup/tools/clisetup/filegen.us.php +++ b/setup/tools/clisetup/filegen.us.php @@ -39,6 +39,7 @@ CLISetup::registerUtility(new class extends UtilityScript 'static/uploads/screenshots/thumb/', 'static/uploads/temp/', 'static/uploads/guide/images/', + 'static/uploads/avatars/' ); public function __construct() diff --git a/setup/updates/1758578400_15.sql b/setup/updates/1758578400_15.sql new file mode 100644 index 00000000..32ee0455 --- /dev/null +++ b/setup/updates/1758578400_15.sql @@ -0,0 +1,3 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_max_avatar_uploads'; +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('acc_max_avatar_uploads', 10, 10, 3, 129, 'premium users may upload this many avatars'); diff --git a/setup/updates/1758578400_16.sql b/setup/updates/1758578400_16.sql new file mode 100644 index 00000000..4b2f0d43 --- /dev/null +++ b/setup/updates/1758578400_16.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `avatarborder` tinyint unsigned NOT NULL DEFAULT 2 AFTER `avatar`; diff --git a/static/css/aowow.css b/static/css/aowow.css index 9d9f653b..7692cdd9 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -1161,7 +1161,8 @@ span.iconblizzard { .iconsmall-premiumred del { background-image:url(../images/Icon/small/border/premiumred.png); } .iconmedium-premiumred del { background-image:url(../images/Icon/medium/border/premiumred.png); } .iconlarge-premiumred del { - background-image:url(../images/logos/special/subscribe/patron-icon.png); + background-image:url(../images/Icon/large/border/premiumred.png); +/* background-image:url(../images/logos/special/subscribe/patron-icon.png); aowow - yeah, no */ height:85px; } diff --git a/static/js/global.js b/static/js/global.js index af34dd52..b389cbd3 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -1124,6 +1124,25 @@ function g_GetStaffColorFromRoles(roles) { return ''; } +// aowow - stand in for WH.User.getCommentRoleLabel +function g_GetCommentRoleLabel(roles, title) { + if (title) { + return title; + } + + if (roles & U_GROUP_ADMIN) { + return g_user_roles[2]; // LANG.administrator_abbrev + } + else if (roles & U_GROUP_MOD) { + return g_user_roles[4]; // LANG.moderator + } + else if (roles & U_GROUP_PREMIUMISH) { + return LANG.premiumuser; + } + + return null; +}; + function g_formatDate(sp, elapsed, theDate, time, alone) { var today = new Date(); var event_day = new Date(); @@ -13957,6 +13976,49 @@ Listview.templates = { $(div).show(); }, + applyAuthorTitle: function (container, title) + { + if (!title.label) + return; + + let cssClass = ['comment-reply-author-label'].concat(title.classes); + + $WH.ae(container, $WH.ct(' ')); // aowow - LANG.wordspace_punct + + if (title.url) + $WH.ae(container, $WH.ce('a', { className: cssClass.join(' '), href: title.url }, $WH.ct(`<${ title.label }>`))); + else + $WH.ae(container, $WH.ce('span', { className: cssClass.join(' ') }, $WH.ct(`<${ title.label }>`))); + }, + + getAuthorTitle: function (author) + { + let title = { + classes: [], + label: undefined, + url: undefined + }; + + if (g_pageInfo.author === author) { + title.label = LANG.guideAuthor; + return title; + } + + let user = g_users[author]; + if (user) { + // aowow - let roleColor = WH.User.getCommentTitleClass(_.roles, _.tierClass, user); + let roleColor = g_GetStaffColorFromRoles(user.roles); + if (roleColor) { + title.classes.push(roleColor); + } + // aowow - title.label = WH.User.getCommentRoleLabel(user.roles, user.title, user.tierTitle); + title.label = g_GetCommentRoleLabel(user.roles, user.title); + title.url = /* user.tierTitle && !user.title ? '/?premium' : */ ''; // aowow - tierTitle being the premium tier ("Rare|Epic|Legendary Premium User") + } + + return title; + }, + updateReplies: function(comment) { this.updateRepliesCell(comment); @@ -14063,7 +14125,13 @@ Listview.templates = { row.attr('data-replyid', reply.id); row.attr('data-idx', i); - row.find('.reply-text').addClass(g_GetStaffColorFromRoles(reply.roles)); + + // aowow - let cssClass = WH.User.getCommentRoleClass(reply.roles, reply.username); + let cssClass = g_GetStaffColorFromRoles(reply.roles); + if (!['comment-blue', 'comment-green'].includes(cssClass) && owner) { + cssClass = 'comment-green'; // comment-guide-author + } + row.find('.reply-text').addClass(cssClass); var replyWhen = $(''); replyWhen.text(g_formatDate(null, elapsed, creationDate)); @@ -14074,12 +14142,7 @@ Listview.templates = { replyByUserLink.attr('href', '?user=' + reply.username); replyByUserLink.text(reply.username); replyBy.append(replyByUserLink); - - if (owner) - { - $WH.ae(replyBy[0], $WH.ct(' ')); - $WH.ae(replyBy[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>'))); - } + this.applyAuthorTitle(replyBy[0], this.getAuthorTitle(reply.username)) replyBy.append(' ').append(replyWhen).append(' ').append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(creationDate))); @@ -14170,7 +14233,6 @@ Listview.templates = { updateCommentAuthor: function(comment, container) { var user = g_users[comment.user]; - let owner = g_pageInfo.author === comment.user; var postedOn = new Date(comment.date); var elapsed = (g_serverTime - postedOn) / 1000; @@ -14178,12 +14240,18 @@ Listview.templates = { container.append(LANG.lvcomment_by); container.append($WH.sprintf('$2', comment.user, comment.user)); - if (owner) - { - $WH.ae(container[0], $WH.ct(' ')); - $WH.ae(container[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>'))); + // aowow - avatar recovered and transplanted from commentsv1 version + if (user != null && user.avatar) { + var icon = Icon.createUser(user.avatar, user.avatarmore, 0, null, (user.roles & U_GROUP_PREMIUM) ? user.border : Icon.STANDARD_BORDER, 0, Icon.getPrivilegeBorder(user.reputation)); + icon.style.marginRight = '3px'; + icon.style.cssFloat = 'left'; + + container.css('lineHeight', '25px'); + container.append(icon); } + // aowow - end recover container.append(g_getReputationPlusAchievementText(user.gold, user.silver, user.copper, user.reputation)); + this.applyAuthorTitle(container[0], this.getAuthorTitle(comment.user)); container.append($WH.sprintf(' $3', comment.id, comment.id, g_formatDate(null, elapsed, postedOn))); container.append(' '); container.append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(postedOn))); diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php index fe9b076b..54d67966 100644 --- a/template/pages/account.tpl.php +++ b/template/pages/account.tpl.php @@ -146,9 +146,7 @@ if ($this->bans):
        - +
        -user::isInGroup(U_GROUP_PREMIUM) && 0): ?> +user::isInGroup(U_GROUP_PREMIUM)): ?> avMode == 2 ? ' checked="checked"' : '');?> />   
        @@ -226,7 +224,7 @@ if ($this->bans):
        - Go to Avatar Manager +
        @@ -255,18 +253,46 @@ if ($this->bans):
        • '.Lang::account('inactive'); ?>
        • '.Lang::account('active'); ?>
        -Manage Avatars +

        -

        Manage Premium Borders

        - Todo - - +

        + premiumborderMessage): ?> +
        + + +
        +
        +
        +
        + +
        +
        + + + @@ -280,9 +306,7 @@ if ($this->bans): _.add('', {id: 'premium'}); _.flush(); - +
        diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php new file mode 100644 index 00000000..59d130f7 --- /dev/null +++ b/template/pages/image-crop.tpl.php @@ -0,0 +1,45 @@ +brick('header'); +?> +
        +
        +
        + +brick('announcement'); + +$this->brick('pageTemplate'); +?> +
        +

        h1; ?>

        + + +
        +
        + +
        + +
        + +
        + +
        +
        + +

        +
        + + + +
        +
        +
        +
        + +brick('footer'); ?> From a48e94cd8bc8c2c9a9283253b243f64e3b2686e1 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:49:03 +0200 Subject: [PATCH 0986/1249] Template/Update (Part 46 - VI) * account management rework: Delete account --- endpoints/account/confirm-delete.php | 128 ++++++++++++++++++ endpoints/account/delete.php | 71 ++++++++++ .../response/templateresponse.class.php | 8 +- includes/defines.php | 3 +- includes/user.class.php | 5 +- localization/locale_dede.php | 2 + localization/locale_enus.php | 2 + localization/locale_eses.php | 2 + localization/locale_frfr.php | 2 + localization/locale_ruru.php | 2 + localization/locale_zhcn.php | 2 + static/css/delete.css | 42 ++++++ .../confirm-delete-account_0.tpl.php | 26 ++++ .../confirm-delete-account_2.tpl.php | 26 ++++ .../confirm-delete-account_3.tpl.php | 26 ++++ .../confirm-delete-account_4.tpl.php | 26 ++++ .../confirm-delete-account_6.tpl.php | 26 ++++ .../confirm-delete-account_8.tpl.php | 26 ++++ template/localized/delete-account_0.tpl.php | 15 ++ template/localized/delete-account_2.tpl.php | 15 ++ template/localized/delete-account_3.tpl.php | 15 ++ template/localized/delete-account_4.tpl.php | 15 ++ template/localized/delete-account_6.tpl.php | 15 ++ template/localized/delete-account_8.tpl.php | 15 ++ template/mails/delete-account_0.tpl | 21 +++ template/mails/delete-account_2.tpl | 21 +++ template/mails/delete-account_3.tpl | 21 +++ template/mails/delete-account_4.tpl | 21 +++ template/mails/delete-account_6.tpl | 21 +++ template/mails/delete-account_8.tpl | 21 +++ template/pages/delete.tpl.php | 26 ++++ 31 files changed, 661 insertions(+), 6 deletions(-) create mode 100644 endpoints/account/confirm-delete.php create mode 100644 endpoints/account/delete.php create mode 100644 static/css/delete.css create mode 100644 template/localized/confirm-delete-account_0.tpl.php create mode 100644 template/localized/confirm-delete-account_2.tpl.php create mode 100644 template/localized/confirm-delete-account_3.tpl.php create mode 100644 template/localized/confirm-delete-account_4.tpl.php create mode 100644 template/localized/confirm-delete-account_6.tpl.php create mode 100644 template/localized/confirm-delete-account_8.tpl.php create mode 100644 template/localized/delete-account_0.tpl.php create mode 100644 template/localized/delete-account_2.tpl.php create mode 100644 template/localized/delete-account_3.tpl.php create mode 100644 template/localized/delete-account_4.tpl.php create mode 100644 template/localized/delete-account_6.tpl.php create mode 100644 template/localized/delete-account_8.tpl.php create mode 100644 template/mails/delete-account_0.tpl create mode 100644 template/mails/delete-account_2.tpl create mode 100644 template/mails/delete-account_3.tpl create mode 100644 template/mails/delete-account_4.tpl create mode 100644 template/mails/delete-account_6.tpl create mode 100644 template/mails/delete-account_8.tpl create mode 100644 template/pages/delete.tpl.php diff --git a/endpoints/account/confirm-delete.php b/endpoints/account/confirm-delete.php new file mode 100644 index 00000000..b5d0b6b9 --- /dev/null +++ b/endpoints/account/confirm-delete.php @@ -0,0 +1,128 @@ + [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + protected array $expectedPOST = array( + 'submit' => [FILTER_UNSAFE_RAW ], + 'cancel' => [FILTER_UNSAFE_RAW ], + 'confirm' => [FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ], + 'key' => [FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']] + ); + + public bool $confirm = true; // just to select the correct localized brick + public string $username = ''; + public string $deleteFormTarget = '?account=confirm-delete'; + public ?array $inputbox = null; + public string $key = ''; + + private bool $success = false; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('accDelete')); + + $this->username = User::$username; + + parent::generate(); + + $msg = Lang::account('inputbox', 'error', 'purgeTokenUsed'); + + // display default confirm template + if ($this->assertGET('key') && DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_get['key'])) + { + $this->key = $this->_get['key']; + return; + } + + // perform action and display status + if ($this->assertPOST('key') && ($userId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_post['key']))) + { + if ($this->_post['cancel']) + $msg = $this->cancel($userId); + else if ($this->_post['submit'] && $this->_post['confirm']) + $msg = $this->purge($userId); + } + + // throw error and display in status + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $this->success ? 'success' : 'error'), + 'message' => $this->success ? $msg : '', + 'error' => $this->success ? '' : $msg + )]; + } + + private function cancel(int $userId) : string + { + if (DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "" WHERE `id` = ?d', ACC_STATUS_NONE, $userId)) + { + $this->success = true; + return Lang::account('inputbox', 'message', 'deleteCancel'); + } + + return Lang::main('intError'); + } + + private function purge(int $userId) : string + { + // empty all user settings and cookies + DB::Aowow()->query('DELETE FROM ?_account_cookies WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_reputation WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `userId` = ?d', $userId); // cascades to aowow_account_weightscale_data + + // delete profiles, unlink chars + DB::Aowow()->query('DELETE pp FROM ?_profiler_profiles pp JOIN ?_account_profiles ap ON ap.`profileId` = pp.`id` WHERE ap.`accountId` = ?d', $userId); + // DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d', $userId); // already deleted via FK? + + // delete all sessions and bans + DB::Aowow()->query('DELETE FROM ?_account_banned WHERE `userId` = ?d', $userId); + DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `userId` = ?d', $userId); + + // delete forum posts (msg: This post was from a user who has deleted their account. (no translations at src); comments/replies are unaffected) + // ... + + // replace username with userId and empty fields + DB::Aowow()->query( + 'UPDATE ?_account SET + `login` = "", `passHash` = "", `username` = `id`, `email` = NULL, `userGroups` = 0, `userPerms` = 0, + `curIp` = "", `prevIp` = "", `curLogin` = 0, `prevLogin` = 0, + `locale` = 0, `debug` = 0, `avatar` = 0, `wowicon` = "", `title` = "", `description` = "", `excludeGroups` = 0, + `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "", `renameCooldown` = 0 + WHERE `id` = ?d', + ACC_STATUS_DELETED, $userId + ); + + $this->success = true; + return Lang::account('inputbox', 'message', 'deleteOk'); + } +} + +?> diff --git a/endpoints/account/delete.php b/endpoints/account/delete.php new file mode 100644 index 00000000..34da70e2 --- /dev/null +++ b/endpoints/account/delete.php @@ -0,0 +1,71 @@ + ['filter' => FILTER_UNSAFE_RAW] + ); + + public string $username = ''; + public string $deleteFormTarget = '?account=delete'; + public ?array $inputbox = null; + + public function __construct(string $pageParam) + { + if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) + $this->generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + array_unshift($this->title, Lang::account('accDelete')); + + parent::generate(); + + $this->username = User::$username; + + if ($this->_post['proceed']) + { + $error = false; + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` NOT IN (?a) AND `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', [ACC_STATUS_NEW, ACC_STATUS_NONE, ACC_STATUS_PURGING], User::$id)) + { + $token = Util::createHash(40); + + DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + ACC_STATUS_PURGING, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id); + + Util::sendMail(User::$email, 'delete-account', [$token, User::$email, User::$username]); + } + else + $error = true; + + $this->inputbox = ['inputbox-status', array( + 'head' => Lang::account('inputbox', 'head', $error ? 'error' : 'success'), + 'message' => $error ? '' : Lang::account('inputbox', 'message', 'deleteAccSent', [User::$email]), + 'error' => $error ? Lang::account('inputbox', 'error', 'isRecovering') : '' + )]; + } + } +} + +?> diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index bef62121..bc2cda60 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -472,14 +472,16 @@ class TemplateResponse extends BaseResponse 'viError' => $_SESSION['error']['vi'] ?? null ); + // we cannot blanket NUMERIC_CHECK the data as usernames of deleted users are their id which does not support String.lower() + if ($this->contribute & CONTRIBUTE_CO) - $community['co'] = Util::toJSON(CommunityContent::getComments($this->type, $this->typeId)); + $community['co'] = Util::toJSON(CommunityContent::getComments($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); if ($this->contribute & CONTRIBUTE_SS) - $community['ss'] = Util::toJSON(CommunityContent::getScreenshots($this->type, $this->typeId)); + $community['ss'] = Util::toJSON(CommunityContent::getScreenshots($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); if ($this->contribute & CONTRIBUTE_VI) - $community['vi'] = Util::toJSON(CommunityContent::getVideos($this->type, $this->typeId)); + $community['vi'] = Util::toJSON(CommunityContent::getVideos($this->type, $this->typeId), JSON_UNESCAPED_UNICODE); unset($_SESSION['error']); diff --git a/includes/defines.php b/includes/defines.php index 29900e2e..eb18180f 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -69,7 +69,8 @@ define('ACC_STATUS_RECOVER_PASS', 3); // currently recover define('ACC_STATUS_CHANGE_EMAIL', 4); // currently changing contact email define('ACC_STATUS_CHANGE_PASS', 5); // currently changing password define('ACC_STATUS_CHANGE_USERNAME', 6); // currently changing username -define('ACC_STATUS_DELETED', 7); // is deleted - only a stub remains +define('ACC_STATUS_PURGING', 7); // deletion is pending +define('ACC_STATUS_DELETED', 99); // is deleted - only a stub remains // Session Status define('SESSION_ACTIVE', 1); diff --git a/includes/user.class.php b/includes/user.class.php index ebf180af..e310239f 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -280,10 +280,11 @@ class User 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() - WHERE { a.`email` = ? } { a.`login` = ? } + WHERE { a.`email` = ? } { a.`login` = ? } AND `status` <> ?d GROUP BY a.`id`', $email ?: DBSIMPLE_SKIP, - !$email ? $nameOrEmail : DBSIMPLE_SKIP + !$email ? $nameOrEmail : DBSIMPLE_SKIP, + ACC_STATUS_DELETED ); if (!$query) diff --git a/localization/locale_dede.php b/localization/locale_dede.php index c1e1e179..3004a8d5 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Ihr Kennwort wurde erfolgreich geändert.", 'deleteAccSent' => "Eine E-Mail mit einem Bestätigungslink wurde an %s gesendet.", 'deleteOk' => "Ihr Konto wurde erfolgreich entfernt. Wir hoffen, Sie bald wiederzusehen!

        Sie können dieses Fenster jetzt schließen.", + 'deleteCancel' => "Die Kontolöschung wurde abgebrochen.", 'createAccSent' => 'Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um Euer Konto zu erstellen.

        Falls du keine Bestätigungsnachricht erhalten hast klicke hier um eine neue zu senden.', 'recovUserSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euren Benutzernamen zu erhalten.", 'recovPassSent' => "Eine Nachricht wurde soeben an %s versandt. Folgt einfach den darin enthaltenen Anweisungen, um euer Kennwort zurückzusetzen.", @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Dieser Schlüssel zur Änderung der E-Mail-Adresse wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', 'passTokenUsed' => 'Dieser Schlüssel zur Änderung des Kennworts wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', + 'purgeTokenUsed' => 'Dieser Schlüssel zum Löschen des Kontos wurde entweder bereits verwendet oder ist ungültig. Besuchen Sie Ihre Kontoeinstellungen, um es erneut zu versuchen.', 'passTokenLost' => "Kein Token wurde bereitgestellt. Wenn Sie in einer E-Mail einen Link zum Zurücksetzen des Kennworts erhalten haben, kopieren Sie die gesamte URL (einschließlich des Tokens am Ende) in die Adressleiste Ihres Browsers.", 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", 'loginExceeded' => "Die maximale Anzahl an Anmelde-Versuchen von dieser IP wurde überschritten. Bitte versucht es in %s erneut.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 9ec1ff02..9c239530 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Your password has been changed successfully.", 'deleteAccSent' => "An email has been sent to %s with confirmation link attached.", 'deleteOk' => "Your account has been successfully removed. We hope to see you again soon!

        You may now close this window.", + 'deleteCancel' => "Account deletion was canceled.", 'createAccSent' => 'An email was sent to %s. Simply follow the instructions to create your account.

        If you don\'t receive the verification email, click here to send another one.', 'recovUserSent' => "An email was sent to %s. Simply follow the instructions to recover your username.", 'recovPassSent' => "An email was sent to %s. Simply follow the instructions to reset your password." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Either that email change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', 'passTokenUsed' => 'Either that password change key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', + 'purgeTokenUsed' => 'Either that account delete key has already been used, or it\'s not a valid key. Visit your Account Settings page to try again.', 'passTokenLost' => "No token was provided. If you received a reset password link in an email, please copy and paste the entire URL (including the token at the end) into your browser's location bar.", 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'loginExceeded' => "The maximum number of logins from this IP has been exceeded. Please try again in %s.", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index e281220a..0b178b77 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Tu contraseña ha sido cambiada correctamente.", 'deleteAccSent' => "Se ha enviado un correo electrónico a %s con el enlace de confirmación adjunto.", 'deleteOk' => "Tu cuenta ha sido eliminada correctamente. ¡Esperamos verte de nuevo pronto!

        Ahora puedes cerrar esta ventana.", + 'deleteCancel' => "La eliminación de la cuenta fue cancelada.", 'createAccSent' => 'Un correo fue enviado a %s. Sigue las instrucciones para crear tu cuenta.

        Si no recibes el correo de verificación, haz clic aquí para enviar otro.', 'recovUserSent' => "Un correo fue enviado a %s. Sigue las instrucciones para recuperar tu nombre de usuario.", 'recovPassSent' => "Un correo fue enviado a %s. Sigue las instrucciones para restablecer tu contraseña." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Ese código de cambio de correo electrónico ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', 'passTokenUsed' => 'Ese código de cambio de contraseña ya ha sido usado, o no es válido. Visita tu página de configuración de cuenta para intentarlo de nuevo.', + 'purgeTokenUsed' => 'Esa clave de eliminación de cuenta ya ha sido usada, o no es válida. Visita tu página de configuración de cuenta para intentarlo de nuevo.', 'passTokenLost' => "No se recibió ningún código de petición. Si recibiste un enlace para restablecer tu contraseña por correo, por favor copia y pega la dirección completa (incluyendo el código del final) en la barra de dirección de tu navegador.", 'isRecovering' => "Esta cuenta ya se encuentra en proceso de recuperación. Sigue las instrucciones en tu correo o espera %s para que el token expire.", 'loginExceeded' => "Has excedido la cantidad de inicios de sesión con esta IP. Por favor intenta en %s.", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 2d19e8cb..ffa2d017 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Votre mot de passe a été changé avec succès.", 'deleteAccSent' => "Un courriel a été envoyé à %s avec le lien de confirmation.", 'deleteOk' => "Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt !

        Vous pouvez maintenant fermer cette fenêtre.", + 'deleteCancel' => "La suppression du compte a été annulée.", 'createAccSent' => 'Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu\'il contient pour créer votre compte.

        Si vous ne recevez pas l\'email de vérification, cliquez ici pour en envoyer un autre.', 'recovUserSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour récupérer votre nom d'utilisateur.", 'recovPassSent' => "Un courriel vous a été envoyé à %s. Veuillez suivre les instructions qu'il contient pour réinitialiser votre mot de passe." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => "Cette clé de changement d'adresse courriel a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", 'passTokenUsed' => "Cette clé de changement de mot de passe a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", + 'purgeTokenUsed' => "Cette clé de suppression de compte a déjà été utilisée ou n'est pas valide. Visitez votre page de paramètres du compte pour réessayer.", 'passTokenLost' => "Aucun jeton n'a été fourni. Si vous avez reçu un lien de réinitialisation du mot de passe dans un courriel, merci de copier et coller l'URL entière (y compris le jeton à la fin) dans la barre d'adresse de votre navigateur.", 'isRecovering' => "Ce compte est déjà en train d'être récupéré. Suivez les instruction dans l'email reçu ou attendez %s pour que le token expire.", 'loginExceeded' => "Le nombre maximum de connections depuis cette IP a été dépassé. Essayez de nouevau dans %s.", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 9238b977..72e8a01c 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "Ваш пароль был уÑпешно изменен.", 'deleteAccSent' => "ПиÑьмо Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸ÐµÐ¼ было отправлено на %s.", 'deleteOk' => "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была уÑпешно удалена. ÐадеемÑÑ ÑƒÐ²Ð¸Ð´ÐµÑ‚ÑŒ Ð²Ð°Ñ Ñнова!

        Теперь вы можете закрыть Ñто окно.", + 'deleteCancel' => "Удаление учетной запиÑи было отменено.", 'createAccSent' => 'ПиÑьмо Ñ Ð¸Ð½ÑтрукциÑми Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи было отправлено на Ð°Ð´Ñ€ÐµÑ %s/b>. Следуйте инÑтрукциÑм, Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð»Ð¶ÐµÐ½Ð¸Ñ Ñ€ÐµÐ³Ð¸Ñтрации.

        ЕÑли вы не получили пиÑьмо Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ, нажмите здеÑÑŒ, чтобы отправить его повторно.', 'recovUserSent' => "ПиÑьмо Ñ Ð¸Ð½ÑтрукциÑми Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи было отправлено на Ð°Ð´Ñ€ÐµÑ %s/b>. ПроÑто Ñледуйте инÑтрукциÑм Ð´Ð»Ñ Ð²Ð¾ÑÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ пользователÑ.", 'recovPassSent' => "ПиÑьмо Ñ Ð¸Ð½ÑтрукциÑми Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи было отправлено на Ð°Ð´Ñ€ÐµÑ %s/b>. ПроÑто Ñледуйте инÑтрукциÑм Ð´Ð»Ñ ÑброÑа паролÑ." @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'Этот ключ Ð´Ð»Ñ Ñмены email уже был иÑпользован или недейÑтвителен. ПоÑетите вашу Ñтраницу наÑтроек учетной запиÑи, чтобы попробовать Ñнова.', 'passTokenUsed' => 'Этот ключ Ð´Ð»Ñ Ñмены Ð¿Ð°Ñ€Ð¾Ð»Ñ ÑƒÐ¶Ðµ был иÑпользован или недейÑтвителен. ПоÑетите вашу Ñтраницу наÑтроек учетной запиÑи, чтобы попробовать Ñнова.', + 'purgeTokenUsed' => 'Этот ключ Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ð¾Ð¹ запиÑи уже был иÑпользован или недейÑтвителен. ПоÑетите вашу Ñтраницу наÑтроек учетной запиÑи, чтобы попробовать Ñнова.', 'passTokenLost' => "Ключ не был получен. ЕÑли вы ÑброÑили пароль по ÑÑылке из пиÑьма, отправленного на email, пожалуйÑта, Ñкопируйте URL целиком и вÑтавьте в адреÑную Ñтроку (Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ ÐºÐ»ÑŽÑ‡, указанный в конце ÑÑылки).", 'isRecovering' => "Эта ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ уже воÑÑтанавливаетÑÑ. Следуйте инÑтрукциÑм в пиÑьме или дождитеÑÑŒ иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ Ñрока дейÑÑ‚Ð²Ð¸Ñ Ñ‚Ð¾ÐºÐµÐ½Ð° через %s.", 'loginExceeded' => "ДоÑтигнуто макÑимальное количеÑтво попыток входа Ñ Ñтого IP. ПожалуйÑта, попробуйте Ñнова через %s.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index c72151a8..34b40587 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -1035,6 +1035,7 @@ $lang = array( 'passChangeOk' => "您的密ç å·²æˆåŠŸæ›´æ”¹ã€‚", 'deleteAccSent' => "å·²å‘ %s å‘é€äº†ä¸€å°å¸¦æœ‰ç¡®è®¤é“¾æŽ¥çš„邮件。", 'deleteOk' => "您的账户已æˆåŠŸåˆ é™¤ã€‚å¸Œæœ›ä¸ä¹…åŽèƒ½å†æ¬¡è§åˆ°æ‚¨ï¼

        您现在å¯ä»¥å…³é—­æ­¤çª—å£ã€‚", + 'deleteCancel' => "è´¦æˆ·åˆ é™¤å·²å–æ¶ˆã€‚", 'createAccSent' => '电å­é‚®ä»¶å‘é€åˆ°%s。åªè¯·æŒ‰ç…§è¯´æ˜Žåˆ›å»ºæ‚¨çš„账户。

        如果您没有收到验è¯é‚®ä»¶ï¼Œç‚¹å‡»è¿™é‡Œé‡æ–°å‘é€ã€‚', 'recovUserSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªè¯·æŒ‰ç…§è¯´æ˜Žæ¢å¤æ‚¨çš„用户å。", 'recovPassSent' => "电å­é‚®ä»¶å‘é€åˆ°%s。åªè¯·æŒ‰ç…§è¯´æ˜Žé‡ç½®æ‚¨çš„密ç ã€‚" @@ -1042,6 +1043,7 @@ $lang = array( 'error' => array( 'mailTokenUsed' => 'è¯¥é‚®ç®±æ›´æ”¹å¯†é’¥å·²è¢«ä½¿ç”¨ï¼Œæˆ–ä¸æ˜¯æœ‰æ•ˆå¯†é’¥ã€‚请访问您的账户设置页é¢é‡æ–°å°è¯•。', 'passTokenUsed' => 'è¯¥å¯†ç æ›´æ”¹å¯†é’¥å·²è¢«ä½¿ç”¨ï¼Œæˆ–䏿˜¯æœ‰æ•ˆå¯†é’¥ã€‚请访问您的账户设置页é¢é‡æ–°å°è¯•。', + 'purgeTokenUsed' => 'è¯¥è´¦æˆ·åˆ é™¤å¯†é’¥å·²è¢«ä½¿ç”¨ï¼Œæˆ–ä¸æ˜¯æœ‰æ•ˆå¯†é’¥ã€‚请访问您的账户设置页é¢é‡æ–°å°è¯•。', 'passTokenLost' => "未æä¾›ä»¤ç‰Œã€‚如果您在邮件中收到é‡ç½®å¯†ç é“¾æŽ¥ï¼Œè¯·å°†æ•´ä¸ªç½‘å€ï¼ˆåŒ…括最åŽçš„令牌)å¤åˆ¶å¹¶ç²˜è´´åˆ°æµè§ˆå™¨åœ°å€æ ä¸­ã€‚", 'isRecovering' => "æ­¤å¸æˆ·å·²æ¢å¤ã€‚按照电å­é‚®ä»¶ä¸­çš„说明或等待%såŽä»¤ç‰Œè¿‡æœŸã€‚", 'loginExceeded' => "这个IP最大登录次数已超过。请在%såŽå†æ¬¡å°è¯•。", diff --git a/static/css/delete.css b/static/css/delete.css new file mode 100644 index 00000000..257d8d12 --- /dev/null +++ b/static/css/delete.css @@ -0,0 +1,42 @@ +.account-delete-box { + background: #161616; + border-radius: 3px; + box-sizing: border-box; + line-height: 1.7em; + margin: 0 auto; + max-width: 100%; + padding: 15px; + width: 34em; +} + +.account-delete-box [class^="heading-size-"] { + margin-top: 0; + text-align: center; +} + +.account-delete-box-warning, +.account-delete-box-alternative { + font-size: 175%; + line-height: 1.2; +} + +.account-delete-box-warning, +.account-delete-box-warning * { + color: #ff4040 !important +} + +.account-delete-box-warning b { + display: block; + font-size: 200%; + font-weight: 900; + text-align: center; +} + +.account-delete-box-confirm { + text-align: center; +} + +/* from global.css */ +.text p { + margin: 10px 0px; +} diff --git a/template/localized/confirm-delete-account_0.tpl.php b/template/localized/confirm-delete-account_0.tpl.php new file mode 100644 index 00000000..db78173a --- /dev/null +++ b/template/localized/confirm-delete-account_0.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_2.tpl.php b/template/localized/confirm-delete-account_2.tpl.php new file mode 100644 index 00000000..74e8b617 --- /dev/null +++ b/template/localized/confirm-delete-account_2.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_3.tpl.php b/template/localized/confirm-delete-account_3.tpl.php new file mode 100644 index 00000000..637f018f --- /dev/null +++ b/template/localized/confirm-delete-account_3.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_4.tpl.php b/template/localized/confirm-delete-account_4.tpl.php new file mode 100644 index 00000000..e4c023c3 --- /dev/null +++ b/template/localized/confirm-delete-account_4.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_6.tpl.php b/template/localized/confirm-delete-account_6.tpl.php new file mode 100644 index 00000000..18f58f57 --- /dev/null +++ b/template/localized/confirm-delete-account_6.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/confirm-delete-account_8.tpl.php b/template/localized/confirm-delete-account_8.tpl.php new file mode 100644 index 00000000..af869141 --- /dev/null +++ b/template/localized/confirm-delete-account_8.tpl.php @@ -0,0 +1,26 @@ + diff --git a/template/localized/delete-account_0.tpl.php b/template/localized/delete-account_0.tpl.php new file mode 100644 index 00000000..633484f0 --- /dev/null +++ b/template/localized/delete-account_0.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_2.tpl.php b/template/localized/delete-account_2.tpl.php new file mode 100644 index 00000000..5f34b9b5 --- /dev/null +++ b/template/localized/delete-account_2.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_3.tpl.php b/template/localized/delete-account_3.tpl.php new file mode 100644 index 00000000..15db9de0 --- /dev/null +++ b/template/localized/delete-account_3.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_4.tpl.php b/template/localized/delete-account_4.tpl.php new file mode 100644 index 00000000..987545c9 --- /dev/null +++ b/template/localized/delete-account_4.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_6.tpl.php b/template/localized/delete-account_6.tpl.php new file mode 100644 index 00000000..12544c3f --- /dev/null +++ b/template/localized/delete-account_6.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/localized/delete-account_8.tpl.php b/template/localized/delete-account_8.tpl.php new file mode 100644 index 00000000..b8b84244 --- /dev/null +++ b/template/localized/delete-account_8.tpl.php @@ -0,0 +1,15 @@ + diff --git a/template/mails/delete-account_0.tpl b/template/mails/delete-account_0.tpl new file mode 100644 index 00000000..7bbef9db --- /dev/null +++ b/template/mails/delete-account_0.tpl @@ -0,0 +1,21 @@ +# 2025 +Please verify your request to be forgotten +Greetings, + +We’ve just received a request to exercise the “right to be forgotten†from the following email address %2$s in accordance with our Privacy Policy. + +Please click on following link HOST_URL?account=confirm-delete&key=%1$s to confirm your selection. You will get one last chance to review your choices once you are back on the site. + +Should you choose to proceed with this process, we will permanently delete or anonymize any Personal Data linked to your account. + +This information will include, but is not limited to: + + * Your Identity %3$s, and the email address associated with this login. + * Your current Premium status and data, should you be a Premium member. + * Your profile information and preferences. + * In some cases, content that you've authored, including comments, guides and forum posts. + * Note that game data connected to your gaming identities will re-appear when other users request data updates, unless you delete that data at the source. + +Once we receive your final confirmation, we will be removing your Personal Data. + +If you have any questions or need further assistance, please contact CONTACT_EMAIL. diff --git a/template/mails/delete-account_2.tpl b/template/mails/delete-account_2.tpl new file mode 100644 index 00000000..7ccda5d2 --- /dev/null +++ b/template/mails/delete-account_2.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Veuillez vérifier votre demande de droit à l'oubli +Bonjour, + +Nous venons de recevoir une demande d'exercice du « droit à l'oubli » de l'adresse e-mail suivante %2$s conformément à notre politique de confidentialité. + +Veuillez cliquer sur le lien suivant HOST_URL?account=confirm-delete&key=%1$s pour confirmer votre choix. Vous aurez une dernière chance de revoir vos choix une fois de retour sur le site. + +Si vous choisissez de poursuivre ce processus, nous supprimerons ou anonymiserons définitivement toutes les données personnelles liées à votre compte. + +Ces informations incluront, sans s'y limiter : + + * Votre identité %3$s, et l'adresse e-mail associée à cette connexion. + * Votre statut Premium actuel et les données, si vous êtes membre Premium. + * Vos informations de profil et préférences. + * Dans certains cas, le contenu que vous avez créé, y compris les commentaires, guides et messages sur le forum. + * Notez que les données de jeu liées à vos identités de jeu réapparaîtront lorsque d'autres utilisateurs demanderont des mises à jour de données, sauf si vous supprimez ces données à la source. + +Une fois que nous aurons reçu votre confirmation finale, nous supprimerons vos données personnelles. + +Si vous avez des questions ou besoin d'aide supplémentaire, veuillez contacter CONTACT_EMAIL. diff --git a/template/mails/delete-account_3.tpl b/template/mails/delete-account_3.tpl new file mode 100644 index 00000000..b8ce03a4 --- /dev/null +++ b/template/mails/delete-account_3.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Bitte bestätigen Sie Ihre Anfrage auf Vergessenwerden +Hallo, + +Wir haben gerade eine Anfrage zum "Recht auf Vergessenwerden" von der folgenden E-Mail-Adresse %2$s gemäß unserer Datenschutzrichtlinie erhalten. + +Bitte klicken Sie auf den folgenden Link HOST_URL?account=confirm-delete&key=%1$s, um Ihre Auswahl zu bestätigen. Sie erhalten eine letzte Gelegenheit, Ihre Auswahl zu überprüfen, sobald Sie wieder auf der Website sind. + +Wenn Sie sich entscheiden, diesen Prozess fortzusetzen, werden wir alle mit Ihrem Konto verknüpften personenbezogenen Daten dauerhaft löschen oder anonymisieren. + +Diese Informationen umfassen unter anderem: + + * Ihre Identität %3$s und die mit diesem Login verknüpfte E-Mail-Adresse. + * Ihren aktuellen Premium-Status und Daten, falls Sie ein Premium-Mitglied sind. + * Ihre Profilinformationen und Präferenzen. + * In einigen Fällen von Ihnen erstellte Inhalte, einschließlich Kommentare, Guides und Forenbeiträge. + * Beachten Sie, dass Spieldaten, die mit Ihren Spielidentitäten verbunden sind, wieder erscheinen, wenn andere Nutzer Datenaktualisierungen anfordern, es sei denn, Sie löschen diese Daten an der Quelle. + +Sobald wir Ihre endgültige Bestätigung erhalten haben, werden wir Ihre personenbezogenen Daten entfernen. + +Wenn Sie Fragen haben oder weitere Unterstützung benötigen, kontaktieren Sie bitte CONTACT_EMAIL. diff --git a/template/mails/delete-account_4.tpl b/template/mails/delete-account_4.tpl new file mode 100644 index 00000000..a1738dfe --- /dev/null +++ b/template/mails/delete-account_4.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +è¯·éªŒè¯æ‚¨çš„被é—忘æƒè¯·æ±‚ +您好, + +我们刚刚收到æ¥è‡ªä»¥ä¸‹ç”µå­é‚®ä»¶åœ°å€ %2$s 的“被é—忘æƒâ€è¯·æ±‚ï¼Œä¾æ®æˆ‘们的éšç§æ”¿ç­–。 + +请点击以下链接 HOST_URL?account=confirm-delete&key=%1$s 以确认您的选择。返回网站åŽï¼Œæ‚¨å°†æœ‰æœ€åŽä¸€æ¬¡æœºä¼šå®¡æŸ¥æ‚¨çš„选择。 + +如果您选择继续此æµç¨‹ï¼Œæˆ‘们将永久删除或匿å化与您的账户相关的所有个人数æ®ã€‚ + +这些信æ¯åŒ…括但ä¸é™äºŽï¼š + + * 您的身份 %3$s,以åŠä¸Žæ­¤ç™»å½•å…³è”的电å­é‚®ä»¶åœ°å€ã€‚ + * 您当å‰çš„高级会员状æ€å’Œæ•°æ®ï¼ˆå¦‚适用)。 + * 您的个人资料信æ¯å’Œå好设置。 + * 在æŸäº›æƒ…å†µä¸‹ï¼Œæ‚¨åˆ›ä½œçš„å†…å®¹ï¼ŒåŒ…æ‹¬è¯„è®ºã€æŒ‡å—和论å›å¸–å­ã€‚ + * 请注æ„,与您的游æˆèº«ä»½ç›¸å…³çš„æ¸¸æˆæ•°æ®åœ¨å…¶ä»–ç”¨æˆ·è¯·æ±‚æ•°æ®æ›´æ–°æ—¶ä¼šé‡æ–°å‡ºçŽ°ï¼Œé™¤éžæ‚¨åœ¨æºå¤´åˆ é™¤è¿™äº›æ•°æ®ã€‚ + +一旦我们收到您的最终确认,我们将删除您的个人数æ®ã€‚ + +如有任何疑问或需è¦è¿›ä¸€æ­¥å¸®åŠ©ï¼Œè¯·è”ç³» CONTACT_EMAIL。 diff --git a/template/mails/delete-account_6.tpl b/template/mails/delete-account_6.tpl new file mode 100644 index 00000000..421351f6 --- /dev/null +++ b/template/mails/delete-account_6.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +Por favor, verifique su solicitud de derecho al olvido +Saludos, + +Acabamos de recibir una solicitud para ejercer el "derecho al olvido" desde la siguiente dirección de correo electrónico %2$s de acuerdo con nuestra Política de Privacidad. + +Por favor, haga clic en el siguiente enlace HOST_URL?account=confirm-delete&key=%1$s para confirmar su selección. Tendrá una última oportunidad de revisar sus opciones una vez que regrese al sitio. + +Si decide continuar con este proceso, eliminaremos o anonimizaremos permanentemente cualquier dato personal vinculado a su cuenta. + +Esta información incluirá, pero no se limitará a: + + * Su identidad %3$s y la dirección de correo electrónico asociada a este inicio de sesión. + * Su estado Premium actual y datos, si es miembro Premium. + * Su información de perfil y preferencias. + * En algunos casos, contenido que haya creado, incluyendo comentarios, guías y publicaciones en foros. + * Tenga en cuenta que los datos de juego conectados a sus identidades de juego volverán a aparecer cuando otros usuarios soliciten actualizaciones de datos, a menos que elimine esos datos en la fuente. + +Una vez que recibamos su confirmación final, eliminaremos sus datos personales. + +Si tiene alguna pregunta o necesita más ayuda, por favor contacte a CONTACT_EMAIL. diff --git a/template/mails/delete-account_8.tpl b/template/mails/delete-account_8.tpl new file mode 100644 index 00000000..f9f916c4 --- /dev/null +++ b/template/mails/delete-account_8.tpl @@ -0,0 +1,21 @@ +# GPTed from 2025 source +ПожалуйÑта, подтвердите ваш Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° удаление данных +ЗдравÑтвуйте, + +Мы только что получили Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° реализацию "права быть забытым" Ñ Ð°Ð´Ñ€ÐµÑа Ñлектронной почты %2$s в ÑоответÑтвии Ñ Ð½Ð°ÑˆÐµÐ¹ Политикой конфиденциальноÑти. + +ПожалуйÑта, перейдите по Ñледующей ÑÑылке HOST_URL?account=confirm-delete&key=%1$s, чтобы подтвердить Ñвой выбор. ПоÑле Ð²Ð¾Ð·Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð½Ð° Ñайт у Ð²Ð°Ñ Ð±ÑƒÐ´ÐµÑ‚ поÑледний ÑˆÐ°Ð½Ñ Ð¿ÐµÑ€ÐµÑмотреть Ñвое решение. + +ЕÑли вы решите продолжить процеÑÑ, мы навÑегда удалим или анонимизируем вÑе перÑональные данные, ÑвÑзанные Ñ Ð²Ð°ÑˆÐµÐ¹ учетной запиÑью. + +Эта Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ включать, но не ограничиватьÑÑ: + + * Вашу личноÑть %3$s и Ð°Ð´Ñ€ÐµÑ Ñлектронной почты, ÑвÑзанный Ñ Ñтим входом. + * Ваш текущий ÑÑ‚Ð°Ñ‚ÑƒÑ Ð¸ данные Premium, еÑли вы ÑвлÑетеÑÑŒ Premium-учаÑтником. + * Вашу информацию Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ Ð¸ предпочтениÑ. + * Ð’ некоторых ÑлучаÑÑ… Ñозданный вами контент, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ ÐºÐ¾Ð¼Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ð¸Ð¸, руководÑтва и ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð½Ð° форуме. + * Обратите внимание, что игровые данные, ÑвÑзанные Ñ Ð²Ð°ÑˆÐ¸Ð¼Ð¸ игровыми идентификаторами, поÑвÑÑ‚ÑÑ Ñнова, когда другие пользователи запроÑÑÑ‚ обновление данных, еÑли только вы не удалите Ñти данные у иÑточника. + +ПоÑле Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ окончательного Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ Ð¼Ñ‹ удалим ваши перÑональные данные. + +ЕÑли у Ð²Ð°Ñ ÐµÑть вопроÑÑ‹ или вам нужна Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒ, пожалуйÑта, ÑвÑжитеÑÑŒ Ñ CONTACT_EMAIL. diff --git a/template/pages/delete.tpl.php b/template/pages/delete.tpl.php new file mode 100644 index 00000000..77753488 --- /dev/null +++ b/template/pages/delete.tpl.php @@ -0,0 +1,26 @@ +brick('header'); +?> +
        +
        +
        +brick('announcement'); + + $this->brick('pageTemplate'); + +if ($this->inputbox): + $this->brick(...$this->inputbox); // $templateName, [$templateVars] +elseif ($this->confirm): + $this->localizedBrick('confirm-delete-account'); +else: + $this->localizedBrick('delete-account'); +endif; +?> +
        +
        + + +brick('footer'); ?> From 6557e70d5c5e55d67f91ab416ab7a735c3cfcbdf Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Mon, 11 Aug 2025 16:00:18 +0200 Subject: [PATCH 0987/1249] Template/Update (Part 47) * split global.js into its components, so it can be reasonably processed by setup * make reputation requirements configurable * move Markup and Locale back into global.js (removed associated build scripts) * extend Icon to display iconId in lightbox popup --- .gitignore | 3 +- endpoints/class/class.php | 2 - endpoints/compare/compare.php | 1 - endpoints/icon/get-id-from-name.php | 29 + endpoints/item/item.php | 1 - endpoints/items/items.php | 2 +- endpoints/itemset/itemset.php | 2 +- endpoints/npc/npc.php | 2 +- endpoints/object/object.php | 2 - endpoints/pet/pet.php | 2 - endpoints/petcalc/petcalc.php | 1 - endpoints/profile/profile.php | 1 - endpoints/race/race.php | 2 - endpoints/search/search.php | 1 - endpoints/spell/spell.php | 2 - includes/cfg.class.php | 28 +- .../response/templateresponse.class.php | 2 - .../{locales.ss.php => global-js.ss.php} | 10 +- setup/tools/filegen/markup.ss.php | 24 - .../filegen/templates/global.js/0_user.js | 73 + .../tools/filegen/templates/global.js/ajax.js | 51 + .../filegen/templates/global.js/animations.js | 123 + .../templates/global.js/announcement.js | 157 + .../filegen/templates/global.js/audio.js | 377 + .../templates/global.js/clicktocopy.js | 122 + .../filegen/templates/global.js/comments.js | 459 + .../templates/global.js/conditionList.js | 329 + .../templates/global.js/contacttool.js | 550 + .../filegen/templates/global.js/cookies.js | 37 + .../filegen/templates/global.js/dialog.js | 568 + .../templates/global.js/dom_manipulation.js | 252 + .../filegen/templates/global.js/favorites.js | 262 + .../filegen/templates/global.js/guide.js | 368 + .../tools/filegen/templates/global.js/icon.js | 404 + .../filegen/templates/global.js/lightbox.js | 161 + .../tools/filegen/templates/global.js/line.js | 38 + .../filegen/templates/global.js/links.js | 138 + .../filegen/templates/global.js/listview.js | 5029 ++++ .../templates/global.js/listview_templates.js | 7070 +++++ .../filegen/templates/global.js/livesearch.js | 368 + .../{locale.js.in => global.js/locale.js} | 24 +- .../filegen/templates/global.js/mapper.js | 1113 + .../filegen/templates/global.js/mapviewer.js | 293 + .../{Markup.js.in => global.js/markup.js} | 1110 +- .../tools/filegen/templates/global.js/menu.js | 987 + .../filegen/templates/global.js/messagebox.js | 16 + .../templates/global.js/modelviewer.js | 676 + .../templates/global.js/pagetemplate.js | 630 + .../templates/global.js/positioning.js | 10 + .../filegen/templates/global.js/profiler.js | 19 + .../templates/global.js/progressbar.js | 122 + .../filegen/templates/global.js/rectangle.js | 35 + .../filegen/templates/global.js/redbutton.js | 48 + .../templates/global.js/screenshots.js | 624 + .../templates/global.js/search_lvbrowse.js | 84 + .../filegen/templates/global.js/showonmap.js | 5 + .../filegen/templates/global.js/slider.js | 289 + .../filegen/templates/global.js/summary.js | 78 + .../filegen/templates/global.js}/swfobject.js | 0 .../tools/filegen/templates/global.js/tabs.js | 364 + .../filegen/templates/global.js/tracking.js | 130 + .../filegen/templates/global.js/ui_ux.js | 887 + .../filegen/templates/global.js/utilities.js | 254 + .../filegen/templates/global.js/videos.js | 539 + .../tools/filegen/templates/global.js/vote.js | 28 + .../tools/filegen/templates/global.js/wow.js | 417 + .../tools/filegen/templates/global.js/wsa.js | 4 + setup/tools/setupScript.class.php | 49 +- setup/updates/1758578400_17.sql | 1 + static/js/Draggable.js | 134 +- static/js/global.js | 24060 ---------------- static/js/locale_zhcn.js | 2 +- static/js/video.js | 1870 +- 73 files changed, 26256 insertions(+), 25699 deletions(-) create mode 100644 endpoints/icon/get-id-from-name.php rename setup/tools/filegen/{locales.ss.php => global-js.ss.php} (86%) delete mode 100644 setup/tools/filegen/markup.ss.php create mode 100644 setup/tools/filegen/templates/global.js/0_user.js create mode 100644 setup/tools/filegen/templates/global.js/ajax.js create mode 100644 setup/tools/filegen/templates/global.js/animations.js create mode 100644 setup/tools/filegen/templates/global.js/announcement.js create mode 100644 setup/tools/filegen/templates/global.js/audio.js create mode 100644 setup/tools/filegen/templates/global.js/clicktocopy.js create mode 100644 setup/tools/filegen/templates/global.js/comments.js create mode 100644 setup/tools/filegen/templates/global.js/conditionList.js create mode 100644 setup/tools/filegen/templates/global.js/contacttool.js create mode 100644 setup/tools/filegen/templates/global.js/cookies.js create mode 100644 setup/tools/filegen/templates/global.js/dialog.js create mode 100644 setup/tools/filegen/templates/global.js/dom_manipulation.js create mode 100644 setup/tools/filegen/templates/global.js/favorites.js create mode 100644 setup/tools/filegen/templates/global.js/guide.js create mode 100644 setup/tools/filegen/templates/global.js/icon.js create mode 100644 setup/tools/filegen/templates/global.js/lightbox.js create mode 100644 setup/tools/filegen/templates/global.js/line.js create mode 100644 setup/tools/filegen/templates/global.js/links.js create mode 100644 setup/tools/filegen/templates/global.js/listview.js create mode 100644 setup/tools/filegen/templates/global.js/listview_templates.js create mode 100644 setup/tools/filegen/templates/global.js/livesearch.js rename setup/tools/filegen/templates/{locale.js.in => global.js/locale.js} (74%) create mode 100644 setup/tools/filegen/templates/global.js/mapper.js create mode 100644 setup/tools/filegen/templates/global.js/mapviewer.js rename setup/tools/filegen/templates/{Markup.js.in => global.js/markup.js} (84%) create mode 100644 setup/tools/filegen/templates/global.js/menu.js create mode 100644 setup/tools/filegen/templates/global.js/messagebox.js create mode 100644 setup/tools/filegen/templates/global.js/modelviewer.js create mode 100644 setup/tools/filegen/templates/global.js/pagetemplate.js create mode 100644 setup/tools/filegen/templates/global.js/positioning.js create mode 100644 setup/tools/filegen/templates/global.js/profiler.js create mode 100644 setup/tools/filegen/templates/global.js/progressbar.js create mode 100644 setup/tools/filegen/templates/global.js/rectangle.js create mode 100644 setup/tools/filegen/templates/global.js/redbutton.js create mode 100644 setup/tools/filegen/templates/global.js/screenshots.js create mode 100644 setup/tools/filegen/templates/global.js/search_lvbrowse.js create mode 100644 setup/tools/filegen/templates/global.js/showonmap.js create mode 100644 setup/tools/filegen/templates/global.js/slider.js create mode 100644 setup/tools/filegen/templates/global.js/summary.js rename {static/js => setup/tools/filegen/templates/global.js}/swfobject.js (100%) create mode 100644 setup/tools/filegen/templates/global.js/tabs.js create mode 100644 setup/tools/filegen/templates/global.js/tracking.js create mode 100644 setup/tools/filegen/templates/global.js/ui_ux.js create mode 100644 setup/tools/filegen/templates/global.js/utilities.js create mode 100644 setup/tools/filegen/templates/global.js/videos.js create mode 100644 setup/tools/filegen/templates/global.js/vote.js create mode 100644 setup/tools/filegen/templates/global.js/wow.js create mode 100644 setup/tools/filegen/templates/global.js/wsa.js create mode 100644 setup/updates/1758578400_17.sql delete mode 100644 static/js/global.js diff --git a/.gitignore b/.gitignore index d4c2da01..c23b513e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,7 @@ # generated files /static/js/profile_all.js -/static/js/locale.js -/static/js/Markup.js +/static/js/global.js /static/widgets/power.js /static/widgets/power/demo.html /static/widgets/searchbox.js diff --git a/endpoints/class/class.php b/endpoints/class/class.php index 70922cc8..50ca4666 100644 --- a/endpoints/class/class.php +++ b/endpoints/class/class.php @@ -19,8 +19,6 @@ class ClassBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 12]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::CHR_CLASS; public int $typeId = 0; public ?string $expansion = null; diff --git a/endpoints/compare/compare.php b/endpoints/compare/compare.php index f2cffbe0..5268795d 100644 --- a/endpoints/compare/compare.php +++ b/endpoints/compare/compare.php @@ -20,7 +20,6 @@ class CompareBaseResponse extends TemplateResponse [SC_JS_FILE, 'js/Draggable.js'], [SC_JS_FILE, 'js/filters.js'], [SC_JS_FILE, 'js/Summary.js'], - [SC_JS_FILE, 'js/swfobject.js'], [SC_CSS_FILE, 'css/Summary.css'] ); protected array $expectedGET = array( diff --git a/endpoints/icon/get-id-from-name.php b/endpoints/icon/get-id-from-name.php new file mode 100644 index 00000000..178602cc --- /dev/null +++ b/endpoints/icon/get-id-from-name.php @@ -0,0 +1,29 @@ + ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[\w_-]+$/']] + ); + + protected function generate() : void + { + if (!$this->assertGET('name')) + { + $this->result = 'null'; + return; + } + + $this->result = 0; + if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_icons WHERE `name` = ?', $this->_get['name'])) + $this->result = $id; + } +} + +?> diff --git a/endpoints/item/item.php b/endpoints/item/item.php index 5d81b9b2..f476b5b9 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -18,7 +18,6 @@ class ItemBaseResponse extends TemplateResponse implements ICache protected array $breadcrumb = [0, 0]; protected array $scripts = array( - [SC_JS_FILE, 'js/swfobject.js'], [SC_JS_FILE, 'js/profile.js'], [SC_JS_FILE, 'js/filters.js'] ); diff --git a/endpoints/items/items.php b/endpoints/items/items.php index 97d4a9d1..21aff287 100644 --- a/endpoints/items/items.php +++ b/endpoints/items/items.php @@ -19,7 +19,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache protected array $breadcrumb = [0, 0]; protected array $dataLoader = ['weight-presets']; - protected array $scripts = [[SC_JS_FILE, 'js/filters.js'], [SC_JS_FILE, 'js/swfobject.js']]; + protected array $scripts = [[SC_JS_FILE, 'js/filters.js']]; protected array $expectedGET = array( 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] ); diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php index 0b017380..398fe55e 100644 --- a/endpoints/itemset/itemset.php +++ b/endpoints/itemset/itemset.php @@ -17,7 +17,7 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 2]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js'], [SC_JS_FILE, 'js/Summary.js']]; + protected array $scripts = [[SC_JS_FILE, 'js/Summary.js']]; public int $type = Type::ITEMSET; public int $typeId = 0; diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index 38de19f9..3d4624e2 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -17,7 +17,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 4]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js'], [SC_CSS_FILE, 'css/Profiler.css']]; + protected array $scripts = [[SC_CSS_FILE, 'css/Profiler.css']]; public int $type = Type::NPC; public int $typeId = 0; diff --git a/endpoints/object/object.php b/endpoints/object/object.php index 203fa6a0..9c6a1ba2 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -17,8 +17,6 @@ class ObjectBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 5]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::OBJECT; public int $typeId = 0; public ?Book $book = null; diff --git a/endpoints/pet/pet.php b/endpoints/pet/pet.php index df2af810..f383a0f7 100644 --- a/endpoints/pet/pet.php +++ b/endpoints/pet/pet.php @@ -17,8 +17,6 @@ class PetBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 8]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::PET; public int $typeId = 0; public ?string $expansion = null; diff --git a/endpoints/petcalc/petcalc.php b/endpoints/petcalc/petcalc.php index 0532008d..cff9539b 100644 --- a/endpoints/petcalc/petcalc.php +++ b/endpoints/petcalc/petcalc.php @@ -19,7 +19,6 @@ class PetcalcBaseResponse extends TemplateResponse [SC_CSS_FILE, 'css/talentcalc.css'], [SC_CSS_FILE, 'css/talent.css'], [SC_JS_FILE, 'js/petcalc.js'], - [SC_JS_FILE, 'js/swfobject.js'], [SC_CSS_FILE, 'css/petcalc.css'] ); diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index ee921ccb..284ed283 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -19,7 +19,6 @@ class ProfileBaseResponse extends TemplateResponse protected array $scripts = array( [SC_JS_FILE, 'js/filters.js'], [SC_JS_FILE, 'js/TalentCalc.js'], - [SC_JS_FILE, 'js/swfobject.js'], [SC_JS_FILE, 'js/profile_all.js'], [SC_JS_FILE, 'js/profile.js'], [SC_JS_FILE, 'js/Profiler.js'], diff --git a/endpoints/race/race.php b/endpoints/race/race.php index 2134bedc..1bb261c6 100644 --- a/endpoints/race/race.php +++ b/endpoints/race/race.php @@ -23,8 +23,6 @@ class RaceBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 13]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::CHR_RACE; public int $typeId = 0; public ?string $expansion = null; diff --git a/endpoints/search/search.php b/endpoints/search/search.php index dc2687cd..edf8cb73 100644 --- a/endpoints/search/search.php +++ b/endpoints/search/search.php @@ -23,7 +23,6 @@ class SearchBaseResponse extends TemplateResponse implements ICache protected string $pageName = 'search'; protected ?int $activeTab = parent::TAB_DATABASE; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; protected array $expectedGET = array( 'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] ); diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 05111983..8f1e6645 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -23,8 +23,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 1]; - protected array $scripts = [[SC_JS_FILE, 'js/swfobject.js']]; - public int $type = Type::SPELL; public int $typeId = 0; public array $reagents = [false, null]; diff --git a/includes/cfg.class.php b/includes/cfg.class.php index 138e541d..b82cb1ad 100644 --- a/includes/cfg.class.php +++ b/includes/cfg.class.php @@ -48,17 +48,17 @@ class Cfg private static $isLoaded = false; private static $rebuildScripts = array( - // 'rep_req_border_unco' => ['global'], // currently not a template or buildScript - // 'rep_req_border_rare' => ['global'], - // 'rep_req_border_epic' => ['global'], - // 'rep_req_border_lege' => ['global'], - 'profiler_enable' => ['realms', 'realmMenu'], - 'battlegroup' => ['realms', 'realmMenu'], - 'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'], - 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power'], - 'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'], - 'contact_email' => ['markup'], - 'locales' => ['locales'] + 'rep_req_border_uncommon' => ['globaljs'], + 'rep_req_border_rare' => ['globaljs'], + 'rep_req_border_epic' => ['globaljs'], + 'rep_req_border_legendary' => ['globaljs'], + 'profiler_enable' => ['realms', 'realmMenu'], + 'battlegroup' => ['realms', 'realmMenu'], + 'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'], + 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power'], + 'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'], + 'contact_email' => ['globaljs'], + 'locales' => ['globaljs'] ); public static function load() : void @@ -294,16 +294,16 @@ class Cfg yield $k => self::$store[$k]; } - public static function applyToString(string $string) : string + public static function applyToString(string $string, bool $nf = true) : string { return preg_replace_callback( ['/CFG_([A-Z_]+)/', '/((HOST|STATIC)_URL)/'], - function ($m) { + function ($m) use ($nf) { if (!isset(self::$store[strtolower($m[1])])) return $m[1]; [$val, $flags, , , ] = self::$store[strtolower($m[1])]; - return $flags & (self::FLAG_TYPE_FLOAT | self::FLAG_TYPE_INT) ? Lang::nf($val) : $val; + return ($flags & (self::FLAG_TYPE_FLOAT | self::FLAG_TYPE_INT)) && $nf ? Lang::nf($val) : $val; }, $string ); diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index bc2cda60..a8760bd4 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -111,8 +111,6 @@ class TemplateResponse extends BaseResponse [SC_JS_FILE, 'widgets/power.js', SC_FLAG_NO_TIMESTAMP | SC_FLAG_APPEND_LOCALE], [SC_JS_FILE, 'js/locale_%s.js', SC_FLAG_LOCALIZED ], [SC_JS_FILE, 'js/global.js' ], - [SC_JS_FILE, 'js/locale.js' ], - [SC_JS_FILE, 'js/Markup.js' ], [SC_CSS_FILE, 'css/basic.css' ], [SC_CSS_FILE, 'css/global.css' ], [SC_CSS_FILE, 'css/aowow.css' ], diff --git a/setup/tools/filegen/locales.ss.php b/setup/tools/filegen/global-js.ss.php similarity index 86% rename from setup/tools/filegen/locales.ss.php rename to setup/tools/filegen/global-js.ss.php index 4eef9790..28fd1d55 100644 --- a/setup/tools/filegen/locales.ss.php +++ b/setup/tools/filegen/global-js.ss.php @@ -9,8 +9,6 @@ if (!CLI) die('not in cli mode'); -// Create 'locale.js'-file in static/js - /* 0: { // English id: LOCALE_ENUS, @@ -55,11 +53,13 @@ CLISetup::registerSetup("build", new class extends SetupScript use TrTemplateFile; protected $info = array( - 'locales' => [[], CLISetup::ARGV_PARAM, 'Compiles the Locale Object (static/js/locale.js) with available languages.'] + 'globaljs' => [[], CLISetup::ARGV_PARAM, 'Compiles the global javascript file (static/js/global.js).'] ); - protected $fileTemplateDest = ['static/js/locale.js']; - protected $fileTemplateSrc = ['locale.js.in']; + protected $fileTemplateDest = ['static/js/global.js']; + protected $fileTemplateSrc = ['global.js']; + + private bool $numFmt = false; private function locales() : string { diff --git a/setup/tools/filegen/markup.ss.php b/setup/tools/filegen/markup.ss.php deleted file mode 100644 index 70ceecba..00000000 --- a/setup/tools/filegen/markup.ss.php +++ /dev/null @@ -1,24 +0,0 @@ - [[], CLISetup::ARGV_PARAM, 'Fills the markup parser (static/js/Markup.js) with site variables.'] - ); - - protected $fileTemplateSrc = ['Markup.js.in']; - protected $fileTemplateDest = ['static/js/Markup.js']; -}); - -?> diff --git a/setup/tools/filegen/templates/global.js/0_user.js b/setup/tools/filegen/templates/global.js/0_user.js new file mode 100644 index 00000000..7202a369 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/0_user.js @@ -0,0 +1,73 @@ +// Needed for IE because it's dumb + +'abbr article aside audio canvas details figcaption figure footer header hgroup mark menu meter nav output progress section summary time video'.replace(/\w+/g,function(n){document.createElement(n)}) + + +/* +User-related functions +TODO: Move global variables/functions into User class +*/ + +// IMPORTANT: If you update/change the permission groups below make sure to also update them in User.inc.php! + +/*********/ +/* ROLES */ +/*********/ + +var U_GROUP_TESTER = 0x1; +var U_GROUP_ADMIN = 0x2; +var U_GROUP_EDITOR = 0x4; +var U_GROUP_MOD = 0x8; +var U_GROUP_BUREAU = 0x10; +var U_GROUP_DEV = 0x20; +var U_GROUP_VIP = 0x40; +var U_GROUP_BLOGGER = 0x80; +var U_GROUP_PREMIUM = 0x100; +var U_GROUP_LOCALIZER = 0x200; +var U_GROUP_SALESAGENT = 0x400; +var U_GROUP_SCREENSHOT = 0x800; +var U_GROUP_VIDEO = 0x1000; +var U_GROUP_APIONLY = 0x2000; +var U_GROUP_PENDING = 0x4000; + + +/******************/ +/* ROLE SHORTCUTS */ +/******************/ + +var U_GROUP_STAFF = U_GROUP_ADMIN | U_GROUP_EDITOR | U_GROUP_MOD | U_GROUP_BUREAU | U_GROUP_DEV | U_GROUP_BLOGGER | U_GROUP_LOCALIZER | U_GROUP_SALESAGENT; +var U_GROUP_EMPLOYEE = U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_DEV; +var U_GROUP_GREEN_TEXT = U_GROUP_MOD | U_GROUP_BUREAU | U_GROUP_DEV; +var U_GROUP_PREMIUMISH = U_GROUP_PREMIUM | U_GROUP_EDITOR; +var U_GROUP_MODERATOR = U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU; +var U_GROUP_COMMENTS_MODERATOR = U_GROUP_BUREAU | U_GROUP_MODERATOR | U_GROUP_LOCALIZER; +var U_GROUP_PREMIUM_PERMISSIONS = U_GROUP_PREMIUM | U_GROUP_STAFF | U_GROUP_VIP; + +var g_users = {}; +var g_favorites = []; +var g_customColors = {}; + +function g_isUsernameValid(username) { + return (username.match(/[^a-z0-9]/i) == null && username.length >= 4 && username.length <= 16); +} + +var User = new function() { + var self = this; + + /**********/ + /* PUBLIC */ + /**********/ + + self.hasPermissions = function(roles) + { + if(!roles) + return true; + + return !!(g_user.roles & roles); + } + + /**********/ + /* PRIVATE */ + /**********/ + +}; diff --git a/setup/tools/filegen/templates/global.js/ajax.js b/setup/tools/filegen/templates/global.js/ajax.js new file mode 100644 index 00000000..22761baa --- /dev/null +++ b/setup/tools/filegen/templates/global.js/ajax.js @@ -0,0 +1,51 @@ +function Ajax(url, opt) +{ + if (!url) + return; + + var _; + + try { _ = new XMLHttpRequest() } catch (e) + { + try { _ = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) + { + try { _ = new ActiveXObject("Microsoft.XMLHTTP") } catch (e) + { + if (window.createRequest) + _ = window.createRequest(); + else + { + alert(LANG.message_ajaxnotsupported); + return; + } + } + } + } + + this.request = _; + + $WH.cO(this, opt); + this.method = this.method || (this.params && 'POST') || 'GET'; + + _.open(this.method, url, this.async == null ? true : this.async); + _.onreadystatechange = Ajax.onReadyStateChange.bind(this); + + if (this.method.toUpperCase() == 'POST') + _.setRequestHeader('Content-Type', (this.contentType || 'application/x-www-form-urlencoded') + '; charset=' + (this.encoding || 'UTF-8')); + + _.send(this.params); +} + +Ajax.onReadyStateChange = function() +{ + if (this.request.readyState == 4) + { + if (this.request.status == 0 || (this.request.status >= 200 && this.request.status < 300)) + this.onSuccess != null && this.onSuccess(this.request, this); + else + this.onFailure != null && this.onFailure(this.request, this); + + if (this.onComplete != null) + this.onComplete(this.request, this); + } +}; diff --git a/setup/tools/filegen/templates/global.js/animations.js b/setup/tools/filegen/templates/global.js/animations.js new file mode 100644 index 00000000..a41697ef --- /dev/null +++ b/setup/tools/filegen/templates/global.js/animations.js @@ -0,0 +1,123 @@ +/* + * jQuery Color Animations + * Copyright 2007 John Resig + * Released under the MIT and GPL licenses. + */ + +(function(jQuery){ + + // We override the animation for all of these color styles + jQuery.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor'], function(i,attr){ + jQuery.fx.step[attr] = function(fx){ + if ( fx.state == 0 ) { + fx.start = getColor( fx.elem, attr ); + fx.end = getRGB( fx.end ); + } + + fx.elem.style[attr] = "rgb(" + [ + Math.max(Math.min( parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0]), 255), 0), + Math.max(Math.min( parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1]), 255), 0), + Math.max(Math.min( parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2]), 255), 0) + ].join(",") + ")"; + } + }); + + // Color Conversion functions from highlightFade + // By Blair Mitchelmore + // http://jquery.offput.ca/highlightFade/ + + // Parse strings looking for color tuples [255,255,255] + function getRGB(color) { + var result; + + // Check if we're already dealing with an array of colors + if ( color && color.constructor == Array && color.length == 3 ) + return color; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) + return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])]; + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) + return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55]; + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) + return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)]; + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) + return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)]; + + // Otherwise, we're most likely dealing with a named color + return colors[jQuery.trim(color).toLowerCase()]; + } + + function getColor(elem, attr) { + var color; + + do { + color = jQuery.curCSS(elem, attr); + + // Keep going until we find an element that has color, or we hit the body + if ( color != '' && color != 'transparent' || jQuery.nodeName(elem, "body") ) + break; + + attr = "backgroundColor"; + } while ( elem = elem.parentNode ); + + return getRGB(color); + }; + + // Some named colors to work with + // From Interface by Stefan Petre + // http://interface.eyecon.ro/ + + var colors = { + aqua:[0,255,255], + azure:[240,255,255], + beige:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + cyan:[0,255,255], + darkblue:[0,0,139], + darkcyan:[0,139,139], + darkgrey:[169,169,169], + darkgreen:[0,100,0], + darkkhaki:[189,183,107], + darkmagenta:[139,0,139], + darkolivegreen:[85,107,47], + darkorange:[255,140,0], + darkorchid:[153,50,204], + darkred:[139,0,0], + darksalmon:[233,150,122], + darkviolet:[148,0,211], + fuchsia:[255,0,255], + gold:[255,215,0], + green:[0,128,0], + indigo:[75,0,130], + khaki:[240,230,140], + lightblue:[173,216,230], + lightcyan:[224,255,255], + lightgreen:[144,238,144], + lightgrey:[211,211,211], + lightpink:[255,182,193], + lightyellow:[255,255,224], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,165,0], + pink:[255,192,203], + purple:[128,0,128], + violet:[128,0,128], + red:[255,0,0], + silver:[192,192,192], + white:[255,255,255], + yellow:[255,255,0] + }; + +})(jQuery); diff --git a/setup/tools/filegen/templates/global.js/announcement.js b/setup/tools/filegen/templates/global.js/announcement.js new file mode 100644 index 00000000..0149b984 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/announcement.js @@ -0,0 +1,157 @@ +var Announcement = function(opt) +{ + if (!opt) + opt = {}; + + $WH.cO(this, opt); + + if (this.parent) + this.parentDiv = $WH.ge(this.parent); + else + return; + + if (g_user.id > 0 && (!g_cookiesEnabled() || g_getWowheadCookie('announcement-' + this.id) == 'closed')) + return; + + this.initialize(); +}; + +Announcement.prototype = { + initialize: function() + { + // aowow - animation fix + // this.parentDiv.style.display = 'none'; + this.parentDiv.style.opacity = '0'; + + if (this.mode === undefined || this.mode == 1) + this.parentDiv.className = 'announcement announcement-contenttop'; + else + this.parentDiv.className = 'announcement announcement-pagetop'; + + var div = this.innerDiv = $WH.ce('div'); + div.className = 'announcement-inner text'; + this.setStyle(this.style); + + var a = null; + var id = parseInt(this.id); + + if (g_user && (g_user.roles & (U_GROUP_ADMIN|U_GROUP_BUREAU)) > 0 && Math.abs(id) > 0) + { + if (id < 0) + { + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&status=2'; + a.onclick = function() { return confirm('Are you sure you want to delete ' + this.name + '?'); }; + $WH.ae(a, $WH.ct('Delete')); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.style.marginRight = '10px'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&status=' + (this.status == 1 ? 0 : 1); + a.onclick = function() { return confirm('Are you sure you want to delete ' + this.name + '?'); }; + $WH.ae(a, $WH.ct((this.status == 1 ? 'Disable' : 'Enable'))); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + } + + a = $WH.ce('a'); + a.style.cssFloat = a.style.styleFloat = 'right'; + a.style.marginRight = '22px'; + a.href = '?admin=announcements&id=' + Math.abs(id) + '&edit'; + $WH.ae(a, $WH.ct('Edit announcement')); + var small = $WH.ce('small'); + $WH.ae(small, a); + $WH.ae(div, small); + } + + var markupDiv = $WH.ce('div'); + markupDiv.id = this.parent + '-markup'; + $WH.ae(div, markupDiv); + + if (id >= 0) + { + a = $WH.ce('a'); + + a.id = 'closeannouncement'; + a.href = 'javascript:;'; + a.className = 'announcement-close'; + if (this.nocookie) + a.onclick = this.hide.bind(this); + else + a.onclick = this.markRead.bind(this); + + $WH.ae(div, a); + g_addTooltip(a, LANG.close); + } + + $WH.ae(div, $WH.ce('div', { style: { clear: 'both' } })); + + $WH.ae(this.parentDiv, div); + + this.setText(this.text); + + setTimeout(this.show.bind(this), 500); // Delay to avoid visual lag + }, + + show: function() + { + // $(this.parentDiv).animate({ + // opacity: 'show', + // height: 'show' + // },{ + // duration: 333 + // }); + + // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed + this.parentDiv.style.opacity = '100'; + this.parentDiv.style.height = (this.parentDiv.offsetHeight + 10) + 'px'; + g_trackEvent('Announcements', 'Show', '' + this.name); + }, + + hide: function() + { + // $(this.parentDiv).animate({ + // opacity: 'hide', + // height: 'hide' + // },{ + // duration: 200 + // }); + + // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed + this.parentDiv.style.opacity = '0'; + this.parentDiv.style.height = '0px'; + setTimeout(function() { + this.parentDiv.style.display = 'none'; + }.bind(this), 400); + }, + + markRead: function() + { + g_trackEvent('Announcements', 'Close', '' + this.name); + g_setWowheadCookie('announcement-' + this.id, 'closed'); + this.hide(); + }, + + setStyle: function(style) + { + this.style = style; + this.innerDiv.setAttribute('style', style); + }, + + setText: function(text) + { + this.text = text; + Markup.printHtml(this.text, this.parent + '-markup'); + g_addAnalyticsToNode($WH.ge(this.parent + '-markup'), { + 'category': 'Announcements', + 'actions': { + 'Follow link': function(node) { return true; } + } + }, this.id); + } +}; diff --git a/setup/tools/filegen/templates/global.js/audio.js b/setup/tools/filegen/templates/global.js/audio.js new file mode 100644 index 00000000..106a93c4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/audio.js @@ -0,0 +1,377 @@ +var g_audiocontrols = { + __windowloaded: false, +}; +var g_audioplaylist = {}; + +// aowow - why is window.JSON here, wedged between the audio controls. It's only used for SearchBrowseButtons (and sourced by Listview) +if (!window.JSON) { + window.JSON = { + parse: function (sJSON) { + return eval("(" + sJSON + ")"); + }, + + stringify: function (obj) { + if (obj instanceof Object) + { + var str = ''; + if (obj.constructor === Array) + { + for (var i = 0; i < obj.length; str += this.stringify(obj[i]) + ',', i++) {} + return '[' + str.substr(0, str.length - 1) + ']'; + } + if (obj.toString !== Object.prototype.toString) + return '"' + obj.toString().replace(/"/g, '\\$&') + '"'; + + for (var e in obj) + str += '"' + e.replace(/"/g, '\\$&') + '":' + this.stringify(obj[e]) + ','; + + return '{' + str.substr(0, str.length - 1) + '}'; + } + + return typeof obj === 'string' ? '"' + obj.replace(/"/g, '\\$&') + '"' : String(obj); + } + } +} + +AudioControls = function () +{ + var fileIdx = -1; + var canPlay = false; + var looping = false; + var fullPlayer = false; + var autoStart = false; + var controls = {}; + var playlist = []; + var url = ''; + + function updatePlayer(_self, itr, doPlay) + { + var elAudio = $WH.ce('audio'); + elAudio.preload = 'none'; + elAudio.controls = 'true'; + $(elAudio).click(function (s) { s.stopPropagation() }); + elAudio.style.marginTop = '5px'; + + controls.audio.parentNode.replaceChild(elAudio, controls.audio); + controls.audio = elAudio; + $WH.aE(controls.audio, 'ended', setNextTrack.bind(_self)); + + if (doPlay) + { + elAudio.preload = 'auto'; + autoStart = true; + $WH.aE(controls.audio, 'canplaythrough', autoplay.bind(this)); + } + + if (!canPlay) + controls.table.style.visibility = 'visible'; + + var file; + do + { + fileIdx += itr; + if (fileIdx > playlist.length - 1) + { + fileIdx = 0; + if (!canPlay) + { + var div = $WH.ce('div'); + // div.className = 'minibox'; Aowow custom + div.className = 'minibox minibox-left'; + $WH.st(div, $WH.sprintf(LANG.message_browsernoaudio, file.type)); + controls.table.parentNode.replaceChild(div, controls.table); + return + } + } + + if (fileIdx < 0) + fileIdx = playlist.length - 1; + + file = playlist[fileIdx]; + } + while (controls.audio.canPlayType(file.type) == ''); + + var elSource = $WH.ce('source'); + elSource.src = file.url; + elSource.type = file.type; + $WH.ae(controls.audio, elSource); + + if (controls.hasOwnProperty('title')) + { + if (url) + { + $WH.ee(controls.title); + var a = $WH.ce('a'); + a.href = url; + $WH.st(a, '"' + file.title + '"'); + $WH.ae(controls.title, a); + } + else + $WH.st(controls.title, '"' + file.title + '"'); + } + + if (controls.hasOwnProperty('trackdisplay')) + $WH.st(controls.trackdisplay, '' + (fileIdx + 1) + ' / ' + playlist.length); + + if (!canPlay) + { + canPlay = true; + for (var i = fileIdx + 1; i <= playlist.length - 1; i++) + { + if (controls.audio.canPlayType(playlist[i].type)) + { + $(controls.controlsdiv).children('a').removeClass('button-red-disabled'); + break; + } + } + } + + if (controls.hasOwnProperty('addbutton')) + { + $(controls.addbutton).removeClass('button-red-disabled'); + // $WH.st(controls.addbutton, LANG.add); Aowow: doesnt work with RedButtons + RedButton.setText(controls.addbutton, LANG.add); + } + } + + function autoplay() + { + if (!autoStart) + return; + + autoStart = false; + controls.audio.play(); + } + + this.init = function (files, parent, opt) + { + if (!$WH.is_array(files)) + return; + + if (files.length == 0) + return; + + if ((parent.id == '') || g_audiocontrols.hasOwnProperty(parent.id)) + { + var i = 0; + while (g_audiocontrols.hasOwnProperty('auto-audiocontrols-' + (++i))) {} + parent.id = 'auto-audiocontrols-' + i; + } + + g_audiocontrols[parent.id] = this; + + if (typeof opt == 'undefined') + opt = {}; + + looping = !!opt.loop; + if (opt.hasOwnProperty('url')) + url = opt.url; + + playlist = files; + controls.div = parent; + + if (!opt.listview) + { + var tbl = $WH.ce('table', { className: 'audio-controls' }); + controls.table = tbl; + controls.table.style.visibility = 'hidden'; + $WH.ae(controls.div, tbl); + + var tr = $WH.ce('tr'); + $WH.ae(tbl, tr); + + var td = $WH.ce('td'); + $WH.ae(tr, td); + + controls.audio = $WH.ce('div'); + $WH.ae(td, controls.audio); + + controls.title = $WH.ce('div', { className: 'audio-controls-title' }); + $WH.ae(td, controls.title); + + controls.controlsdiv = $WH.ce('div', { className: 'audio-controls-pagination' }); + $WH.ae(td, controls.controlsdiv); + + var prevBtn = createButton(LANG.previous, true); + $WH.ae(controls.controlsdiv, prevBtn); + $WH.aE(prevBtn, 'click', this.btnPrevTrack.bind(this)); + + controls.trackdisplay = $WH.ce('div', { className: 'audio-controls-pagination-track' }); + $WH.ae(controls.controlsdiv, controls.trackdisplay); + + var nextBtn = createButton(LANG.next, true); + $WH.ae(controls.controlsdiv, nextBtn); + $WH.aE(nextBtn, 'click', this.btnNextTrack.bind(this)) + } + else + { + fullPlayer = true; + var div = $WH.ce('div'); + controls.table = div; + $WH.ae(controls.div, div); + + controls.audio = $WH.ce('div'); + $WH.ae(div, controls.audio); + + controls.trackdisplay = opt.trackdisplay; + controls.controlsdiv = $WH.ce('span'); + $WH.ae(div, controls.controlsdiv); + } + + if (g_audioplaylist.isEnabled() && !opt.fromplaylist) + { + var addBtn = createButton(LANG.add); + $WH.ae(controls.controlsdiv, addBtn); + $WH.aE(addBtn, 'click', this.btnAddToPlaylist.bind(this, addBtn)); + controls.addbutton = addBtn; + + if (fullPlayer) + addBtn.style.verticalAlign = '50%'; + } + + if (g_audiocontrols.__windowloaded) + this.btnNextTrack(); + }; + + function setNextTrack() + { + updatePlayer(this, 1, (looping || (fileIdx < (playlist.length - 1)))); + } + + this.btnNextTrack = function () + { + updatePlayer(this, 1, (canPlay && (controls.audio.readyState > 1) && (!controls.audio.paused))); + }; + + this.btnPrevTrack = function () + { + updatePlayer(this, -1, (canPlay && (controls.audio.readyState > 1) && (!controls.audio.paused))); + }; + + this.btnAddToPlaylist = function (_self) + { + if (fullPlayer) + { + for (var i = 0; i < playlist.length; i++) + g_audioplaylist.addSound(playlist[i]); + } + else + g_audioplaylist.addSound(playlist[fileIdx]); + + _self.className += ' button-red-disabled'; + // $WH.st(_self, LANG.added); // Aowow doesn't work with RedButtons + RedButton.setText(_self, LANG.added); + }; + + this.isPlaying = function () + { + return !controls.audio.paused; + }; + + this.removeSelf = function () + { + controls.table.parentNode.removeChild(controls.table); + delete g_audiocontrols[controls.div]; + }; + + function createButton(text, disabled) + { + return $WH.g_createButton(text, null, { + disabled: disabled, + // 'float': false, Aowow - adapted style + // style: 'margin:0 12px; display:inline-block' + style: 'margin:0 12px; display:inline-block; float:inherit; ' + }); + } +}; + +$WH.aE(window, 'load', function () +{ + g_audiocontrols.__windowloaded = true; + for (var i in g_audiocontrols) + if (i.substr(0, 2) != '__') + g_audiocontrols[i].btnNextTrack(); +}); + +AudioPlaylist = function () +{ + var enabled = false; + var playlist = []; + var player, container; + + this.init = function () + { + if (!$WH.localStorage.isSupported()) + return; + + enabled = true; + + var tracks; + if (tracks = $WH.localStorage.get('AudioPlaylist')) + playlist = JSON.parse(tracks); + }; + + this.savePlaylist = function () + { + if (!enabled) + return false; + + $WH.localStorage.set('AudioPlaylist', JSON.stringify(playlist)); + }; + + this.isEnabled = function () + { + return enabled; + }; + + this.addSound = function (track) + { + if (!enabled) + return false; + + this.init(); + playlist.push(track); + this.savePlaylist(); + }; + + this.deleteSound = function (idx) + { + if (idx < 0) + playlist = []; + else + playlist.splice(idx, 1); + + this.savePlaylist(); + + if (!player.isPlaying()) + { + player.removeSelf(); + this.setAudioControls(container); + } + + if (playlist.length == 0) + $WH.Tooltip.hide(); + }; + + this.getList = function () + { + var buf = []; + for (var i = 0; i < playlist.length; i++) + buf.push(playlist[i].title); + + return buf; + }; + + this.setAudioControls = function (parent) + { + if (!enabled) + return false; + + container = parent; + player = new AudioControls(); + player.init(playlist, container, { loop: true, fromplaylist: true }); + }; +}; + +g_audioplaylist = (new AudioPlaylist); +g_audioplaylist.init(); diff --git a/setup/tools/filegen/templates/global.js/clicktocopy.js b/setup/tools/filegen/templates/global.js/clicktocopy.js new file mode 100644 index 00000000..9225b6f4 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/clicktocopy.js @@ -0,0 +1,122 @@ +$WH.clickToCopy = function (el, textOrFn, opt) +{ + opt = opt || {}; + + $WH.aE(el, 'click', $WH.clickToCopy.copy.bind(null, el, textOrFn, opt)); + // $WH.preventSelectStart(el); + + el.classList.add('click-to-copy'); + + if (opt.modifyTooltip) + { + el._fixTooltip = function (e) { + return e + '
        ' + $WH.ce('span', { className: 'q2', innerHTML: $WH.clickToCopy.getTooltip(false, opt) }).outerHTML; + }; + + opt.overrideOtherTooltips = false; + } + + // aowow - fitted to old system + // $WH.Tooltips.attach( + $WH.Tooltip.simple( + el, + $WH.clickToCopy.getTooltip.bind(null, false, opt), + undefined, + // { + /* byCursor: */ !opt.attachToElement, + // stopPropagation: opt.overrideOtherTooltips + // } + ); +}; + +$WH.clickToCopy.copy = function (el, textOrFn, opt, ev) +{ + ev.preventDefault(); + ev.stopPropagation(); + + if (textOrFn === undefined) + { + if (!el.childNodes[0] || !el.childNodes[0].textContent) + { + let text = 'Could not find text to copy.'; + // $WH.error(text, el); + + if (opt.attachToElement) + $WH.Tooltip.show(el, text, 'q10'); + else + $WH.Tooltip.showAtCursor(ev, text, 'q10'); + + return; + } + + textOrFn = el.childNodes[0].textContent; + } + else if (typeof textOrFn === 'function') + textOrFn = textOrFn(); + + $WH.copyToClipboard(textOrFn); + + if (opt.attachToElement) + $WH.Tooltip.show(el, $WH.clickToCopy.getTooltip(true, opt)); + else + $WH.Tooltip.showAtCursor(ev, $WH.clickToCopy.getTooltip(true, opt)); +}; + +$WH.clickToCopy.getTooltip = function (clicked, opt) +{ + let txt = ''; + let attr = undefined; + + if (clicked) + { + txt = ' ' + LANG.copied; + attr = { className: 'q1 icon-tick' }; + } + else + txt = LANG.clickToCopy; + + let tt = $WH.ce('div', attr, $WH.ct(txt)); + + if (opt.prefix) + { + tt.style.marginTop = '10px'; + let prefix = typeof opt.prefix === 'function' ? opt.prefix() : opt.prefix; + return prefix + tt.outerHTML; + } + + return tt.outerHTML; +}; + +$WH.copyToClipboard = function (text, t) +{ + if (!$WH.copyToClipboard.hiddenInput) + { + $WH.copyToClipboard.hiddenInput = $WH.ce('textarea', { className: 'hidden-element' }); + $WH.ae(document.body, $WH.copyToClipboard.hiddenInput); + } + + $WH.copyToClipboard.hiddenInput.value = text; + + let isEmpty = $WH.copyToClipboard.hiddenInput.value === ''; + if (isEmpty) + $WH.copyToClipboard.hiddenInput.value = LANG.nothingToCopy_tip; + + $WH.copyToClipboard.hiddenInput.focus(); + $WH.copyToClipboard.hiddenInput.select(); + + if (!document.execCommand('copy')) + prompt(null, text); + + $WH.copyToClipboard.hiddenInput.blur(); + + if (t) + { + if (isEmpty) + $WH.Tooltips.showFadingTooltipAtCursor(LANG.nothingToCopy_tip, t, 'q10'); + else + { + let e = $WH.ce('span', { className: 'q1 icon-tick' }, $WH.ct(' ' + LANG.copied)); + $WH.Tooltips.showFadingTooltipAtCursor(e.outerHTML, t); + } + } +}; diff --git a/setup/tools/filegen/templates/global.js/comments.js b/setup/tools/filegen/templates/global.js/comments.js new file mode 100644 index 00000000..352a6be2 --- /dev/null +++ b/setup/tools/filegen/templates/global.js/comments.js @@ -0,0 +1,459 @@ +/* Note: comment replies are called "comments" because part of this code was taken from another project of mine. */ + +function SetupReplies(post, comment) +{ + SetupAddEditComment(post, comment, false); + SetupShowMoreComments(post, comment); + + post.find('.comment-reply-row').each(function () { SetupRepliesControls($(this), comment); }); + post.find('.comment-reply-row').hover(function () { $(this).find('span').attr('data-hover', 'true'); }, function () { $(this).find('span').attr('data-hover', 'false'); }); +} + +function SetupAddEditComment(post, comment, edit) +{ + /* Variables that will be set by Initialize() */ + var Form = null; + var Body = null; + var AddButton = null; + var TextCounter = null; + var AjaxLoader = null; + var FormContainer = null; + var DialogTableRowContainer = null; + + /* Constants */ + var MIN_LENGTH = 15; + var MAX_LENGTH = 600; + + /* State keeping booleans */ + var Initialized = false; + var Active = false; + var Flashing = false; + var Submitting = false; + + /* Shortcuts */ + var CommentsTable = post.find('.comment-replies > table'); + var AddCommentLink = post.find('.add-reply'); + var CommentsCount = comment.replies.length; + + if(edit) + Open(); + else + AddCommentLink.click(function () { Open(); }); + + function Initialize() + { + if (Initialized) + return; + + Initialized = true; + + var row = $('
        ' + + '' + + '
        ' + + '' + + '' + + '' + + '
        ' + + '' + + '' + + '' + + '' + + '
        ' + + 'Text counter placeholder' + + '
        ' + + '
        ' + + '
        \n"; +echo '
        '.PHP_EOL; if ($this->infobox): ?> + + attributes): ?> + + +
        @@ -18,11 +21,13 @@ echo " \n"; infobox; ?> + contributions): ?> + \n"; + echo ' '.PHP_EOL; elseif (is_object($objective)): // has icon set (spell / item / ...) or unordered linked list echo $objective?->renderContainer(20, $iconOffset, true); endif; endforeach; if ($this->end): - echo " \n"; + echo ' '.PHP_EOL; endif; if ($this->suggestedPl): - echo ' \n"; + echo ' '.PHP_EOL; endif; ?> +
        @@ -31,6 +36,7 @@ echo " \n"; contributions; ?> + \n"; if ($this->contribute & CONTRIBUTE_SS): ?> + + contribute & CONTRIBUTE_VI && ($this->user::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO) || !empty($this->community['vi']))): ?> + + contribute & CONTRIBUTE_SS): ?> + + contribute & CONTRIBUTE_VI && ($this->user::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO) || !empty($this->community['vi']))): ?> + + \n"; + echo '
        '.PHP_EOL; endif; ?> diff --git a/template/bricks/inputbox-form-email.tpl.php b/template/bricks/inputbox-form-email.tpl.php index dfc7be5d..c12bff9b 100644 --- a/template/bricks/inputbox-form-email.tpl.php +++ b/template/bricks/inputbox-form-email.tpl.php @@ -3,6 +3,7 @@ use \Aowow\Lang; ?> +
        diff --git a/template/bricks/inputbox-form-signup.tpl.php b/template/bricks/inputbox-form-signup.tpl.php index 20724f66..876ff2e5 100644 --- a/template/bricks/inputbox-form-signup.tpl.php +++ b/template/bricks/inputbox-form-signup.tpl.php @@ -3,6 +3,7 @@ use \Aowow\Lang; ?> +
        + diff --git a/template/bricks/mail.tpl.php b/template/bricks/mail.tpl.php index 27bdbe48..384955f2 100644 --- a/template/bricks/mail.tpl.php +++ b/template/bricks/mail.tpl.php @@ -3,36 +3,44 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + if (['header' => $header, 'subject' => $subject, 'text' => $text, 'attachments' => $attachments] = $this->mail): $offset ??= 0; // in case we have multiple icons on the page (prominently quest-rewards) - echo '

        '.Lang::mail('mailDelivery', $header)."

        \n"; + echo '

        '.Lang::mail('mailDelivery', $header).'

        '.PHP_EOL; if ($subject): - echo '
        '.$subject."
        \n"; + echo '
        '.$subject.'
        '.PHP_EOL; endif; if ($text): - echo '
        '.$text."
        \n"; + echo '
        '.$text.'
        '.PHP_EOL; endif; if ($attachments): ?> + + renderContainer(20, $offset, true); endforeach; ?> +
        + map): if ($foundIn): echo '
        '.$foundIn[0].' '; echo Lang::concat($mapperData, true, function ($areaData, $areaId) use ($foundIn) { return ''.$foundIn[$areaId].' ('.array_sum(array_column($areaData, 'count')).')'; }); - echo ".
        \n"; + echo '.'.PHP_EOL; else: - echo "
        \n"; + echo '
        '.PHP_EOL; endif; if (isset($mapper['zone']) && $mapper['zone'] < 0): ?> +
        + +
        + +
        + +
        + +
        + +
        + + + diff --git a/template/bricks/markup.tpl.php b/template/bricks/markup.tpl.php index 69801b13..95d7038c 100644 --- a/template/bricks/markup.tpl.php +++ b/template/bricks/markup.tpl.php @@ -5,4 +5,5 @@ //]]>
        + diff --git a/template/bricks/pageTemplate.tpl.php b/template/bricks/pageTemplate.tpl.php index 7934d751..17c37a7a 100644 --- a/template/bricks/pageTemplate.tpl.php +++ b/template/bricks/pageTemplate.tpl.php @@ -1,40 +1,44 @@ diff --git a/template/bricks/reagentList.tpl.php b/template/bricks/reagentList.tpl.php index 0bdd4a13..273f77f0 100644 --- a/template/bricks/reagentList.tpl.php +++ b/template/bricks/reagentList.tpl.php @@ -9,6 +9,7 @@ + + +

        concat('title'); ?>

        @@ -47,39 +55,49 @@ if ($this->altHomeLogo): featuredBox): ?>
        + featuredBox): ?> +
        + featuredBox['overlays']): ?> + +
        +
        diff --git a/template/pages/icon.tpl.php b/template/pages/icon.tpl.php index 5ec5f25a..59a3a906 100644 --- a/template/pages/icon.tpl.php +++ b/template/pages/icon.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -18,6 +21,7 @@ ?>
        + brick('redButtons'); ?> @@ -27,9 +31,11 @@ + brick('markup', ['markup' => $this->article]); ?> +

        diff --git a/template/pages/icons.tpl.php b/template/pages/icons.tpl.php index 2ab62c48..35f59368 100644 --- a/template/pages/icons.tpl.php +++ b/template/pages/icons.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [31]]); ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php index 59d130f7..6084bf49 100644 --- a/template/pages/image-crop.tpl.php +++ b/template/pages/image-crop.tpl.php @@ -3,17 +3,21 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); ?> +

        h1; ?>

        diff --git a/template/pages/item.tpl.php b/template/pages/item.tpl.php index 23b1c768..070fb0b1 100644 --- a/template/pages/item.tpl.php +++ b/template/pages/item.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -18,15 +21,19 @@ ?>
        + brick('redButtons'); ?>

        h1; ?>

        + unavailable): ?> +
        + brick('markup', ['markup' => $this->article]); if ($this->map): - echo "

        ".$this->map[4]."

        \n"; + echo '

        '.$this->map[4].'

        '.PHP_EOL; $this->brick('mapper'); endif; if ($this->transfer): - echo "
        \n ".$this->transfer."\n"; + echo '
        '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; if ($this->subItems): ?> +

        + subItems['data'], ceil(count($this->subItems['data']) / 2)) as $columns): ?> +
          + ['name' => $name, 'enchantment' => $enchantment, 'chance' => $chance]): echo '
        • ...'.$name.' '.Lang::item('_chance', [$chance]).'
          '; - echo Lang::concat($enchantment, Lang::CONCAT_NONE, fn($txt, $eId) => ''.$txt.'')."
        • \n"; + echo Lang::concat($enchantment, Lang::CONCAT_NONE, fn($txt, $eId) => ''.$txt.'').'
        '.PHP_EOL; endforeach; ?> +
        + brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [0]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [0]]); ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        @@ -37,6 +43,7 @@ $this->brick('redButtons'); slotList): ?> +
        @@ -45,11 +52,13 @@ if ($this->slotList): makeOptionsList($this->slotList, $f['sl'], 28); ?>
        + typeList): ?> +
        @@ -62,6 +71,7 @@ if ($this->typeList): }); ?>
        +
        @@ -141,7 +151,7 @@ if ($this->typeList):
        - + makeRadiosList('gb', Lang::main('gb'), $f['gb'] ?? '', 24, fn($v, &$k) => ($k = $k ?: '') || 1); ?>
        diff --git a/template/pages/itemset.tpl.php b/template/pages/itemset.tpl.php index 5bc4b51c..42a3b36f 100644 --- a/template/pages/itemset.tpl.php +++ b/template/pages/itemset.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -18,60 +21,73 @@ ?>
        + brick('redButtons'); + $this->brick('redButtons'); if ($this->expansion): - echo '

        '.$this->h1."

        \n"; + echo '

        '.$this->h1.'

        '.PHP_EOL; else: - echo '

        '.$this->h1."

        \n"; + echo '

        '.$this->h1.'

        '.PHP_EOL; endif; if ($this->unavailable): ?> +
        + brick('markup', ['markup' => $this->article]); echo $this->description; ?> +
        + pieces as [, $icon]): echo $icon->renderContainer(20, $iconIdx, true); endforeach; ?> +

        bonusExt; ?>

        - +
          + spells as [$nItems, $spellId, $text]): - echo '
        • '.Lang::itemset('_pieces', [$nItems]).''.$text."
        • \n"; + echo '
        • '.Lang::itemset('_pieces', [$nItems]).''.$text.'
        • '.PHP_EOL; endforeach; ?> +
        + summary): ?> @@ -82,6 +98,7 @@ if ($this->summary): + diff --git a/template/pages/itemsets.tpl.php b/template/pages/itemsets.tpl.php index 23297b55..3f2d09c9 100644 --- a/template/pages/itemsets.tpl.php +++ b/template/pages/itemsets.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [2]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [2]]); ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        diff --git a/template/pages/list-page-generic.tpl.php b/template/pages/list-page-generic.tpl.php index 72ca1ac3..c4e1d1cb 100644 --- a/template/pages/list-page-generic.tpl.php +++ b/template/pages/list-page-generic.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -14,7 +17,9 @@ $this->brick('pageTemplate'); ?> +
        + brick('redButtons'); @@ -38,15 +43,20 @@ echo '

        '.$this->tabsTitle.'

        '; endif; ?> +
        + lvTabs): $this->brick('lvTabs'); ?> +
        + +
        diff --git a/template/pages/maintenance.tpl.php b/template/pages/maintenance.tpl.php index b3f9ecfa..b65cbd3d 100644 --- a/template/pages/maintenance.tpl.php +++ b/template/pages/maintenance.tpl.php @@ -1,4 +1,9 @@ - + + diff --git a/template/pages/maps.tpl.php b/template/pages/maps.tpl.php index 8e54af9d..daf37bbe 100644 --- a/template/pages/maps.tpl.php +++ b/template/pages/maps.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        diff --git a/template/pages/npc.tpl.php b/template/pages/npc.tpl.php index ba68c727..6f2de964 100644 --- a/template/pages/npc.tpl.php +++ b/template/pages/npc.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -18,6 +21,7 @@ ?>
        + brick('redButtons'); ?>

        h1.($this->subname ? ' <'.$this->subname.'>' : ''); ?>

        @@ -28,47 +32,55 @@ if ($this->accessory): echo '
        '.Lang::npc('accessoryFor').' '; echo Lang::concat($this->accessory, true, fn ($v) => ''.$v[1].''); - echo ".
        \n"; + echo '.
        '.PHP_EOL; endif; if ($this->placeholder): ?> +
        placeholder);?>
        + map): $this->brick('mapper'); else: - echo ' '.Lang::npc('unkPosition')."\n"; + echo ' '.Lang::npc('unkPosition').''.PHP_EOL; endif; if ([$quoteGroups, $count] = $this->quotes): ?> +

        + reputation): ?> +

        + brick('markup', ['markup' => $this->smartAI]); ?> +

        diff --git a/template/pages/npcs.tpl.php b/template/pages/npcs.tpl.php index 90100955..46a6189a 100644 --- a/template/pages/npcs.tpl.php +++ b/template/pages/npcs.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [4]]); ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        @@ -33,6 +39,7 @@ $this->brick('redButtons'); makeOptionsList(Lang::npc('rank'), $f['cl'], 28); ?>
        + petFamPanel): ?>
        @@ -41,7 +48,9 @@ $this->brick('redButtons'); makeOptionsList(Lang::game('fa'), $f['fa'], 28); ?>
        + + diff --git a/template/pages/object.tpl.php b/template/pages/object.tpl.php index b3e076e5..ad44552b 100644 --- a/template/pages/object.tpl.php +++ b/template/pages/object.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -18,6 +21,7 @@ ?>
        + brick('redButtons'); ?>

        h1; ?>

        @@ -26,7 +30,7 @@ $this->brick('markup', ['markup' => $this->article]); if ($this->relBoss): - echo "
        ".sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1])."
        \n"; + echo '
        '.sprintf(Lang::gameObject('npcLootPH'), $this->h1, $this->relBoss[0], $this->relBoss[1]).'
        '.PHP_EOL; echo '
        '; endif; diff --git a/template/pages/objects.tpl.php b/template/pages/objects.tpl.php index d5055534..aeacf714 100644 --- a/template/pages/objects.tpl.php +++ b/template/pages/objects.tpl.php @@ -3,26 +3,32 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [5]]); ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        ucFirst(Lang::main('name')).Lang::main('colon'); ?>
        diff --git a/template/pages/privilege.tpl.php b/template/pages/privilege.tpl.php index 7aafd824..f86c7cdb 100644 --- a/template/pages/privilege.tpl.php +++ b/template/pages/privilege.tpl.php @@ -1,8 +1,11 @@ brick('header'); ?> +
        @@ -16,9 +19,11 @@

        h1;?>

        privReqPoints;?>


        + brick('markup', ['markup' => $this->article]); ?> +
        diff --git a/template/pages/privileges.tpl.php b/template/pages/privileges.tpl.php index 6e86449b..5f4005eb 100644 --- a/template/pages/privileges.tpl.php +++ b/template/pages/privileges.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -23,11 +26,13 @@
        + privileges as $id => [$earned, $name, $value]): - echo ' \n"; + echo ' '.PHP_EOL; endforeach; ?> +
         
        '.$name.'
        '.Lang::nf($value)."
         
        '.$name.'
        '.Lang::nf($value).'
        diff --git a/template/pages/profile.tpl.php b/template/pages/profile.tpl.php index 2edd318d..fed434d7 100644 --- a/template/pages/profile.tpl.php +++ b/template/pages/profile.tpl.php @@ -1,8 +1,11 @@ brick('header'); ?> +
        diff --git a/template/pages/profiler.tpl.php b/template/pages/profiler.tpl.php index f036e1f0..8880f490 100644 --- a/template/pages/profiler.tpl.php +++ b/template/pages/profiler.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        diff --git a/template/pages/profiles.tpl.php b/template/pages/profiles.tpl.php index db39deb1..7879c37c 100644 --- a/template/pages/profiles.tpl.php +++ b/template/pages/profiles.tpl.php @@ -3,28 +3,34 @@ use \Aowow\Lang; -$this->brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => array_slice($this->pageTemplate['breadcrumb'], 0, 3)]); -# pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before , it will become firstChild (a text node) + # pr_setRegionRealm($WH.ge('fi').firstChild, realm, region) - never have \n\s before , it will become firstChild (a text node) ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        @@ -93,14 +99,19 @@ $this->brick('redButtons'); roster): ?> +

        roster;?>

        + +
        + +
        renderFilter(12); ?> diff --git a/template/pages/quest.tpl.php b/template/pages/quest.tpl.php index b59bb47a..104827e9 100644 --- a/template/pages/quest.tpl.php +++ b/template/pages/quest.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -18,70 +21,80 @@ ?>
        + brick('redButtons'); ?>

        h1; ?>

        + unavailable): ?>
        + objectives): - echo $this->objectives."\n"; + echo $this->objectives.PHP_EOL; elseif ($this->requestItems): - echo '

        '.Lang::quest('progress')."

        \n"; - echo $this->requestItems."\n"; + echo '

        '.Lang::quest('progress').'

        '.PHP_EOL; + echo $this->requestItems.PHP_EOL; elseif ($this->offerReward): - echo '

        '.Lang::quest('completion')."

        \n"; - echo $this->offerReward."\n"; + echo '

        '.Lang::quest('completion').'

        '.PHP_EOL; + echo $this->offerReward.PHP_EOL; endif; $iconOffset = 0; if ($this->end || $this->objectiveList): ?> + + objectiveList as $objective): if (is_string($objective)): // just text line - echo ' \n"; + echo ' '.PHP_EOL; elseif (is_array($objective)): // proxy npc data ['id' => $id, 'text' => $text, 'qty' => $qty, 'proxy' => $proxies] = $objective; - echo '

         

        '.$objective."

         

        '.$objective.'

         

        '.$text.''.($qty ? ' ('.$qty.')' : '').'
        \n"; + echo '

         

        '.$text.''.($qty ? ' ('.$qty.')' : '').'
        '.PHP_EOL; endforeach; - echo "

         

        ".$this->end."

         

        '.$this->end.'

         

        '.Lang::quest('suggestedPl', [$this->suggestedPl])."

         

        '.Lang::quest('suggestedPl', [$this->suggestedPl]).'
        + providedItem): ?> +
        @@ -91,6 +104,7 @@ if ($this->end || $this->objectiveList): + brick('mapper'); if ($this->details): - echo '

        '.Lang::quest('description')."

        \n" . $this->details."\n"; + echo '

        '.Lang::quest('description').'

        '.PHP_EOL; + echo ' '.$this->details.PHP_EOL; endif; if ($this->requestItems && $this->objectives): ?> +

        + offerReward && ($this->requestItems || $this->objectives)): ?> +

        + rewards): - echo '

        '.Lang::main('rewards')."

        \n"; + echo '

        '.Lang::main('rewards').'

        '.PHP_EOL; if ($choice): $this->brick('rewards', ['rewTitle' => Lang::quest('rewardChoices'), 'rewards' => $choice, 'offset' => $iconOffset]); @@ -125,7 +144,7 @@ if ([$spells, $items, $choice, $money] = $this->rewards): if ($spells): if ($choice): - echo "
        \n"; + echo '
        '.PHP_EOL; endif; $this->brick('rewards', ['rewTitle' => $spells['title'], 'rewards' => $spells['cast'], 'offset' => $iconOffset, 'extra' => $spells['extra']]); @@ -134,7 +153,7 @@ if ([$spells, $items, $choice, $money] = $this->rewards): if ($items || $money): if ($choice || $spells): - echo "
        \n"; + echo '
        '.PHP_EOL; endif; $this->brick('rewards', array( @@ -149,26 +168,28 @@ endif; if ([$xp, $rep, $title, $tp, $honor, $arena] = $this->gains): ?> +

          +
          '.Lang::nf($xp).' '.Lang::quest('experience')."
          \n"; + echo '
        • '.Lang::nf($xp).' '.Lang::quest('experience').'
        • '.PHP_EOL; endif; if ($rep): foreach ($rep as $r): - echo '
        • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name']."
        • \n"; + echo '
        • '.sprintf($r['qty'][0] < 0 ? '%s' : '%s', $r['qty'][1]).' '.Lang::npc('repWith').' '.$r['name'].'
        • '.PHP_EOL; endforeach; endif; if ($title): - echo '
        • '.Lang::quest('rewardTitle', $title)."
        • \n"; + echo '
        • '.Lang::quest('rewardTitle', $title).'
        • '.PHP_EOL; endif; if ($tp): - echo '
        • '.Lang::quest('bonusTalents', [$tp])."
        • \n"; + echo '
        • '.Lang::quest('bonusTalents', [$tp]).'
        • '.PHP_EOL; endif; if ($arena || $honor): @@ -181,20 +202,21 @@ if ([$xp, $rep, $title, $tp, $honor, $arena] = $this->gains): if ($arena): echo ' '.$arena.''; endif; - echo "\n"; + echo ''.PHP_EOL; endif; - echo "
        \n"; + echo ' '.PHP_EOL; endif; $this->brickIf($this->mail, 'mail', ['offset' => ++$iconOffset]); if ($this->transfer): - echo "
        "; - echo "
        \n ".$this->transfer."\n"; + echo '
        '.PHP_EOL; + echo '
        '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; - ?> +

        diff --git a/template/pages/quests.tpl.php b/template/pages/quests.tpl.php index 427e5aaa..1dbeb100 100644 --- a/template/pages/quests.tpl.php +++ b/template/pages/quests.tpl.php @@ -1,28 +1,34 @@ brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [3]]); ?> +
        -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

        h1; ?>

        diff --git a/template/pages/roster.tpl.php b/template/pages/roster.tpl.php index 88ed8975..7159bf1b 100644 --- a/template/pages/roster.tpl.php +++ b/template/pages/roster.tpl.php @@ -1,21 +1,25 @@ brick('header'); ?> +
        brick('announcement'); - -$this->brick('pageTemplate'); + $this->brick('announcement'); + $this->brick('pageTemplate'); ?> +
        + brick('redButtons'); ?>

        h1; ?>

        @@ -25,9 +29,11 @@ $this->brick('pageTemplate'); ?>
        + brick('lvTabs'); ?> +
        diff --git a/template/pages/screenshot.tpl.php b/template/pages/screenshot.tpl.php index 819607b7..60559162 100644 --- a/template/pages/screenshot.tpl.php +++ b/template/pages/screenshot.tpl.php @@ -3,20 +3,23 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); - -$this->brick('infobox'); + $this->brick('pageTemplate'); + $this->brick('infobox'); ?> +

        h1; ?>

        diff --git a/template/pages/search.tpl.php b/template/pages/search.tpl.php index a0845be4..a66adbed 100644 --- a/template/pages/search.tpl.php +++ b/template/pages/search.tpl.php @@ -3,29 +3,35 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); + $this->brick('pageTemplate'); ?>
        + brick('redButtons'); + $this->brick('redButtons'); if (count($this->lvTabs)): echo '

        '.Lang::main('foundResult').' '.$this->search.''; if ($this->invalidTerms): echo ''.Lang::main('ignoredTerms', [$this->invalidTerms]).''; endif; - echo "

        \n"; + echo ''.PHP_EOL; ?> +
        + brick('lvTabs'); @@ -34,14 +40,16 @@ else: if ($this->invalidTerms): echo ''.Lang::main('ignoredTerms', [$this->invalidTerms]).''; endif; - echo "\n"; + echo ''.PHP_EOL; ?> +
        +
        diff --git a/template/pages/sound-playlist.tpl.php b/template/pages/sound-playlist.tpl.php index 1d561627..59ea456a 100644 --- a/template/pages/sound-playlist.tpl.php +++ b/template/pages/sound-playlist.tpl.php @@ -1,10 +1,11 @@ brick('header'); ?> +
        diff --git a/template/pages/sound.tpl.php b/template/pages/sound.tpl.php index 6dbb1a32..619a7f28 100644 --- a/template/pages/sound.tpl.php +++ b/template/pages/sound.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
        @@ -16,6 +19,7 @@ ?>
        + brick('redButtons'); ?> @@ -27,6 +31,7 @@ $this->brickIf($this->map, 'mapper'); ?> +
          + reagents[0]): - echo "
          \n"; + echo '
          '.PHP_EOL; endif; endif; ?> +
          brick('markup', ['markup' => $this->article]); + $this->brick('markup', ['markup' => $this->article]); if ($this->transfer): - echo "
          \n ".$this->transfer."\n"; + echo '
          '.PHP_EOL; + echo ' '.$this->transfer.PHP_EOL; endif; ?> @@ -133,36 +144,43 @@ endif;
          + stances): ?> + + items): ?> + + effects as $i => $e): ?> + \n"; + echo ''.PHP_EOL; if ($idx == count($e['modifies'][$type]) - 1 || !(($idx + 1) % 3)) - echo ""; + echo ''; if ($idx == 17 && count($e['modifies'][$type]) > 21): $folded = true; ?> + + +
          '.Lang::spell('_gcd');?> gcd;?>
          stances;?>
          items;?>
          +
          ".implode("
          ", $e['footer'])."\n"; + echo '
          '.implode('
          ', $e['footer']).'
          '.PHP_EOL; endif; if ($e['markup']): @@ -173,6 +191,7 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML if ($e['icon']): ?> + renderContainer(iconIdxOffset: $iconTabIdx); ?> @@ -182,12 +201,14 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML + $si, 'spellName' => $sn, 'item' => $it, 'icon' => $ic, 'chance' => $ch] = $e['perfectItem']; ?> +
          renderContainer(0, $iconTabIdx, true); ?>
          @@ -201,7 +222,9 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML if ($e['modifies']): ?> +
          + [$icon, $ranks]): if (!$idx || !($idx % 3)) - echo ""; + echo ''; $icon->renderContainer(iconIdxOffset: $iconTabIdx); // just to assign iconOffset - echo "
          typeId."\">".($type ? $icon->text : "".$icon->text."")."".($ranks ? "
          (".Lang::spell('_rankRange', $ranks).")" : '')."
          '.($type ? $icon->text : ''.$icon->text.'').''.($ranks ? '
          ('.Lang::spell('_rankRange', $ranks).')' : '').'
          @@ -234,18 +258,22 @@ $WH.aE(window,\'load\',function(){$WH.ge(\'spelleffectmarkup-'.$i.'\').innerHTML
          +
          @@ -272,7 +303,9 @@ if ($this->attributes): ?>

          @@ -283,6 +316,7 @@ $this->brick('lvTabs'); $this->brick('contribute'); ?> +
          diff --git a/template/pages/spells.tpl.php b/template/pages/spells.tpl.php index 0579cdad..e3452808 100644 --- a/template/pages/spells.tpl.php +++ b/template/pages/spells.tpl.php @@ -1,28 +1,34 @@ brick('header'); -$f = $this->filter->values; // shorthand + /** @var PageTemplate $this */ + + $this->brick('header'); + $f = $this->filter->values; // shorthand ?> +
          brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [1]]); + $this->brick('pageTemplate', ['fiQuery' => $this->filter->query, 'fiMenuItem' => [1]]); ?> +
          -brick('headIcons'); -$this->brick('redButtons'); +brick('headIcons'); + + $this->brick('redButtons'); ?> +

          h1; ?>

          @@ -33,6 +39,7 @@ $this->brick('redButtons'); makeOptionsList(Lang::game('sc') , $f['sc'], 28,); ?>
          + classPanel): ?>
          ucFirst(Lang::game('class')).Lang::main('colon'); ?>
          @@ -42,11 +49,13 @@ $this->brick('redButtons'); makeOptionsList(Lang::game('cl') , $f['cl'], 28, fn($v, $k, &$e) => $v && ($e = ['class' => 'c'.$k])); ?>
          + glyphPanel): ?> +
          @@ -55,7 +64,9 @@ if ($this->glyphPanel): makeOptionsList(Lang::game('gl') , $f['gl'], 28); ?>
          + + diff --git a/template/pages/talent.tpl.php b/template/pages/talent.tpl.php index dbc85fe8..2bb03f46 100644 --- a/template/pages/talent.tpl.php +++ b/template/pages/talent.tpl.php @@ -1,10 +1,11 @@ brick('header'); ?> +
          diff --git a/template/pages/text-page-generic.tpl.php b/template/pages/text-page-generic.tpl.php index cda04390..4c05960c 100644 --- a/template/pages/text-page-generic.tpl.php +++ b/template/pages/text-page-generic.tpl.php @@ -1,11 +1,15 @@ brick('header'); ?> +
          + brick('announcement'); @@ -13,11 +17,13 @@ if ([$typeStr, $id] = $this->doResync): ?> +
          + inputbox): $this->brick(...$this->inputbox); // $templateName, [$templateVars] else: ?> +
          h1 ? '

          '.$this->h1.'

          ' : '');?> @@ -35,10 +42,13 @@ else: echo $this->extraHTML ?? ''; ?> +
          + +
          diff --git a/template/pages/user.tpl.php b/template/pages/user.tpl.php index 3e4190da..81bf0372 100644 --- a/template/pages/user.tpl.php +++ b/template/pages/user.tpl.php @@ -3,8 +3,11 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
          @@ -14,37 +17,50 @@ $this->brick('pageTemplate'); ?> + + brick('infobox'); ?> +
          + userIcon): ?> +

          h1; ?>

          +

          h1; ?>

          + +

          description): ?> +
          +
          + lvTabs)): ?> + +
          diff --git a/template/pages/video.tpl.php b/template/pages/video.tpl.php index f3cded7d..a069c3ab 100644 --- a/template/pages/video.tpl.php +++ b/template/pages/video.tpl.php @@ -3,20 +3,23 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + $this->brick('header'); ?> +
          brick('announcement'); + $this->brick('announcement'); -$this->brick('pageTemplate'); - -$this->brick('infobox'); + $this->brick('pageTemplate'); + $this->brick('infobox'); ?> +

          h1; ?>

          From 1c88254a0624ff8756472a67df49f834c3ff9802 Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 10 Jun 2026 06:13:19 +0200 Subject: [PATCH 1249/1249] style: drop redundant semicolons after function declarations (filegen templates + static js) --- .../filegen/templates/global.js/animations.js | 2 +- .../filegen/templates/global.js/guide.js | 2 +- .../filegen/templates/global.js/listview.js | 6 ++--- .../templates/global.js/listview_templates.js | 26 +++++++++---------- .../filegen/templates/global.js/mapper.js | 4 +-- .../filegen/templates/global.js/ui_ux.js | 2 +- static/js/TalentCalc.js | 2 +- static/js/admin-article.js | 2 +- static/js/admin.js | 2 +- static/js/article-description.js | 2 +- static/js/basic.js | 2 +- static/js/fileuploader.js | 2 +- static/js/maps.js | 2 +- static/js/petcalc.js | 2 +- static/js/profile.js | 2 +- static/js/talent.js | 2 +- static/js/user.js | 2 +- static/js/video.js | 2 +- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/setup/tools/filegen/templates/global.js/animations.js b/setup/tools/filegen/templates/global.js/animations.js index a41697ef..da37f6fe 100644 --- a/setup/tools/filegen/templates/global.js/animations.js +++ b/setup/tools/filegen/templates/global.js/animations.js @@ -68,7 +68,7 @@ } while ( elem = elem.parentNode ); return getRGB(color); - }; + } // Some named colors to work with // From Interface by Stefan Petre diff --git a/setup/tools/filegen/templates/global.js/guide.js b/setup/tools/filegen/templates/global.js/guide.js index 254abab5..20822e62 100644 --- a/setup/tools/filegen/templates/global.js/guide.js +++ b/setup/tools/filegen/templates/global.js/guide.js @@ -228,7 +228,7 @@ function g_enhanceTextarea (ta, opt) { } ta.data("wh-enhanced", true); -}; +} $WH.createOptionsMenuWidget = function (id, txt, opt) { var chevron = $WH.createOptionsMenuWidget.chevron; diff --git a/setup/tools/filegen/templates/global.js/listview.js b/setup/tools/filegen/templates/global.js/listview.js index 476c2a04..79394951 100644 --- a/setup/tools/filegen/templates/global.js/listview.js +++ b/setup/tools/filegen/templates/global.js/listview.js @@ -2961,7 +2961,7 @@ Listview.extraCols = { var mText = ConditionList.createCell(row.condition); Markup.printHtml(mText, td); - return; + }, getVisibleText: function(row) { @@ -3875,7 +3875,7 @@ Listview.funcBox = { } ); } - return; + }, coFlagOutOfDate: function(comment) @@ -3975,7 +3975,7 @@ Listview.funcBox = { } // aowow: custom end - return; + } }, diff --git a/setup/tools/filegen/templates/global.js/listview_templates.js b/setup/tools/filegen/templates/global.js/listview_templates.js index d05913fd..8217c524 100644 --- a/setup/tools/filegen/templates/global.js/listview_templates.js +++ b/setup/tools/filegen/templates/global.js/listview_templates.js @@ -144,7 +144,7 @@ Listview.templates = { $(wrapper).append(revText); } - return; + }, getVisibleText: function(guide) { @@ -359,7 +359,7 @@ Listview.templates = { span.addClass('q2'); $(td).append(span); - return; + }, sortFunc: function(a, b) { @@ -2208,7 +2208,7 @@ Listview.templates = { a.addClass('listview-cleartext'); a.attr('href', '?user=' + user.username); $(td).append(a); - return; + }, getVisibleText: function(user) { return user.username; @@ -2226,7 +2226,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.reputation)); - return; + }, sortFunc: function(a, b) { if (b.reputation == a.reputation) @@ -2251,7 +2251,7 @@ Listview.templates = { sp.html(buf); $(td).append(sp); - return; + }, sortFunc: function(a, b) { var sumA = (a.gold * 1000 * 1000) + (a.silver * 1000) + a.copper; @@ -2267,7 +2267,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.comments)); - return; + }, sortFunc: function(a, b) { if (a.comments == b.comments) @@ -2281,7 +2281,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.posts)); - return; + }, sortFunc: function(a, b) { if (a.posts == b.posts) @@ -2295,7 +2295,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.screenshots)); - return; + }, sortFunc: function(a, b) { if (a.screenshots == b.screenshots) @@ -2309,7 +2309,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.reports)); - return; + }, sortFunc: function(a, b) { if (a.reports == b.reports) @@ -2323,7 +2323,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.votes)); - return; + }, sortFunc: function(a, b) { if (a.votes == b.votes) @@ -2337,7 +2337,7 @@ Listview.templates = { type: 'text', compute: function(user, c) { $(c).append($WH.number_format(user.uploads)); - return; + }, sortFunc: function(a, c) { if (a.uploads == c.uploads) @@ -2396,7 +2396,7 @@ Listview.templates = { else { $(td).append(l_reputation_names[rep.action]); } - return; + }, getVisibleText: function(rep) { return l_reputation_names[rep.action]; @@ -2418,7 +2418,7 @@ Listview.templates = { } $(td).append(span); - return; + }, getVisibleText: function(rep) { return rep.amount; diff --git a/setup/tools/filegen/templates/global.js/mapper.js b/setup/tools/filegen/templates/global.js/mapper.js index 243af3b5..03f067f6 100644 --- a/setup/tools/filegen/templates/global.js/mapper.js +++ b/setup/tools/filegen/templates/global.js/mapper.js @@ -152,7 +152,7 @@ function Mapper(opt, noScroll) this.setZones(opt.zoneparent, opt.zones); this.updateMap(noScroll); -}; +} Mapper.sizes = [ [ 488, 325, 'normal'], @@ -1080,7 +1080,7 @@ Mapper.prototype = { this.onPinUpdate && this.onPinUpdate(this); - return; + }, pinOver: function() diff --git a/setup/tools/filegen/templates/global.js/ui_ux.js b/setup/tools/filegen/templates/global.js/ui_ux.js index aa3a6821..b4663894 100644 --- a/setup/tools/filegen/templates/global.js/ui_ux.js +++ b/setup/tools/filegen/templates/global.js/ui_ux.js @@ -151,7 +151,7 @@ function g_GetCommentRoleLabel(roles, title) return LANG.premiumuser; return null; -}; +} function g_formatDate(sp, elapsed, theDate, time, alone) { diff --git a/static/js/TalentCalc.js b/static/js/TalentCalc.js index 50a2a5e8..66750bf7 100644 --- a/static/js/TalentCalc.js +++ b/static/js/TalentCalc.js @@ -2289,7 +2289,7 @@ function TalentCalc() { _setPoints(basePoints, -1); _setGlyphSlots(lvl); _refreshGlyphs(); - }; + } function _setLock(locked) { if (_locked != locked) { diff --git a/static/js/admin-article.js b/static/js/admin-article.js index eb0215dd..b65655c8 100644 --- a/static/js/admin-article.js +++ b/static/js/admin-article.js @@ -100,4 +100,4 @@ function updateSnippet() { $("#snippet").val(a) } }) -}; \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js index 36899e33..b119e1e3 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -16,4 +16,4 @@ function ar_ValidateUrl(a) { else { return "You used invalid characters in your URL.\n\nYou can only use the following:\n a to z\n 0 to 9\n = _ & . / -" } -}; +} diff --git a/static/js/article-description.js b/static/js/article-description.js index 9dd67556..7d07af4f 100644 --- a/static/js/article-description.js +++ b/static/js/article-description.js @@ -81,4 +81,4 @@ function updatePlaceholder() { else $("#description").text(data); }) -}; \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/basic.js b/static/js/basic.js index c0af5d79..251f1aaf 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -553,7 +553,7 @@ $WH.eO = function(z) { // Duplicate object $WH.dO = function(s) { - function f(){}; + function f(){} f.prototype = s; return new f; } diff --git a/static/js/fileuploader.js b/static/js/fileuploader.js index 025cdf33..f8eed06f 100644 --- a/static/js/fileuploader.js +++ b/static/js/fileuploader.js @@ -1367,7 +1367,7 @@ qq.extend(qq.UploadHandlerXhr.prototype, { } for (key in this._options.customHeaders){ xhr.setRequestHeader(key, this._options.customHeaders[key]); - }; + } xhr.send(file); }, _onComplete: function(id, xhr){ diff --git a/static/js/maps.js b/static/js/maps.js index 03224dd8..368ae8e7 100644 --- a/static/js/maps.js +++ b/static/js/maps.js @@ -88,4 +88,4 @@ function ma_UpdateLink(_) { } $WH.ge('link-to-this-map').href = b; -}; +} diff --git a/static/js/petcalc.js b/static/js/petcalc.js index 824e6baf..2d4c85ad 100644 --- a/static/js/petcalc.js +++ b/static/js/petcalc.js @@ -119,4 +119,4 @@ function pc_readPound() { pc_object.setWhBuild(pc_build); } } -}; +} diff --git a/static/js/profile.js b/static/js/profile.js index 36a7369f..bf750c55 100644 --- a/static/js/profile.js +++ b/static/js/profile.js @@ -588,7 +588,7 @@ function pr_addEquipButton(id, itemId) $('#' + id).append(button); - return; + } }); } diff --git a/static/js/talent.js b/static/js/talent.js index 61cf8c8b..cc1ebdf3 100644 --- a/static/js/talent.js +++ b/static/js/talent.js @@ -143,4 +143,4 @@ function tc_readPound() { } } } -}; +} diff --git a/static/js/user.js b/static/js/user.js index 40b34959..6cdf2450 100644 --- a/static/js/user.js +++ b/static/js/user.js @@ -165,7 +165,7 @@ Listview.funcBox.beforeUserComments = function() }).bind(this); $WH.ae(d, i); } - }; + } this.customFilter = function (comment, i) { diff --git a/static/js/video.js b/static/js/video.js index dc00cb8a..58059ace 100644 --- a/static/js/video.js +++ b/static/js/video.js @@ -801,7 +801,7 @@ function () { if (!resizing) { // aowow - /uploads/videos/ not seen on server // aOriginal.href = g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'; - aOriginal.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId);; + aOriginal.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId); var hasFrom = video.date && video.user; if (hasFrom) { var
          ucFirst(Lang::main('name')).Lang::main('colon'); ?>