A web app designed to provide a useful tool for a popular mobile game.
The site consists of an interface that mimics the in-game UI, allowing the user to easily search all available units and build a team. Each unit can be equipped, and clicking on a unit allows the user to view detailed information about it. This allows the user to build a team of units that they may or may not have to make ideal teams for events in game quickly.
- Uses a custom PHP script to parse a large JSON file containing the game's data and inserts all relevant data into a SQL database.
- Uses Ajax/jQuery to retrieve JSON data containing information and artwork about the unit from the game's wiki API.
- Uses the Twitter typeahead.js and underscore.js libraries to query the database to produce search suggestions based on the units in the database.
// import.php
// Takes the Game data in JSON format and parses out the needed info,
// then inserts into the database.
<?php
// require the config file.
require(__DIR__ . "/../includes/config.php");
// check for correct usage
if(count($argv) != 2)
{
exit("Invalid number of arguments. Usage: 'import <path>'\n");
}
// get the file path from the command line argument
$path = $argv[1];
// check if file exists
if(!file_exists($path))
{
exit("($path) doesn't exist. Usage: 'import <path>'\n");
}
// check if file is readable
if(!is_readable($path))
{
exit("($path) is not readable. Usage: 'import <path>'\n");
}
// read the json file contents
$jsondata = file_get_contents($path);
// convert json object to php associative array
$jdata = json_decode($jsondata, true);
// iterate through each unit
foreach($jdata as $key => $data)
{
//print($key . " " . $data["name"] . "\n");
// Arena Type is a int 1-7 based off the ai type
$arenaType;
if (empty($data["ai"])) $arenaType = "0";
else
{
if($data["ai"][0]["target conditions"] === "random" && $data["ai"][0]["target type"] === "party") // type 1
$arenaType = "1";
else if($data["ai"][0]["target conditions"] === "hp_50pr_over" && $data["ai"][0]["target type"] === "enemy") // type 2
$arenaType = "2";
else if($data["ai"][0]["target conditions"] === "random" && $data["ai"][0]["target type"] === "enemy") // type 3
$arenaType = "3";
else if($data["ai"][0]["target conditions"] === "hp_50pr_under" && $data["ai"][0]["target type"] === "enemy") // type 4
$arenaType = "4";
else if($data["ai"][0]["target conditions"] === "hp_50pr_under" && $data["ai"][0]["target type"] === "party") // type 5
$arenaType = "5";
else if($data["ai"][0]["target conditions"] === "hp_25pr_under" && $data["ai"][0]["target type"] === "party") // type 6
$arenaType = "6";
else if($data["ai"][0]["target conditions"] === "hp_75pr_under" && $data["ai"][0]["target type"] === "party") // type 7
$arenaType = "7";
}
//insert the needed info into the unit table
query("INSERT INTO `unit`(`id`, `name`, `rarity`, `cost`, `element`, `hits`, `lord damage range`, `max bc generated`,
`stats`, `imp`, `leader skill`, `extra skill`, `bb`, `sbb`, `ubb`, `ai`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", $key, $data["name"],
$data["rarity"], $data["cost"], strtoupper($data["element"]), $data["hits"], $data["lord damage range"], $data["max bc generated"],
json_encode($data["stats"]), json_encode($data["imp"]), empty($data["leader skill"]) ? "<NONE>" : json_encode($data["leader skill"]),
empty($data["extra skill"]) ? "<NONE>" : json_encode($data["extra skill"]), empty($data["bb"]) ? "<NONE>" : json_encode($data["bb"]),
empty($data["sbb"]) ? "<NONE>" : json_encode($data["sbb"]), empty($data["ubb"]) ? "<NONE>" : json_encode($data["ubb"]), $arenaType);
}
?>
/**
* scripts.js
*
* Global JavaScript.
*/
// Holds the unit data as an object for each of the selected units
var Units = Array(6);
// Holds the sphere data as an object for each of the selected spheres
var Spheres = Array(12);
// The typing for each of the selected units, defaults to lord
var UnitTypes = ["_lord", "_lord", "_lord", "_lord", "_lord", "_lord"];
// The index of the currently selected unit/sphere
var SelectedIndex = 0;
// an array of the unit info window type buttons
var TypeBtns;
// an array of hover areas for extra info
var HoverAreas;
// execute when the DOM is fully loaded
$(function () {
// disable click and drag
$("#Table_01").on("dragstart", function (event) { event.preventDefault(); });
// put each unit and sphere div element into the buttons array
var buttons = [$("#unit1"), $("#unit2"), $("#unit3"), $("#unit4"), $("#unit5"), $("#unit6"),
$("#sphclk1"), $("#sphclk2"), $("#sphclk3"), $("#sphclk4"), $("#sphclk5"), $("#sphclk6"),
$("#sphclk7"), $("#sphclk8"), $("#sphclk9"), $("#sphclk10"), $("#sphclk11"), $("#sphclk12")];
// call OnButtonClick whenever any unit or sphere div is clicked
$.each(buttons, function (index, value) {
value.click(function() {
OnButtonClick(index);
});
});
// add click listener functions to the unit info window buttons
$("#UExit").click(function() {
$("#UInfoWindow").hide("fast");
});
TypeBtns = [$("#UTypeAClk"), $("#UTypeBClk"), $("#UTypeGClk"), $("#UTypeLClk"), $("#UTypeOClk")];
$.each(TypeBtns, function (index, value) {
value.click(function() {
$.each(TypeBtns, function (index, value) {
var e = value["selector"].replace("Clk", "");
$(e).hide(0);
});
var e = value["selector"].replace("Clk", "");
$(e).show(0);
UnitTypes[SelectedIndex] = index === 0 ? "anima" : index === 1 ? "breaker" : index === 2 ? "guardian" : index === 3 ? "_lord" : "oracle";
UnitInfoBox();
});
});
// Add the areas to register hover events
// HoverAreas = [$("#UDef"), $("#UArenaType"), $("#UAtk"), ]
HoverAreas = [
$("#UCost"), $("#UTitle"), $("#UArenaType"), $("#UHps"), $("#UAtk"), $("#UDef"), $("#URec"), $("#UImpHps"), $("#UImpAtk"), $("#UImpDef"), $("#UImpRec"),
$("#ULS"), $("#UES"), $("#UBB"), $("#USBB"), $("#UUBB"), $("#UTypeAClk"), $("#UTypeBClk"), $("#UTypeGClk"), $("#UTypeLClk"), $("#UTypeOClk")
];
$.each(HoverAreas, function(index, value) {
value.mouseenter(function () {
// output raw json data (for now) about the skill to the extra info window
var u = Units[SelectedIndex];
var s = "";
switch (value["selector"]) {
case "#UArenaType":
switch (u["ai"]) {
case "1":
s = "<p>60% chance to use BB/SBB with no requirements. </p><p><strong>Position doesn't matter.</strong></p>";
break;
case "2":
s = "<p>68% chance to use BB/SBB if any enemy is above 50% HPs, 20% chance otherwise.</p><p><strong>Position near the top of the team.</strong></p>";
break;
case "3":
s = "<p>68% chance to use BB/SBB with no requirements.</p><p><strong>Position doesn't matter.</strong></p>";
break;
case "4":
s = "<p>72% chance to use BB/SBB if any enemy is below 50% HPs, 30% chance otherwise.</p><p><strong>Position near the bottom of the team.</strong></p>";
break;
case "5":
s = "<p>84% chance to use BB/SBB if any party member is below 50% HPs, 20% chance otherwise.</p><p><strong>Position doesn't matter.</strong></p>";
break;
case "6":
s = "<p>100% chance to use BB/SBB if any party(healer unit) or enemy(dps unit) is below 25% HPs, 0% chance otherwise.</p><p><strong>Position toward the middle/bottom of the team.</strong></p>";
break;
case "7":
s = "<p>100% chance to use BB/SBB if any party member is below 75% HPs, 0% chance otherwise.</p><p><strong>Position doesn't matter.</strong></p>";
break;
}
break;
case "#ULS":
s = u["leader skill"] === "<NONE>" ? "<strong>NONE</strong>" : JSON.stringify(JSON.parse(u["leader skill"])["effects"], null, 2);
break;
case "#UES":
s = u["extra skill"] === "<NONE>" ? "<strong>NONE</strong>" : JSON.stringify(JSON.parse(u["extra skill"])["effects"], null, 2);
break;
case "#UBB":
s = u["bb"] === "<NONE>" ? "<strong>NONE</strong>" : JSON.stringify(JSON.parse(u["bb"])["levels"][9]["effects"], null, 2);
break;
case "#USBB":
s = u["sbb"] === "<NONE>" ? "<strong>NONE</strong>" : JSON.stringify(JSON.parse(u["sbb"])["levels"][9]["effects"], null, 2);
break;
case "#UUBB":
s = u["ubb"] === "<NONE>" ? "<strong>NONE</strong>" : JSON.stringify(JSON.parse(u["ubb"])["levels"][9]["effects"], null, 2);
break;
}
// output the data and change the font size if needed
$("#UExInfo").css("font-size", (s.length > 800 ? "x-small" : s.length > 565 ? "smaller" : s.length > 200 ? "inherit" : "large")).html(s);
}).mouseleave(function (a) {
$("#UExInfo").html("");
});
});
});
/**
* called when a unit or sphere div is clicked
* toggle the visibility of the search form and info window
*/
function OnButtonClick(index) {
// global copy for lookahead source
SelectedIndex = index;
// configure typeahead
configure();
// change the placeholder text in the search form and erase any current input
if (SelectedIndex < 6)
$("#q").attr("placeholder", "UNIT" + (SelectedIndex + 1) + " : Unit Name, Rarity, Element").val("");
else
$("#q").attr("placeholder", "UNIT" + (SelectedIndex % 6 + 1) + "(SPHERE " + (SelectedIndex < 12 ? 1 : 2) + ") : Sphere Name").val("");
// show the form
$("#form").show("fast");
// give focus to text box
$("#q").focus();
// show the unit info window
UnitInfoBox();
}
/**
* Toggles all unit type buttons off and enables the correct one
*/
function InitTypeButtons() {
$.each(TypeBtns, function (index, value) {
var e = value["selector"].replace("Clk", "");
$(e).hide(0);
});
var t = UnitTypes[SelectedIndex];
var e ="#UType" + (t === "anima" ? "A" : t === "breaker" ? "B" : t === "guardian" ? "G" : t === "_lord" ? "L" : "O" );
$(e).show();
}
/**
* Shows info for the hovered unit or sphere.
*/
function UnitInfoBox() {
// show info about the hovered unit, do nothing if the slot is empty
if (Units[SelectedIndex] === undefined) return;
InitTypeButtons();
var u = Units[SelectedIndex];
$("#UInfoWindow").children("h2").html(u["name"]);
$("#UTitle").html(u["rarity"] + "* " + u["name"]).css("color", u["element"] === "FIRE" ? "red" : u["element"] === "WATER" ? "#008fd8" : u["element"] === "THUNDER" ? "yellow" :
u["element"] === "EARTH" ? "green" : u["element"] === "LIGHT" ? "white" : u["element"] === "DARK" ? "purple" : "white");
$("#UCost").html(u["cost"]);
$("#UArenaType").html(u["ai"]);
$("#ULS").html(u["leader skill"] === "<NONE>" ? "<strong>NONE</strong>" : "<strong>" + JSON.parse(u["leader skill"])["name"] + "</strong> - " + JSON.parse(u["leader skill"])["desc"]);
$("#UES").html(u["extra skill"] === "<NONE>" ? "<strong>NONE</strong>" : "<strong>" + JSON.parse(u["extra skill"])["name"] + "</strong> - " + JSON.parse(u["extra skill"])["desc"]);
$("#UBB").html(u["bb"] === "<NONE>" ? "<strong>NONE</strong>" : "<strong>" + JSON.parse(u["bb"])["name"] + "</strong> - " + JSON.parse(u["bb"])["desc"]);
$("#USBB").html(u["sbb"] === "<NONE>" ? "<strong>NONE</strong>" : "<strong>" + JSON.parse(u["sbb"])["name"] + "</strong> - " + JSON.parse(u["sbb"])["desc"]);
$("#UUBB").html(u["ubb"] === "<NONE>" ? "<strong>NONE</strong>" : "<strong>" + JSON.parse(u["ubb"])["name"] + "</strong> - " + JSON.parse(u["ubb"])["desc"]);
$("#UHps").html((UnitTypes[SelectedIndex] === "anima" ? Math.floor((JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["hp max"] + JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["hp min"]) / 2) :
JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["hp"]) + parseInt(JSON.parse(u["imp"])["max hp"])); //add sphere stats here
$("#UAtk").html((UnitTypes[SelectedIndex] === "breaker" ? (JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["atk max"] + JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["atk min"]) / 2 :
JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["atk"]) + parseInt(JSON.parse(u["imp"])["max atk"])); //add sphere stats here
$("#UDef").html((UnitTypes[SelectedIndex] === "breaker" || UnitTypes[SelectedIndex] === "guardian" || UnitTypes[SelectedIndex] === "oracle" ?
(JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["def max"] + JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["def min"]) / 2 :
JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["def"]) + parseInt(JSON.parse(u["imp"])["max def"])); //add sphere stats here
$("#URec").html((UnitTypes[SelectedIndex] === "anima" || UnitTypes[SelectedIndex] === "guardian" || UnitTypes[SelectedIndex] === "oracle" ?
(JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["rec max"] + JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["rec min"]) / 2 :
JSON.parse(u["stats"])[UnitTypes[SelectedIndex]]["rec"]) + parseInt(JSON.parse(u["imp"])["max rec"])); //add sphere stats here
$("#UImpHps").html(parseInt(JSON.parse(u["imp"])["max hp"]));
$("#UImpAtk").html(parseInt(JSON.parse(u["imp"])["max atk"]));
$("#UImpDef").html(parseInt(JSON.parse(u["imp"])["max def"]));
$("#UImpRec").html(parseInt(JSON.parse(u["imp"])["max rec"]));
// show the window
$("#UInfoWindow").show("fast");
}
/**
* Hides the search box and resets the SelectedElement var
*/
function HideForm() {
$("#form").hide("fast");
}
/**
* Configures typeahead
*/
function configure()
{
// configure typeahead
// https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md
$("#q").typeahead({
autoselect: true,
highlight: true,
minLength: 1
},
{
source: search,
templates: {
empty: "nothing found yet",
suggestion: _.template("<p><%- name %><% if (typeof(rarity) !== 'undefined')" +
"{ %>, <%- rarity %>*<% }" +
" else if (typeof(sphere_type_text) !== 'undefined')" +
"{ %>, <%- sphere_type_text %><% " +
"} %></p>")
}
});
// get image for selected unit from http://bravefrontierglobal.wikia.com/
// using the wikia API http://bravefrontierglobal.wikia.com/api/v1/#!/Articles/getDetails_get_1
// ex.: http://bravefrontierglobal.wikia.com/api.php?action=query&titles=Gazia&prop=imageinfo&iiprop=url&format=json
$("#q").on("typeahead:selected", function (eventObject, suggestion, name) {
if (SelectedIndex < 6) {
// create the query string for the api
var parameters = { query: encodeURI(suggestion.name) + "&abstract=100&width=115&height=299" }
// get the image for this unit
$.getJSON("api.php", parameters)
.done(function (data, textStatus, jqXHR) {
//get the image title and url from the fetched obj
var title = "", thmb = "";
var obj = data["items"][Object.keys(data["items"])[0]];
if (obj) {
title = obj["title"];
thmb = obj["thumbnail"];
}
// insert the data into the correct div
var elem = "#u" + (SelectedIndex + 1).toString();
$(elem).children("img").attr({ src: thmb, alt: title });
elem = "#ut" + (SelectedIndex + 1).toString();
var t = title.split(" ");
var o = "<h5>" + suggestion["rarity"] + "* ";
for (var i = 0; i < t.length - 1; i++) {
o += t[i] + " ";
}
o += "</h5><h4>" + t[t.length - 1] + "</h4>";
$(elem).html(o);
// store the selected unit data in the Units array
Units[SelectedIndex] = suggestion;
// reset unit type
UnitTypes[SelectedIndex] = "_lord";
// hide query box
HideForm();
// show unit info window
UnitInfoBox();
})
.fail(function(jqXHR, textStatus, errorThrown) {
// log error to browser's console
console.log(errorThrown.toString());
});
} else {
// insert the data into the correct div
var uo = (SelectedIndex % 6 + 1).toString() + "_" + (SelectedIndex < 12 ? 1 : 2).toString();
$("#sphTxt" + uo).css("font-size", suggestion["name"].length <= 10 ? "" : suggestion["name"].length < 24 ? "small" : "smaller");
$("#sphTxt" + uo).css("padding-top", suggestion["name"].length <= 12 ? "5px" : suggestion["name"].length < 24 ? "2px" : "0");
$("#sphTxt" + uo).html(suggestion["name"]);
$("#sphOrb" + uo).show("slow");
// store the selected sphere data in the Spheres array
HideForm();
Spheres[SelectedIndex - 6] = suggestion;
}
});
// hide text box when it loses focus
$("#q").focusout(function(eventData) {
HideForm();
});
}
/**
* Searches database for typeahead's suggestions.
*/
function search(query, cb)
{
// get places matching query (asynchronously
var parameters = {
input: query
};
$.getJSON(SelectedIndex < 6 ? "searchUnit.php" : "searchSphere.php", parameters)
.done(function(data, textStatus, jqXHR) {
// call typeahead's callback with search results (i.e., places)
cb(data);
})
.fail(function(jqXHR, textStatus, errorThrown) {
// log error to browser's console
console.log(errorThrown.toString());
});
}