Просмотр исходного кода

Merge branch 'master' into Custom_Filters

Iñigo Valentin 6 лет назад
Родитель
Сommit
3188d4b3a4

+ 17 - 3
application/Constant.php

@@ -1,6 +1,7 @@
 <?php
 
     $DIFFICULTY = [
+        "EASY" => 0,
         "NORMAL" => 1,
         "HARD" => 2,
         "HELL" => 3,
@@ -78,9 +79,9 @@
 
     $SCENARIO = [
         'GAREN_FOREST' => 1,
-        'MT._SIZ' => 2,
+        'MT_SIZ' => 2,
         'KABIR_RUINS' => 3,
-        'MT._WHITE_RAGON' => 4,
+        'MT_WHITE_RAGON' => 4,
         'TELAIN_FOREST' => 5,
         'HYDENI_RUINS' => 6,
         'TAMOR_DESERT' => 7,
@@ -108,7 +109,7 @@
         'HALL_OF_LIGHT' => 7001,
         'GIANTS_KEEP' => 8001,
         'DRAGONS_LAIR' => 9001,
-
+        // TODO: Lumel
     ];
 
     $ELEMENTAL_RIFT_DUNGEON = [
@@ -127,6 +128,19 @@
         'LEVEL_5' => 5,
     ];
 
+    $LABYRINTH_STAGES = [
+        'TARTARUS' => 1,
+        'KOTTOS' => 2,
+        'LEOS' => 3,
+        'AQUILES' => 4,
+        'NORMAL' => 5,
+        'RESCUE' => 6,
+        'EXPLODE' => 7,
+        'COOL_TIME' => 8,
+        'SPEED_LIMIT' => 9,
+        'TIME_LIMIT' => 10
+    ];
+
     $BUILDING = [
         'SUMMONERS_TOWER' => 1,
         'SUMMONHENGE' => 2,

+ 81 - 28
application/Controller.php

@@ -26,7 +26,7 @@
          *
          * @param Array $params POST parameters of the request.
          */
-        public function __construct($uid, $params){
+        public function __construct($params){
 
             global $path;
             global $UID;
@@ -50,6 +50,33 @@
                     case "LOGIN":
                         require_once($path["action"] . "login.php");
                         action($db);
+                        return;
+                        break;
+                    case "LOGOUT":
+                        require_once($path["action"] . "logout.php");
+                        action($db);
+                        return;
+                        break;
+                    case "NEW_TEAM":
+                        require_once($path["action"] . "new_team.php");
+                        action($db);
+                        return;
+                        break;
+                    case "SAVE_RUN_TEAM":
+                        require_once($path["action"] . "save_run_team.php");
+                        action($db);
+                        return;
+                        break;
+                    case "EDIT_PROFILE":
+                        require_once($path["action"] . "edit_profile.php");
+                        $response = action($db);
+                        if (strlen($response) > 1){
+                            echo($response);
+                            return;
+                        }
+                        else{
+                            return;
+                        }
                         break;
                     default:
                         require_once($path["page"] . "Error_Page.php");
@@ -58,37 +85,55 @@
                 }
             }
 
-            // TODO: Validate private/public user with cookie and redirect
-            $UID = $uid;
-            if (preg_match("/\d{7}/", $UID)){
-error_log('1');
-                $q = $db->query("SELECT count(uid) AS c FROM player WHERE uid = $UID;");
-                $r = $q->fetchArray(SQLITE3_ASSOC);
-                if ($r["c"] == 0){
-                    require_once($path["page"] . "Landing_Page.php");
-                    $page = new Landing_Page($db);
-                    require_once($page->view);
-                    return;
-                }
+            if (isset($_COOKIE["uid"]) && isset($_SESSION["uid"]) && $_COOKIE["uid"] != $_COOKIE["uid"]){
+                // Both cookie and session are set, but they are different. Delete them both.
+                setcookie("uid", "", time() - 3600);
+                $_SESSION["session"] = false;
+                $_SESSION["uid"] = "";
+                require_once($path["page"] . "Landing_Page.php");
+                $page = new Landing_Page($db);
+                require_once($page->view);
+                return;
+            }
+            elseif (isset($_COOKIE["uid"]) && !isset($_SESSION["uid"])){
+                $_SESSION["session"] = true;
+                $_SESSION["uid"] = $_COOKIE["uid"];
+            }
+            elseif (!isset($_COOKIE["uid"]) && isset($_SESSION["uid"])){
+                setcookie("uid", $_SESSION["uid"], time() + 5 * 24 * 60 * 60);
+            }
+
+            // Validate session
+            $UID = $_SESSION["uid"];
+            $q = $db->query("SELECT count(uid) AS c FROM player WHERE uid = '$UID';");
+            $r = $q->fetchArray(SQLITE3_ASSOC);
+            if ($r["c"] == 0){
+                require_once($path["page"] . "Landing_Page.php");
+                $page = new Landing_Page($db);
+                require_once($page->view);
+                return;
             }
             else{
-error_log('2');
-                if ($_SESSION['session'] == true && isset($_SESSION['uid'])){
-error_log('2.1');
-                    $UID = $_SESSION['uid'];
-                    header("Location: /$UID" . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));
-                    return;
-                }
-                else{
-error_log('2.2');
-                    require_once($path["page"] . "Landing_Page.php");
-                    $page = new Landing_Page($db);
-                    require_once($page->view);
-                    return;
+                // Reset cokie and renew session.
+                setcookie("uid", $_SESSION["uid"], time() + 5 * 24 * 60 * 60);
+                $_SESSION["session"] = true;
+                $_SESSION["uid"] = $UID;
+                // Recover username if lost
+                if (!isset($_SESSION["name"]) || strlen($_SESSION["name"]) == 0){
+                    $q = $db->query("SELECT name FROM player WHERE uid = '$UID';");
+                    $r = $q->fetchArray(SQLITE3_ASSOC);
+                    $_SESSION["name"] = $r["name"];
                 }
             }
 
+            // If UID not in URL, redirect and end.
+            if ($pars[0] != $UID){
+                header("Location: /$UID" . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));
+                return;
+            }
+
             // Select the model to load.
+            array_shift($pars); // Remove first one, should be UID
             if (sizeof($pars) == 0){
                 require_once($path["page"] . "Home_Page.php");
                 $page = new Home_Page($db);
@@ -110,6 +155,14 @@ error_log('2.2');
                     require_once($path["page"] . "Runs_Page.php");
                     $page = new Runs_Page($db);
                 }
+                elseif (strtoupper($pars[0]) == "TEAMS"){
+                    require_once($path["page"] . "Teams_Page.php");
+                    $page = new Teams_Page($db);
+                }
+                elseif (strtoupper($pars[0]) == "PROFILE"){
+                    require_once($path["page"] . "Profile_Page.php");
+                    $page = new Profile_Page($db);
+                }
                 elseif (strtoupper($pars[0]) == "MONSTERS"){
                     if (count($pars) == 1){
                         require_once($path["page"] . "Monsters_Page.php");
@@ -140,11 +193,11 @@ error_log('2.2');
                         $page = new Report_Skillup_Page($db, $pars[1]);
                     }
                     elseif (strtoupper($pars[1]) == "RUNEUPGRADE"){
-                    require_once($path["page"] . "Report_Runeupgrade_Page.php");
+                        require_once($path["page"] . "Report_Runeupgrade_Page.php");
                         $page = new Report_Runeupgrade_Page($db, $pars[1]);
                     }
                     elseif (strtoupper($pars[1]) == "RUNECRAFT"){
-                    require_once($path["page"] . "Report_Runecraft_Page.php");
+                        require_once($path["page"] . "Report_Runecraft_Page.php");
                         $page = new Report_Runecraft_Page($db, $pars[1]);
                     }
 

+ 5 - 2
application/Filter.php

@@ -2,8 +2,6 @@
 
     global $QUALITY;
     global $RUNE_STAT;
-    
-    $a = "VARIALEA";
 
     $FILTER_CATALOG = [
         "element" => 0,
@@ -143,4 +141,9 @@
         "helper" => false,
         "number" => 20,
     ];
+
+    $FILTER_TEAM = [
+        "name" => "",
+        "area" => null
+    ];
 ?>

+ 69 - 0
application/action/edit_profile.php

@@ -0,0 +1,69 @@
+<?php
+
+    function generateApi($length = 16) {
+        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+        $charactersLength = strlen($characters);
+        $randomString = '';
+        for ($i = 0; $i < $length; $i++) {
+            $randomString .= $characters[rand(0, $charactersLength - 1)];
+        }
+        return $randomString;
+    }
+
+    function action($db = null){
+        global $AREA_TYPE;
+        global $DIFFICULTY;
+        global $SCENARIO;
+        global $DUNGEON;
+        global $ELEMENTAL_RIFT_DUNGEON;
+        global $LABYRINTH_STAGES;
+        global $UID;
+
+        $response = null;
+
+        $player = filter_input(INPUT_POST, 'uid');
+
+        if (filter_input(INPUT_POST, "mail")){
+            $mail = filter_input(INPUT_POST, 'mail');
+            // TODO: Validate mail
+            $s = $db->prepare('UPDATE player SET mail = :mail WHERE uid = :uid;');
+error_log("UPDATE player SET mail = '$mail' WHERE uid = '$player';");
+            $s->bindValue(':uid', $player);
+            $s->bindValue(':mail', $mail);
+            if(!$s->execute()){
+                return -1;
+            }
+        }
+
+        if (filter_input(INPUT_POST, "pass")){
+            $pass = sha1(filter_input(INPUT_POST, 'pass'));
+            $currentPass = sha1(filter_input(INPUT_POST, 'currentPass'));
+            // TODO: Validate current
+            $s = $db->prepare('UPDATE player SET password = :pass WHERE uid = :uid AND password = :currentPass;');
+            $s->bindValue(':uid', $player);
+            $s->bindValue(':pass', $mail);
+            $s->bindValue(':currentPass', $currentPass);
+            if(!$s->execute()){
+                return -2;
+            }
+        }
+
+        if (filter_input(INPUT_POST, "api")){
+            $api = generateApi();
+            $s = $db->prepare('UPDATE player SET api_key = :api WHERE uid = :uid;');
+            $s->bindValue(':uid', $player);
+            $s->bindValue(':api', $api);
+            if(!$s->execute()){
+                return -3;
+            }
+            $response = $api;
+        }
+
+        if ($response == null){
+            return 0;
+        }
+        else{
+            return $response;
+        }
+    }
+?>

+ 1 - 0
application/action/login.php

@@ -18,6 +18,7 @@
             return -4;
         }
         $uid = $r["uid"];
+        setcookie("uid", $uid, time() + 5 * 24 * 60 * 60); // 5 Days
         session_regenerate_id();
         $_SESSION['session'] = true;
         $_SESSION['name'] = $uname;

+ 14 - 0
application/action/logout.php

@@ -0,0 +1,14 @@
+<?php
+    function action($db = null){
+        global $UID;
+        $UID = null;
+        setcookie("uid", null, time() - 3600);
+        session_regenerate_id();
+        $_SESSION['session'] = false;
+        $_SESSION['name'] = "";
+        $_SESSION['uid'] = "";
+        header("Location: /");
+        die();
+        return 0;
+    }
+?>

+ 170 - 0
application/action/new_team.php

@@ -0,0 +1,170 @@
+<?php
+    function action($db = null){
+        global $AREA_TYPE;
+        global $DIFFICULTY;
+        global $SCENARIO;
+        global $DUNGEON;
+        global $ELEMENTAL_RIFT_DUNGEON;
+        global $LABYRINTH_STAGES;
+        global $UID;
+
+        //$player = SQLite3::escapeString($_POST["uid"]);
+        $player = filter_input(INPUT_POST, 'uid');
+        $area_type = filter_input(INPUT_POST, 'area_type');
+        $area = filter_input(INPUT_POST, 'area');
+        $stage = filter_input(INPUT_POST, 'stage');
+        error_log("STAGE1: " . $stage);
+        $difficulty = filter_input(INPUT_POST, 'difficulty');
+        $ids = filter_input(INPUT_POST, 'ids');
+        $name = filter_input(INPUT_POST, 'name');
+        $description = filter_input(INPUT_POST, 'description');
+        $id = preg_split("/[\s,]+/", $ids);
+        $id_count = sizeof($id);
+        if ($name == null || strlen($name) == 0){
+            return -1;
+        }
+        switch ($area_type){
+            case $AREA_TYPE["SCENARIO"]:
+                if ($stage < 0 || $stage > 7){
+                    return -31;
+                }
+                if (!($difficulty == 0 || in_array($difficulty, $DIFFICULTY) || $difficulty == $DIFFICULTY["EASY"])){
+                    return -32;
+                }
+                if ($id_count < 1 || $id_count > 4){
+                    return -33;
+                }
+                if (!in_array($area, $SCENARIO)){
+                    return -34;
+                }
+                break;
+            case $AREA_TYPE["CAIROS_DUNGEON"]:
+                if ($stage < 0 || $stage > 10){
+                    return -35;
+                }
+                $difficulty = null;
+                if ($id_count < 1 || $id_count > 5){
+                    return -36;
+                }
+                if (!in_array($area, $DUNGEON)){
+                    return -37;
+                }
+                break;
+            case $AREA_TYPE["RIFT_DUNGEON"]:
+                $stage = null;
+                $difficulty = null;
+                if ($id_count < 1 || $id_count > 6){
+                    return -39;
+                }
+                if (!in_array($area, $ELEMENTAL_RIFT_DUNGEON)){
+                    return -40;
+                }
+                break;
+            case $AREA_TYPE["RIFT_RAID"]:
+                if ($stage < 0 || $stage > 5){
+                    return -41;
+                }
+                $difficulty = null;
+                if ($id_count < 1 || $id_count > 6){
+                    return -42;
+                }
+                $area = null;
+                break;
+            case $AREA_TYPE["ARENA"]:
+                $area = null;
+                $stage = null;
+                $difficulty = null;
+                break;
+            case $AREA_TYPE["GUILD_WAR"]:
+                $area = null;
+                $stage = null;
+                $difficulty = null;
+                break;
+            case $AREA_TYPE["GUILD_SIEGE"]:
+                $area = null;
+                $stage = null;
+                $difficulty = null;
+                break;
+            case $AREA_TYPE["TARTARUS_LABYRINTH"]:
+                $stage = null;
+                if ($difficulty < 0 || !in_array($difficulty, $DIFFICULTY)){
+                    return -43;
+                }
+                if ($id_count < 1 || $id_count > 5){
+                    return -44;
+                }
+                if (!in_array($area, $LABYRINTH_STAGES)){
+                    return -45;
+                }
+                break;
+            case $AREA_TYPE["TRIAL_OF_ASCENSION"]:
+                if ($difficulty != 0 && $difficulty != $DIFFICULTY["NORMAL"] && $difficulty != $DIFFICULTY["HARD"]){
+                    return -46;
+                }
+                $stage = null;
+                if ($id_count < 1 || $id_count > 5){
+                    return -47;
+                }
+                $area = null;
+                break;
+            case $AREA_TYPE["DIMENSIONAL_HOLE"]:
+                if ($stage < 0 || $stage > 5){
+                    return -48;
+                }
+                $difficulty = null;
+                if ($id_count < 1 || $id_count > 4){
+                    return -49;
+                }
+                if (!in_array($area, $DUNGEON)){
+                    return -50;
+                }
+                break;
+            case $AREA_TYPE["DIMENSIONAL_RIFT"]:
+                if ($difficulty != 0 && $difficulty != $DIFFICULTY["NORMAL"] && $difficulty != $DIFFICULTY["HARD"]){
+                    return -51;
+                }
+                $stage = null;
+                if ($id_count < 1 || $id_count > 5){
+                    return -52;
+                }
+                $area = null;
+                break;
+            default:
+                return -3;
+        }
+        $unique_id = array_unique($id);
+        if ($id_count != sizeof($unique_id)){
+            return -4;
+        }
+        
+        
+        error_log("STAGE2: " . $stage);
+        
+        $s = "SELECT CASE WHEN MAX(id) IS NULL THEN 1 ELSE MAX(id) + 1 END AS id FROM team;";
+        $q = $db->query($s);
+        $r = $q->fetchArray(SQLITE3_ASSOC);
+        $team_id = $r["id"];
+        
+        $statement = $db->prepare('INSERT INTO team (uid, id, name, description, area_type, area, stage, difficulty) VALUES (:uid, :id, :name, :description, :area_type, :area, :stage, :difficulty);');
+        $statement->bindValue(':uid', $player);
+        $statement->bindValue(':id', $team_id);
+        $statement->bindValue(':name', $name);
+        $statement->bindValue(':description', $description);
+        $statement->bindValue(':area_type', $area_type);
+        $statement->bindValue(':area', $area);
+        $statement->bindValue(':stage', $stage);
+        $statement->bindValue(':difficulty', $difficulty);
+        $res = $statement->execute();
+        
+        if (!$res){
+            return -5;
+        }
+        
+        foreach ($id as $i){
+            $s = "INSERT INTO team_unit VALUES ($team_id, $i);";
+            $db->query($s);
+        }
+        
+        return 0;
+    }
+?>

+ 41 - 0
application/action/save_run_team.php

@@ -0,0 +1,41 @@
+<?php
+    function action($db = null){
+        $player = filter_input(INPUT_POST, 'uid');
+        $run = filter_input(INPUT_POST, 'run');
+        $name = filter_input(INPUT_POST, 'name');
+        $description = filter_input(INPUT_POST, 'description');
+        if ($name == null || strlen($name) == 0){
+            return -1;
+        }
+        $s = "SELECT CASE WHEN MAX(id) IS NULL THEN 1 ELSE MAX(id) + 1 END AS id FROM team;";
+        $q = $db->query($s);
+        $r = $q->fetchArray(SQLITE3_ASSOC);
+        $team_id = $r["id"];
+        $s = "SELECT run.area AS area, run.stage AS stage, run.difficulty AS difficulty, k_area.type AS area_type FROM run, k_area WHERE run.uid = '$player' AND run.id = '$run' AND k_area.id = run.area AND run.helper = 0;";
+        $q = $db->query($s);
+        $r = $q->fetchArray(SQLITE3_ASSOC);
+        if (!$r){
+            return -2;
+        }
+        $statement = $db->prepare('INSERT INTO team (uid, id, name, description, area_type, area, stage, difficulty) VALUES (:uid, :id, :name, :description, :area_type, :area, :stage, :difficulty);');
+        $statement->bindValue(':uid', $player);
+        $statement->bindValue(':id', $team_id);
+        $statement->bindValue(':name', $name);
+        $statement->bindValue(':description', $description);
+        $statement->bindValue(':area_type', $r["area_type"]);
+        $statement->bindValue(':area', $r["area"]);
+        $statement->bindValue(':stage', $r["stage"]);
+        $statement->bindValue(':difficulty', $r["difficulty"]);
+        $res = $statement->execute();
+        if (!$res){
+            return -3;
+        }
+        $s = "SELECT unit FROM run_party WHERE run = $run AND unit IS NOT NULL;";
+        $q = $db->query($s);
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $s = "INSERT INTO team_unit VALUES ('$team_id', '" . $r["unit"] . "');";
+            $db->query($s);
+        }
+        return 0;
+    }
+?>

+ 37 - 25
application/bin/log-profile.py

@@ -81,22 +81,6 @@ def openDatabase(name):
     print('Configuring database...')
     try:
         db = sqlite3.connect(name)
-        cursor = db.cursor()
-        cursor.execute('PRAGMA foreign_keys = OFF;')
-        cursor.execute('DELETE FROM scenario')
-        cursor.execute('DELETE FROM defense')
-        cursor.execute('DELETE FROM unit')
-        cursor.execute('DELETE FROM unit_skill')
-        cursor.execute('DELETE FROM rune')
-        cursor.execute('DELETE FROM building')
-        cursor.execute('DELETE FROM decoration')
-        cursor.execute('DELETE FROM inventory')
-        cursor.execute('DELETE FROM summon_special')
-        cursor.execute('DELETE FROM guild')
-        cursor.execute('DELETE FROM guild_member')
-        cursor.execute('DELETE FROM rune_craft')
-        db.commit()
-        cursor.close()
         return db
     except sqlite3.IntegrityError as e:
         print("Error executing statement: " + str(e))
@@ -105,6 +89,41 @@ def openDatabase(name):
         print("I/O Error creating database " + name + ": " + str(e))
         raise
 
+"""
+Clears the user tables.
+
+It also disables referencial integrity.
+
+:param db: Sqlite database connection.
+:param data: Data in json format.
+:raises IntegrityError: The queryes couldn't bre executed.
+"""
+def clearData(db, data):
+    print('Clearing previous data...')
+    try:
+        uid = data["wizard_info"]["wizard_id"]
+        cursor = db.cursor()
+        cursor.execute('PRAGMA foreign_keys = OFF;')
+        cursor.execute('DELETE FROM scenario WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM defense WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM unit WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM unit_skill WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM rune WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM building WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM decoration WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM inventory WHERE uid = ?', (uid))
+        #cursor.execute('DELETE FROM summon_special')
+        #cursor.execute('DELETE FROM guild')
+        cursor.execute('DELETE FROM guild_member WHERE uid = ?', (uid))
+        cursor.execute('DELETE FROM rune_craft WHERE uid = ?', (uid))
+        db.commit()
+        cursor.close();
+    except sqlite3.IntegrityError as e:
+        print("Error executing statement: " + str(e))
+        raise
+    return
+
+
 """
 Closes the database.
 
@@ -659,7 +678,7 @@ def parseRunes(db, data):
             substat_3_enchant = 0
             substat_3_grind = 0
         if len(rune["sec_eff"]) > 3:
-            substat_4 = rune["sec_eff"][0][0]
+            substat_4 = rune["sec_eff"][3][0]
             substat_4_value = rune["sec_eff"][3][1]
             substat_4_enchant = rune["sec_eff"][3][2]
             substat_4_grind = rune["sec_eff"][3][3]
@@ -929,14 +948,6 @@ def get_max_efficiency(substats, efficiency, level, stars, quality):
     substat_upgrades_remaining = (4 - math.floor((min(level, 12) / 3))) - max((3 - quality), 0)
     new_stats = max(min(4 - len(substats), substat_upgrades_remaining), 0)
     old_stats = substat_upgrades_remaining - new_stats
-    
-    #if id == 16042097882:
-    #    print("substat_upgrades_remaining: " + str(substat_upgrades_remaining))
-    #    print("quality: " + str(quality))
-    #    print("level: " + str(level))
-    #    print("math.floor((min(level, 12) / 3)): " + str(math.floor((min(level, 12) / 3))))
-    #    print("old_stats: " + str(old_stats))
-    #    print("new_stats: " + str(new_stats))
 
     if old_stats > 0:
         # we can repeatedly upgrade the most value of the existing stats
@@ -1053,6 +1064,7 @@ if verifyKey(db, data, key) == False:
     print("Invalid API KEY...")
     sys.exit(-1)
 else:
+    clearData(db, data)
     parsePlayer(db, data)
     parseScenarios(db, data)
     parseDefense(db, data)

+ 97 - 0
application/entity/Team.php

@@ -0,0 +1,97 @@
+<?php
+
+    require_once($path["entity"] . "K_Area.php");
+    require_once($path["entity"] . "Unit.php");
+
+
+    /**
+     * A Team.
+     *
+     * Represents an object from the table 'unit'.
+     */
+    class Team extends Entity{
+
+        /**
+         * Owner identifier.
+         */
+        public $uid;
+
+        /**
+         * Team identifier.
+         */
+        public $id;
+
+        /**
+         * Team name.
+         */
+        public $name;
+
+        /**
+         * Team description.
+         */
+        public $description;
+
+        /**
+         * List of {@see Unit}s in the team.
+         */
+        public $unit = [];
+
+        /**
+         * Area the team is designed for.
+         */
+        public $area;
+
+        /**
+         * Stage the team is designed for. It can be null.
+         */
+        public $stage;
+
+        /**
+         * Constructor.
+         *
+         * Searches the database and retrieves the information about the
+         * monster, populating it and it's items.
+         *
+         * @param SQLite3 $db Connection to the database.
+         * @param int $id Monster identifier.
+         * @param complete If false, query only monster and k_monster.
+         */
+        public function __construct($db, $id, $complete = true){
+            global $path;
+            parent::__construct($db);
+            $s = "
+              SELECT
+                uid,
+                id,
+                name,
+                description,
+                area_type,
+                area,
+                stage,
+                difficulty
+              FROM team
+              WHERE id = '$id';
+            ";
+            $q = $this->db->query($s);
+            $r = $q->fetchArray(SQLITE3_ASSOC);
+            $this->uid = $r["uid"];
+            $this->id = $r["id"];
+            $this->name = $r["name"];
+            $this->description = $r["description"];
+            $this->area = new K_Area($this->db, $r["area"], $r["area_type"]);
+            $this->stage = $r["stage"];
+            if ($complete){
+                $s = "
+                  SELECT unit
+                  FROM team_unit
+                  WHERE team = '$id';
+                ";
+                $q = $this->db->query($s);
+                while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                    array_push($this->unit, new Unit($this->db, $r["unit"], false));
+                }
+            }
+        }
+
+    }
+?>

+ 48 - 0
application/page/Profile_Page.php

@@ -0,0 +1,48 @@
+ <?php
+
+    require_once($path["page"] . "Page.php");
+
+
+    /**
+     * User profile page.
+     */
+    class Profile_Page extends Page{
+
+        public $name;
+        public $mail;
+        public $api_key;
+
+        /**
+         * Constructor.
+         *
+         * Retrieves the data and initializes the variables.
+         *
+         * @param SQLite3 $db Connection to the database.
+         */
+        public function __construct($db){
+            global $path;
+            global $root;
+            global $UID;
+            parent::__construct($db);
+            $this->view = $path["view"] . "profile.php";
+            $s = "
+              SELECT
+                name,
+                mail,
+                api_key
+              FROM player
+              WHERE uid = '$UID';
+            ";
+            $q = $this->db->query($s);
+            if ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                $this->name = $r["name"];
+                $this->mail = $r["mail"];
+                $this->api_key = $r["api_key"];
+            }
+            $this->title = $this->name ." - SWDB";
+            $this->description = $this->name . " user profile";
+            $this->canonical = $root . "profile/";
+        }
+
+    }
+?>

+ 11 - 1
application/page/Report_Runeupgrade_Page.php

@@ -3,6 +3,16 @@
     require_once($path["page"] . "Page.php");
     require_once($path["entity"] . "Unit.php");
     require_once($path["entity"] . "Rune.php");
+    
+    class Upgrade{
+        private $db;
+        public $unit = null;
+        public $rune = [];
+        
+        public function __construct($db){
+            $this->db = $db;
+        }
+    }
 
     /**
      * Upgradeable runes report page.
@@ -296,7 +306,7 @@
                     break;
             }
             if (strlen($this->filters["monster_name"]) > 0){
-                $s = $s . " AND k_unit.name LIKE = '%" . $this->filters["monster_name"] . "%' ";
+                $s = $s . " AND upper(k_unit.name) LIKE = upper('%" . $this->filters["monster_name"] . "%') ";
             }
             if (!$this->filters["monster_in_storage"]){
                 //$s = $s . " AND unit.in_storage = 0 ";

+ 101 - 0
application/page/Teams_Page.php

@@ -0,0 +1,101 @@
+ <?php
+
+    require_once($path["page"] . "Page.php");
+    require_once($path["entity"] . "Team.php");
+
+    /**
+     * Monster list page.
+     */
+    class Teams_Page extends Page{
+
+        /**
+         * Array of @{see Team}s.
+         */
+        public $teams = [];
+
+        /**
+         * The filters the page can handle.
+         */
+        public $filters;
+
+        /**
+         * List of all units for team creation
+         */
+        public $units = [];
+
+
+        /**
+         * Constructor.
+         *
+         * Retrieves the data and initializes the variables.
+         *
+         * @param SQLite3 $db Connection to the database.
+         */
+        public function __construct($db){
+            global $path;
+            global $root;
+            global $UID;
+            parent::__construct($db);
+            $this->view = $path["view"] . "teams.php";
+            $this->parse_filters();
+            $s = $this->build_query();
+            $q = $this->db->query($s);
+            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                array_push($this->teams, new Team($db, $r["id"]));
+            }
+            $s = "
+              SELECT id
+              FROM unit
+              WHERE unit.uid = '$UID';
+            ";
+            $q = $this->db->query($s);
+            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+                array_push($this->units, new Unit($this->db, $r["id"], false));
+            }
+            $this->title = "Teams - SWDB";
+            $this->description = "Teams";
+            $this->canonical = $root . "teams/";
+        }
+
+        /**
+         * Parses the request looking for the selected filters, validates them
+         * and adds them to the $filters array.
+         */
+        private function parse_filters(){
+            global $FILTER_TEAM;
+            global $AREA_TYPE;
+            $this->filters = $FILTER_TEAM;
+            if (isset($_GET["area"]) && in_array($_GET["area"], $AREA_TYPE)){
+                $this->filters["area"] = $_GET["area"];
+            }
+            if (isset($_GET["name"]) && strlen($_GET["name"]) > 0){
+                $this->filters["name"] = SQLite3::escapeString($_GET["name"]);
+            }
+            return;
+        }
+
+        /**
+         * Builds the query to the rune table using the selected or default
+         * filters.
+         *
+         * @return The query to be executed.
+         */
+        private function build_query(){
+            global $ELEMENT;
+            global $UID;
+            $s = "
+              SELECT id
+              FROM team
+              WHERE uid = '$UID'
+            ";
+            if (in_array($this->filters["area"], $ELEMENT)){
+                $s = $s . " AND area  = '" . strtoupper($this->filters["area"]) . "') ";
+            }
+            if ($this->filters["name"] != ""){
+                $s = $s . " AND upper(name) LIKE '%" . strtoupper($this->filters["name"]) . "%' ";
+            }
+            return $s;
+        }
+
+    }
+?>

+ 34 - 0
application/view/inc/filter_teams.php

@@ -0,0 +1,34 @@
+<?php
+    global $AREA_TYPE;
+?>
+<form action='/<?=$UID?>/teams/' method='get'  id='filter_monsters' class='filter'>
+    <table>
+        <tr>
+            <td class='filter' colspan='2'>
+                <span>Name:</span>
+                <input type='text' name='name' id='filter_name' value='<?=$page->filters["name"]?>'/>
+            </td>
+            <td>
+                <span>Area:</span>
+                <select name='area'>
+                    <option value=''> <option>
+                    <option value='<?=$AREA_TYPE["SCENARIO"]?>'>Scenario</option>
+                    <option value='<?=$AREA_TYPE["CAIROS_DUNGEON"]?>'>Cairos Dungeon</option>
+                    <option value='<?=$AREA_TYPE["RIFT_DUNGEON"]?>'>Rift Dungeon</option>
+                    <option value='<?=$AREA_TYPE["RIFT_RAID"]?>'>Rift Raid</option>
+                    <option value='<?=$AREA_TYPE["DIMENSIONAL_HOLE"]?>'>Dimensional Hole></option>
+                    <option value='<?=$AREA_TYPE["AREAN"]?>'>Arena</option>
+                    <option value='<?=$AREA_TYPE["GUILD_WAR"]?>'>Guild War</option>
+                    <option value='<?=$AREA_TYPE["GUILD_SIEGE"]?>'>Guild Siege</option>
+                    <option value='<?=$AREA_TYPE["TARTARUS_LABYRINTH"]?>'>Tartarus Labyrinth</option>
+                    <option value='<?=$AREA_TYPE["TRIAL_OF_ASCENSION"]?>'>Trial of Ascension</option>
+                    <option value='<?=$AREA_TYPE["WORLD_BOSS"]?>'>World Boss</option>
+                    <option value='<?=$AREA_TYPE["DIMENSIONAL_RIFT"]?>'>Dimensinal Rift</option>
+                </select>
+            </td>
+        </tr>
+    </table>
+    <div id='filter_apply'>
+        <input type='submit' value='Apply'/>
+    </div>
+</form>

+ 3 - 0
application/view/inc/header.php

@@ -33,6 +33,9 @@
             <a href='/<?=$UID?>/runs/'>
                 Runs
             </a>
+            <a href='/<?=$UID?>/teams/'>
+                Teams
+            </a>
             <a href='/<?=$UID?>/report/'>
                 Reports
             </a>

+ 279 - 0
application/view/profile.php

@@ -0,0 +1,279 @@
+<!DOCTYPE html>
+<html lang='en'>
+    <head>
+        <meta content='text/html; charset=utf-8' http-equiv='content-type'/>
+        <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1'/>
+        <title><?=$page->title?></title>
+        <link rel='shortcut icon' href='<?=$page->favicon?>'/>
+        <!-- CSS files -->
+        <link rel='stylesheet' type='text/css' href='<?=$path["css"]?>ui.css'/>
+        <link rel='stylesheet' type='text/css' href='<?=$path["css"]?>profile.css'/>
+        <!-- Meta tags -->
+        <link rel='canonical' href='<?=$page->canonical?>'/>
+        <link rel='author' href='<?=$page->author?>'/>
+        <link rel='publisher' href='<?=$page->author?>'/>
+        <meta name='description' content='<?=$page->description?>'/>
+        <meta property='og:title' content='<?=$page->title?>'/>
+        <meta property='og:url' content='<?=$page->canonical?>'/>
+        <meta property='og:description' content='<?=$page->description?>'/>
+        <meta property='og:image' content='<?=$page->icon?>'/>
+        <meta property='og:site_name' content='<?=$page->name?>'/>
+        <meta property='og:type' content='website'/>
+        <meta property='og:locale' content='en'/>
+        <meta name='twitter:card' content='summary'/>
+        <meta name='twitter:title' content='<?=$page->title?>'/>
+        <meta name='twitter:description' content='<?=$page->description?>'/>
+        <meta name='twitter:image' content='<?=$page->icon?>'/>
+        <meta name='twitter:url' content='<?=$page->canonical?>'/>
+        <meta name='robots' content='index follow'/>
+        <script>
+
+            /**
+             * Redirects to the logout action.
+             */
+            function logout(){
+                window.location = '/action/logout/';
+            }
+
+            /**
+             * Shows an error window.
+             *
+             * @param msg Error message
+             */
+            function showError(msg){
+                document.getElementById('error_window').style.display = 'block';
+                document.getElementById('error_msg').innerHTML = msg;
+            }
+
+            /**
+             * Closes the error window.
+             */
+            function closeError(){
+                document.getElementById('error_window').style.display = 'none';
+                document.getElementById('error_msg').innerHTML = ' ';
+            }
+
+            /**
+             * Shows the mail change form.
+             */
+            function changeMail(){
+                document.getElementById('static_mail').style.display = 'none';
+                document.getElementById('change_mail').style.display = 'none';
+                document.getElementById('edit_mail').style.display = 'block';
+                document.getElementById('save_mail').style.display = 'block';
+            }
+
+            /**
+             * Saves an email change.
+             * 
+             * Validates the new email format and makes an AJAX request. If
+             * succesfull, hides the email change form.
+             */
+            function saveMail(){
+                var http = new XMLHttpRequest();
+                var mail = document.getElementById('edit_mail').value;
+                if (mail.length < 1){
+                    showError('Please enter a new email.');
+                    return;
+                }
+                if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(mail) == false){
+                    showError('Invalid email');
+                    return;
+                }
+                http.open('POST', '/action/edit_profile', true);
+                http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+                http.onreadystatechange = function(){
+                    if (this.readyState == 4){
+                        if (this.status == 200 || this.status == 204){
+                            document.getElementById('static_mail').innerHTML = mail;
+                            document.getElementById('static_mail').style.display = 'block';
+                            document.getElementById('change_mail').style.display = 'block';
+                            document.getElementById('edit_mail').style.display = 'none';
+                            document.getElementById('save_mail').style.display = 'none';
+                        }
+                        else{
+                            showError('An error ocurred: ' + this.responseText + ' (' + this.status + ').');
+                        }
+                    }
+                }
+                http.send('uid=<?=$UID?>&mail=' + mail);
+            }
+
+            /**
+             * Shows the password change form.
+             */
+            function changePass(){
+                document.getElementById('static_pass').style.display = 'none';
+                document.getElementById('change_pass').style.display = 'none';
+                document.getElementById('edit_pass_current').value = '';
+                document.getElementById('edit_pass_current').value = '';
+                document.getElementById('edit_pass_new').value = '';
+                document.getElementById('edit_pass_repeat').style.display = 'block';
+                document.getElementById('edit_pass_new').style.display = 'block';
+                document.getElementById('edit_pass_repeat').style.display = 'block';
+                document.getElementById('save_pass').style.display = 'block';
+            }
+
+            /**
+             * Saves a password change.
+             * 
+             * Validates the new password and makes an AJAX request. If
+             * succesfull, hides the password change form.
+             */
+            function savePass(){
+                var http = new XMLHttpRequest();
+                var pass = document.getElementById('edit_pass_new').value;
+                var currentPass = document.getElementById('edit_pass_current').value;
+                var repeatPass = document.getElementById('edit_pass_repeat').value;
+                if (pass.length < 1){
+                    showError('Please enter a new password.');
+                    return;
+                }
+                if (mail.length < 6){
+                    showError('Minimum password length is 6 characters.');
+                    return;
+                }
+                if (currentPass.length < 1){
+                    showError('Please enter your current password.');
+                    return;
+                }
+                if (pass != repeatPass){
+                    showError('Entered passwords do not match.');
+                    return;
+                }
+                http.open('POST', '/action/edit_profile', true);
+                http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+                http.onreadystatechange = function(){
+                    if (this.readyState == 4){
+                        if (this.status == 200 || this.status == 204){
+                            document.getElementById('static_pass').style.display = 'block';
+                            document.getElementById('change_pass').style.display = 'block';
+                            document.getElementById('edit_pass_current').style.display = 'none';
+                            document.getElementById('edit_pass_new').style.display = 'none';
+                            document.getElementById('edit_pass_repeat').style.display = 'none';
+                            document.getElementById('save_pass').style.display = 'none';
+                        }
+                        else{
+                            showError('An error ocurred: ' + this.responseText + ' (' + this.status + ').');
+                        }
+                    }
+                }
+                http.send('uid=<?=$UID?>&pass=' + pass + '&currentPass=' + currentPass);
+            }
+
+            /**
+             * Generates a new random API key.
+             *
+             * Makes a request. On success, updates the API in the UI.
+             */
+            function regenerateApi(){
+                var http = new XMLHttpRequest();
+                http.open('POST', '/action/edit_profile', true);
+                http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+                http.onreadystatechange = function(){
+                    if (this.readyState == 4){
+                        if (this.status == 200 || this.status == 204){
+                            document.getElementById('api_key').innerHTML = this.responseText;
+                        }
+                        else{
+                            showError('An error ocurred: ' + this.responseText + ' (' + this.status + ').');
+                        }
+                    }
+                }
+                http.send('uid=<?=$UID?>&api=1');
+            }
+        </script>
+    </head>
+    <body>
+<?php
+        include __DIR__ . "/inc/header.php";
+?>
+        <section id='monster' class='content'>
+            <h2>
+                <span>
+                    <?=$page->name?>
+                </span>
+            </h2>
+            <article>
+                <table id='profile'>
+                    <tr>
+                        <td class='title'>
+                            Username:
+                        </td>
+                        <td class='value'>
+                            <span>
+                                <?=$page->name?>
+                            </span>
+                        </td>
+                        <td class='action'>
+                            <input onclick='logout();' type='button' value='Logout'/>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class='title'>
+                            eMail:
+                        </td>
+                        <td class='value'>
+                            <span id='static_mail'>
+                                <?=$page->mail?>
+                            </span>
+                            <input id='edit_mail' type='text' value='<?=$page->mail?>'/>
+                        </td>
+                        <td class='action'>
+                            <input id='change_mail' onclick='changeMail();' type='button' value='Change'/>
+                            <input id='save_mail' onclick='saveMail();' type='button' value='Save'/>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class='title'>
+                            API key:
+                        </td>
+                        <td class='value'>
+                            <span id='api_key'>
+                                <?=$page->api_key?>
+                            </span>
+                        </td>
+                        <td class='action'>
+                            <input id='regenerate_api' onclick='regenerateApi();' type='button' value='Regenerate'/>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class='title'>
+                            Password:
+                        </td>
+                        <td class='value'>
+                            <span id='static_pass'>
+                                ********
+                            </span>
+                            <input id='edit_pass_current' type='password' placeholder='Currrent password'/>
+                            <input id='edit_pass_new' type='password' placeholder='New password'/>
+                            <input id='edit_pass_repeat' type='password' placeholder='Repeat password'/>
+                        </td>
+                        <td class='action'>
+                            <input id='change_pass' onclick='changePass();' type='button' value='Change'/>
+                            <input id='save_pass' onclick='savePass();' type='button' value='Save'/>
+                        </td>
+                    </tr>
+                </table>
+            </article>
+        </section> <!-- runes -->
+        <div id='error_window'>
+            <section class='content'>
+                <h2>
+                    <span>
+                        Error
+                    </span>
+                </h2>
+                <article>
+                    <p id='error_msg'>
+                    </p>
+                    <input type='button' value='Close' onclick='closeError();'/>
+                </article>
+            </section>
+        </div>
+
+<?php
+        include __DIR__ . "/inc/footer.php";
+?>
+    </body>
+</html>

+ 78 - 0
application/view/runs.php

@@ -1,6 +1,7 @@
 <?php
     global $AREA_TYPE;
     global $SCENARIO;
+    global $UID;
 ?>
 <!DOCTYPE html>
 <html lang='en'>
@@ -33,6 +34,36 @@
         <meta name='twitter:image' content='<?=$page->icon?>'/>
         <meta name='twitter:url' content='<?=$page->canonical?>'/>
         <meta name='robots' content='index follow'/>
+        <script>
+            function saveForm(id, show){
+                if (show === true){
+                    document.getElementById('save_team').style.display = 'block';
+                    document.getElementById('save_id').value = id;
+                }
+                else{
+                    document.getElementById('save_team').style.display = 'none';
+                }
+            }
+
+            function saveTeam(){
+                var id = document.getElementById('save_id').value;
+                var name = document.getElementById('save_team_name').value;
+                var description = document.getElementById('save_team_description').value;
+                if (name == ''){
+                    alert('Name required')
+                    return;
+                }
+                var xhttp = new XMLHttpRequest();
+                xhttp.onreadystatechange = function() {
+                    if (this.readyState == 4 && this.status == 200) {
+                        saveForm(null, false);
+                    }
+                };
+                xhttp.open("POST", "/action/save_run_team/", true);
+                xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+                xhttp.send('uid=<?=$UID?>&run=' + id + '&name=' + encodeURI(name) + '&description=' + encodeURI(description));
+            }
+        </script>
     </head>
     <body>
 <?php
@@ -50,6 +81,9 @@
 
             </section> <!-- #filters -->
             <section class='content'>
+                <h2>
+                    Runs
+                </h2>
                 <article>
                     <table id='runs'>
                         <tr>
@@ -71,6 +105,7 @@
                         </tr>
 <?php
                         foreach ($page->runs as $run){
+                            $enable_save = true;
 ?>
                             <tr>
                                 <td class='area'>
@@ -108,6 +143,7 @@
                                 <td class='team'>
 <?php
                                     if ($run->helper == 1){
+                                        $enable_save = false;
 ?>
                                         <div class='monster_panel helper'>
                                             <img class='monster' title='Friend/Menstor' src='<?=$path["img"]["currency"]?>socialpoint.png'/>
@@ -116,6 +152,7 @@
                                     }
                                     foreach ($run->party as $party){
                                         if ($party instanceof K_Unit){
+                                            $enable_save = false;
                                             $disabled = "disabled";
                                         }
                                         else{
@@ -233,6 +270,15 @@
                                         </div>
 <?php
                                     }
+?>
+                                </td>
+                                <td class='action'>
+<?php
+                                    if ($enable_save){
+?>
+                                        <img alt='Save Team' class='save_team' onclick='saveForm(<?=$run->id?>, true);' src='<?=$path["img"]["icon"]?>save.png'/>
+<?php
+                                    }
 ?>
                                 </td>
                             </tr>
@@ -242,6 +288,38 @@
                     </table>
                 </article>
             </section>
+            <section id='save_team' class='content'>
+                <h2>
+                    Save run
+                </h2>
+                <article>
+                    <input id='save_id' type='hidden' value=''/>
+                    <table>
+                        <tr>
+                            <td class='save_title'>
+                                Name
+                            </td>
+                            <td class='save_values'>
+                                <input type='text' id='save_team_name' name='name'/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class='save_title'>
+                                Description
+                            </td>
+                            <td class='save_values'>
+                                <textarea id='save_team_description' name='description'></textarea>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td colspan='2'>
+                                <input type='button' value='Save' onClick='saveTeam();'/>
+                                <input type='button' value='Close' onClick='saveForm(null, false);'/>
+                            </td>
+                        </tr>
+                    </table>
+                </article>
+            </section>
 <?php
         include __DIR__ . "/inc/footer.php";
 ?>

+ 709 - 0
application/view/teams.php

@@ -0,0 +1,709 @@
+<?php
+    global $AREA_TYPE;
+    global $SCENARIO;
+    global $DUNGEON;
+    global $LABYRINTH_STAGES;
+    global $DIFFICULTY;
+    global $ELEMENTAL_RIFT_DUNGEON;
+    global $UID;
+?>
+<!DOCTYPE html>
+<html lang='en'>
+    <head>
+        <meta content='text/html; charset=utf-8' http-equiv='content-type'/>
+        <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1'/>
+        <title><?=$page->title?></title>
+        <link rel='shortcut icon' href='<?=$page->favicon?>'/>
+        <!-- CSS files -->
+        <link rel='stylesheet' type='text/css' href='<?=$path["css"]?>ui.css'/>
+        <link rel='stylesheet' type='text/css' href='<?=$path["css"]?>teams.css'/>
+        <link rel='stylesheet' type='text/css' href='<?=$path["css"]?>filters.css'/>
+        <!-- Script files -->
+        <script src="<?=$path["js"]?>ui.js"></script>
+        <script src="<?=$path["js"]?>teams.js"></script>
+        <!-- Meta tags -->
+        <link rel='canonical' href='<?=$page->canonical?>'/>
+        <link rel='author' href='<?=$page->author?>'/>
+        <link rel='publisher' href='<?=$page->author?>'/>
+        <meta name='description' content='<?=$page->description?>'/>
+        <meta property='og:title' content='<?=$page->title?>'/>
+        <meta property='og:url' content='<?=$page->canonical?>'/>
+        <meta property='og:description' content='<?=$page->description?>'/>
+        <meta property='og:image' content='<?=$page->icon?>'/>
+        <meta property='og:site_name' content='<?=$page->name?>'/>
+        <meta property='og:type' content='website'/>
+        <meta property='og:locale' content='en'/>
+        <meta name='twitter:card' content='summary'/>
+        <meta name='twitter:title' content='<?=$page->title?>'/>
+        <meta name='twitter:description' content='<?=$page->description?>'/>
+        <meta name='twitter:image' content='<?=$page->icon?>'/>
+        <meta name='twitter:url' content='<?=$page->canonical?>'/>
+        <meta name='robots' content='index follow'/>
+        <script>
+            units = [];
+            max_units = 0;
+            selected_units = 0;
+            selected_ids = [];
+<?php
+            foreach ($page->units as $unit){
+?>
+                units.push(
+                    {
+                        id: <?=$unit->id?>,
+                        name: "<?=$unit->title?>",
+                        stars: <?=$unit->stars?>,
+                        level: <?=$unit->level?>,
+                        image: '<?=$unit->unit->get_image()?>',
+                        awakens_from: '<?=$unit->unit->awakens_from?>',
+                        awakens_to: '<?=$unit->unit->awakens_to?>',
+                        element: '<?=$unit->unit->element_name?>'
+                    }
+                );
+<?php
+            }
+?>
+            /**
+             * Shows or hiddes the new team panel.
+             * 
+             * @param show True to show, false to hide.
+             */
+            function showNewTeam(show){
+                if (show === true){
+                    document.getElementById('new_team').style.display = 'block';
+                }
+                else{
+                    document.getElementById('new_team').style.display = 'none';
+                }
+            }
+
+            function saveTeam(){
+                if (selected_units > max_units){
+                    alert('Too many units selected.');
+                    return;
+                }
+                var name = document.getElementById('new_team_name').value;
+                var description = document.getElementById('new_team_description').value;
+                var area_type = document.getElementById('new_area_type').options[document.getElementById('new_area_type').selectedIndex].value;
+                var area = null;
+                var difficulty = null;
+                var stage = null;
+                switch (area_type){
+                    case '<?=$AREA_TYPE["SCENARIO"]?>':
+                        area = document.getElementById('new_area_scenario').options[document.getElementById('new_area_scenario').selectedIndex].value;
+                        difficulty = document.getElementById('new_area_difficulty').options[document.getElementById('new_area_difficulty').selectedIndex].value;
+                        stage = document.getElementById('new_area_stage').options[document.getElementById('new_area_stage').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["CAIROS_DUNGEON"]?>':
+                        area = document.getElementById('new_area_cairos_dungeon').options[document.getElementById('new_area_cairos_dungeon').selectedIndex].value;
+                        stage = document.getElementById('new_area_stage').options[document.getElementById('new_area_stage').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["RIFT_DUNGEON"]?>':
+                        area = document.getElementById('new_area_rift_dungeon').options[document.getElementById('new_area_rift_dungeon').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["RIFT_RAID"]?>':
+                        area = document.getElementById('new_area_rift_raid').options[document.getElementById('new_area_rift_raid').selectedIndex].value;
+                        stage = document.getElementById('new_area_stage').options[document.getElementById('new_area_stage').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["ARENA"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["GUILD_WAR"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["GUILD_SIEGE"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["TARTARUS_LABYRINTH"]?>':
+                        difficulty = document.getElementById('new_area_difficulty').options[document.getElementById('new_area_difficulty').selectedIndex].value;
+                        stage = document.getElementById('new_area_stage').options[document.getElementById('new_area_stage').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["TRIAL_OF_ASCENSION"]?>':
+                        difficulty = document.getElementById('new_area_difficulty').options[document.getElementById('new_area_difficulty').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["DIMENSIONAL_HOLE"]?>':
+                        area = document.getElementById('new_area_dimensional_hole').options[document.getElementById('new_area_dimensional_hole').selectedIndex].value;
+                        stage = document.getElementById('new_area_stage').options[document.getElementById('new_area_stage').selectedIndex].value;
+                        break;
+                    case '<?=$AREA_TYPE["DIMENSIONAL_RIFT"]?>':
+                        difficulty = document.getElementById('new_area_difficulty').options[document.getElementById('new_area_difficulty').selectedIndex].value;
+                        break;
+                    default:
+                        alert('Unknown Area');
+                        return;
+                }
+                if (!Number.isInteger(parseInt(area))){
+                    area = 0;
+                }
+                if (!Number.isInteger(parseInt(stage))){
+                    stage = 0;
+                }
+                if (!Number.isInteger(parseInt(difficulty))){
+                    difficulty = 0;
+                }
+
+                var ids = selected_ids.join(',');
+                var xhttp = new XMLHttpRequest();
+                xhttp.onreadystatechange = function() {
+                    if (this.readyState == 4 && this.status == 200) {
+                        location.reload(); 
+                    }
+                };
+                xhttp.open("POST", "/action/new_team/", true);
+                xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+                xhttp.send('uid=<?=$UID?>&ids=' + ids + '&area_type=' + area_type + '&area=' + area + '&difficulty=' + difficulty + '&stage=' + stage + '&name=' + encodeURI(name) + '&description=' + encodeURI(description));
+
+            }
+
+            /**
+             * Searches a unit with the pattern entered in the 'new_unit_id'
+             * field. Shows a clickable list of matching units.
+             */
+            function searchUnit(){
+                key = document.getElementById('new_unit_id').value;
+                var autocomplete = document.getElementById('new_unit_autocomplete');
+                while (autocomplete.firstChild) {
+                    autocomplete.removeChild(autocomplete.firstChild);
+                }
+                if (key.length == 0){
+                    autocomplete.style.display = 'none';
+                }
+                for (var i = 0; i < units.length; i++) {
+                    if (units[i].name.toUpperCase().includes(key.toUpperCase())){
+                        autocomplete.style.display = 'block';
+                        var line = document.createElement('div');
+                        var img = document.createElement('img');
+                        img.setAttribute('src', units[i].image);
+                        var nam = document.createElement('span');
+                        var tex = document.createTextNode(units[i].name + '(' + units[i].stars + '*, Lv. ' + units[i].level + ')');
+                        nam.appendChild(tex);
+                        line.appendChild(img);
+                        line.appendChild(nam);
+                        sid = units[i].id;
+                        line.setAttribute('onclick', 'selectUnit(' + sid + ');');
+                        autocomplete.appendChild(line);
+                    }
+                }
+            }
+
+            /**
+             * Selects a unit from the list and adds it to the new team.
+             * The search list is hidden afterwards.
+             *
+             * @param id Unit id.
+             */
+            function selectUnit(id){
+                var name = null;
+                var stars = null;
+                var level = null;
+                var image = null;
+                var element = null;
+                for (var i = 0; i < units.length; i ++) {
+                    if (units[i].id == id){
+                        name = units[i].name;
+                        stars = units[i].stars;
+                        level = units[i].level;
+                        image = units[i].image;
+                        element = units[i].element;
+                        break;
+                    }
+                }
+                if (name != null){
+                    var monsterPanel = document.createElement('div');
+                    monsterPanel.setAttribute('class', 'monster_panel');
+                    monsterPanel.setAttribute('id', 'monster_panel_' + id);
+                    monsterPanel.setAttribute('onclick', 'deleteUnit(' + id + ')');
+                    var monsterStars = document.createElement('span');
+                    monsterStars.setAttribute('class', 'stars');
+                    for (var i = 0; i < stars; i ++){
+                        var star = document.createElement('img');
+                        // TODO: Handle star color.
+                        star.setAttribute('class', 'star star_purple');
+                        star.setAttribute('src', '<?=$path["img"]["icon"]?>star.png');
+                        monsterStars.appendChild(star);
+                    }
+                    var monsterImage = document.createElement('img');
+                    monsterImage.setAttribute('class', 'monster border_' + element);
+                    monsterImage.setAttribute('src', image);
+                    var monsterLevel = document.createElement('span');
+                    monsterLevel.setAttribute('class', 'level');
+                    monsterLevel.appendChild(document.createTextNode(level));
+                    monsterPanel.appendChild(monsterStars);
+                    monsterPanel.appendChild(monsterImage);
+                    monsterPanel.appendChild(monsterLevel);
+                    var teamPanel = document.getElementById('new_team_units');
+                    teamPanel.appendChild(monsterPanel);
+                    // Hide the selector
+                    var autocomplete = document.getElementById('new_unit_autocomplete');
+                    while (autocomplete.firstChild) {
+                        autocomplete.removeChild(autocomplete.firstChild);
+                    }
+                    autocomplete.style.display = 'none';
+                    document.getElementById('new_unit_id').value = '';
+                    selected_units ++;
+                    if (selected_units >= max_units){
+                        document.getElementById('new_unit').style.display = 'none';
+                    }
+                    else{
+                        document.getElementById('new_unit').style.display = 'block';
+                    }
+                    selected_ids.push(id);
+                }
+            }
+
+            /**
+             * Deletes a unit from the new team form.
+             * 
+             * @param id Unit id.
+             */
+            function deleteUnit(id){
+                var position = -1;
+                for (var i = 0; i < units.length; i ++) {
+                    if (units[i].id == id){
+                        position = i;
+                        break;
+                    }
+                }
+                if (position >= 0 && position < units.length){
+                    units.splice(position, 1);
+                    document.getElementById('new_team_units').removeChild(document.getElementById('monster_panel_' + id));
+                    selected_units --;
+                    if (selected_units >= max_units){
+                        document.getElementById('new_unit').style.display = 'none';
+                    }
+                    else{
+                        document.getElementById('new_unit').style.display = 'block';
+                    }
+                }
+            }
+
+            /**
+             * Shows the stage selector in the new team panel.
+             * 
+             * @param total The number of stages to show.
+             */
+            function populateStage(total){
+                var sel = document.getElementById('new_area_stage');
+                while (sel.firstChild) {
+                    sel.removeChild(sel.firstChild);
+                }
+                var opt;
+                var att;
+                // Empty
+                opt = document.createElement('option');
+                tex = document.createTextNode('');
+                opt.appendChild(tex);
+                sel.appendChild(opt);
+                for (var i = 1; i <= total; i ++){
+                    opt = document.createElement('option');
+                    tex = document.createTextNode('Stage ' + i);
+                    att = document.createAttribute('value');
+                    att.value = i;
+                    opt.setAttributeNode(att);
+                    opt.appendChild(tex);
+                    sel.appendChild(opt);
+                }
+            }
+
+            /**
+             * Shows the difficulty selector in the new team panel.
+             * 
+             * @param labyrinth Indicates if the area type is the Tartarus
+             *        labyrinth. If so, it includes 'Easy'.
+             * @param toa Indicates if the area type is the Tower of
+             *        Ascension. If so, it excludes 'Hell'.
+             */
+            function populateDifficulty(labyrinth = false, toa = false){
+                var sel = document.getElementById('new_area_difficulty');
+                while (sel.firstChild) {
+                    sel.removeChild(sel.firstChild);
+                }
+                var opt;
+                var att;
+                var tex;
+                // Empty
+                opt = document.createElement('option');
+                tex = document.createTextNode('');
+                opt.appendChild(tex);
+                sel.appendChild(opt);
+                // Easy (Labyrinth only)
+                if (labyrinth){
+                    opt = document.createElement('option');
+                    tex = document.createTextNode('Easy difficulty');
+                    att = document.createAttribute('value');
+                    att.value = '<?=$DIFFICULTY["EASY"]?>';
+                    opt.setAttributeNode(att);
+                    opt.appendChild(tex);
+                    sel.appendChild(opt);
+                }
+                // Normal
+                opt = document.createElement('option');
+                tex = document.createTextNode('Normal difficulty');
+                att = document.createAttribute('value');
+                att.value = '<?=$DIFFICULTY["NORMAL"]?>';
+                opt.setAttributeNode(att);
+                opt.appendChild(tex);
+                sel.appendChild(opt);
+                // Hard
+                opt = document.createElement('option');
+                tex = document.createTextNode('Hard difficulty');
+                att = document.createAttribute('value');
+                att.value = '<?=$DIFFICULTY["HARD"]?>';
+                opt.setAttributeNode(att);
+                opt.appendChild(tex);
+                sel.appendChild(opt);
+                // Hell
+                if (!toa){
+                    opt = document.createElement('option');
+                    tex = document.createTextNode('Hell difficulty');
+                    att = document.createAttribute('value');
+                    att.value = '<?=$DIFFICULTY["HELL"]?>';
+                    opt.setAttributeNode(att);
+                    opt.appendChild(tex);
+                    sel.appendChild(opt);
+                }
+            }
+
+            /**
+             * Shows the appropiate area selector when the area type is
+             * selected in the new team panel.
+             */
+            function selectNewAreaType(){
+                var type = document.getElementById('new_area_type').options[document.getElementById('new_area_type').selectedIndex].value;
+                document.getElementById('new_area_scenario').style.display = 'none';
+                document.getElementById('new_area_scenario').selectedIndex = 0;
+                document.getElementById('new_area_cairos_dungeon').style.display = 'none';
+                document.getElementById('new_area_cairos_dungeon').selectedIndex = 0;
+                document.getElementById('new_area_rift_dungeon').style.display = 'none';
+                document.getElementById('new_area_rift_dungeon').selectedIndex = 0;
+                document.getElementById('new_area_tartarus_labyrinth').style.display = 'none';
+                document.getElementById('new_area_tartarus_labyrinth').selectedIndex = 0;
+                document.getElementById('new_area_dimensional_hole').style.display = 'none';
+                document.getElementById('new_area_dimensional_hole').selectedIndex = 0;
+                document.getElementById('new_area_difficulty').style.display = 'none';
+                document.getElementById('new_area_difficulty').selectedIndex = 0;
+                document.getElementById('new_area_stage').style.display = 'none';
+                document.getElementById('new_area_stage').selectedIndex = 0;
+                switch (type){
+                    case '<?=$AREA_TYPE["SCENARIO"]?>':
+                        document.getElementById('new_area_scenario').style.display = 'inline';
+                        max_units = 4;
+                        break;
+                    case '<?=$AREA_TYPE["CAIROS_DUNGEON"]?>':
+                        document.getElementById('new_area_cairos_dungeon').style.display = 'inline';
+                        max_units = 5;
+                        break;
+                    case '<?=$AREA_TYPE["RIFT_DUNGEON"]?>':
+                        document.getElementById('new_area_rift_dungeon').style.display = 'inline';
+                        max_units = 6;
+                        break;
+                    case '<?=$AREA_TYPE["RIFT_RAID"]?>':
+                        document.getElementById('new_area_rift_raid').style.display = 'inline';
+                        max_units = 6
+                        break;
+                    case '<?=$AREA_TYPE["ARENA"]?>':
+                        max_units = 4;
+                        //document.getElementById('new_area_arena').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["GUILD_WAR"]?>':
+                        max_units = 3;
+                        document.getElementById('new_area_guild_war').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["GUILD_SIEGE"]?>':
+                        max_units = 3;
+                        document.getElementById('new_area_guild_siege').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["TARTARUS_LABYRINTH"]?>':
+                        max_units = 5;
+                        document.getElementById('new_area_tartarus_labyrinth').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["TRIAL_OF_ASCENSION"]?>':
+                        max_units = 5;
+                        //document.getElementById('new_area_trial_of_ascension').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["WORLD_BOSS"]?>':
+                        //max_units = 5;
+                        //document.getElementById('new_area_world_boss').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["DIMENSIONAL_HOLE"]?>':
+                        max_units = 4;
+                        document.getElementById('new_area_dimensional_hole').style.display = 'inline';
+                        break;
+                    case '<?=$AREA_TYPE["DIMENSIONAL_RIFT"]?>':
+                        max_units = 5;
+                        //document.getElementById('new_area_dimensional_rift').style.display = 'inline';
+                        break;
+                }
+                if (selected_units >= max_units || type == ''){
+                    document.getElementById('new_unit').style.display = 'none';
+                }
+                else{
+                    document.getElementById('new_unit').style.display = 'block';
+                }
+            }
+
+            /**
+             * Populates the difficulty and stage selectors when an area is
+             * selected in the new team panel.
+             */
+            function selectNewArea(){
+                var type = document.getElementById('new_area_type').options[document.getElementById('new_area_type').selectedIndex].value;
+                document.getElementById('new_area_stage').selectedIndex = 0;
+                document.getElementById('new_area_difficulty').selectedIndex = 0;
+                document.getElementById('new_area_stage').style.display = 'none';
+                document.getElementById('new_area_difficulty').style.display = 'none';
+                switch (type){
+                    case '<?=$AREA_TYPE["SCENARIO"]?>':
+                        document.getElementById('new_area_stage').style.display = 'inline';
+                        document.getElementById('new_area_difficulty').style.display = 'inline';
+                        populateStage(7);
+                        populateDifficulty();
+                        break;
+                    case '<?=$AREA_TYPE["CAIROS_DUNGEON"]?>':
+                        document.getElementById('new_area_stage').style.display = 'inline';
+                        populateStage(10);
+                        populateDifficulty();
+                        break;
+                    case '<?=$AREA_TYPE["RIFT_DUNGEON"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["RIFT_RAID"]?>':
+                        document.getElementById('new_area_stage').style.display = 'inline';
+                        populateStage(5);
+                        break;
+                    case '<?=$AREA_TYPE["ARENA"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["GUILD_WAR"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["GUILD_SIEGE"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["TARTARUS_LABYRINTH"]?>':
+                        document.getElementById('new_area_difficulty').style.display = 'inline';
+                        populateDifficulty(true, false);
+                        break;
+                    case '<?=$AREA_TYPE["TRIAL_OF_ASCENSION"]?>':
+                        document.getElementById('new_area_difficulty').style.display = 'inline';
+                        populateDifficulty(false, true);
+                        break;
+                    case '<?=$AREA_TYPE["WORLD_BOSS"]?>':
+                        break;
+                    case '<?=$AREA_TYPE["DIMENSIONAL_HOLE"]?>':
+                        document.getElementById('new_area_stage').style.display = 'inline';
+                        populateStage(5);
+                        break;
+                    case '<?=$AREA_TYPE["DIMENSIONAL_RIFT"]?>':
+                        break;
+                }
+            }
+        </script>
+    </head>
+    <body>
+<?php
+        include __DIR__ . "/inc/header.php";
+?>
+            <section class='filters'>
+                <h2>
+                    Team filters
+                </h2>
+<?php
+                $filters = $page->filters;
+                include __DIR__ . "/inc/filter_team.php";
+?>
+            </section> <!-- #filters -->
+            <section class='list'>
+                <h2>
+                    Teams
+                </h2>
+                <article>
+                    <table id='teams'>
+                        <tr>
+                            <th>
+                                Team
+                            </th>
+                            <th>
+                                Description
+                            </th>
+                            <th>
+                                Units
+                            </th>
+                        </tr>
+<?php
+                        $odd = "odd";
+                        foreach ($page->teams as $team){
+?>
+                            <tr>
+                                <td class='team <?=$odd?>'>
+                                    <h4>
+                                        <?=$team->name?>
+                                    </h4>
+<?php
+                                    $area = $team->area->name;
+                                    if (intval($team->stage) > 0){
+                                        $area = $area . " - " . $team->stage;
+                                    }
+?>
+                                    <h5>
+                                        <?=$area?>
+                                    </h5>
+                                </td>
+                                <td class='description <?=$odd?>'>
+                                    <?=$team->description?>
+                                </td>
+                                <td class='units <?=$odd?>'>
+<?php
+                                    foreach ($team->unit as $unit){
+?>
+                                        <a href='/<?=$UID?>/monsters/<?=$unit->id?>'>
+                                            <div class='monster_panel'>
+                                                <?=html_unit_panel($unit)?>
+                                            </div>
+                                        </a>
+<?php
+                                    }
+?>
+                                </td>
+                            </tr>
+<?php
+                            if ($odd == ""){
+                                $odd = "odd";
+                            }
+                            else{
+                                $odd = "";
+                            }
+                        }
+?>
+                    </table>
+                    <input type='button' onClick='showNewTeam(true);' value='New Team'/>
+                </article>
+            </section> <!-- .list -->
+
+<?php
+        include __DIR__ . "/inc/footer.php";
+?>
+        <section id='new_team' class='content'>
+            <h2>
+                New team
+            </h2>
+            <article>
+                <table>
+                    <tr>
+                        <td class='new_title'>
+                            Area
+                        </td>
+                        <td class='new_values'>
+                            <select id='new_area_type' name='new_area_type' onChange='selectNewAreaType();'>
+                                <option value=''> </option>
+                                <option value='<?=$AREA_TYPE["SCENARIO"]?>'>Scenario</option>
+                                <option value='<?=$AREA_TYPE["CAIROS_DUNGEON"]?>'>Cairos Dungeon</option>
+                                <option value='<?=$AREA_TYPE["RIFT_DUNGEON"]?>'>Rift Dungeon</option>
+                                <option value='<?=$AREA_TYPE["RIFT_RAID"]?>'>Rift Raid</option>
+                                <option value='<?=$AREA_TYPE["DIMENSIONAL_HOLE"]?>'>Dimensional Hole</option>
+                                <option value='<?=$AREA_TYPE["ARENA"]?>'>Arena</option>
+                                <option value='<?=$AREA_TYPE["GUILD_WAR"]?>'>Guild War</option>
+                                <option value='<?=$AREA_TYPE["GUILD_SIEGE"]?>'>Guild Siege</option>
+                                <option value='<?=$AREA_TYPE["TARTARUS_LABYRINTH"]?>'>Tartarus Labyrinth</option>
+                                <option value='<?=$AREA_TYPE["TRIAL_OF_ASCENSION"]?>'>Trial of Ascension</option>
+                                <!--<option value='<?=$AREA_TYPE["WORLD_BOSS"]?>'>World Boss</option>-->
+                                <option value='<?=$AREA_TYPE["DIMENSIONAL_RIFT"]?>'>Dimensinal Rift</option>
+                            </select>
+                            <select id='new_area_scenario' class='new_area' onChange='selectNewArea();'>
+                                <option value=''> </option>
+                                <option value='<?=$SCENARIO['GAREN_FOREST']?>'>Garen Forest</option>
+                                <option value='<?=$SCENARIO['MT_SIZ']?>'>Mt. Siz</option>
+                                <option value='<?=$SCENARIO['KABIR_RUINS']?>'>Kabir Ruins</option>
+                                <option value='<?=$SCENARIO['MT_WHITE_RAGON']?>'>Mt. White Ragon</option>
+                                <option value='<?=$SCENARIO['TELAIN_FOREST']?>'>Telain Forest</option>
+                                <option value='<?=$SCENARIO['HYDENI_RUINS']?>'>Hydeni Ruins</option>
+                                <option value='<?=$SCENARIO['TAMOR_DESERT']?>'>Tamor Desert</option>
+                                <option value='<?=$SCENARIO['VROFAGUS_RUINS']?>'>Vrofagus Ruins</option>
+                                <option value='<?=$SCENARIO['FAIMON_VOLCANO']?>'>Faimon Volcano</option>
+                                <option value='<?=$SCENARIO['AIDEN_FOREST']?>'>Aiden Forest</option>
+                                <option value='<?=$SCENARIO['FERUN_CASTLE']?>'>Ferun Castle</option>
+                                <option value='<?=$SCENARIO['MT_RUNAR']?>'>Mt. Runar</option>
+                                <option value='<?=$SCENARIO['CHIRUKA_REMAINS']?>'>Charuca Remains</option>
+                            </select>
+                            <select id='new_area_cairos_dungeon' class='new_area' onChange='selectNewArea();'>
+                                <option value=''> </option>
+                                <option value='<?=$DUNGEON['GIANTS_KEEP']?>'>Giant&#39;s Keep</option>
+                                <option value='<?=$DUNGEON['DRAGONS_LAIR']?>'>Dragon&#39;s Lair</option>
+                                <option value='<?=$DUNGEON['NECROPOLIS']?>'>Necropolis</option>
+                                <option value='<?=$DUNGEON['HALL_OF_MAGIC']?>'>Hall of Magic</option>
+                                <option value='<?=$DUNGEON['HALL_OF_FIRE']?>'>Hall of Fire</option>
+                                <option value='<?=$DUNGEON['HALL_OF_WATER']?>'>Hall of Water</option>
+                                <option value='<?=$DUNGEON['HALL_OF_WIND']?>'>Hall of Wind</option>
+                                <option value='<?=$DUNGEON['HALL_OF_LIGHT']?>'>Hall of Light</option>
+                                <option value='<?=$DUNGEON['HALL_OF_DARK']?>'>Hall of Dark</option>
+                            </select>
+                            <select id='new_area_rift_dungeon' class='new_area' onChange='selectNewArea();'>
+                                <option value=''> </option>
+                                <option value='<?=$ELEMENTAL_RIFT_DUNGEON['FIRE_BEAST']?>'>Fire Beast</option>
+                                <option value='<?=$ELEMENTAL_RIFT_DUNGEON['ICE_BEAST']?>'>Ice Beast</option>
+                                <option value='<?=$ELEMENTAL_RIFT_DUNGEON['WIND_BEAST']?>'>Wind Beast</option>
+                                <option value='<?=$ELEMENTAL_RIFT_DUNGEON['LIGHT_BEAST']?>'>Light Beast</option>
+                                <option value='<?=$ELEMENTAL_RIFT_DUNGEON['DARK_BEAST']?>'>Dark Beast</option>
+                            </select>
+                            <select id='new_area_dimensional_hole' class='new_area' onChange='selectNewArea();'>
+                                <option value=''> </option>
+                                <option value='<?=$DUNGEON['FOREST_OF_ROARING_BEASTS']?>'>Forest or Roaring Beasts</option>
+                                <option value='<?=$DUNGEON['KARZHAN_REMAINS_WARBEAR']?>'>Karzhan Remains (Warbear)</option>
+                                <option value='<?=$DUNGEON['KARZHAN_REMAINS_WARBEAR']?>'>Karzhan Remains (Inugami)</option>
+                                <option value='<?=$DUNGEON['SANCTUARY_OF_DREAMING_FAIRIES']?>'>Sanctuary of Dreaming Fairies</option>
+                                <option value='<?=$DUNGEON['ELLUNIA_REMAINS_FAIRY']?>'>Ellunia Remains (Fairy)</option>
+                                <option value='<?=$DUNGEON['ELLUNIA_REMAINS_PIXIE']?>'>Ellunia Remains (Pixie)</option>
+                                <!-- TODO: Lumel -->
+                            </select>
+                            <select id='new_area_tartarus_labyrinth' class='new_area' onChange='selectNewArea();'>
+                                <option value=''> </option>
+                                <option value='<?=$LABYRINTH_STAGES['TARTARUS']?>'>Tartarus</option>
+                                <option value='<?=$LABYRINTH_STAGES['KOTTOS']?>'>Kottos</option>
+                                <option value='<?=$LABYRINTH_STAGES['LEOS']?>'>Leos</option>
+                                <option value='<?=$LABYRINTH_STAGES['AQUILES']?>'>Aquiles</option>
+                                <option value='<?=$LABYRINTH_STAGES['NORMAL']?>'>Normal Stage</option>
+                                <option value='<?=$LABYRINTH_STAGES['RESCUE']?>'>Rescue Stage</option>
+                                <option value='<?=$LABYRINTH_STAGES['EXPLODE']?>'>Explode Stage</option>
+                                <option value='<?=$LABYRINTH_STAGES['COOL_TIME']?>'>Cool Time Stage</option>
+                                <option value='<?=$LABYRINTH_STAGES['SPEED_LIMIT']?>'>Speed Limit Stage</option>
+                                <option value='<?=$LABYRINTH_STAGES['TIME_LIMIT']?>'>Time Limit Stage</option>
+                                <!-- TODO: Lumel -->
+                            </select>
+                            <select id='new_area_difficulty'>
+                            </select>
+                            <select id='new_area_stage'>
+                            </select>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class='new_title'>
+                            Name
+                        </td>
+                        <td class='new_values'>
+                            <input type='text' id='new_team_name' name='name'/>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class='new_title'>
+                            Description
+                        </td>
+                        <td class='new_values'>
+                            <textarea id='new_team_description' name='description'></textarea>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class='new_title'>
+                            Units:
+                            <div class='new_unit' id='new_unit'>
+                                <input id='new_unit_id' onInput='searchUnit();'/>
+                                <div id='new_unit_autocomplete' class='new_unit_autocomplete'>
+                            </div>
+                        </td>
+                        <td class='new_values'>
+                            <div id='new_team_units'>
+                            </div>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td colspan='2'>
+                            <input type='button' value='Save' onClick='saveTeam();'/>
+                            <input type='button' value='Close' onClick='showNewTeam(false);'/>
+                        </td>
+                    </tr>
+                </table>
+            </article>
+        </section>
+    </body>
+</html>

Разница между файлами не показана из-за своего большого размера
+ 568 - 134
install_data/setup.sql


+ 69 - 0
public/css/profile.css

@@ -0,0 +1,69 @@
+table#profile{
+    margin: auto;
+    border-collapse: collapse;
+    border: 0.1em solid #000000;
+}
+
+table#profile td{
+    padding: 0.3em;
+    border-top: 0.1em solid #000000;
+    border-bottom: 0.1em solid #000000;
+}
+
+table#profile td.title{
+    text-align: right;
+    width: 6em;
+}
+
+table#profile td.value{
+    width: 10em;
+    text-align: left;
+}
+
+table#profile td.action{
+    width: 6em;
+    text-align: left;
+}
+
+table#profile td input, span{
+    width: 100%;
+}
+
+input#edit_mail,
+input#edit_pass_current,
+input#edit_pass_new,
+input#edit_pass_repeat,
+input#save_mail,
+input#save_pass{
+    display: none;
+}
+
+input#change_mail,
+span#static_mail,
+span#static_pass,
+span#api_key{
+    display: block;
+}
+
+div#error_window{
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: #00000088;
+    text-align: center;
+    display: none;
+}
+
+div#error_window h2{
+    text-align: left;
+}
+
+div#error_window section{
+    margin: 5em auto auto auto;
+    min-height: 6em;
+    width: 60%;
+    text-align: center;
+}
+

+ 33 - 0
public/css/runs.css

@@ -113,3 +113,36 @@ table#runs td.reward div.item_rune table.rune{
 table#runs td.reward div.item_rune:hover table.rune{
     display: block;
 }
+
+table#runs td.action img.save_team{
+    width: 1em;
+    height: 1em;
+    cursor: pointer;
+}
+
+
+section#save_team{
+    z-index: 1;
+    display: none;
+    position: fixed;
+    width: 80%;
+    height: 70%;
+    margin: 0 10%;
+    top: 10%;
+    filter: drop-shadow(0 0 10em #000000) drop-shadow(0 0 6em #000000);
+}
+
+section#save_team table{
+    width: 100%;
+}
+
+section#save_team table td.save_title{
+    width: 10em;
+    text-align: right;
+    padding-right: 2em;
+}
+
+section#save_team table td.save_values{
+    text-align: left;
+    padding-right: 2em;
+}

+ 125 - 0
public/css/teams.css

@@ -0,0 +1,125 @@
+table#teams{
+    width: 100%;
+    border-collapse: collapse;
+    border: 0.1em solid #000000;
+}
+
+table#teams th{
+    border-top: 0.1em solid #000000;
+    border-bottom: 0.1em solid #000000;
+}
+
+table#teams td{
+    border-bottom: 0.1em solid #000000;
+    text-align: left;
+    padding: 0.2em;
+}
+
+table#teams td.odd{
+    background-color: #00000033;
+}
+
+table#teams td.team h4{
+    margin: 0.2em;
+}
+
+table#teams td.team h5{
+    margin: 0.1em;
+}
+
+table#teams td.units div.monster_panel{
+    font-size: 25%;
+    margin-bottom: -2em;
+}
+
+section#new_team{
+    z-index: 1;
+    display: none;
+    position: fixed;
+    width: 80%;
+    height: 70%;
+    margin: 0 10%;
+    top: 10%;
+    filter: drop-shadow(0 0 10em #000000) drop-shadow(0 0 6em #000000);
+}
+
+section#new_team table{
+    width: 100%;
+}
+
+section#new_team table td.new_title{
+    width: 10em;
+    text-align: right;
+    padding-right: 2em;
+}
+
+section#new_team table td.new_values{
+    text-align: left;
+    padding-right: 2em;
+}
+
+section#new_team select.new_area{
+    display: none;
+}
+
+section#new_team select#new_area_difficulty{
+    display: none;
+}
+
+section#new_team select#new_area_stage{
+    display: none;
+}
+
+section#new_team div#new_team_units{
+    background-color: #00000033;
+    border-radius: 0.5em;
+    height: 5em;
+    text-align: left;
+}
+
+section#new_team div#new_team_units div.monster_panel{
+    font-size: 50%;
+    cursor: not-allowed;
+}
+
+section#new_team div.new_unit{
+    text-align: right;
+    display: none;
+}
+
+section#new_team div.new_unit div.new_unit_autocomplete{
+    border: 0.1em solid #000000;
+    display: none;
+    border-radius: 0.5em;
+    margin: auto auto auto 1em;
+    position: absolute;
+}
+
+section#new_team div.new_unit div.new_unit_autocomplete div{
+    border: 0.05em solid #000000;
+    text-align: left;
+    text-transform: capitalize;
+    font-variant: small-caps;
+    cursor: pointer;
+    color: var(--input-color);
+    font-weight: var(--input-font-weight);
+    padding: var(--input-padding);
+    background: var(--input-background);
+    border: var(--input-border);
+    border-radius: var(--input-border-radius);
+    text-shadow: var(--input-text-shadow);
+    box-shadow: var(--input-box-shadow);
+}
+
+section#new_team div.new_unit div.new_unit_autocomplete div span{
+    vertical-align: middle;
+}
+
+section#new_team div.new_unit div.new_unit_autocomplete div img{
+    height: 1.5em;
+    width: 1.5em;
+    vertical-align: middle;
+    border-radius: 0.1em;
+    border: 0.1em solid #000000;
+}
+

+ 1 - 1
public/css/ui.css

@@ -273,7 +273,7 @@ footer table#footer_table td{
         line-height: 0.9em;
     }
 }
-input, select, a.a_button{
+input, select, a.a_button, textarea{
     font-size: 100%;
     text-transform: capitalize;
     font-variant: small-caps;

+ 1 - 9
public/index.php

@@ -2,15 +2,7 @@
     include __DIR__ . "/../application/config.php";
     include_once($path["controller"]);
 
-    //$request = $_SERVER['REQUEST_URI'];
     $request = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
     $params = explode('/', $request);
-    array_shift($params);
-    $uid = "";
-    if (count($params) > 0 && preg_match("/\d{7}/", $params[0])){
-        $uid = $params[0];
-        array_shift($params);
-    }
-
-    $controller = new Controller($uid, $params);
+    $controller = new Controller($params);
 ?>

Некоторые файлы не были показаны из-за большого количества измененных файлов