Browse Source

Run logs are back. They also log artifact drops now. Actions are now secured. Some small fixes to recent bugs fixed.

Iñigo Valentin 5 years ago
parent
commit
e9c010ff0e

+ 6 - 2
application/API/v1/profile/PUT.php

@@ -1493,11 +1493,15 @@ try{
         case "GetUnitCollection":
             // TODO: The new user/player distinction wont work here
             // No identification in this JSON, get player id from databases
-            $statement = $db->prepare("SELECT uid FROM player WHERE api_key = :api_key;");
+            header("HTTP/1.1 501 Not implemented.");
+            syslog(LOG_INFO, "[APIv1] HTTP/1.1 501 Not implemented.");
+            return 501;
+            
+            $statement = $db->prepare("SELECT id FROM player WHERE api_key = :api_key;");
             $statement->bindValue(":api_key", $key, SQLITE3_TEXT);
             $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
             if ($r){
-                $uid = $r["uid"];
+                $uid = $r["id"];
             }
             else{
                 header("HTTP/1.1 401 Invalid credentials.");

+ 111 - 0
application/API/v1/run/POST.php

@@ -1038,6 +1038,117 @@ function log_cairos_run($db, $id, $player_id, $json){
                 }
                 $statement->execute();
                 break;
+            case INVENTORY_TYPE_ID::ARTIFACT:
+                $artifact = $response->{"changed_item_list"}[0]->{"info"};
+                $statement = $db->prepare("
+                  INSERT INTO run_drop_artifact (
+                    run,
+                    id,
+                    type,
+                    attribute,
+                    archetype,
+                    quality,
+                    value,
+                    efficiency,
+                    max_efficiency,
+                    stat,
+                    stat_value,
+                    effect_1,
+                    effect_1_value,
+                    effect_2,
+                    effect_2_value,
+                    effect_3,
+                    effect_3_value,
+                    effect_4,
+                    effect_4_value
+                  ) VALUES (
+                    :run,
+                    :id,
+                    :type,
+                    :attribute,
+                    :archetype,
+                    :quality,
+                    :value,
+                    :efficiency,
+                    :max_efficiency,
+                    :stat,
+                    :stat_value,
+                    :effect_1,
+                    :effect_1_value,
+                    :effect_2,
+                    :effect_2_value,
+                    :effect_3,
+                    :effect_3_value,
+                    :effect_4,
+                    :effect_4_value
+                  );
+                ");
+                $statement->bindValue(":run", $id, SQLITE3_INTEGER);
+                $statement->bindValue(":id", $artifact->{"rid"}, SQLITE3_INTEGER);
+                $statement->bindValue(":type", $artifact->{"type"}, SQLITE3_INTEGER);
+                $statement->bindValue(":attribute", $artifact->{"attribute"}, SQLITE3_INTEGER);
+                $statement->bindValue(":archetype", $artifact->{"unit_style"}, SQLITE3_INTEGER);
+                $statement->bindValue(":quality", $artifact->{"rank"}, SQLITE3_INTEGER);
+                switch ($artifact->{"rank"}){
+                    case QUALITY_ID::LEGEND:
+                        $statement->bindValue(":value", 37660, SQLITE3_INTEGER);
+                        break;
+                    case QUALITY_ID::HERO:
+                        $statement->bindValue(":value", 25250, SQLITE3_INTEGER);
+                        break;
+                    case QUALITY_ID::RARE:
+                        $statement->bindValue(":value", 18461, SQLITE3_INTEGER);
+                        break;
+                    case QUALITY_ID::MAGIC:
+                        $statement->bindValue(":value", 9330, SQLITE3_INTEGER);
+                        break;
+                    default:
+                        $statement->bindValue(":value", 3870, SQLITE3_INTEGER);
+                }
+                // TODO:
+                //$efficiency = calculate_artifact_efficiency($artifact);
+                //$max_efficiency = calculate_maximum_efficiency($artifact);
+                $statement->bindValue(":efficiency", null, SQLITE3_FLOAT);
+                $statement->bindValue(":max_efficiency", null, SQLITE3_FLOAT);
+                $statement->bindValue(":stat", $artifact->{"pri_effect"}[0], SQLITE3_INTEGER);
+                $statement->bindValue(":stat_value", $artifact->{"pri_effect"}[1], SQLITE3_INTEGER);
+
+                if (sizeof($artifact->{"sec_effects"}) > 0){
+                    $statement->bindValue(":effect_1", $artifact->{"sec_effects"}[0][0], SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_1_value", $artifact->{"sec_effects"}[0][1], SQLITE3_INTEGER);
+                }
+                else{
+                    $statement->bindValue(":effect_1", null, SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_1_value", 0, SQLITE3_INTEGER);
+                }
+                if (sizeof($artifact->{"sec_effects"}) > 1){
+                    $statement->bindValue(":effect_2", $artifact->{"sec_effects"}[1][0]);
+                    $statement->bindValue(":effect_2_value", $artifact->{"sec_effects"}[1][1]);
+                }
+                else{
+                    $statement->bindValue(":effect_2", null, SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_2_value", 0, SQLITE3_INTEGER);
+                }
+                if (sizeof($artifact->{"sec_effects"}) > 2){
+                    $statement->bindValue(":effect_3", $artifact->{"sec_effects"}[2][0], SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_3_value", $artifact->{"sec_effects"}[2][1], SQLITE3_INTEGER);
+                }
+                else{
+                    $statement->bindValue(":effect_3", null, SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_3_value", 0, SQLITE3_INTEGER);
+                }
+                if (sizeof($artifact->{"sec_effects"}) > 3){
+                    $statement->bindValue(":effect_4", $artifact->{"sec_effects"}[3][0], SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_4_value", $artifact->{"sec_effects"}[3][1], SQLITE3_INTEGER);
+                }
+                else{
+                    $statement->bindValue(":effect_4", null, SQLITE3_INTEGER);
+                    $statement->bindValue(":effect_4_value", 0, SQLITE3_INTEGER);
+                }
+                $statement->execute();
+                // Instantiate so efficiencies are calculated.
+                new Artifact($artifact->{"rid"});
+                break;
         }
     }
 

+ 6 - 0
application/Constant.php

@@ -987,6 +987,12 @@ final class INVENTORY_TYPE_ID{
      * Rune.
      */
     const RUNE = 8;
+    
+    /**
+     * Artifact.
+     * @todo Every artifact or an specific type? verify.
+     */
+    const ARTIFACT = 73;
 
     /**
      * Scroll.

+ 9 - 4
application/Controller.php

@@ -80,12 +80,9 @@ class Controller extends Context{
 
         if (
           count($this->params) > 1 && 
-          (
-            strtoupper($this->params[0]) == "ACTION"
-          ) ||
           strtoupper($this->params[0]) == "API"
         ){
-            // For API calls, or login action calls, skip authentication
+            // For API calls, skip authentication and end here.
             return;
         }
 
@@ -121,6 +118,14 @@ class Controller extends Context{
         else{
             // Unauthenticated user. Continue
         }
+        
+        if (
+            count($this->params) > 1 &&
+            strtoupper($this->params[0]) == "ACTION"
+            ){
+                // For ACTION calls, skip user detection and end here.
+                return;
+        }
 
         // Validate URL (check if it has the player ID).
         $this->player = null;

+ 1 - 1
application/action/optimize_get_options.php

@@ -15,7 +15,7 @@
  * Reads the POST parameters looking for the following KEYS:
  *  player: The player ID.
  *  unit: The unit ID.
- *  tuning: {@todo Documentproperly}.
+ *  tuning: {@todo Document properly}.
  *  options: JSON with rune sets.
  *
  * @return int|string a JSON with the runes on success, negative integers on error. 0 if no errors

+ 10 - 3
application/action/rate_team.php

@@ -18,12 +18,19 @@
  *  score: New score.
  * Validates the data and sets the score.
  *
- * @todo: Check that the user can rate this team.
  * @return int 201 on success, HTTP erro status codes on error.
  * @category Action
  */
 function action(){
-    $player = filter_input(INPUT_GET, "player");
+    $player_id = filter_input(INPUT_GET, "player");
+    $statement = get_context()->get_db()->prepare("
+        SELECT count(user) AS c FROM player WHERE id = :player AND user = :user");
+    $statement->bindValue(":player", $player_id, SQLITE3_TEXT);
+    $statement->bindValue(":user", get_context()->get_user()->get_id(), SQLITE3_TEXT);
+    if ($statement->execute()->fetchArray(SQLITE3_ASSOC)["c"] != 1){
+        // The user doesn't own the team.
+        return 401;
+    }
     $team = filter_input(INPUT_GET, "team");
     $score = intval(filter_input(INPUT_GET, "score"));
     $statement = get_context()->get_db()->prepare("
@@ -34,7 +41,7 @@ function action(){
         id = :team;
     ");
     $statement->bindValue(":score", $score, SQLITE3_TEXT);
-    $statement->bindValue(":player", $player, SQLITE3_TEXT);
+    $statement->bindValue(":player", $player_id, SQLITE3_TEXT);
     $statement->bindValue(":team", $team, SQLITE3_TEXT);
     $statement->execute();
     return 201;

+ 89 - 4
application/entity/Artifact.php

@@ -189,6 +189,91 @@ class Artifact extends Entity{
                 $statement->bindValue(':max_efficiency', $this->max_efficiency, SQLITE3_FLOAT);
                 $statement->bindValue(':artifact', $this->id, SQLITE3_INTEGER);
                 $statement->execute();
+                $statement = get_context()->get_db()->prepare("
+                  UPDATE run_drop_artifact
+                  SET
+                    efficiency = :efficiency,
+                    max_efficiency = :max_efficiency
+                  WHERE id = :artifact
+                ");
+                $statement->bindValue(':efficiency', $this->efficiency, SQLITE3_FLOAT);
+                $statement->bindValue(':max_efficiency', $this->max_efficiency, SQLITE3_FLOAT);
+                $statement->bindValue(':artifact', $this->id, SQLITE3_INTEGER);
+                $statement->execute();
+            }
+        }
+        else{
+            $statement = get_context()->get_db()->prepare("
+              SELECT
+                id,
+                type,
+                attribute,
+                archetype,
+                quality,
+                value,
+                efficiency,
+                max_efficiency,
+                effect_1,
+                effect_2,
+                effect_3,
+                effect_4
+              FROM run_drop_artifact
+              WHERE id = :id;
+            ");
+            $statement->bindValue(':id', $id, SQLITE3_TEXT);
+            $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+            if ($r){
+                $this->id = $r["id"];
+                $this->type = $r["type"];
+                if ($r["type"] == ARTIFACT_TYPE::ARCHETYPE){
+                    $this->archetypal = true;
+                }
+                elseif ($r["type"] == ARTIFACT_TYPE::ELEMENT){
+                    $this->elemental = true;
+                }
+                $this->element = $r["attribute"];
+                $this->archetype = $r["archetype"];
+                $this->level = 0;
+                $this->quality = $r["quality"];
+                $this->original_quality = $r["quality"];
+                $this->value = $r["value"];
+                $this->efficiency = $r["efficiency"];
+                $this->max_efficiency = $r["max_efficiency"];
+                $this->locked = false;
+                $this->quality_name = $this->set_quality_name($this->quality);
+                $this->original_quality_name = $this->set_quality_name($this->original_quality);
+                
+                $this->stat = new Artifact_Stat($this->id);
+                if (is_int($r["effect_1"])){
+                    array_push($this->effects, new Artifact_Effect($this->id, ARTIFACT_EFFECT_SLOT_ID::EFFECT_1));
+                }
+                if (is_int($r["effect_2"])){
+                    array_push($this->effects, new Artifact_Effect($this->id, ARTIFACT_EFFECT_SLOT_ID::EFFECT_2));
+                }
+                if (is_int($r["effect_3"])){
+                    array_push($this->effects, new Artifact_Effect($this->id, ARTIFACT_EFFECT_SLOT_ID::EFFECT_3));
+                }
+                if (is_int($r["effect_4"])){
+                    array_push($this->effects, new Artifact_Effect($this->id, ARTIFACT_EFFECT_SLOT_ID::EFFECT_4));
+                }
+                $this->flag_stats = true;
+                
+                // Get efficiencies, or calculate them if not yet set.
+                if ($this->efficiency == null || $this->efficiency == "" || intval($this->efficiency) == 0){
+                    $this->efficiency = $this->calculate_efficiency();
+                    $this->max_efficiency = $this->calculate_maximum_efficiency();
+                    $statement = get_context()->get_db()->prepare("
+                      UPDATE run_drop_artifact
+                      SET
+                        efficiency = :efficiency,
+                        max_efficiency = :max_efficiency
+                      WHERE id = :artifact
+                    ");
+                    $statement->bindValue(':efficiency', $this->efficiency, SQLITE3_FLOAT);
+                    $statement->bindValue(':max_efficiency', $this->max_efficiency, SQLITE3_FLOAT);
+                    $statement->bindValue(':artifact', $this->id, SQLITE3_INTEGER);
+                    $statement->execute();
+                }
             }
         }
     }
@@ -239,13 +324,13 @@ class Artifact extends Entity{
           ";
 
         for ($i = 0; $i <= 3; $i ++){
-            if (sizeof($this->effects) > $i && $this->effects[$i] != null && $this->effects[$i]->value > 0){
-                $value = $this->effects[$i]->value;
-                $value = $this->effects[$i]->value;
+            if (sizeof($this->effects) > $i && $this->effects[$i] != null && $this->effects[$i]->get_value() > 0){
+                $value = $this->effects[$i]->get_value();
+                $value = $this->effects[$i]->get_value();
                 $statement = get_context()->get_db()->prepare($query);
                 $statement->bindValue(':initial', 1, SQLITE3_TEXT);
                 $statement->bindValue(':upgrade', 1, SQLITE3_TEXT);
-                $statement->bindValue(':effect', $this->effects[$i]->effect, SQLITE3_TEXT);
+                $statement->bindValue(':effect', $this->effects[$i]->get_effect(), SQLITE3_TEXT);
                 $r_roll = $statement->execute()->fetchArray(SQLITE3_ASSOC);
                 if ($r_roll ){
                     $min_initial = 0;//$r_initial["min"];

+ 43 - 0
application/entity/Artifact_Effect.php

@@ -92,6 +92,49 @@ class Artifact_Effect extends Entity{
             }
 
         }
+        else{
+            // Not found. Maybe as a run drop?
+            $select = "effect_1 AS effect, effect_1_value AS value";
+            switch ($slot){
+                case ARTIFACT_EFFECT_SLOT_ID::EFFECT_1:
+                    $select = "effect_1 AS effect, effect_1_value AS value";
+                    break;
+                case ARTIFACT_EFFECT_SLOT_ID::EFFECT_2:
+                    $select = "effect_2 AS effect, effect_2_value AS value";
+                    break;
+                case ARTIFACT_EFFECT_SLOT_ID::EFFECT_3:
+                    $select = "effect_3 AS effect, effect_3_value AS value";
+                    break;
+                case ARTIFACT_EFFECT_SLOT_ID::EFFECT_4:
+                    $select = "effect_4 AS effect, effect_4_value AS value";
+                    break;
+            }
+            $statement = get_context()->get_db()->prepare("
+              SELECT $select
+              FROM run_drop_artifact
+              WHERE id = :id
+            ");
+            $statement->bindValue(':id', $artifact, SQLITE3_TEXT);
+            $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+            if ($r){
+                $this->slot = $slot;
+                $this->effect = $r["effect"];
+                $this->value = $r["value"];
+                $this->enchant = false;
+                $this->grind = 0;
+                $statement = get_context()->get_db()->prepare("
+                  SELECT name
+                  FROM effect_artifact
+                  WHERE id = :id;
+                ");
+                $statement->bindValue(':id', $this->effect, SQLITE3_TEXT);
+                $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+                if ($r){
+                    $this->name = str_replace("{}", $this->value, $r["name"]);
+                }
+                
+            }
+        }
     }
 
     /**

+ 28 - 2
application/entity/Artifact_Stat.php

@@ -49,8 +49,7 @@ class Artifact_Stat extends Entity{
             stat,
             value
           FROM artifact_stat
-          WHERE
-            artifact = :artifact
+          WHERE artifact = :artifact
         ");
         $statement->bindValue(':artifact', $artifact_id, SQLITE3_TEXT);
         $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
@@ -69,6 +68,33 @@ class Artifact_Stat extends Entity{
                     break;
             }
         }
+        else{
+            // Not found, maybe as a run drop?
+            $statement = get_context()->get_db()->prepare("
+              SELECT
+                stat,
+                stat_value
+              FROM run_drop_artifact
+              WHERE id = :artifact
+            ");
+            $statement->bindValue(':artifact', $artifact_id, SQLITE3_TEXT);
+            $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+            if ($r){
+                $this->stat = $r["stat"];
+                $this->value = $r["stat_value"];
+                switch ($this->stat){
+                    case ARTIFACT_STAT_ID::HP:
+                        $this->name = "HP";
+                        break;
+                    case ARTIFACT_STAT_ID::ATK:
+                        $this->name = "ATK";
+                        break;
+                    case ARTIFACT_STAT_ID::DEF:
+                        $this->name = "DEF";
+                        break;
+                }
+            }
+        }
     }
     /**
      * Retrieves the artifact stat identifier.

+ 61 - 2
application/entity/Enchantment.php

@@ -158,18 +158,77 @@ class Enchantment extends Entity{
             $this->inmemorial = $r["inmemorial"];
             if ($r["inmemorial"] == 0){
                 $this->rune_set = new Rune_Set($r["rune"]);
-                $this->name = $this->rune_set->get_name() . " " . $r["stat"];
+                $this->name = $this->rune_set->get_name() . " " . $this->stat_name;
                 $this->inmemorial = false;
             }
             else{
                 $this->inmemorial = true;
-                $this->name = "Inmemorial " . $r["stat"];
+                $this->name = "Inmemorial " . $this->stat_name;
             }
             $this->min = $r["min"];
             $this->max = $r["max"];
             $this->value = $r["value"];
             $this->amount = $r["amount"];
         }
+        else{
+            // Not found, maybe as a run drop?
+            $statement = get_context()->get_db()->prepare("
+              SELECT
+                run_drop_enchantment.id AS id,
+                type,
+                run_drop_enchantment.quality AS quality,
+                rune,
+                run_drop_enchantment.stat AS stat,
+                value,
+                enchantment_type.inmemorial AS inmemorial,
+                enchantment_type.ancient AS ancient,
+                enchantment_range.min AS min,
+                enchantment_range.max AS max
+              FROM
+                run_drop_enchantment,
+                enchantment_type,
+                enchantment_range
+              WHERE
+                run = :run AND
+                run_drop_enchantment.type = enchantment_type.id AND
+                enchantment_type.gem = enchantment_range.gem AND
+                enchantment_type.gem = enchantment_range.gem AND
+                enchantment_type.ancient = enchantment_range.ancient AND
+                run_drop_enchantment.quality = enchantment_range.quality AND
+                run_drop_enchantment.stat = enchantment_range.stat;
+            ");
+            $statement->bindValue(':id', $id, SQLITE3_TEXT);
+            $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+            if ($r){
+                $this->player_id = get_context()->get_player()->get_id();
+                if ($r["type"] == ENCHANTMENT_TYPE_ID::ENCHANT_GEM || $r["type"] == ENCHANTMENT_TYPE_ID::INMEMORIAL_GEM){
+                    $this->gem = 1;
+                }
+                else{
+                    $this->gem = 0;
+                }
+                $this->quality = $r["quality"];
+                $this->quality_name = $this->quality_name($r["quality"]);
+                $this->value = 0;
+                $this->stat = $r["stat"];
+                $this->stat_name = $this->stat_name($r["stat"]);
+                $this->ancient = filter_var($r["ancient"], FILTER_VALIDATE_BOOLEAN);
+                $this->inmemorial = filter_var($r["inmemorial"], FILTER_VALIDATE_BOOLEAN);
+                if ($r["inmemorial"] == 0){
+                    $this->rune_set = new Rune_Set($r["rune"]);
+                    $this->name = $this->rune_set->get_name() . " " . $this->stat_name;
+                    $this->inmemorial = false;
+                }
+                else{
+                    $this->inmemorial = true;
+                    $this->name = "Inmemorial " . $this->stat_name;
+                }
+                $this->min = $r["min"];
+                $this->max = $r["max"];
+                $this->value = $r["value"];
+                $this->amount = 1;
+            }
+        }
     }
 
     /**

+ 17 - 1
application/entity/Player.php

@@ -26,6 +26,11 @@ require_once(PATH::ENTITY . "Server.php");
  */
 class Player extends Entity{
 
+    /**
+     * @var int User ID.
+     */
+    private $user;
+    
     /**
      * @var int In-game player ID.
      */
@@ -237,6 +242,7 @@ class Player extends Entity{
     public function __construct($id){
         $statement = get_context()->get_db()->prepare("
           SELECT
+            user,
             id,
             server,
             country,
@@ -276,6 +282,7 @@ class Player extends Entity{
         $statement->bindValue(':id', $id, SQLITE3_TEXT);
         $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
         if ($r){
+            $this->user = $r["user"];
             $this->id = $r["id"];
             $this->server = new Server($r["server"]);
             $this->country = $r["country"];
@@ -536,11 +543,20 @@ class Player extends Entity{
         }
         $this->flag_record = true;
     }
+    
+    /**
+     * Retrieves the user identifier
+     *
+     * @return int User ID.
+     */
+    public function get_user(){
+        return $this->user;
+    }
 
     /**
      * Retrieves the player identifier
      *
-     * @return int Plyaer ID.
+     * @return int Player ID.
      */
     public function get_id(){
         return $this->id;

+ 386 - 392
application/entity/Run.php

@@ -25,145 +25,111 @@ require_once(PATH::ENTITY . "Rune_Set.php");
  *
  * @category Entity
  * @todo Consider artifact drops.
- * @todo Currently not working, fix.
  */
 class Run extends Entity{
-
+    
     /**
      * @var int Owner's player identifier.
      */
-    public $player;
-
+    private $player;
+    
     /**
      * @var int Run identifier.
      */
-    public $id;
-
+    private $id;
+    
     /**
-     * @var DateTime Run start datestamp.
+     * @var DateTime Run start times.
      */
-    public $dtime;
-
+    private $start_time;
+    
     /**
      * @var Area Area.
      */
-    public $area;
-
+    private $area;
+    
     /**
      * @var int Stage.
      */
-    public $stage;
-
+    private $stage;
+    
     /**
      * @var int Difficulty (only scenario and dimension hole)
      */
-    public $difficulty;
-
+    private $difficulty;
+    
     /**
-     * @var bool Win or lost run.
+     * @var bool Won or lost run.
      */
-    public $win;
-
+    private $win;
+    
     /**
      * @var int Clear time, milliseconds.
      */
-    public $time;
-
-    /**
-     * @var int Reward mana.
-     */
-    public $mana = 0;
-
-    /**
-     * @var int Reward energy.
-     */
-    public $energy = 0;
-
-    /**
-     * @var int Reward crystal.
-     */
-    public $crystal = 0;
-
-    /**
-     * @var int Reward Honor Points (Arena).
-     */
-    public $honor_points = 0;
-
-    /**
-     * @var int Reward Guild Points.
-     */
-    public $guild_points = 0;
-
+    private $time;
+    
     /**
      * @var bool Indicates if a frind/mentor helped.
      */
-    public $helper;
-
+    private $helper;
+    
     /**
      * @var int Score in battles that are rated (WB, rift dungeons...).
      */
-    public $score;
-
+    private $score;
+    
     /**
      * @var String Rank in battles that are rated (WB, rift dungeons...).
      */
-    public $rank;
-
+    private $rank;
+    
+    /**
+     * @var bool Internal flag to check if the run parti has been loaded.
+     */
+    private $flag_party = false;
+    
     /**
      * @var Unit[]|Monster[] Units or monsters used.
      * If the unit is still owned, it will be a instance of Unit.
      * If not, it will be a instance of monster.
      */
-    public $party = [];
-
+    private $party = [];
+    
     /**
      * @var int ID of the leader unit or monster.
      */
-    public $leader;
-
+    private $leader;
+    
     /**
      * @var int[] List of IDs of the frontline units or monsters.
      */
-    public $frontline = [];
-
-    /**
-     * @var Item[] Dropped items.
-     */
-    public $item = [];
-
-    /**
-     * @var Monster[] Units dropped.
-     */
-    public $monster = [];
-
-    /**
-     * @var int Monster[] Units whose secret dungeons were found on the run.
-     */
-    public $sd = [];
-
-    /**
-     * @var mixed[] List of dropped summon pieces.
-     *
-     * Formated as:
-     *  [@{see Monster}, amount]
-     */
-    public $unit_piece = [];
-
-    /**
-     * @var int Number of shapeshifting stones dropped;
-     */
-    public $shapeshifting = 0;
-
+    private $frontline = [];
+    
+    
     /**
-     * @var Rune[] List of Runes dropped.
+     * @var bool Internal flag to check if run rewards have been loaded.
      */
-    public $rune = [];
-
+    private $flag_reward = false;
+    
     /**
-     * @var Enchantment[] List of Enchantments dropped.
+     * @var array List of run rewards.
      */
-    public $enchantment = [];
-
+    private $reward = [
+        "mana" => 0,
+        "energy" => 0,
+        "crystal" => 0,
+        "honor_points" => 0,
+        "guild_points" => 0,
+        "item" => [],
+        "monster" => null,
+        "sd" => null,
+        "monster_piece" => [],
+        "shapeshifting" => 0,
+        "rune" => [],
+        "enchantment" => [],
+        "artifact" => []
+    ];
+    
     /**
      * Constructor.
      *
@@ -173,7 +139,7 @@ class Run extends Entity{
      * @param int $id Run identifier.
      */
     public function __construct($id){
-
+        
         $statement = get_context()->get_db()->prepare("
           SELECT
             id,
@@ -201,327 +167,355 @@ class Run extends Entity{
         if ($r){
             $this->id = $r["id"];
             $this->player = $r["player"];
-            $this->dtime = $r["dtime"];
+            $this->start_time = new Datetime();
+            $this->start_time->setTimestamp($r["dtime"]);
             $this->area = new Area($r["area"], $r["area_type"]);
             $this->stage = $r["stage"];
             $this->difficulty = $r["difficulty"];
-            $this->win = $r["win"];
+            $this->win = filter_var($r["win"], FILTER_VALIDATE_BOOLEAN);
             $this->time = $r["time"];
-            $this->mana = $r["mana"];
-            $this->energy = $r["energy"];
-            $this->crystal = $r["crystal"];
-            $this->honor_points = $r["honor_points"];
-            $this->guild_points = $r["guild_points"];
-            $this->helper = $r["helper"];
+            $this->reward["mana"] = $r["mana"];
+            $this->reward["energy"] = $r["energy"];
+            $this->reward["crystal"] = $r["crystal"];
+            $this->reward["honor_points"] = $r["honor_points"];
+            $this->reward["guild_points"] = $r["guild_points"];
+            $this->helper = filter_var($r["helper"], FILTER_VALIDATE_BOOLEAN);
             $this->score = $r["score"];
             $this->rank = $r["rank"];
-            $statement = get_context()->get_db()->prepare("
-                SELECT
-                  unit,
-                  monster,
-                  (SELECT count(id) FROM unit WHERE unit.id = run_party.unit) AS owned,
-                  leader,
-                  front
-                FROM run_party
-                WHERE run = :run
-                ORDER BY
-                  front DESC,
-                  leader DESC;
-            ");
-            $statement->bindValue(':run', $id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                if ($r["owned"] > 0){
-                    array_push($this->party, new Unit($r["unit"], false));
-                }
-                else{
-                    array_push($this->party, new Monster($r["monster"], false));
-                }
-                if ($r["leader"] == 1){
-                    $this->leader = $r["unit"];
-                }
-                if ($r["front"] == 1){
-                    array_push($this->frontline, $r["unit"]);
-                }
-            }
-            $statement = get_context()->get_db()->prepare("
-                SELECT
-                  item,
-                  amount
-                FROM run_drop_item
-                WHERE run = :run;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                $i = new Item($r["item"]);
-                $i->amount =  $r["amount"];
-                array_push($this->item, $i);
-            }
-            $statement = get_context()->get_db()->prepare("
-                SELECT monster
-                FROM run_drop_monster
-                WHERE run = :run;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                array_push($this->monster, new Monster($r["monster"]));
-            }
-            $statement = get_context()->get_db()->prepare("
-                SELECT monster
-                FROM run_drop_sd
-                WHERE run = :run;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                array_push($this->sd, new Monster($r["monster"]));
-            }
-            $statement = get_context()->get_db()->prepare("
-                SELECT
-                  monster,
-                  amount
-                FROM run_drop_monster_pieces
-                WHERE run = :run;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                $i = [
-                    "monster" => new Monster($r["monster"]),
-                    "amount" => $r["amount"]
-                ];
-                array_push($this->unit_piece, $i);
+        }
+    }
+    
+    private function load_party(){
+        $statement = get_context()->get_db()->prepare("
+            SELECT
+              unit,
+              monster,
+              (SELECT count(id) FROM unit WHERE unit.id = run_party.unit) AS owned,
+              leader,
+              front
+            FROM run_party
+            WHERE run = :run
+            ORDER BY
+              front DESC,
+              leader DESC;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            if ($r["owned"] > 0){
+                array_push($this->party, new Unit($r["unit"], false));
             }
-            $statement = get_context()->get_db()->prepare("
-                SELECT amount
-                FROM run_drop_shapeshifting
-                WHERE run = :run;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            $r = $q->fetchArray(SQLITE3_ASSOC);
-            if ($r){
-                $this->shapeshifting = $r["amount"];
+            else{
+                array_push($this->party, new Monster($r["monster"], false));
             }
-            $statement = get_context()->get_db()->prepare("
-                SELECT
-                  id,
-                  type,
-                  slot,
-                  stars,
-                  ancient,
-                  quality,
-                  value,
-                  efficiency,
-                  main_stat,
-                  main_stat_value,
-                  innate_stat,
-                  innate_stat_value,
-                  substat_1,
-                  substat_1_value,
-                  substat_2,
-                  substat_2_value,
-                  substat_3,
-                  substat_3_value,
-                  substat_4,
-                  substat_4_value
-                FROM run_drop_rune
-                WHERE run = :run;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                $rune = new Rune($r["id"]);
-                if (!isset($rune->id)){
-                    $rune->id = $r["id"];
-                    $rune->level = 0;
-                    $rune->assigned_to = null;
-                    $rune->type = new Rune_Set($r["type"]);
-                    $rune->slot = $r["slot"];
-                    $rune->stars = $r["stars"];
-                    $rune->ancient = $r["ancient"];
-                    $rune->quality = $r["quality"];
-                    $rune->original_quality = $r["quality"];
-                    $rune->quality_name = $this->get_rune_quality_name($r["quality"]);
-                    $rune->original_quality_name = $this->get_rune_quality_name($r["quality"]);
-                    $rune->value = $r["value"];
-                    $rune->value = 0;
-                    $rune->efficiency = $r["efficiency"];
-                    // TODO: Calculate?
-                    $rune->max_efficiency = $r["efficiency"];
-                    $rune->main_stat = new Rune_Stat(null, null);
-                    $rune->main_stat->slot = RUNE_STAT_SLOT_ID::MAIN;
-                    $rune->main_stat->stat = $r["main_stat"];
-                    $rune->main_stat->stat_name = $this->get_stat_name($r["main_stat"]);
-                    $rune->main_stat->value = $r["main_stat_value"];
-                    if ($r["innate_stat"] != null){
-                        $rune->innate_stat = new Rune_Stat(null, null);
-                        $rune->innate_stat->slot = RUNE_STAT_SLOT_ID::INNATE;
-                        $rune->innate_stat->stat = $r["innate_stat"];
-                        $rune->innate_stat->stat_name = $this->get_stat_name($r["innate_stat"]);
-                        $rune->innate_stat->value = $r["innate_stat_value"];
-                    }
-                    if ($r["substat_1"] != null){
-                        $substat = new Rune_Stat(null, null);
-                        $substat->slot = RUNE_STAT_SLOT_ID::SUBSTAT_1;
-                        $substat->stat = $r["substat_1"];
-                        $substat->stat_name = $this->get_stat_name($r["substat_1"]);
-                        $substat->value = $r["substat_1_value"];
-                        array_push($rune->substats, $substat);
-                    }
-                    if ($r["substat_2"] != null){
-                        $substat = new Rune_Stat(null, null);
-                        $substat->slot = RUNE_STAT_SLOT_ID::SUBSTAT_2;
-                        $substat->stat = $r["substat_2"];
-                        $substat->stat_name = $this->get_stat_name($r["substat_2"]);
-                        $substat->value = $r["substat_2_value"];
-                        array_push($rune->substats, $substat);
-                    }
-                    if ($r["substat_3"] != null){
-                        $substat = new Rune_Stat(null, null);
-                        $substat->slot = RUNE_STAT_SLOT_ID::SUBSTAT_3;
-                        $substat->stat = $r["substat_3"];
-                        $substat->stat_name = $this->get_stat_name($r["substat_3"]);
-                        $substat->value = $r["substat_3_value"];
-                        array_push($rune->substats, $substat);
-                    }
-                    if ($r["substat_4"] != null){
-                        $substat = new Rune_Stat(null, null);
-                        $substat->slot = RUNE_STAT_SLOT_ID::SUBSTAT_4;
-                        $substat->stat = $r["substat_4"];
-                        $substat->stat_name = $this->get_stat_name($r["substat_4"]);
-                        $substat->value = $r["substat_4_value"];
-                        array_push($rune->substats, $substat);
-                    }
-                }
-                array_push($this->rune, $rune);
+            if ($r["leader"] == 1){
+                $this->leader = $r["unit"];
             }
-            $statement = get_context()->get_db()->prepare("
-              SELECT
-                run_drop_enchantment.id AS id,
-                type,
-                run_drop_enchantment.quality AS quality,
-                rune,
-                run_drop_enchantment.stat AS stat,
-                value,
-                enchantment_type.inmemorial AS inmemorial,
-                enchantment_type.ancient AS ancient,
-                enchantment_range.min AS min,
-                enchantment_range.max AS max
-              FROM
-                run_drop_enchantment,
-                enchantment_type,
-                enchantment_range
-              WHERE
-                run = :run AND
-                run_drop_enchantment.type = enchantment_type.id AND
-                enchantment_type.gem = enchantment_range.gem AND
-                enchantment_type.gem = enchantment_range.gem AND
-                enchantment_type.ancient = enchantment_range.ancient AND
-                run_drop_enchantment.quality = enchantment_range.quality AND
-                run_drop_enchantment.stat = enchantment_range.stat;
-            ");
-            $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
-            $q = $statement->execute();
-            while ($r = $q->fetchArray(SQLITE3_ASSOC)){
-                $enchantment = new Enchantment($r["id"]);
-                if (!isset($enchantment->id)){
-                    $enchantment->id = $r["id"];
-                    if ($r["type"] == ENCHANTMENT_TYPE_ID::ENCHANT_GEM || $r["type"] == ENCHANTMENT_TYPE_ID::INMEMORIAL_GEM){
-                            $enchantment->gem = 1;
-                    }
-                    else{
-                            $enchantment->gem = 0;
-                    }
-                    $enchantment->quality = $r["quality"];
-                    $enchantment->value = 0;
-                    $enchantment->stat = $r["stat"];
-                    $enchantment->ancient = $r["ancient"];
-                    $enchantment->inmemorial = $r["inmemorial"];
-                    $enchantment->rune = new Rune_Set($r["rune"]);
-                    $enchantment->min = $r["min"];
-                    $enchantment->max = $r["max"];
-                    $enchantment->value = $r["value"];
-                    $enchantment->amount = 0;
-                    $enchantment->refresh_names();
-                }
-                else{
-                    $enchantment->amount = 0;
-                }
-                array_push($this->enchantment, $enchantment);
+            if ($r["front"] == 1){
+                array_push($this->frontline, $r["unit"]);
             }
         }
+        $this->flag_party = true;
+    }
+    
+    private function load_reward(){
+        $statement = get_context()->get_db()->prepare("
+            SELECT
+              item,
+              amount
+            FROM run_drop_item
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $item = [
+                "inventory" => new Inventory($r["item"]),
+                "amount" => $r["amount"]
+            ];
+            array_push($this->reward["item"], $item);
+        }
+        $statement = get_context()->get_db()->prepare("
+            SELECT unit AS monster
+            FROM run_drop_unit
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $this->reward["monster"] =  new Monster($r["monster"]);
+        }
+        $statement = get_context()->get_db()->prepare("
+            SELECT unit AS monster
+            FROM run_drop_sd
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $this->reward["sd"] =  new Monster($r["monster"]);
+        }
+        $statement = get_context()->get_db()->prepare("
+            SELECT
+              unit AS monster,
+              amount
+            FROM run_drop_unit_pieces
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $i = [
+                "monster" => new Monster($r["monster"]),
+                "amount" => $r["amount"]
+            ];
+            array_push($this->reward["monster_piece"], $i);
+        }
+        $statement = get_context()->get_db()->prepare("
+            SELECT amount
+            FROM run_drop_shapeshifting
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        $r = $q->fetchArray(SQLITE3_ASSOC);
+        if ($r){
+            $this->reward["shapeshifting"] = $r["amount"];
+        }
+        $statement = get_context()->get_db()->prepare("
+            SELECT id
+            FROM run_drop_rune
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $rune = new Rune($r["id"]);
+            array_push($this->reward["rune"], $rune);
+        }
+        $statement = get_context()->get_db()->prepare("
+            SELECT id
+            FROM run_drop_artifact
+            WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $artifact = new Artifact($r["id"]);
+            array_push($this->reward["artifact"], $artifact);
+        }
+        $statement = get_context()->get_db()->prepare("
+          SELECT id
+          FROM run_drop_enchantment
+          WHERE run = :run;
+        ");
+        $statement->bindValue(':run', $this->id, SQLITE3_TEXT);
+        $q = $statement->execute();
+        while ($r = $q->fetchArray(SQLITE3_ASSOC)){
+            $enchantment = new Enchantment($r["id"]);
+            array_push($this->reward[enchantment], $enchantment);
+        }
+        $this->flag_reward = true;
+    }
+    
+    /**
+     * Retrieves the player identifier.
+     *
+     * @return int Player ID.
+     */
+    public function get_player(){
+        return $this->player;
+    }
+    
+    /**
+     * Retrieves the run identifier.
+     *
+     * @return int Run ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+    
+    /**
+     * Gets the time of the run start.
+     *
+     * If $format is supplied, a string in the desired format will be returned. If not, the raw
+     * DateTime object will e returned.
+     *
+     * @param string $format A date format (optional).
+     * @return DateTime | string The raw date, or a formatted version.
+     */
+    public function get_start_time($format = null){
+        if (null == $format){
+            return $this->start_time;
+        }
+        else{
+            return $this->start_time->format($format);
+        }
+    }
+    
+    /**
+     * Retrieves the area of the run.
+     *
+     * @return Area Run area.
+     */
+    public function get_area(){
+        return $this->area;
+    }
+    
+    /**
+     * Retrieves the stage of the run, if any.
+     *
+     * @return int Stage number, null if none.
+     */
+    public function get_stage(){
+        return $this->stage;
+    }
+    
+    /**
+     * Retrieves the difficulty of the run, if any.
+     *
+     * @see DIFFICULTY_ID
+     * @return int Difficulty ID, null if none.
+     */
+    public function get_difficulty(){
+        return $this->difficulty;
+    }
+    
+    /**
+     * Checks if the run was won.
+     *
+     * @return bool True if voctory, false otherwise.
+     */
+    public function is_win(){
+        return $this->win;
+    }
+    
+    /**
+     * Retrieves the duration of the run in millisecondes.
+     *
+     * @return int Run time.
+     */
+    public function get_time(){
+        return $this->time;
+    }
+    
+    /**
+     * Check if a frind or mentor representative's help was used.
+     *
+     * @return bool True if helper included, false otherwise.
+     */
+    public function had_help(){
+        return $this->helper;
+    }
+    
+    /**
+     * Retrieves the score of the run.
+     *
+     * @return int Run score. Null if it's not a score based run.
+     */
+    public function get_score(){
+        return $this->score;
     }
 
     /**
-     * Gets a quality name frm the mappings.
+     * Retrieves the rank reached on the run.
      *
-     * @param int $quality Quality id.
-     * @return string Quality name (Uppercase)
-     */
-    private function get_rune_quality_name($quality){
-        switch ($quality){
-            case QUALITY_ID::NORMAL:
-                return "NORMAL";
-            case QUALITY_ID::MAGIC:
-                return "MAGIC";
-            case QUALITY_ID::RARE:
-                return "RARE";
-            case QUALITY_ID::HERO:
-                return "HERO";
-            case QUALITY_ID::LEGEND:
-                return "LEGEND";
-            default:
-                return "";
+     * @return string Rank, in human readable form. Null if not a rank based run.
+     */
+    public function get_rank(){
+        return $this->rank;
+    }
+    
+    /**
+     * Retrieves the party used in the run. If one of the units is not still owned, it will get
+     * the base monster instead.
+     *
+     * @return (Unit|Monster)[] Party units or monsters.
+     */
+    public function get_party(){
+        if (! $this->flag_party){
+            $this->load_party();
         }
+        return $this->party;
     }
 
     /**
-     * Gets a stat name from the mappings.
+     * Retrieves the party leader identifier. If the leader unit is not still owned, it will get
+     * the base monster identifierinstead.
      *
-     * @param int $stat Stat id.
-     * @return string Stat name (Uppercase)
-     */
-    private function get_stat_name($stat){
-        switch ($stat){
-            case RUNE_STAT_ID::HP:
-                return "HP";
-                break;
-            case RUNE_STAT_ID::HP_P:
-                return "HP%";
-                break;
-            case RUNE_STAT_ID::ATK:
-                return "ATK";
-                break;
-            case RUNE_STAT_ID::ATK_P:
-                return "ATK%";
-                break;
-            case RUNE_STAT_ID::DEF:
-                return "DEF";
-                break;
-            case RUNE_STAT_ID::DEF_P:
-                return "DEF%";
-                break;
-            case RUNE_STAT_ID::SPD:
-                return "SPD";
-                break;
-            case RUNE_STAT_ID::CRR:
-                return "CRR";
-                break;
-            case RUNE_STAT_ID::CRD:
-                return "CRD";
-                break;
-            case RUNE_STAT_ID::RES:
-                return "RES";
-                break;
-            case RUNE_STAT_ID::ACC:
-                return "ACC";
-                break;
-            default:
-                return "";
+     * @return int Leader unit or monster ID.
+     */
+    public function get_leader(){
+        if (! $this->flag_party){
+            $this->load_party();
         }
+        return $this->leader;
     }
 
+    /**
+     * Retrieves the identifiers of the party members in the frontline. If one of the units is not
+     * still owned, it will get the base monster instead.
+     *
+     * @return int Unit or Monster identifiers.
+     */
+    public function get_frontline(){
+        if (! $this->flag_party){
+            $this->load_party();
+        }
+        return $this->frontline;
+    }
+    
+    /**
+     * Retrieves the list of rewards from the run reward.
+     * 
+     * Here is an example (note that, in most cases, only a few items will be populated for a run):
+     * <code>
+     * [
+     *   "mana" => 2365,                 // Dropped mana, 0 if none
+     *   "energy" => 1,                  // Dropped energy, 0 if none.
+     *   "crystal" => 0,                 // Dropped crystals, 0 if none.
+     *   "honor_points" => 3,            // Dropped honor points, 0 if none.
+     *   "guild_points" => 8,            // Dropped guild points, 0 if none.
+     *   "item" => [                     // Items dropped, empty if none.
+     *     [                               // One item.
+     *       "inventory" => (Inventory),     // Inventory ID.
+     *       "amount => 4                    // Dropped quantity.
+     *     ],
+     *     [                               // One item.
+     *       "inventory" => (Inventory),     // Inventory ID.
+     *       "amount => 3                    // Dropped quantity.
+     *     ],
+     *   ],
+     *   "monster" => (Monster),         // Dropped monster instance, null if none.
+     *   "sd" => (Monster),              // Opened Secret Dungeon monster instance, null if none.
+     *   "monster_piece" => [            // Dropped monster pieces.
+     *     "monster" => (Monster),         // Monster entity
+     *     "amount" => 4                   // Quantity
+     *   ],
+     *   "shapeshifting" => 0,           // Dropped transmogification stones, 0 if none.
+     *   "rune" => [                     // Dropped runes, empty if none.
+     *     (Rune),                         // Rune instance.
+     *     (Rune),                         // Rune instance.
+     *     (Rune)                          // Rune instance.
+     *   ],
+     *   "enchantment" => [              // Dropped enchantments, empty if none.
+     *     (Enchantment),                  // Enchantment instance.
+     *     (Enchantment)                   // Enchantment instance.
+     *   ],
+     *   "artifact" => [                 // Dropped artifacts, empty if none.
+     *     (Artifact),                     // Enchantment instance.
+     *     (Artifact),                     // Artifact instance.
+     *   ]
+     * </code>
+     *
+     * @return mixed[]
+     */
+    public function get_rewards(){
+        if (! $this->flag_reward){
+            $this->load_reward();
+        }
+        return $this->reward;
+    }
+    
 }

+ 97 - 3
application/entity/Rune.php

@@ -198,6 +198,97 @@ class Rune extends Entity{
                 $statement->execute();
             }
         }
+        else{
+            // Not found, maybe reading from a drop?
+            $this->read_drop($id);
+        }
+    }
+    
+    private function read_drop($id){
+        $statement = get_context()->get_db()->prepare("
+          SELECT
+            id,
+            type,
+            slot,
+            stars,
+            ancient,
+            quality,
+            value,
+            efficiency,
+            max_efficiency,
+            main_stat,
+            innate_stat,
+            substat_1,
+            substat_2,
+            substat_3,
+            substat_4
+          FROM run_drop_rune
+          WHERE id = :id;
+        ");
+        $statement->bindValue(':id', $id, SQLITE3_TEXT);
+        $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+        if ($r){
+            $this->id = $r["id"];
+            $this->type = new Rune_Set($r["type"]);
+            $this->slot = $r["slot"];
+            $this->stars = $r["stars"];
+            $this->level = 0;
+            $this->ancient = $r["ancient"];
+            $this->quality = $r["quality"];
+            $this->original_quality = $r["quality"];
+            $this->value = $r["value"];
+            $this->efficiency = $r["efficiency"];
+            $this->max_efficiency = $r["max_efficiency"];
+            $this->locked = false;
+            $this->quality_name = $this->map_quality_name($this->quality);
+            $this->original_quality_name = $this->map_quality_name($this->quality);
+            $stat = new Rune_Stat($this->id, RUNE_STAT_SLOT_ID::MAIN);
+            $this->main_stat = $stat;
+            array_push($this->stats, $stat);
+            if (is_int($r["innate_stat"])){
+                $stat = new Rune_Stat($this->id, RUNE_STAT_SLOT_ID::INNATE);
+                $this->innate_stat = $stat;
+                array_push($this->stats, $stat);
+            }
+            if (is_int($r["substat_1"])){
+                $stat = new Rune_Stat($this->id, RUNE_STAT_SLOT_ID::SUBSTAT_1);
+                array_push($this->substats, $stat);
+                array_push($this->stats, $stat);
+            }
+            if (is_int($r["substat_2"])){
+                $stat = new Rune_Stat($this->id, RUNE_STAT_SLOT_ID::SUBSTAT_2);
+                array_push($this->substats, $stat);
+                array_push($this->stats, $stat);
+            }
+            if (is_int($r["substat_3"])){
+                $stat = new Rune_Stat($this->id, RUNE_STAT_SLOT_ID::SUBSTAT_3);
+                array_push($this->substats, $stat);
+                array_push($this->stats, $stat);
+            }
+            if (is_int($r["substat_4"])){
+                $stat = new Rune_Stat($this->id, RUNE_STAT_SLOT_ID::SUBSTAT_4);
+                array_push($this->substats, $stat);
+                array_push($this->stats, $stat);
+            }
+            $this->flag_stats = true;
+            
+            // Read efficiencies. If not set in DB, calculate and update.
+            if ($this->efficiency == null || $this->efficiency == "" || intval($this->efficiency) == 0){
+                $this->efficiency = $this->calculate_efficiency();
+                $this->max_efficiency = $this->calculate_maximum_efficiency();
+                $statement = get_context()->get_db()->prepare("
+                  UPDATE run_drop_rune
+                  SET
+                    efficiency = :efficiency,
+                    max_efficiency = :max_efficiency
+                  WHERE id = :rune
+                ");
+                $statement->bindValue(':efficiency', $this->efficiency, SQLITE3_FLOAT);
+                $statement->bindValue(':max_efficiency', $this->max_efficiency, SQLITE3_FLOAT);
+                $statement->bindValue(':rune', $this->id, SQLITE3_INTEGER);
+                $statement->execute();
+            }
+        }
     }
 
     /**
@@ -523,6 +614,9 @@ class Rune extends Entity{
      * @return float Efficiency
      */
     private function calculate_efficiency(){
+        if (! $this->flag_stats){
+            $this->load_stats();
+        }
         $eff = [0.0, 0.0, 0.0, 0.0, 0.0];
 
         $query = "
@@ -549,7 +643,7 @@ class Rune extends Entity{
             $statement->bindValue(':initial', 1, SQLITE3_TEXT);
             $statement->bindValue(':upgrade', 0, SQLITE3_TEXT);
             $statement->bindValue(':stars', $this->stars, SQLITE3_TEXT);
-            $statement->bindValue(':stat', $this->innate_stat->stat, SQLITE3_TEXT);
+            $statement->bindValue(':stat', $this->innate_stat->get_stat(), SQLITE3_TEXT);
             $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
             if ($r){
                 $min_initial = 0;//$r["min"];
@@ -566,14 +660,14 @@ class Rune extends Entity{
                 $statement->bindValue(':initial', 1, SQLITE3_TEXT);
                 $statement->bindValue(':upgrade', 0, SQLITE3_TEXT);
                 $statement->bindValue(':stars', $this->stars, SQLITE3_TEXT);
-                $statement->bindValue(':stat', $this->stats[$i]->stat, SQLITE3_TEXT);
+                $statement->bindValue(':stat', $this->stats[$i]->get_stat(), SQLITE3_TEXT);
                 $r_initial = $statement->execute()->fetchArray(SQLITE3_ASSOC);
                 $statement = get_context()->get_db()->prepare($query);
                 $statement->bindValue(':ancient', $this->ancient, SQLITE3_TEXT);
                 $statement->bindValue(':initial', 0, SQLITE3_TEXT);
                 $statement->bindValue(':upgrade', 1, SQLITE3_TEXT);
                 $statement->bindValue(':stars', $this->stars, SQLITE3_TEXT);
-                $statement->bindValue(':stat', $this->stats[$i]->stat, SQLITE3_TEXT);
+                $statement->bindValue(':stat', $this->stats[$i]->get_stat(), SQLITE3_TEXT);
                 $r_upgrade = $statement->execute()->fetchArray(SQLITE3_ASSOC);
                 if ($r_initial && $r_upgrade){
                     $min_initial = 0;//$r_initial["min"];

+ 84 - 11
application/entity/Rune_Stat.php

@@ -63,17 +63,17 @@ class Rune_Stat extends Entity{
     public function __construct($rune, $slot){
 
         $statement = get_context()->get_db()->prepare("
-              SELECT
-                slot,
-                stat,
-                value,
-                enchant,
-                grind
-              FROM rune_stat
-              WHERE
-                rune = :rune AND
-                slot = :slot;
-            ");
+          SELECT
+            slot,
+            stat,
+            value,
+            enchant,
+            grind
+          FROM rune_stat
+          WHERE
+            rune = :rune AND
+            slot = :slot;
+        ");
         $statement->bindValue(':rune', $rune, SQLITE3_TEXT);
         $statement->bindValue(':slot', $slot, SQLITE3_TEXT);
         $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
@@ -124,6 +124,79 @@ class Rune_Stat extends Entity{
                     break;
             }
         }
+        else{
+            // Not found, maybe in run_drop_rune?
+            $select = "main_stat AS stat, main_stat_value AS value";
+            switch ($slot){
+                case RUNE_STAT_SLOT_ID::MAIN:
+                    $select = "main_stat AS stat, main_stat_value AS value";
+                    break;
+                case RUNE_STAT_SLOT_ID::INNATE:
+                    $select = "innate_stat AS stat, innate_stat_value AS value";
+                    break;
+                case RUNE_STAT_SLOT_ID::SUBSTAT_1:
+                    $select = "substat_1 AS stat, substat_1_value AS value";
+                    break;
+                case RUNE_STAT_SLOT_ID::SUBSTAT_2:
+                    $select = "substat_2 AS stat, substat_2_value AS value";
+                    break;
+                case RUNE_STAT_SLOT_ID::SUBSTAT_3:
+                    $select = "substat_3 AS stat, substat_3_value AS value";
+                    break;
+                case RUNE_STAT_SLOT_ID::SUBSTAT_4:
+                    $select = "substat_4 AS stat, substat_4_value AS value";
+                    break;
+            }
+            $statement = get_context()->get_db()->prepare("
+              SELECT $select
+              FROM run_drop_rune
+              WHERE id = :rune;
+            ");
+            $statement->bindValue(':rune', $rune, SQLITE3_TEXT);
+            $r = $statement->execute()->fetchArray(SQLITE3_ASSOC);
+            if ($r){
+                $this->slot = $slot;
+                $this->stat = $r["stat"];
+                $this->value = $r["value"];
+                $this->enchant = false;
+                $this->grind = 0;
+                switch ($this->stat){
+                    case RUNE_STAT_ID::HP:
+                        $this->stat_name = "HP";
+                        break;
+                    case RUNE_STAT_ID::HP_P:
+                        $this->stat_name = "HP%";
+                        break;
+                    case RUNE_STAT_ID::ATK:
+                        $this->stat_name = "ATK";
+                        break;
+                    case RUNE_STAT_ID::ATK_P:
+                        $this->stat_name = "ATK%";
+                        break;
+                    case RUNE_STAT_ID::DEF:
+                        $this->stat_name = "DEF";
+                        break;
+                    case RUNE_STAT_ID::DEF_P:
+                        $this->stat_name = "DEF%";
+                        break;
+                    case RUNE_STAT_ID::SPD:
+                        $this->stat_name = "SPD";
+                        break;
+                    case RUNE_STAT_ID::CRR:
+                        $this->stat_name = "CRR";
+                        break;
+                    case RUNE_STAT_ID::CRD:
+                        $this->stat_name = "CRD";
+                        break;
+                    case RUNE_STAT_ID::RES:
+                        $this->stat_name = "RES";
+                        break;
+                    case RUNE_STAT_ID::ACC:
+                        $this->stat_name = "ACC";
+                        break;
+                }
+            }
+        }
     }
 
     /**

+ 0 - 1
application/page/Runes_Page.php

@@ -294,7 +294,6 @@ class Runes_Page extends Page{
             $statement->bindValue(":slot_$i", $this->filters["SLOT"][$i], SQLITE3_INTEGER);
         }
         $statement->bindValue(":storage_building", BUILDING_ID::MONSTER_STORAGE, SQLITE3_INTEGER);
-error_log($statement->getSQL(true));
         return $statement;
     }
 

+ 2 - 2
application/page/Runs_Page.php

@@ -46,6 +46,7 @@ class Runs_Page extends Page{
      */
     public function __construct(){
         $this->view = PATH::VIEW . "runs.php";
+        $this->parse_filters();
         $result_set = $this->prepare_statement()->execute();
         while ($result = $result_set->fetchArray(SQLITE3_ASSOC)){
             array_push($this->runs, new Run($result["id"]));
@@ -60,7 +61,6 @@ class Runs_Page extends Page{
      * and adds them to the $filters array.
      */
     private function parse_filters(){
-        $this->filters = FILTER::RUN;
         if (isset($_GET["area_type"]) && intval($_GET["area_type"]) > 0){
             $this->filters["AREA_TYPE"] = $_GET["area_type"];
         }
@@ -144,7 +144,7 @@ class Runs_Page extends Page{
         $statement->bindValue(":difficulty", $this->filters["DIFFICULTY"], SQLITE3_INTEGER);
         $statement->bindValue(":date_min", $this->filters["DATE_MIN"], SQLITE3_TEXT);
         $statement->bindValue(":date_max", $this->filters["DATE_MAX"], SQLITE3_TEXT);
-        $statement->bindValue(":limit", $this->filters["LIMIT"], SQLITE3_INTEGER);
+        $statement->bindValue(":limit", $this->filters["NUMBER"], SQLITE3_INTEGER);
         return $statement;
     }
     

+ 57 - 48
application/view/runs.php

@@ -293,31 +293,31 @@
                             </th>
                         </tr>
 <?php
-                        foreach ($page->runs as $run){
+                        foreach ($page->get_runs() as $run){
                             $enable_save = true;
 ?>
-                            <tr id='run_<?=$run->id?>'>
+                            <tr id='run_<?=$run->get_id()?>'>
                                 <td class='area'>
                                     <div class='area'>
-                                        <img title='<?=$run->area->name?>' class='area' src='<?=$run->area->get_image($run->stage)?>'/>
+                                        <img title='<?=$run->get_area()->get_name()?>' class='area' src='<?=$run->get_area()->get_image($run->get_stage())?>'/>
 <?php
-                                        if ($run->stage > 0){
+                                        if ($run->get_stage() > 0){
 ?>
-                                            <span class='stage'><?=$run->stage?></span>
+                                            <span class='stage'><?=$run->get_stage()?></span>
 <?php
                                         }
-                                        if(APPLICATION::valid_id("SCENARIO_ID", $run->area->id)){
+                                        if(APPLICATION::valid_id("SCENARIO_ID", $run->get_area()->get_id())){
 ?>
                                             <span class='difficulty'>
 <?php
                                                 if (
-                                                  $run->area->type == AREA_TYPE_ID::TARTARUS_LABYRINTH ||
-                                                  $run->area->type == AREA_TYPE_ID::DIMENSIONAL_RIFT
+                                                  $run->get_area()->get_type() == AREA_TYPE_ID::TARTARUS_LABYRINTH ||
+                                                  $run->get_area()->get_type() == AREA_TYPE_ID::DIMENSIONAL_RIFT
                                                 ){
-                                                    $difficulty = $run->difficulty;
+                                                    $difficulty = $run->get_difficulty();
                                                 }
                                                 else{
-                                                    $difficulty = $run->difficulty - 1;
+                                                    $difficulty = $run->get_difficulty() - 1;
                                                 }
                                                 for ($i = 0; $i < $difficulty; $i ++){
 ?>
@@ -332,11 +332,11 @@
                                     </div>
                                 </td>
                                 <td class='date'>
-                                    <?=date("Y/m/d H:i:s", $run->dtime);//date_format($date, "Y/m/d H:i:s")?>
+                                    <?=$run->get_start_time("Y/m/d H:i:s")?>
                                 </td>
                                 <td class='team'>
 <?php
-                                    if ($run->helper == 1){
+                                    if ($run->had_help()){
                                         $enable_save = false;
 ?>
                                         <div class='monster_panel helper'>
@@ -346,8 +346,8 @@
                                     }
                                     $prev_front = false;
                                     $front = "undefined";
-                                    foreach ($run->party as $party){
-                                        $front = in_array($party->id, $run->frontline);
+                                    foreach ($run->get_party() as $party){
+                                        $front = in_array($party->get_id(), $run->get_frontline());
                                         if ($front != "undefined" && $prev_front != $front){
 ?>
                                             <hr class='frontline'/>
@@ -362,14 +362,14 @@
                                         }
                                         if ($disabled == ""){
 ?>
-                                            <a target='_blank' href='/<?=get_context()->get_player()->get_id()?>/monster/<?=$party->id?>'>
+                                            <a target='_blank' href='/<?=get_context()->get_player()->get_id()?>/unit/<?=$party->get_id()?>'>
 <?php
                                         }
 ?>
                                         <div class='monster_panel <?=$disabled?>'>
                                             <?=HTML::unit_panel($party)?>
 <?php
-                                                if ($party->id == $run->leader){
+                                                if ($party->get_id() == $run->get_leader()){
 ?>
                                                     <img alt='Leader' class='leader' src='<?=URL::IMG["ICON"]?>leader.png'/>
 <?php
@@ -388,23 +388,23 @@
                                 </td>
                                 <td class='time'>
 <?php
-                                    if ($run->area->type == AREA_TYPE_ID::RIFT_DUNGEON){
+                                    if ($run->get_area()->get_type() == AREA_TYPE_ID::RIFT_DUNGEON){
 ?>
                                         <span class='rank'>
-                                            <?=$run->rank?>
+                                            <?=$run->get_rank()?>
                                         </span>
                                         <span class='score'>
-                                            <?=$run->score?>
+                                            <?=$run->get_score()?>
                                         </span>
 <?php
                                     }
                                     else{
-                                        if ($run->win == 1){
-                                            if ($run->time > 1){
-                                                $s = floor($run->time / 1000);
+                                        if ($run->is_win()){
+                                            if ($run->get_time() > 1){
+                                                $s = floor($run->get_time() / 1000);
                                                 $m = floor($s / 60);
                                                 $s = floor($s % 60);
-                                                $ms = ($run-> time - (1000 * 60 * $m) - (1000 * 60 * $s) / 100);
+                                                $ms = ($run->get_time() - (1000 * 60 * $m) - (1000 * 60 * $s) / 100);
                                                 $s = str_pad($s, 2, '0', STR_PAD_LEFT);
                                                 $ms = str_pad($ms, 2, '0', STR_PAD_LEFT);
 ?>
@@ -422,87 +422,87 @@
                                 </td>
                                 <td class='reward'>
 <?php
-                                    if ($run->energy > 0){
+                                    if ($run->get_rewards()["energy"] > 0){
 ?>
                                         <div class='item'>
                                             <img class='item' title='Energy' src='<?=URL::IMG["CURRENCY"]?>energy.png'/>
-                                            <span class='item'><?=$run->energy?></span>
+                                            <span class='item'><?=$run->get_rewards()["energy"]?></span>
                                         </div>
 <?php
                                     }
-                                    if ($run->mana > 0){
+                                    if ($run->get_rewards()["mana"] > 0){
 ?>
                                         <div class='item'>
                                             <img class='item' title='Mana' src='<?=URL::IMG["CURRENCY"]?>mana.png'/>
-                                            <span class='item'><?=$run->mana?></span>
+                                            <span class='item'><?=$run->get_rewards()["mana"]?></span>
                                         </div>
 <?php
                                     }
-                                    if ($run->crystal > 0){
+                                    if ($run->get_rewards()["crystal"] > 0){
 ?>
                                         <div class='item'>
                                             <img class='item' title='Crystal' src='<?=URL::IMG["CURRENCY"]?>crystal.png'/>
-                                            <span class='item'><?=$run->crystal?></span>
+                                            <span class='item'><?=$run->get_rewards()["crystal"]?></span>
                                         </div>
 <?php
                                     }
-                                    if ($run->honor_points > 0){
+                                    if ($run->get_rewards()["honor_points"] > 0){
 ?>
                                         <div class='item'>
                                             <img class='item' title='Honor Points' src='<?=URL::IMG["CURRENCY"]?>honor.png'/>
-                                            <span class='item'><?=$run->honor_points?></span>
+                                            <span class='item'><?=$run->get_rewards()["honor_points"]?></span>
                                         </div>
 <?php
                                     }
-                                    if ($run->guild_points > 0){
+                                    if ($run->get_rewards()["guild_points"] > 0){
 ?>
                                         <div class='item'>
                                             <img class='item' title='Guild Points' src='<?=URL::IMG["CURRENCY"]?>guildpoint.png'/>
-                                            <span class='item'><?=$run->guild_points?></span>
+                                            <span class='item'><?=$run->get_rewards()["guild_points"]?></span>
                                         </div>
 <?php
                                     }
-                                    foreach ($run->unit as $unit){
+                                    if ($run->get_rewards()["monster"] != null){
 ?>
                                         <div class='item'>
-                                            <img class='item' title='<?=$unit->title?>' src='<?=$unit->get_image()?>'/>
+                                            <img class='item' title='<?=$run->get_rewards()["monster"]->get_name()?>' src='<?=$run->get_rewards()["monster"]->get_image()?>'/>
                                         </div>
 <?php
                                     }
-                                    foreach ($run->item as $item){
+                                    foreach ($run->get_rewards()["item"] as $item){
 ?>
                                         <div class='item'>
-                                            <img class='item' title='<?=$item->name?>' src='<?=$item->get_image()?>'/>
-                                            <span class='item'><?=$item->amount?></span>
+                                            <img class='item' title='<?=$item["inventory"]->get_name()?>' src='<?=$item["inventory"]->get_image()?>'/>
+                                            <span class='item'><?=$item["amount"]?></span>
                                         </div>
 <?php
                                     }
-                                    foreach ($run->sd as $sd){
+                                    if ($run->get_rewards()["sd"] != null){
 ?>
                                         <div class='item'>
-                                            <img class='item' title='<?=$sd->unit->title?>' src='<?=$sd->unit->get_image()?>'/>
+                                            <img class='item' title='<?=$run->get_rewards()["sd"]->get_name()?>' src='<?=$run->get_rewards()["sd"]->get_image()?>'/>
                                             <img class='item' title=' ' src='<?=URL::IMG["MISC"]?>mask-sd.png'/>
                                         </div>
 <?php
                                     }
-                                    foreach ($run->unit_piece as $piece){
+                                    foreach ($run->get_rewards()["monster_piece"] as $piece){
 ?>
                                         <div class='item'>
-                                            <img class='item' title='<?=$piece->unit->title?>' src='<?=$piece->unit->get_image()?>'/>
+                                            <img class='item' title='<?=$piece["monster"]->get_name()?>' src='<?=$piece["monster"]->get_image()?>'/>
                                             <img class='item' title=' ' src='<?=URL::IMG["MISC"]?>mask-piece.png'/>
-                                            <span class='item'><?=$piece->quantity?></span>
+                                            <span class='item'><?=$piece->amount?></span>
                                         </div>
 <?php
                                     }
-                                    if ($run->shapeshifting > 0){
+                                    if ($run->get_rewards()["shapeshifting"] > 0){
 ?>
                                         <div class='item'>
                                             <img class='item' title='Shapeshifting Stones' src='<?=URL::IMG["CURRENCY"]?>costumestone.png'/>
-                                            <span class='item'><?=$run->shapeshifting?></span>
+                                            <span class='item'><?=$run->get_rewards()["shapeshifting"]?></span>
                                         </div>
 <?php
                                     }
-                                    foreach ($run->rune as $rune){
+                                    foreach ($run->get_rewards()["rune"] as $rune){
 ?>
                                         <div class='item item_rune'>
                                             <img class='item' title='Rune' src='<?=URL::IMG["RUNE"]?>base.png'/>
@@ -510,7 +510,16 @@
                                         </div>
 <?php
                                     }
-                                    foreach ($run->enchantment as $enchantment){
+                                    foreach ($run->get_rewards()["artifact"] as $artifact){
+                                        ?>
+                                        <div class='item item_artifact'>
+                                            <img class='item' title='Artifact' src='<?=URL::IMG["ARTIFACT"]?>base.png'/>
+                                            <?=HTML::artifact_table($artifact)?>
+                                        </div>
+<?php
+                                    }
+                                    // TODO: Artifacts
+                                    foreach ($run->get_rewards()["enchantment"] as $enchantment){
 ?>
                                         <div class='item item_enchantment'>
                                             <img class='item' title='Enchantment' src='<?=URL::IMG["GRIND"]?>grind.png'/>
@@ -524,7 +533,7 @@
 <?php
                                     if ($enable_save){
 ?>
-                                        <img alt='Save Team' class='save_team' onclick='saveForm(<?=$run->id?>, true);' src='<?=URL::IMG["ICON"]?>save.png'/>
+                                        <img alt='Save Team' class='save_team' onclick='saveForm(<?=$run->get_id()?>, true);' src='<?=URL::IMG["ICON"]?>save.png'/>
 <?php
                                     }
 ?>

+ 23 - 0
install_data/02_CREATE_DATA.sql

@@ -316,6 +316,29 @@ CREATE TABLE data.run_drop_rune(
     substat_4 INT, --REFERENCES key.rune_stat(id),
     substat_4_value INT CHECK(substat_4_value >= 0)
 );
+
+CREATE TABLE data.run_drop_artifact(
+    run INT NOT NULL REFERENCES run(id),
+    id INT PRIMARY KEY NOT NULL,
+    type INT, --REFERENCES key.archetype(id),
+    attribute INT, --REFERENCES key.element(id),
+    archetype INT, --REFERENCES key.archetype(id),
+    quality INT NOT NULL, --REFERENCES key.quality(id),
+    value INT NOT NULL CHECK(value >= 0),
+    efficiency FLOAT CHECK(efficiency BETWEEN 0 AND 100),
+    max_efficiency FLOAT CHECK(max_efficiency BETWEEN 0 AND 100),
+    stat INT NOT NULL, --REFERENCES key.artifact_stat(id),
+    stat_value INT NOT NULL CHECK(value > 0),
+    effect_1 INT, --REFERENCES key.artifact_effect(id),
+    effect_1_value INT CHECK(effect_1_value >= 0),
+    effect_2 INT, --REFERENCES key.artifact_effect(id),
+    effect_2_value INT CHECK(effect_2_value >= 0),
+    effect_3 INT, --REFERENCES key.artifact_effect(id),
+    effect_3_value INT CHECK(effect_3_value >= 0),
+    effect_4 INT, --REFERENCES key.artifact_effect(id),
+    effect_4_value INT CHECK(effect_4_value >= 0)
+);
+
 CREATE TABLE data.run_drop_item(
     run INT NOT NULL REFERENCES run(id),
     item INT NOT NULL, --REFERENCES key.inventory(id),

+ 12 - 0
public/css/runs.css

@@ -151,6 +151,18 @@ table#runs td.reward div.item_rune:hover table.rune{
     display: block;
 }
 
+table#runs td.reward div.item_artifact table.artifact{
+    display: none;
+    position: absolute;
+    right: 1.8em;
+    bottom: 0;
+    font-size: 75%;
+}
+
+table#runs td.reward div.item_artifact:hover table.artifact{
+    display: block;
+}
+
 table#runs td.reward div.item_enchantment div.enchantment_panel{
     display: none;
     position: absolute;