// Constants ////////////////////////////////////////////////////////////////////////////////////////////////////////// var ERROR_DISPLAY_DURATION = 10000; // ms var FADE_DURATION = 250; // ms var SLIDE_DURATION = 600; // ms // Global Variables /////////////////////////////////////////////////////////////////////////////////////////////////// var __inventory = createManifest(); var __loadingCount = 0; var __recipeBooks = []; var __toolsIncluded = false; // Extensions ///////////////////////////////////////////////////////////////////////////////////////////////////////// Array.prototype.copy = function(copyFunction) { var result = []; for (var i = 0; i < this.length; i++) { if (copyFunction !== undefined) { result[i] = copyFunction(this[i]); } else if (typeof(this[i].copy) === "function") { result[i] = this[i].copy(); } else { result[i] = this[i]; } } return result; }; Array.prototype.toHtml = function(delimiter) { var result = ""; var needsDelimiter = false; for (var i = 0; i < this.length; i++) { if (needsDelimiter) result += delimiter; result += (typeof(this[i].toHtml) === "function") ? this[i].toHtml() : this[i].toString(); } return result; } // UI Functions /////////////////////////////////////////////////////////////////////////////////////////////////////// $(function() { $("#recipe_book_load_button").click(onRecipeLoadButtonClicked); $("#crafting_count").change(onCraftingSelectorChanged); $("#tools_included").change(onIncludeToolsChanged); $("#crafting_selector").focus(onCraftingSelectorFocused); $("#inventory").keyup(onInventoryChanged); loadRecipeBook("data/vanilla.json"); loadRecipeBook("data/buildcraft.json"); loadRecipeBook("data/industrial_craft.json"); loadRecipeBook("data/applied_energetics.json"); loadRecipeBook("data/thermal_expansion.json"); }); function onCraftingSelectorChanged() { var recipeName = $("#crafting_selector").val(); var count = parseInt($("#crafting_count option:selected").val()); if (count === undefined) return; if (! hasRecipe(recipeName)) { $("#crafting_output").slideUp(SLIDE_DURATION); } else { var plan = createCraftingPlan(count, recipeName, __toolsIncluded); var result = createCraftingResult(__inventory); plan.alternatives[0].craft(result); $("#missing_materials").html(result.missingMaterials.toHtml()); $("#crafted_items").html(formatStepList(result.stepList)); $("#leftover_materials").html(result.inventory.toHtml()); $("#crafting_output").slideDown(SLIDE_DURATION); } updatePageState(count, recipeName); } function onCraftingSelectorFocused() { $("#crafting_selector").autocomplete("search"); } function onIncludeToolsChanged() { __toolsIncluded = $("#tools_included").is(":checked"); onCraftingSelectorChanged() } function onInventoryChanged() { __inventory.removeAll(); var inventoryText = $("#inventory").val(); if (inventoryText) { var inventoryLines = inventoryText.split("\n"); for (var i = 0; i < inventoryLines.length; i++) { var line = inventoryLines[i].trim(); var match = /^([0-9]+)(.*)$/.exec(line); var count = (match) ? parseInt(match[1]) : 1; var itemName = (match) ? match[2].trim() : line; if (itemName.length > 0) { __inventory.add(count, itemName); } } } onCraftingSelectorChanged(); } function onRecipeBookLoaded() { if (__loadingCount > 0 || __recipeBooks.length === 0) { $("#crafting").fadeOut(FADE_DURATION); } else if (__loadingCount === 0 && __recipeBooks.length > 0) { updateCraftingSelector(); parseUrlParameters(); $("#crafting").fadeIn(FADE_DURATION); } } function onRecipeLoadButtonClicked() { $("#reipebook_load_button").attr("disabled", true); loadRecipeBook($("#recipe_book_url").val(), function() { $("#recipe_book_url").val(""); $("#reipebook_load_button").removeAttr("disabled"); }); } // UI Helper Functions //////////////////////////////////////////////////////////////////////////////////////////////// function loadRecipeBook(recipeBookUrl) { __loadingCount++; $.ajax({ url: recipeBookUrl, dataType: "text", success: function(text) { try { var recipeBook = createRecipeBook(recipeBookUrl, $.parseJSON(text)); __recipeBooks.push(recipeBook); recipeBook.toHtml().insertBefore($("#recipe_book_table tr").last()); } catch (e) { $("#load_error").html(e); $("#load_error").slideDown(FADE_DURATION).delay(ERROR_DISPLAY_DURATION).slideUp(FADE_DURATION); } }, error: function(response, status, error) { $("#load_error").html(status + ": " + error); $("#load_error").slideDown(FADE_DURATION).delay(ERROR_DISPLAY_DURATION).slideUp(FADE_DURATION); }, complete: function() { __loadingCount--; onRecipeBookLoaded(); } }); } function formatStepList(stepList) { var result = ""; for (var i = 0; i < stepList.length; i++) { result += ""; result += "" + stepList[i].count + ""; result += "" + stepList[i].name + ""; result += ""; } return result; } function parseUrlParameters() { var quantity = $.url().param('quantity'); if (quantity !== undefined) { $("#crafting_count").val(quantity); if ($("#crafting_count option:selected").val() === undefined) { $("#crafting_count").val(1); $("#crafting_error").html("Unsupported quantity: " + quantity); $("#crafting_error").fadeIn(FADE_DURATION).delay(ERROR_DISPLAY_DURATION).fadeOut(FADE_DURATION); } } var recipeName = $.url().param('recipeName'); if (recipeName !== undefined) { $("#crafting_selector").val(recipeName); if (hasRecipe(recipeName)) { onCraftingSelectorChanged(); } else if (recipeName.length > 0) { $("#crafting_error").html("Cannot find recipe for " + recipeName); $("#crafting_error").fadeIn(FADE_DURATION).delay(ERROR_DISPLAY_DURATION).fadeOut(FADE_DURATION); } } } function updateCraftingSelector() { $("#crafting_selector").autocomplete({ source: getAllRecipeNames(), delay: 0, minLength: 0, change: onCraftingSelectorChanged, close: onCraftingSelectorChanged, select: onCraftingSelectorChanged, }); } function updatePageState(count, recipeName) { var newTitle = "Crafting Guide"; var newLink = window.location.pathname; var state = {} if (recipeName !== undefined && recipeName.length > 0) { newTitle = "Crafting Guide: " + count + " " + recipeName; newLink = "?count=" + count + "&recipeName=" + encodeURIComponent(recipeName); newLink = newLink.replace(/%20/g, "+"); state = {count: count, name: recipeName}; } document.title = newTitle; history.pushState(state, newTitle, newLink); ga('send', 'pageview'); } // Crafting Node Object /////////////////////////////////////////////////////////////////////////////////////////////// function createCraftingNode(count, recipeName, shouldIncludeTools, alternatives, recipeBooks) { if (shouldIncludeTools !== true) shouldIncludeTools = false; if (recipeBooks === undefined) recipeBooks = __recipeBooks; var object = { alternatives: [], children: [], count: count, isToolNode: false, name: recipeName, parentNode: undefined, recipe: undefined, shouldIncludeTools: shouldIncludeTools }; if (count !== undefined) { if (alternatives === undefined) { alternatives = findAllRecipes(recipeName, recipeBooks); } if (alternatives.length === 0) return undefined; object.recipe = alternatives.shift(); object.alternatives = alternatives; if (shouldIncludeTools) { for (var i = 0; i < object.recipe.tools.length; i++) { var tool = object.recipe.tools[i]; var node = createCraftingNode(1, tool, shouldIncludeTools, undefined, recipeBooks); if (node) { node.isToolNode = true; node.parentNode = object; object.children.push(node); } } } for (var i = 0; i < object.recipe.input.length; i++) { var input = object.recipe.input[i]; var node = createCraftingNode(input.count, input.name, shouldIncludeTools, undefined, recipeBooks); if (node) { node.parentNode = object; object.children.push(node); } } } object.copy = function() { var result = createCraftingNode(); result.alternatives = object.alternatives.copy(); result.count = object.count; result.isToolNode = object.isToolNode; result.name = object.name; result.parentNode = undefined; result.recipe = object.recipe.copy(); result.shouldIncludeTools = object.shouldIncludeTools; result.children = object.children.copy(); for (var i = 0; i < result.children.length; i++) { result.children[i].parentNode = result; } return result; }; object.craft = function(craftingResult) { if (shouldIncludeTools !== true) shouldIncludeTools = false; if (craftingResult === undefined) { craftingResult = createCraftingResult(); } var shouldContinueCrafting = function() { if (object.parentNode === undefined || object.isToolNode) { return ! craftingResult.inventory.contains(object.count, object.name); } else { return craftingResult.missingMaterials.contains(1, object.name); } } var count = 0; while (shouldContinueCrafting()) { if (count++ > 100000) { throw "Too many iterations"; } if (object.shouldIncludeTools) { for (var i = 0; i < object.recipe.tools; i++) { var tool = object.recipe.tools[i]; var hasTool = craftingResult.inventory.contains(1, tool) || craftingResult.missingMaterials.contains(1, tool); if (! hasTool) { craftingResult.addMissingMaterial(tool); } } } for (var i = 0; i < object.recipe.input.length; i++) { var input = object.recipe.input[i]; for (var j = 0; j < input.count; j++) { if (craftingResult.inventory.contains(1, input.name)) { craftingResult.consumeInventory(input.name); } else { craftingResult.addMissingMaterial(input.name); } } } for (var i = 0; i < object.recipe.output.length; i++) { var output = object.recipe.output[i]; craftingResult.addCraftingResult(output.count, output.name); } } for (var i = 0; i < object.children.length; i++) { var child = object.children[i]; child.craft(craftingResult); } if (object.parentNode === undefined) { craftingResult.sortStepsBy(object); } return craftingResult; }; object.createNextAlternative = function(recipeBooks) { if (recipeBooks === undefined) recipeBooks = __recipeBooks; if (object.alternatives.length === 0) return undefined; var nextAlternative = createCraftingNode( object.count, object.name, object.shouldIncludeTools, object.alternatives, recipeBooks); object.alternatives = []; return nextAlternative; }; object.depthFirstTraversal = function(callback) { var shouldContinue = true; for (var i = 0; i < object.children.length; i++) { var child = object.children[i]; shouldContinue = child.depthFirstTraversal(callback); if (shouldContinue === false) return false; } return (callback(object) !== false); }; object.findNodeWithAlternatives = function() { var result = undefined; object.depthFirstTraversal(function(node) { if (node.alternatives.length >= 1) { result = node; return false; } }); return result; }; object.getNodeAt = function(position) { var node = object; for (var i = 0; i < position.length; i++) { node = node.children[position[i]]; } return node; }; object.getPosition = function() { if (object.parentNode === undefined) return []; var position = object.parentNode.getPosition(); for (var i = 0; i < object.parentNode.children.length; i++) { if (object !== object.parentNode.children[i]) continue; position.push(i); break; } return position; }; object.setNodeAt = function(position, node) { if (position.length === 1) { object.children[position[0]] = node; node.parentNode = object; } else { var child = object.children[position[0]]; child.setNodeAt(position.slice(1), node); } }; return object; } // Crafting Plan Object /////////////////////////////////////////////////////////////////////////////////////////////// function createCraftingPlan(count, recipeName, shouldIncludeTools, recipeBooks) { if (shouldIncludeTools !== true) shouldIncludeTools = false; if (recipeBooks === undefined) recipeBooks = __recipeBooks; var object = { count: count, recipeName: recipeName, alternatives: [] }; var nodesToProcess = [ createCraftingNode(count, recipeName, shouldIncludeTools, undefined, recipeBooks) ]; while (nodesToProcess.length > 0) { var node = nodesToProcess.shift(); var choiceNode = node.findNodeWithAlternatives(); if (choiceNode === undefined) { object.alternatives.push(node); } else if (choiceNode === node) { nodesToProcess.push(node); nodesToProcess.push(node.createNextAlternative(recipeBooks)); } else { nodesToProcess.push(node); var position = choiceNode.getPosition(); var alternateNode = choiceNode.createNextAlternative(recipeBooks); var nextAlternative = node.copy(); nextAlternative.setNodeAt(position, alternateNode); nodesToProcess.push(nextAlternative); } } return object; } // Crafting Result Object ///////////////////////////////////////////////////////////////////////////////////////////// function createCraftingResult(startingInventory) { if (startingInventory === undefined) startingInventory = createManifest(); var object = { inventory: startingInventory.copy(), missingMaterials: createManifest(), stepList: [], }; object.addCraftingResult = function(count, name) { var foundStep = false; for (var i = 0; i < object.stepList.length; i++) { var step = object.stepList[i]; if (step.name === name) { step.count += count; foundStep = true; } } if (! foundStep) { object.stepList.unshift(createIngredient(count, name)); } for (var i = 0; i < count; i++) { if (object.missingMaterials.contains(1, name)) { object.missingMaterials.remove(1, name); } else { object.inventory.add(1, name); } } } object.addMissingMaterial = function(name) { object.missingMaterials.add(1, name); } object.consumeInventory = function(name) { object.inventory.remove(1, name); } object.copy = function() { var result = createCraftingResult(); result.inventory = object.inventory.copy(); result.missingMaterials = object.missingMaterials.copy(); result.stepList = object.stepList.copy(); return result; }; object.sortStepsBy = function(rootNode) { var stepCounts = {}; for (var i = 0; i < object.stepList.length; i++) { var step = object.stepList[i]; stepCounts[step.name] = step.count; } var result = []; rootNode.depthFirstTraversal(function(node) { if (stepCounts[node.name] !== undefined) { result.push(createIngredient(stepCounts[node.name], node.name)); delete stepCounts[node.name]; } }); object.stepList = result; }; return object; } // Ingredient Object ////////////////////////////////////////////////////////////////////////////////////////////////// function createIngredient(count, name) { var object = {count: count, name: name}; object.copy = function() { return createIngredient(object.count, object.name); }; return object; } // Manifest Object //////////////////////////////////////////////////////////////////////////////////////////////////// function createManifest() { var object = {materials: {}, length: 0}; object.add = function(count, name) { if (object.materials[name] === undefined) { object.materials[name] = count; object.length += 1; } else { object.materials[name] = object.materials[name] + count; } }; object.contains = function(count, name) { if (object.materials[name] === undefined) return false; return (object.materials[name] >= count); }; object.copy = function(count, name) { var result = createManifest(); for (var name in object.materials) { if (! object.materials.hasOwnProperty(name)) continue; result.add(object.materials[name], name); } return result; } object.remove = function(count, name) { if (object.materials[name] !== undefined) { var availableCount = object.materials[name]; if (availableCount === count) { delete object.materials[name] object.length -= 1; } else if (availableCount > count) { object.materials[name] -= count; } else { throw "Only " + availableCount + " " + name + ", cannot remove " + count; } } else { throw "No " + name + ", cannot remove " + count; } }; object.removeAll = function() { object.materials = {}; object.length = 0; } object.toHtml = function() { var result = ""; var count = 0; for (var name in object.materials) { result += "" + object.materials[name] + "" + name + ""; count += 1 } if (count === 0) result = "Nothing "; return result; }; return object; } // Recipe Object ////////////////////////////////////////////////////////////////////////////////////////////////////// function createRecipe(data) { var object = { name: '', output: [], input: [], tools: [] }; if (data !== undefined) { $.each(data.output, function(index, value) { object.output.push(createIngredient(value[0], value[1])); }); if (object.output.length > 0) { object.name = object.output[0].name; } $.each(data.input, function(index, value) { object.input.push(createIngredient(value[0], value[1])); }); object.tools = data.tools; } object.copy = function() { var result = createRecipe(); result.name = object.name, result.output = object.output.copy(), result.input = object.input.copy(), result.tools = object.tools.copy() return result; }; return object; } // RecipeBook Object ////////////////////////////////////////////////////////////////////////////////////////////////// function createRecipeBook(sourceUrl, data) { var object = { name: data.name, description: data.description, sourceUrl: sourceUrl, recipes: [] }; $(data.recipes).each(function(i, data) { object.recipes.push(createRecipe(data)); }); object.findAllRecipes = function(recipeName) { var targetName = recipeName.toLowerCase(); var result = [] for (var i = 0; i < object.recipes.length; i++) { var recipe = object.recipes[i]; if (recipe.name.toLowerCase() === targetName) { result.push(recipe) } } return result; }; object.getAllRecipeNames = function() { var set = {}; for (var i = 0; i < object.recipes.length; i++) { set[object.recipes[i].name] = true; } var result = []; for (var name in set) { result.push(name); } return result.sort(); }; object.hasRecipe = function(name) { var targetName = name.toLowerCase(); for (var i = 0; i < object.recipes.length; i++) { var recipe = object.recipes[i]; if (recipe.name.toLowerCase() === targetName) return true; } return false; }; object.toHtml = function() { return $( "" + "" + object.name + "" + "" + object.description + "" + "" ); }; return object; } function hasRecipe(name, recipeBooks) { if (recipeBooks === undefined) recipeBooks = __recipeBooks; for (var i = 0; i < recipeBooks.length; i++) { var recipeBook = recipeBooks[i]; if (recipeBook.hasRecipe(name)) return true; } return false; } function findAllRecipes(name, recipeBooks) { if (recipeBooks === undefined) recipeBooks = __recipeBooks; var result = []; for (var i = 0; i < recipeBooks.length; i++) { var recipeBook = recipeBooks[i]; var recipes = recipeBook.findAllRecipes(name); for (var j = 0; j < recipes.length; j++) { result.push(recipes[j]); } } return result; } function getAllRecipeNames(recipeBooks) { if (recipeBooks === undefined) recipeBooks = __recipeBooks; var set = {}; for (var i = 0; i < recipeBooks.length; i++) { var recipeNames = recipeBooks[i].getAllRecipeNames(); for (var j = 0; j < recipeNames.length; j++) { set[recipeNames[j]] = true; } } var result = []; for (var name in set) { result.push(name); } result.sort(); return result; }