From b642c514864b40145e7cd40cbb8a75ee555ca650 Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Fri, 2 Jan 2015 23:03:06 -0800 Subject: [PATCH] wip --- scripts/reformat | 10 +- src/data/minecraft/recipes.json | 4 + .../crafting_table_controller.coffee | 4 +- .../controllers/mod_pack_controller.coffee | 16 +- src/scripts/main.coffee | 10 +- src/scripts/models/crafting_plan.coffee | 81 +++--- src/scripts/models/crafting_table.coffee | 12 +- src/scripts/models/inventory.coffee | 96 ++++---- src/scripts/models/inventory_parser.coffee | 11 +- src/scripts/models/item.coffee | 56 +++-- src/scripts/models/mod_pack.coffee | 113 ++++----- src/scripts/models/mod_version.coffee | 54 ++-- src/scripts/models/mod_version_parser.coffee | 231 +++++++++++------- src/scripts/models/recipe.coffee | 37 +-- src/scripts/models/stack.coffee | 49 ++++ src/scripts/underscore_mixins.coffee | 18 ++ test/crafting_plan.test.coffee | 46 ++-- test/inventory.test.coffee | 58 ++--- test/inventory_parser.test.coffee | 15 +- test/mod_pack.test.coffee | 115 +++++++++ test/mod_version.test.coffee | 95 +++++++ test/mod_version_parser.test.coffee | 151 ++++++++---- test/test.coffee | 4 + 23 files changed, 848 insertions(+), 438 deletions(-) create mode 100644 src/scripts/models/stack.coffee create mode 100644 src/scripts/underscore_mixins.coffee create mode 100644 test/mod_pack.test.coffee create mode 100644 test/mod_version.test.coffee diff --git a/scripts/reformat b/scripts/reformat index 77d5a0184..5ca6e4fa6 100755 --- a/scripts/reformat +++ b/scripts/reformat @@ -4,7 +4,9 @@ global._ = require 'underscore' global.Backbone = require 'backbone' fs = require 'fs' -RecipeBookParser = require '../src/scripts/models/recipe_book_parser' +ModVersionParser = require '../src/scripts/models/mod_version_parser' + +require '../src/scripts/underscore_mixins' ######################################################################################################################## @@ -33,9 +35,9 @@ else text = fs.readFileSync sourceFileName, 'UTF-8' data = JSON.parse text -parser = new RecipeBookParser -recipeBook = parser.parse data -text = parser.unparse recipeBook +parser = new ModVersionParser +modVersion = parser.parse data +text = parser.unparse modVersion if targetFileName is '-' console.log text diff --git a/src/data/minecraft/recipes.json b/src/data/minecraft/recipes.json index 53e53445c..763f8dba3 100644 --- a/src/data/minecraft/recipes.json +++ b/src/data/minecraft/recipes.json @@ -764,6 +764,10 @@ "output": [[16, "Light Gray Stained Glass Pane"]], "input": [[6, "Light Gray Stained Glass"]], "tools": "Crafting Table" + }, { + "output": [[8, "Light Gray Stained Glass"]], + "input": [[8, "Glass"], "Light Gray Dye"], + "tools": "Crafting Table" }, { "output": "Light Gray Wool", "input": ["Light Gray Dye", "Wool"] diff --git a/src/scripts/controllers/crafting_table_controller.coffee b/src/scripts/controllers/crafting_table_controller.coffee index f965fa613..3ca164dfe 100644 --- a/src/scripts/controllers/crafting_table_controller.coffee +++ b/src/scripts/controllers/crafting_table_controller.coffee @@ -30,7 +30,7 @@ module.exports = class CraftingTableController extends BaseController onModPackChanged: -> return unless @rendered - @model.modPack.enableBooksForRecipe @$nameField.val() + @model.modPack.enableModsForItem @$nameField.val() @_updateNameAutocomplete() @_craft() @@ -134,7 +134,7 @@ module.exports = class CraftingTableController extends BaseController onChanged = => @onNameFieldChanged() @$nameField.autocomplete - source: @model.modPack.gatherNames() + source: @model.modPack.gatherRecipeNames() delay: 0 minLength: 0 change: onChanged diff --git a/src/scripts/controllers/mod_pack_controller.coffee b/src/scripts/controllers/mod_pack_controller.coffee index 97eb72ec2..e0b102ae6 100644 --- a/src/scripts/controllers/mod_pack_controller.coffee +++ b/src/scripts/controllers/mod_pack_controller.coffee @@ -15,7 +15,7 @@ module.exports = class ModPackController extends BaseController constructor: (options={})-> if not options.model? then throw new Error "options.model is required" - @_bookControllers = [] + @_modVersionControllers = [] options.templateName = 'mod_pack' super options @@ -23,8 +23,8 @@ module.exports = class ModPackController extends BaseController # BaseController Overrides ##################################################################### onWillRender: -> - if @model.books.length is 0 - @model.loadAllBooks DefaultBookUrls + if @model.modVersions.length is 0 + @model.loadAllModVersions DefaultBookUrls onDidRender: -> @$table = @$('table') @@ -34,12 +34,12 @@ module.exports = class ModPackController extends BaseController @$('table tr:not(:last-child)').remove() return unless @model? - @_bookControllers = [] - for i in [@model.books.length-1..0] by -1 - book = @model.books[i] - controller = new ModVersionController model:book + @_modVersionControllers = [] + for i in [@model.modVersions.length-1..0] by -1 + modVersion = @model.modVersions[i] + controller = new ModVersionController model:modVersion controller.render() - @_bookControllers.push controller + @_modVersionControllers.push controller @$table.prepend controller.$el super diff --git a/src/scripts/main.coffee b/src/scripts/main.coffee index 0a12c5a34..ea1a63d6d 100644 --- a/src/scripts/main.coffee +++ b/src/scripts/main.coffee @@ -1,10 +1,12 @@ ### -# Crafting Guide - main.coffee -# -# Copyright (c) 2014 by Redwood Labs -# All rights reserved. +Crafting Guide - main.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. ### +require './underscore_mixins' + views = require './views' Logger = require './logger' CraftingGuideRouter = require './crafting_guide_router' diff --git a/src/scripts/models/crafting_plan.coffee b/src/scripts/models/crafting_plan.coffee index 0c04707f5..379e7537b 100644 --- a/src/scripts/models/crafting_plan.coffee +++ b/src/scripts/models/crafting_plan.coffee @@ -29,11 +29,18 @@ module.exports = class CraftingPlan return this craft: (name, quantity=1, have=null)-> + logger.trace "craft(#{name}, #{quantity}, #{have})" + + item = @modPack.findItemByName name + logger.debug "item: #{item}" + if not item? then throw new Error "cannot find an item named: #{name}" + @clear() @result.addInventory(have) if have? - @_expected.add name, quantity + @_expected.add item, quantity @_pending = @_expected.clone() + while not @_pending.isEmpty @_processPending() @@ -47,50 +54,54 @@ module.exports = class CraftingPlan # Private Methods ############################################################################## _processPending: -> - targetItem = @_pending.pop() + targetStack = @_pending.pop() + targetItem = targetStack.item + logger.verbose "processing item: #{targetItem}, craftable? #{targetItem.isCraftable}" return unless targetItem? + return if (not targetItem.isCraftable) or targetItem.isGatherable - return if @modPack.isRawMaterial targetItem.name - - recipes = @modPack.gatherRecipes targetItem.name - return if not recipes.length > 0 - - recipe = recipes[0] + recipe = targetItem.recipes[0] + logger.verbose "recipe: #{recipe}" if @includingTools - for tool in recipe.tools - totalExpected = @result.quantityOf(tool.name) + @_expected.quantityOf(tool.name) - if totalExpected < tool.quantity - @_pending.add tool.name, tool.quantity - @_expected.add tool.name, tool.quantity + for toolItem in recipe.tools + totalExpected = @result.quantityOf(toolItem.slug) + @_expected.quantityOf(toolItem.slug) + if totalExpected < 1 + @_pending.add toolItem + @_expected.add toolItem - while @_totalQuantityOf(targetItem.name) < @_expected.quantityOf(targetItem.name) + while @_totalQuantityOf(targetItem.slug) < @_expected.quantityOf(targetItem.slug) @steps.push recipe - for item in recipe.input - @_processInputItem item + for stack in recipe.input + @_processInputStack stack - for item in recipe.output - @_processOutputItem item + for stack in recipe.output + @_processOutputStack stack - _processInputItem: (item)-> - quantityAvailable = @result.quantityOf item.name - quantityUsed = Math.min quantityAvailable, item.quantity - quantityNeeded = item.quantity - quantityUsed + _processInputStack: (stack)-> + slug = stack.item.slug + quantityAvailable = @result.quantityOf slug + quantityUsed = Math.min quantityAvailable, stack.quantity + quantityNeeded = stack.quantity - quantityUsed + logger.verbose "processing input:#{stack.name}, + a:#{quantityAvailable}, u:#{quantityUsed}, n:#{quantityNeeded}" - @result.remove item.name, quantityUsed - @_pending.add item.name, quantityNeeded - @need.add item.name, quantityNeeded + @result.remove slug, quantityUsed + @_pending.add stack.item, quantityNeeded + @need.add stack.item, quantityNeeded - _processOutputItem: (item)-> - quantityMissing = @need.quantityOf item.name - quantityUsed = Math.min quantityMissing, item.quantity - quantityLeft = item.quantity - quantityUsed + _processOutputStack: (stack)-> + slug = stack.item.slug + quantityMissing = @need.quantityOf slug + quantityUsed = Math.min quantityMissing, stack.quantity + quantityLeft = stack.quantity - quantityUsed + logger.verbose "processing output:#{stack.name}, + m:#{quantityMissing}, u:#{quantityUsed}, l:#{quantityLeft}" - @make.add item.name, item.quantity - @need.remove item.name, quantityUsed - @result.add item.name, quantityLeft + @make.add stack.item, stack.quantity + @need.remove slug, quantityUsed + @result.add stack.item, quantityLeft - - _totalQuantityOf: (name)-> - return @result.quantityOf(name) - @need.quantityOf(name) + _totalQuantityOf: (slug)-> + return @result.quantityOf(slug) - @need.quantityOf(slug) diff --git a/src/scripts/models/crafting_table.coffee b/src/scripts/models/crafting_table.coffee index eb969ae5d..b25758957 100644 --- a/src/scripts/models/crafting_table.coffee +++ b/src/scripts/models/crafting_table.coffee @@ -1,8 +1,8 @@ ### -# Crafting Guide - crafting_table.coffee -# -# Copyright (c) 2014 by Redwood Labs -# All rights reserved. +Crafting Guide - crafting_table.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. ### BaseModel = require './base_model' @@ -30,10 +30,10 @@ module.exports = class CraftingTable extends BaseModel @plan = null return - toolPhrase = if @includingTools then ' includingTools' else '' + toolPhrase = if @includingTools then ' including tools' else '' logger.verbose "calculating build plan for #{@quantity} #{@name}#{toolPhrase} with inventory: #{@have}" - @modPack.enableBooksForRecipe @name + @modPack.enableModsForItem @name plan = new CraftingPlan @modPack, @includingTools plan.includingTools = @includingTools diff --git a/src/scripts/models/inventory.coffee b/src/scripts/models/inventory.coffee index c7184d752..70605d2f4 100644 --- a/src/scripts/models/inventory.coffee +++ b/src/scripts/models/inventory.coffee @@ -7,7 +7,7 @@ BaseModel = require './base_model' {Event} = require '../constants' -Item = require './item' +Stack = require './stack' ######################################################################################################################## @@ -17,91 +17,91 @@ module.exports = class Inventory extends BaseModel super attributes, options @clear() - Object.defineProperty this, 'isEmpty', get:-> @_names.length is 0 + Object.defineProperty @prototype, 'isEmpty', get:-> @_slugs.length is 0 # Public Methods ############################################################################### - add: (name, quantity=1)-> + add: (item, quantity=1)-> return if quantity is 0 - item = @_items[name] - if not item? - item = new Item name:name, quantity:quantity - @_items[name] = item - @_names.push name - @_names.sort() + stack = @_stacks[item.slug] + if not stack? + stack = new Stack item:item, quantity:quantity + @_stacks[item.slug] = stack + @_slugs.push item.slug + @_slugs.sort() else - item.quantity += quantity + stack.quantity += quantity - @trigger Event.add, this, name, quantity + @trigger Event.add, this, item, quantity @trigger Event.change, this return this addInventory: (inventory)-> - inventory.each (item)=> @add item.name, item.quantity + inventory.each (stack)=> @add stack.item, stack.quantity return this clear: -> - @_items = {} - @_names = [] + @_stacks = {} + @_slugs = [] clone: -> inventory = new Inventory - @each (item)-> inventory.add item.name, item.quantity + @each (stack)-> inventory.add stack.item, stack.quantity return inventory each: (onItem)-> - for name in @_names - item = @_items[name] - onItem item + for slug in @_slugs + stack = @_stacks[slug] + onItem stack - hasAtLeast: (name, quantity=1)-> + hasAtLeast: (slug, quantity=1)-> if quantity is 0 then return true - item = @_items[name] - return false unless item? - return item.quantity >= quantity + stack = @_stacks[slug] + return false unless stack? + return stack.quantity >= quantity pop: -> - name = @_names.pop() - return null unless name? + slug = @_slugs.pop() + return null unless slug? - item = @_items[name] - delete @_items[name] + stack = @_stacks[slug] + delete @_stacks[slug] - @trigger Event.remove, this, item.name, item.quantity + @trigger Event.remove, this, stack.item, stack.quantity @trigger Event.change, this - return item + return stack - quantityOf: (name)-> - item = @_items[name] - return 0 unless item? - return item.quantity + quantityOf: (slug)-> + stack = @_stacks[slug] + return 0 unless stack? + return stack.quantity - remove: (name, quantity=1)-> + remove: (slug, quantity=1)-> return if quantity is 0 - item = @_items[name] - if not item? then throw new Error "cannot remove #{name} since it is not in this inventory" - if item.quantity < quantity - throw new Error "cannot remove #{quantity} #{name} because there is only #{item.quantity} in this inventory" + stack = @_stacks[slug] + if not stack? then throw new Error "cannot remove #{slug} since it is not in this inventory" + if stack.quantity < quantity + throw new Error "cannot remove #{quantity} #{slug} because there is only #{stack.quantity} in this inventory" - item.quantity -= quantity - if item.quantity is 0 - delete @_items[name] - @_names = _(@_names).without name + stack.quantity -= quantity + if stack.quantity is 0 + delete @_stacks[slug] + @_slugs = _(@_slugs).without slug - @trigger Event.remove, this, name, quantity + @trigger Event.remove, this, slug, quantity @trigger Event.change, this return this toList: -> result = [] - @each (item)-> - if item.quantity > 1 - result.push [item.quantity, item.name] + @each (stack)-> + if stack.quantity > 1 + result.push [stack.quantity, stack.item.slug] else - result.push item.name + result.push stack.item.slug return result # Object Overrides ############################################################################# @@ -110,9 +110,9 @@ module.exports = class Inventory extends BaseModel result = [@constructor.name, " (", @cid, ") { items: ["] needsDelimiter = false - @each (item)-> + @each (stack)-> if needsDelimiter then result.push ', ' - result.push item.toString() + result.push stack.toString() needsDelimiter = true result.push ']' diff --git a/src/scripts/models/inventory_parser.coffee b/src/scripts/models/inventory_parser.coffee index 513b0dccc..0a52e3f33 100644 --- a/src/scripts/models/inventory_parser.coffee +++ b/src/scripts/models/inventory_parser.coffee @@ -1,11 +1,12 @@ ### -# Crafting Guide - inventory_parser.coffee -# -# Copyright (c) 2014 by Redwood Labs -# All rights reserved. +Crafting Guide - inventory_parser.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. ### Inventory = require './inventory' +Item = require './item' ######################################################################################################################## @@ -24,6 +25,6 @@ module.exports = class InventoryParser quantity = if match? then parseInt(match[1]) else 1 if name.length > 0 - inventory.add name, quantity + inventory.add new Item(name:name), quantity return inventory diff --git a/src/scripts/models/item.coffee b/src/scripts/models/item.coffee index 9880f6fcc..b6e91d907 100644 --- a/src/scripts/models/item.coffee +++ b/src/scripts/models/item.coffee @@ -14,38 +14,48 @@ module.exports = class Item extends BaseModel @DEFAULT_STACK_SIZE = 64 constructor: (attributes={}, options={})-> - attributes.name ?= '' - attributes.quantity ?= 1 - attributes.stackSize ?= Item.DEFAULT_STACK_SIZE + if not attributes.name? then throw new Error 'attributes.name is required' + + attributes.isGatherable ?= false + attributes.recipes ?= [] + attributes.slug ?= _.slugify attributes.name + attributes.stackSize ?= Item.DEFAULT_STACK_SIZE super attributes, options - Object.defineProperty this, 'stackQuantity', get:@getStackQuantity + Object.defineProperty @prototype, 'isCraftable', get:-> @recipes.length > 0 # Public Methods ############################################################################### - canMerge: (item)-> - return item.name is @name + addRecipe: (recipe)-> + output = recipe.output[0].item + if output isnt this then throw new Error "invalid recipe for #{@name} because it makes a #{output.name}" - merge: (item)-> - if not @canMerge(item) then throw new Error "cannot merge #{item} into #{this}" - @quantity += item.quantity + @recipes.push recipe - # Property Methods ############################################################################# - - getStackQuantity: -> - count = 0 - extra = @quantity - while extra > @stackSize - extra -= @stackSize - count += 1 - - return count:count, extra:extra + compareTo: (that)-> + if this.name isnt that.name + return if this.name < that.name then -1 else +1 + return 0 # Object Overrides ############################################################################# toString: -> - result = "#{@constructor.name} (#{@cid}) { name:\"#{@name}\", quantity:#{@quantity}" + result = [] + result.push @constructor.name + result.push ' ('; result.push @cid; result.push ') { ' + result.push 'name:"'; result.push @name; result.push '", ' + result.push 'isGatherable:'; result.push @isGatherable + + if _.slugify(@name) isnt @slug + result.push ', slug:'; result.push @slug + if @stackSize isnt Item.DEFAULT_STACK_SIZE - result += ", stackSize:#{@stackSize}" - result += ' }' - return result + result.push ', stackSize:'; result.push @stackSize + + if @recipes.length > 0 + result.push ', recipes:' + result.push @recipes.length + result.push ' items' + + result.push '}' + return result.join '' diff --git a/src/scripts/models/mod_pack.coffee b/src/scripts/models/mod_pack.coffee index e8f45d141..ef77fb1ed 100644 --- a/src/scripts/models/mod_pack.coffee +++ b/src/scripts/models/mod_pack.coffee @@ -9,32 +9,43 @@ BaseModel = require './base_model' {Event} = require '../constants' ModVersionParser = require './mod_version_parser' {RequiredMods} = require '../constants' +util = require 'util' ######################################################################################################################## module.exports = class ModPack extends BaseModel constructor: (attributes={}, options={})-> - attributes.books ?= [] + attributes.modVersions ?= [] super attributes, options @_parser = new ModVersionParser # Public Methods ############################################################################### - enableBooksForRecipe: (name)-> - for book in @books - continue if book.enabled - if book.hasRecipe name - book.enabled = true + enableModsForItem: (name)-> + for modVersion in @modVersions + continue if modVersion.enabled + if modVersion.hasRecipe name + modVersion.enabled = true - gatherNames: (options={})-> + findItemByName: (name, options={})-> + options.includeDisabled ?= false + + for modVersion in @modVersions + continue unless modVersion.enabled or options.includeDisabled + item = modVersion.findItemByName name + return item if item? + + return null + + gatherRecipeNames: (options={})-> options.includeDisabled ?= false nameData = {} - for book in @books - continue unless book.enabled or options.includeDisabled - book.gatherNames nameData + for modVersion in @modVersions + continue unless modVersion.enabled or options.includeDisabled + modVersion.gatherRecipeNames nameData result = [] names = _.keys(nameData).sort() @@ -42,76 +53,56 @@ module.exports = class ModPack extends BaseModel result.push nameData[name] return result - gatherRecipes: (name, options={})-> - options.includeDisabled ?= false - - result = [] - for book in @books - continue unless book.enabled or options.includeDisabled - - book.gatherRecipes name, result - - return result - hasRecipe: (name, options={})-> options.includeDisabled ?= false - for book in @books - continue unless book.enabled or options.includeDisabled - return true if book.hasRecipe name + for modVersion in @modVersions + continue unless modVersion.enabled or options.includeDisabled + return true if modVersion.hasRecipe name return false - isRawMaterial: (name, options={})-> - options.includeDisabled ?= false - - for book in @books - continue unless book.enabled or options.includeDisabled - return true if book.isRawMaterial name - - return false - - loadBook: (url)-> + loadModVersion: (url)-> w.promise (resolve, reject)=> @trigger Event.load.started, this, url $.ajax url: url dataType: 'json' success: (data, status, xhr)=> - resolve @onBookLoaded(url, data, status, xhr) + resolve @onModVersionLoaded(url, data, status, xhr) error: (xhr, status, error)=> - reject @onBookLoadFailed(url, error, status, xhr) + reject @onModVersionLoadFailed(url, error, status, xhr) - loadBookData: (data)-> - book = @_parser.parse data - @books.push book - @_sortBooks() - book.on Event.change, => @trigger Event.change, this + loadModVersionData: (data)-> + modVersion = @_parser.parse data + @modVersions.push modVersion + @modVersions.sort (a, b)-> a.compareTo b + modVersion.on Event.change, => @trigger Event.change, this - return book + return modVersion - loadAllBooks: (urlList)-> - promises = (@loadBook(url) for url in urlList) + loadAllModVersions: (urlList)-> + promises = (@loadModVersion(url) for url in urlList) return w.settle promises # Event Methods ################################################################################ - onBookLoaded: (url, data, status, xhr)-> + onModVersionLoaded: (url, data, status, xhr)-> try - book = @loadBookData data + modVersion = @loadModVersionData data - logger.info "loaded recipe book from #{url}: #{book}" - @trigger Event.load.succeeded, this, book + logger.info "loaded ModVersion from #{url}: #{modVersion}" + @trigger Event.load.succeeded, this, modVersion @trigger Event.load.finished, this @trigger Event.change, this - return book + return modVersion catch e - @onBookLoadFailed url, e, status, xhr + @onModVersionLoadFailed url, e, status, xhr - onBookLoadFailed: (url, error, status, xhr)-> + onModVersionLoadFailed: (url, error, status, xhr)-> message = if error.stack? then error.stack else error - logger.error "failed to load recipe book from #{url}: #{message}" + logger.error "failed to load ModVersion from #{url}: #{message}" @trigger Event.load.failed, this, error.message @trigger Event.load.finished, this return error @@ -119,20 +110,4 @@ module.exports = class ModPack extends BaseModel # Object Overrides ############################################################################# toString: -> - return "ModPack (#{@cid}) {books:#{@books.length} items}" - - _sortBooks:-> - @books.sort (a, b)-> - if a.modName is b.modName then return 0 - - aRequired = a.modName in RequiredMods - bRequired = b.modName in RequiredMods - - if aRequired and bRequired - return if a.modName < b.modName then -1 else +1 - else if aRequired - return -1 - else if bRequired - return +1 - else - return if a.modName < b.modName then -1 else +1 + return "ModPack (#{@cid}) {modVersions:#{@modVersions.length} items}" diff --git a/src/scripts/models/mod_version.coffee b/src/scripts/models/mod_version.coffee index c56fad43e..ce185c952 100644 --- a/src/scripts/models/mod_version.coffee +++ b/src/scripts/models/mod_version.coffee @@ -17,34 +17,48 @@ module.exports = class ModVersion extends BaseModel if _.isEmpty(attributes.modVersion) then throw new Error 'modVersion cannot be empty' attributes.description ?= '' - attributes.rawMaterials ?= [] - attributes.recipes ?= [] + attributes.items ?= {} attributes.enabled ?= attributes.modName in RequiredMods super attributes, options # Public Methods ############################################################################### - gatherNames: (result)-> - for recipe in @recipes - continue if result[recipe.name] - result[recipe.name] = value:recipe.name, label:"#{recipe.name} (from #{@modName} #{@modVersion})" + addItem: (item)-> + if @items[item.slug]? then throw new Error "duplicate item for #{item.slug}" + @items[item.slug] = item + return this + + compareTo: (that)-> + if this.modName is that.modName then return 0 + + thisRequired = this.modName in RequiredMods + thatRequired = that.modName in RequiredMods + + if thisRequired and thatRequired + return if this.modName < that.modName then -1 else +1 + else if thisRequired + return -1 + else if thatRequired + return +1 + else + return if this.modName < that.modName then -1 else +1 + + findItemByName: (name)-> + slug = _.slugify name + return @items[slug] + + gatherRecipeNames: (result={})-> + for slug, item of @items + continue if result[item.slug] + continue unless item.isCraftable + result[item.slug] = value:item.name, label:"#{item.name} (from #{@modName} #{@modVersion})" return result - gatherRecipes: (name, result)-> - for recipe in @recipes - if recipe.name is name - result.push recipe - - return result - - isRawMaterial: (name)-> - return name in @rawMaterials - hasRecipe: (name)-> - for recipe in @recipes - return true if recipe.name is name - return false + item = @findItemByName name + return false unless item? + return item.recipes.length > 0 # Object Overrides ############################################################################# @@ -53,4 +67,4 @@ module.exports = class ModVersion extends BaseModel enabled:#{@enabled}, modName:#{@modName}, modVersion:#{@modVersion}, - recipes:#{@recipes.length} items}" + items:#{_.keys(@items).length} items}" diff --git a/src/scripts/models/mod_version_parser.coffee b/src/scripts/models/mod_version_parser.coffee index 0db34b711..295889356 100644 --- a/src/scripts/models/mod_version_parser.coffee +++ b/src/scripts/models/mod_version_parser.coffee @@ -6,8 +6,10 @@ All rights reserved. ### Item = require './item' -Recipe = require './recipe' ModVersion = require './mod_version' +Recipe = require './recipe' +Stack = require './stack' +util = require 'util' ######################################################################################################################## @@ -20,21 +22,21 @@ module.exports = class ModVersionParser '1': new V1 parse: (data)-> - if not data? then throw new Error 'recipe book data is missing' + if not data? then throw new Error 'mod description data is missing' if not data.version? then throw new Error 'version is required' parser = @_parsers["#{data.version}"] - if not parser? then throw new Error "cannot parse version #{data.version} recipe books" + if not parser? then throw new Error "cannot parse version #{data.version} mod descriptions" return parser.parse data - unparse: (ModVersion, version=ModVersionParser.CURRENT_VERSION)-> - if not ModVersion? then throw new Error 'recipe book is required' + unparse: (modVersion, version=ModVersionParser.CURRENT_VERSION)-> + if not modVersion? then throw new Error 'modVersion is required' parser = @_parsers["#{version}"] if not parser? then throw new Error "version #{version} is not supported" - return parser.unparse ModVersion + return parser.unparse modVersion ######################################################################################################################## @@ -46,62 +48,77 @@ module.exports.V1 = class V1 parse: (data)-> return @_parseModVersion data - unparse: (ModVersion)-> - return @_unparseModVersion ModVersion + unparse: (modVersion)-> + return @_unparseModVersion modVersion # Private Methods ############################################################################## - _parseModVersion: (data)-> - if not data? then throw new Error 'recipe book data is missing' - if not data.version? then throw new Error 'version is required' - if not data.mod_name? then throw new Error 'mod_name is required' - if not data.mod_version? then throw new Error 'mod_version is required' - if not _.isArray(data.recipes) then throw new Error 'recipes must be an array' - - book = new ModVersion version:data.version, modName:data.mod_name, modVersion:data.mod_version - book.description = data.description or '' - - book.rawMaterials = data.raw_materials or [] - - for index in [0...data.recipes.length] - @_errorLocation = "recipe #{index + 1}" - recipeData = data.recipes[index] - recipe = @_parseRecipe recipeData - recipe._originalIndex = index - book.recipes.push recipe - - return book - - _parseRecipe: (data, options={})-> - if not data? then throw new Error "recipe data is missing for #{@_errorLocation}" - if not data.output? then throw new Error "#{@_errorLocation} is missing output" - - output = @_parseItemList data.output, field:'output', canBeEmpty:false - @_errorLocation = "recipe for #{output[0].name}" - - if not data.input? then throw new Error "#{@_errorLocation} is missing input" - data.tools ?= [] - - input = @_parseItemList data.input, field:'input', canBeEmpty:true - tools = @_parseItemList data.tools, field:'tools', canBeEmpty:true - - return new Recipe input:input, output:output, tools:tools + _findOrCreateItem: (name)-> + item = @modVersion.findItemByName name + if not item? + item = new Item name:name + @modVersion.addItem item + return item _parseItemList: (data, options={})-> if not data? then throw new Error "#{@_errorLocation} must have an #{options.field} field" if not _.isArray(data) then data = [data] - if data.length is 0 and not options.canBeEmpty - throw new Error "#{options.field} for #{@_errorLocation} cannot be empty" result = [] - for index in [0...data.length] - itemData = data[index] - result.push @_parseItem itemData, field:options.field, index:index - + for name in data + item = @_findOrCreateItem name + result.push item return result - _parseItem: (data, options={})-> + _parseModVersion: (data)-> + if not data? then throw new Error 'mod description data is missing' + if not data.version? then throw new Error 'version is required' + if not data.mod_name? then throw new Error 'mod_name is required' + if not data.mod_version? then throw new Error 'mod_version is required' + if not _.isArray(data.recipes) then throw new Error 'recipes must be an array' + + @modVersion = modVersion = new ModVersion modName:data.mod_name, modVersion:data.mod_version + modVersion.description = data.description or '' + + @_parseRawMaterials data.raw_materials + + for index in [0...data.recipes.length] + @_errorLocation = "recipe #{index + 1}" + recipeData = data.recipes[index] + @_parseRecipe recipeData + + @modVersion = null + return modVersion + + _parseRawMaterials: (data, options={})-> + return unless data? and data.length > 0 + + results = [] + for name in data + item = @_findOrCreateItem name + item.isGatherable = true + results.push item + + _parseRecipe: (data, options={})-> + if not data? then throw new Error "recipe data is missing for #{@_errorLocation}" + if not data.output? then throw new Error "#{@_errorLocation} is missing output" + + output = @_parseStackList data.output, field:'output', canBeEmpty:false + item = output[0].item + @_errorLocation = "recipe for #{item.name}" + + if not data.input? then throw new Error "#{@_errorLocation} is missing input" + data.tools ?= [] + + input = @_parseStackList data.input, field:'input', canBeEmpty:true + tools = @_parseItemList data.tools, field:'tools' + + recipe = new Recipe input:input, output:output, tools:tools + item.addRecipe recipe + return recipe + + _parseStack: (data, options={})-> errorBase = "#{options.field} element #{options.index} for #{@_errorLocation}" if not data? then throw new Error "#{errorBase} is missing" @@ -112,42 +129,67 @@ module.exports.V1 = class V1 if data.length isnt 2 then throw new Error "#{errorBase} must have at least one element" if not _.isNumber(data[0]) then throw new Error "#{errorBase} must start with a number" - return new Item quantity:data[0], name:data[1] + item = @_findOrCreateItem data[1] + return new Stack item:item, quantity:data[0] - _unparseModVersion: (ModVersion)-> + _parseStackList: (data, options={})-> + if not data? then throw new Error "#{@_errorLocation} must have an #{options.field} field" + + if not _.isArray(data) then data = [data] + if data.length is 0 and not options.canBeEmpty + throw new Error "#{options.field} for #{@_errorLocation} cannot be empty" + + result = [] + for index in [0...data.length] + stackData = data[index] + result.push @_parseStack stackData, field:options.field, index:index + + return result + + _parseItemList: (data, options={})-> + if not data? then throw new Error "#{@_errorLocation} must have an #{options.field} field" + + if not _.isArray(data) then data = [data] + + result = [] + for name in data + item = @_findOrCreateItem name + result.push item + return result + + # Un-parsing Methods ########################################################################### + + _unparseModVersion: (modVersion)-> result = [] result.push '{\n' result.push ' "version": 1,\n' - result.push ' "mod_name": "' + ModVersion.modName + '",\n' - result.push ' "mod_version": "' + ModVersion.modVersion + '",\n' - if ModVersion.description.length > 0 - result.push ' "description": "' + ModVersion.description + '",\n' + result.push ' "mod_name": "' + modVersion.modName + '",\n' + result.push ' "mod_version": "' + modVersion.modVersion + '",\n' + if modVersion.description.length > 0 + result.push ' "description": "' + modVersion.description + '",\n' - if ModVersion.rawMaterials.length > 0 + rawMaterials = (item.name for slug, item of modVersion.items when item.isGatherable) + rawMaterials.sort() + if rawMaterials.length > 0 result.push ' "raw_materials": [\n' firstItem = true - materials = ModVersion.rawMaterials.slice() - materials.sort() - for material in materials + for material in rawMaterials if not firstItem then result.push ',\n' result.push ' "' + material + '"' firstItem = false result.push '\n ],\n' result.push ' "recipes": [\n' - recipes = ModVersion.recipes.slice() - recipes.sort (a, b)-> - if a.name isnt b.name - return if a.name < b.name then -1 else +1 - if a._originalIndex isnt b._originalIndex - return if a._originalIndex < b._originalIndex then -1 else +1 - return 0 + + items = (item for slug, item of modVersion.items when item.isCraftable) + items.sort (a, b)-> a.compareTo b firstItem = true - for recipe in recipes - result.push if firstItem then ' {\n' else ' }, {\n' - @_unparseRecipe recipe, result - firstItem = false + for item in items + for recipe in item.recipes + result.push if firstItem then ' {\n' else ' }, {\n' + @_unparseRecipe recipe, result + firstItem = false result.push ' }\n' result.push ' ]\n' @@ -157,12 +199,12 @@ module.exports.V1 = class V1 _unparseRecipe: (recipe, result=[])-> result.push ' "output": ' - @_unparseItemList recipe.output, result, sort:false + @_unparseStackList recipe.output, result, sort:false if recipe.input.length > 0 result.push ',\n' result.push ' "input": ' - @_unparseItemList recipe.input, result + @_unparseStackList recipe.input, result if recipe.tools.length > 0 result.push ',\n' @@ -172,38 +214,51 @@ module.exports.V1 = class V1 result.push '\n' return result - _unparseItemList: (itemList, result, options={})-> + _unparseStackList: (stackList, result, options={})-> options.sort ?= true - if itemList.length is 0 + if stackList.length is 0 result.push '[]' - else if itemList.length is 1 - item = itemList[0] - if item.quantity is 1 - result.push '"' + item.name + '"' + else if stackList.length is 1 + stack = stackList[0] + if stack.quantity is 1 + result.push '"' + stack.name + '"' else - result.push '[[' + item.quantity + ', "' + item.name + '"]]' + result.push '[[' + stack.quantity + ', "' + stack.name + '"]]' else result.push '[' - items = itemList.slice() + stacks = stackList.slice() if options.sort - items.sort (a, b)-> + stacks.sort (a, b)-> if a.quantity isnt b.quantity return if a.quantity > b.quantity then -1 else +1 - if a.name isnt b.name - return if a.name < b.name then -1 else +1 + if a.item.name isnt b.item.name + return if a.item.name < b.item.name then -1 else +1 return 0 firstItem = true - for item in items + for stack in stacks result.push ', ' if not firstItem - if item.quantity is 1 - result.push '"' + item.name + '"' + if stack.quantity is 1 + result.push '"' + stack.name + '"' else - result.push '[' + item.quantity + ', "' + item.name + '"]' + result.push '[' + stack.quantity + ', "' + stack.name + '"]' firstItem = false result.push ']' return result + + _unparseItemList: (itemList, result)-> + if itemList.length is 1 + result.push '"'; result.push itemList[0].name; result.push '"' + else + firstItem = true + for item in itemList + result.push if firstItem then '["' else '", "' + result.push item.name + firstItem = false + result.push '"]' + + return result diff --git a/src/scripts/models/recipe.coffee b/src/scripts/models/recipe.coffee index bde069211..19aece04e 100644 --- a/src/scripts/models/recipe.coffee +++ b/src/scripts/models/recipe.coffee @@ -12,26 +12,29 @@ BaseModel = require './base_model' module.exports = class Recipe extends BaseModel constructor: (attributes={}, options={})-> - if not attributes.output? then throw new Error "attributes.output is required" - attributes.input ?= [] - attributes.tools ?= [] + if not attributes.input? then throw new Error 'attributes.input is required' + if not attributes.output? then throw new Error 'attributes.output is required' + + attributes.pattern ?= (i for i in [0...attributes.input.length]).join('') + attributes.tools ?= [] super attributes, options - Object.defineProperty @prototype, 'name', get:-> @output[0].name + Object.defineProperty @prototype, 'name', get:-> @output[0].item.name # Public Methods ############################################################################### make: (inventory, missing)-> - for item in @input - needed = item.quantity + for stack in @input + item = stack.item + needed = stack.quantity while needed > 0 - if inventory.hasAtLeast item.name - inventory.remove item.name + if inventory.hasAtLeast item.slug + inventory.remove item.slug else - missing.add item.name + missing.add item.slug - for item in @output - inventory.add item.name, item.quantity + for stack in @output + inventory.add stack.item, stack.quantity return this @@ -42,26 +45,26 @@ module.exports = class Recipe extends BaseModel result.push ", input:[" needsDelimiter = false - for inputItem in @input + for stack in @input if needsDelimiter then result.push ', ' - result.push inputItem.toString() + result.push stack.toString() needsDelimiter = true result.push ']' result.push ", output:[" needsDelimiter = false - for outputItem in @output + for stack in @output if needsDelimiter then result.push ', ' - result.push outputItem.toString() + result.push stack.toString() needsDelimiter = true result.push ']' if @tools.length > 0 result.push ", tools:[" needsDelimiter = false - for tool in @tools + for stack in @tools if needsDelimiter then result.push ', ' - result.push tool.toString() + result.push stack.toString() needsDelimiter = true result.push ']' diff --git a/src/scripts/models/stack.coffee b/src/scripts/models/stack.coffee new file mode 100644 index 000000000..36c3cf663 --- /dev/null +++ b/src/scripts/models/stack.coffee @@ -0,0 +1,49 @@ +### +Crafting Guide - stack.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. +### + +######################################################################################################################## + +module.exports = class Stack + + constructor: (attributes={})-> + if not attributes.item? then throw new Error 'item is required' + attributes.quantity ?= 1 + + @item = attributes.item + @quantity = attributes.quantity + + Object.defineProperty @prototype, 'name', get:-> @item?.name + + Object.defineProperty @prototype, 'stackQuantity', get:@getStackQuantity + + # Public Methods ############################################################################### + + canMerge: (stack)-> + return @item.slug is stack.item.slug + + merge: (stack)-> + if not @canMerge stack + throw new Error "this stack of #{@item.name} cannot merge a stack of #{@stack.name}" + + @quantity += stack.quantity + return this + + # Property Methods ############################################################################# + + getStackQuantity: -> + count = 0 + extra = @quantity + while extra > @item.stackSize + extra -= @item.stackSize + count += 1 + + return count:count, extra:extra + + # Object Overrides ############################################################################# + + toString: -> + return "#{@quantity} #{@item.name}" diff --git a/src/scripts/underscore_mixins.coffee b/src/scripts/underscore_mixins.coffee new file mode 100644 index 000000000..a498162f3 --- /dev/null +++ b/src/scripts/underscore_mixins.coffee @@ -0,0 +1,18 @@ +### +Crafting Guide - underscore.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. +### + +_.mixin + + slugify: (text)-> + return null unless text? + + result = text.toLowerCase() + result = result.replace /[^a-zA-Z0-9_]/g, '_' + result = result.replace /_+/, '_' + result = result.replace /_$/, '' + result = result.replace /^_/, '' + return result diff --git a/test/crafting_plan.test.coffee b/test/crafting_plan.test.coffee index f8aaf35d2..5b9d3b2be 100644 --- a/test/crafting_plan.test.coffee +++ b/test/crafting_plan.test.coffee @@ -1,24 +1,24 @@ ### -# Crafting Guide - crafting_plan.test.coffee -# -# Copyright (c) 2014 by Redwood Labs -# All rights reserved. +Crafting Guide - crafting_plan.test.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. ### -ModPack = require '../src/scripts/models/mod_pack' -CraftingPlan = require '../src/scripts/models/crafting_plan' +ModPack = require '../src/scripts/models/mod_pack' +CraftingPlan = require '../src/scripts/models/crafting_plan' ######################################################################################################################## -catalog = plan = null +modPack = plan = null ######################################################################################################################## describe 'CraftingPlan', -> beforeEach -> - catalog = new ModPack - catalog.loadBookData { + modPack = new ModPack + modPack.loadModVersionData { version: 1 mod_name: 'Minecraft' mod_version: '1.7.10' @@ -31,8 +31,10 @@ describe 'CraftingPlan', -> { input:[[2, 'Iron Ingot'], 'Stick'], tools:'Crafting Table', output:'Iron Sword' } ] } + logger.debug "modPack: #{modPack}" + logger.debug "modVersion: #{modPack.modVersions[0]}" - plan = new CraftingPlan catalog + plan = new CraftingPlan modPack describe 'craft', -> @@ -40,40 +42,40 @@ describe 'CraftingPlan', -> it 'can craft a single step recipe', -> plan.craft 'Oak Plank' - plan.need.toList().should.eql ['Oak Log'] - plan.result.toList().should.eql [[4, 'Oak Plank']] + plan.need.toList().should.eql ['oak_log'] + plan.result.toList().should.eql [[4, 'oak_plank']] it 'can craft a multi-step recipe', -> plan.craft 'Crafting Table' - plan.need.toList().should.eql ['Oak Log'] - plan.result.toList().should.eql ['Crafting Table'] + plan.need.toList().should.eql ['oak_log'] + plan.result.toList().should.eql ['crafting_table'] it 'can craft a multi-step recipe using tools', -> plan.craft 'Furnace' - plan.need.toList().should.eql [[8, 'Cobblestone']] - plan.result.toList().should.eql ['Furnace'] + plan.need.toList().should.eql [[8, 'cobblestone']] + plan.result.toList().should.eql ['furnace'] it 'can craft a multi-step recipe re-using tools', -> plan.craft 'Iron Sword' - plan.need.toList().should.eql [[2, 'Iron Ore'], 'Oak Log', [2, '{furnace fuel}']] - plan.result.toList().should.eql ['Iron Sword', [2, 'Oak Plank'], [3, 'Stick']] + plan.need.toList().should.eql [[2, 'furnace_fuel'], [2, 'iron_ore'], 'oak_log'] + plan.result.toList().should.eql ['iron_sword', [2, 'oak_plank'], [3, 'stick']] describe 'with building tools', -> it 'can craft a multi-step recipe using tools', -> plan.includingTools = true plan.craft 'Furnace' - plan.need.toList().should.eql [[8, 'Cobblestone'], 'Oak Log'] - plan.result.toList().should.eql ['Crafting Table', 'Furnace'] + plan.need.toList().should.eql [[8, 'cobblestone'], 'oak_log'] + plan.result.toList().should.eql ['crafting_table', 'furnace'] it 'can craft a multi-step recipe re-using tools', -> plan.includingTools = true plan.craft 'Iron Sword' plan.need.toList().should.eql [ - [8, 'Cobblestone'], [2, 'Iron Ore'], [2, 'Oak Log'], [2, '{furnace fuel}'], + [8, 'cobblestone'], [2, 'furnace_fuel'], [2, 'iron_ore'], [2, 'oak_log'] ] plan.result.toList().should.eql [ - 'Crafting Table', 'Furnace', 'Iron Sword', [2, 'Oak Plank'], [3, 'Stick'] + 'crafting_table', 'furnace', 'iron_sword', [2, 'oak_plank'], [3, 'stick'] ] describe 'using existing inventory', -> diff --git a/test/inventory.test.coffee b/test/inventory.test.coffee index c53116af1..d010042c4 100644 --- a/test/inventory.test.coffee +++ b/test/inventory.test.coffee @@ -20,30 +20,30 @@ describe 'Inventory', -> beforeEach -> inventory = new Inventory - inventory.add 'wool', 4 - inventory.add 'string', 20 - inventory.add 'boat' + inventory.add new Item(name:'Wool'), 4 + inventory.add new Item(name:'String'), 20 + inventory.add new Item(name:'Boat') describe 'add', -> it 'can add to an empty inventory', -> - inventory.add 'iron ingot', 4 - item = inventory._items['iron ingot'] - item.constructor.name.should.equal 'Item' - item.name.should.equal 'iron ingot' - item.quantity.should.equal 4 + inventory.add new Item(name:'Iron Ingot'), 4 + stack = inventory._stacks['iron_ingot'] + stack.constructor.name.should.equal 'Stack' + stack.name.should.equal 'Iron Ingot' + stack.quantity.should.equal 4 it 'can augment quantity of existing items', -> - inventory.add 'wool', 2 + inventory.add new Item(name:'Wool'), 2 inventory.toList().should.eql ['boat', [20, 'string'], [6, 'wool']] it 'can add zero quantity', -> - inventory.add 'wool', 0 + inventory.add new Item(name:'Wool'), 0 inventory.toList().should.eql ['boat', [20, 'string'], [4, 'wool']] it 'emits the proper events', -> events = new EventRecorder inventory - inventory.add 'iron ingot', 10 + inventory.add new Item(name:'Iron Ingot'), 10 events.names.should.eql [Event.add, Event.change] describe 'addInventory', -> @@ -51,11 +51,11 @@ describe 'Inventory', -> it 'can add to an empty inventory', -> newInventory = new Inventory newInventory.addInventory inventory - newInventory._names.should.eql ['boat', 'string', 'wool'] + newInventory._slugs.should.eql ['boat', 'string', 'wool'] it 'can add a mix of new and existing items', -> newInventory = new Inventory - newInventory.add 'string', 2 + newInventory.add new Item(name:'String'), 2 newInventory.addInventory inventory newInventory.toList().should.eql ['boat', [22, 'string'], [4, 'wool']] @@ -64,12 +64,12 @@ describe 'Inventory', -> it 'creates an empty inventory from an empty inventory', -> a = new Inventory b = a.clone() - b._names.should.eql [] + b._slugs.should.eql [] it 'faithfully copies an existing inventory', -> copy = inventory.clone() - copy._names.should.eql ['boat', 'string', 'wool'] - (item.quantity for name, item of copy._items).should.eql [1, 20, 4] + logger.debug "copy.toList(): #{copy.toList()}" + copy.toList().should.eql ['boat', [20, 'string'], [4, 'wool']] describe 'each', -> @@ -81,17 +81,17 @@ describe 'Inventory', -> it 'works when items have only been added', -> result = [] - inventory.each (item)-> result.push item.name - result.should.eql ['boat', 'string', 'wool'] + inventory.each (stack)-> result.push stack.name + result.should.eql ['Boat', 'String', 'Wool'] it 'works when items have been augmented', -> - inventory.add 'iron ingot' - inventory.add 'boat' - inventory.add 'wool', 2 + inventory.add new Item name:'Iron Ingot' + inventory.add new Item name:'Boat' + inventory.add new Item(name:'Wool'), 2 result = [] - inventory.each (item)-> result.push item.name - result.should.eql ['boat', 'iron ingot', 'string', 'wool'] + inventory.each (stack)-> result.push stack.name + result.should.eql ['Boat', 'Iron Ingot', 'String', 'Wool'] describe 'hasAtLeast', -> @@ -101,7 +101,7 @@ describe 'Inventory', -> it 'always returns true for zero quantity', -> inventory.hasAtLeast('chicken', 0).should.be.true - inventory.hasAtLeast('wool', 0).should.be.true + inventory.hasAtLeast('Wool', 0).should.be.true it 'works for a quantity above 1', -> inventory.hasAtLeast('wool', 3).should.be.true @@ -116,9 +116,9 @@ describe 'Inventory', -> expect(result).to.be.null it 'completely removes the last item', -> - result = inventory.pop() - result.name.should.equal 'wool' - result.quantity.should.equal 4 + stack = inventory.pop() + stack.name.should.equal 'Wool' + stack.quantity.should.equal 4 inventory.toList().should.eql ['boat', [20, 'string']] it 'triggers the right events', -> @@ -138,11 +138,11 @@ describe 'Inventory', -> it 'removes a single item by default', -> inventory.remove 'wool' - inventory._items.wool.quantity.should.equal 3 + inventory._stacks.wool.quantity.should.equal 3 it 'removes a quantity above 1', -> inventory.remove 'wool', 3 - inventory._items.wool.quantity.should.equal 1 + inventory._stacks.wool.quantity.should.equal 1 it 'emits the proper events', -> events = new EventRecorder inventory diff --git a/test/inventory_parser.test.coffee b/test/inventory_parser.test.coffee index 616cd6c82..bb254b652 100644 --- a/test/inventory_parser.test.coffee +++ b/test/inventory_parser.test.coffee @@ -7,6 +7,7 @@ Inventory = require '../src/scripts/models/inventory' InventoryParser = require '../src/scripts/models/inventory_parser' +Item = require '../src/scripts/models/item' ######################################################################################################################## @@ -20,22 +21,22 @@ describe 'InventoryParser', -> it 'returns an empty Inventory for an empty string', -> result = parser.parse '' - result._names.length.should.equal 0 + result.toList().should.eql [] it 'can parse a single item without quantity', -> - result = parser.parse 'wool' - result._items.wool.quantity.should.equal 1 + result = parser.parse 'Wool' + result.toList().should.eql ['wool'] it 'can parse a single item with quantity', -> result = parser.parse '4 wool' - result._items.wool.quantity.should.equal 4 + result.toList().should.eql [[4, 'wool']] it 'can parse multiple mixed-type items', -> - result = parser.parse '4 wool\n10 string\nboat\n\n' + result = parser.parse '4 Wool\n10 String\nBoat\n\n' result.toList().should.eql ['boat', [10, 'string'], [4, 'wool']] it 're-uses the given inventory object', -> inventory = new Inventory - inventory.add 'string', 8 - result = parser.parse '4 wool', inventory + inventory.add new Item(name:'String'), 8 + result = parser.parse '4 Wool', inventory result.toList().should.eql [[8, 'string'], [4, 'wool']] diff --git a/test/mod_pack.test.coffee b/test/mod_pack.test.coffee new file mode 100644 index 000000000..cd457ce47 --- /dev/null +++ b/test/mod_pack.test.coffee @@ -0,0 +1,115 @@ +### +Crafting Guide - mod_pack.test.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. +### + +Item = require '../src/scripts/models/item' +ModPack = require '../src/scripts/models/mod_pack' +ModVersion = require '../src/scripts/models/mod_version' + +######################################################################################################################## + +buildcraft = industrialCraft = minecraft = modPack = null + +######################################################################################################################## + +describe 'ModPack', -> + + beforeEach -> + minecraft = new ModVersion modName:'Minecraft', modVersion:'1.7.10' + minecraft.addItem new Item name:'Wool' + minecraft.addItem new Item name:'Bed', recipes:[''] + + buildcraft = new ModVersion modName:'Buildcraft', modVersion:'4.0' + buildcraft.addItem new Item name:'Stone Gear', recipes:[''] + buildcraft.addItem new Item name:'Bed', recipes:[''] + + industrialCraft = new ModVersion modName:'Industrial Craft', modVersion:'2.0' + industrialCraft.addItem new Item name:'Resin' + industrialCraft.addItem new Item name:'Rubber', recipes:[''] + + modPack = new ModPack modVersions:[minecraft, buildcraft, industrialCraft] + + describe 'enableModsForItem', -> + + it 'it ignores already-enabled mod versions', -> + buildcraft.enabled = true + modPack.enableModsForItem 'Stone Gear' + + minecraft.enabled.should.be.true + buildcraft.enabled.should.be.true + industrialCraft.enabled.should.be.false + + it 'it ignores mod versions not containing the item', -> + modPack.enableModsForItem 'Stone Gear' + + minecraft.enabled.should.be.true + buildcraft.enabled.should.be.true + industrialCraft.enabled.should.be.false + + it 'enables disabled mod versions with the item', -> + modPack.enableModsForItem 'Rubber' + + minecraft.enabled.should.be.true + buildcraft.enabled.should.be.false + industrialCraft.enabled.should.be.true + + describe 'findItemByName', -> + + it 'finds the requested item', -> + item = modPack.findItemByName 'Bed' + item.name.should.equal 'Bed' + + it 'ignores disabled mod versions', -> + item = modPack.findItemByName 'Stone Gear' + expect(item).to.be.null + + it "doesn't ignore mod versions when include disabled is requested", -> + item = modPack.findItemByName 'Stone Gear', includeDisabled:true + item.name.should.equal 'Stone Gear' + + describe 'gatherRecipeNames', -> + + it 'finds all registered item names', -> + buildcraft.enabled = true + industrialCraft.enabled = true + (i.value for i in modPack.gatherRecipeNames()).sort().should.eql ['Bed', 'Rubber', 'Stone Gear'] + + it 'ignores duplicate item names', -> + buildcraft.enabled = true + names = modPack.gatherRecipeNames() + bedName = (e for e in names when e.value is 'Bed')[0] + bedName.should.eql value:'Bed', label:'Bed (from Minecraft 1.7.10)' + + it 'alphabetizes the item names', -> + buildcraft.enabled = true + industrialCraft.enabled = true + (i.value for i in modPack.gatherRecipeNames()).should.eql ['Bed', 'Rubber', 'Stone Gear'] + + it 'ignores non-craftable items', -> + (n.value for n in modPack.gatherRecipeNames()).sort().should.eql ['Bed'] + + it 'ignores disabled mod versions', -> + (n.value for n in modPack.gatherRecipeNames()).should.not.include 'Stone Gear' + + it "doesn't ignore disabled mod versions when include disabled is requested", -> + (n.value for n in modPack.gatherRecipeNames(includeDisabled:true)).should.include 'Stone Gear' + + describe 'hasRecipe', -> + + it 'returns true when the item is present and has recipes', -> + modPack.hasRecipe('Bed').should.be.true + + it 'returns false when the item is not present', -> + modPack.hasRecipe('Iron Sword').should.be.false + + it 'returns false when the item does not have recipes', -> + modPack.hasRecipe('Wool').should.be.false + + it 'ignores disabled mod versions', -> + modPack.hasRecipe('Stone Gear').should.be.false + + it 'includes disabled mod versions when include disabled is requested', -> + modPack.hasRecipe('Stone Gear', includeDisabled:true).should.be.true diff --git a/test/mod_version.test.coffee b/test/mod_version.test.coffee new file mode 100644 index 000000000..006e050e3 --- /dev/null +++ b/test/mod_version.test.coffee @@ -0,0 +1,95 @@ +### +Crafting Guide - mod_version.test.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. +### + +Item = require '../src/scripts/models/item' +ModVersion = require '../src/scripts/models/mod_version' + +######################################################################################################################## + +modVersion = null + +######################################################################################################################## + +describe 'ModVersion', -> + + beforeEach -> modVersion = new ModVersion modName:'Test', modVersion:'0.0' + + describe 'constructor', -> + + it 'requires a mod name', -> + expect(-> new ModVersion modVersion:'0.0').to.throw Error, 'modName cannot be empty' + + it 'requires a mod version', -> + expect(-> new ModVersion modName:'Test').to.throw Error, 'modVersion cannot be empty' + + it 'supplies default values', -> + modVersion.description.should.equal '' + modVersion.items.should.eql {} + modVersion.enabled.should.be.false + + describe 'addItem', -> + + it 'refuses to add duplicates', -> + modVersion.addItem new Item name:'Wool' + expect(-> modVersion.addItem new Item name:'Wool').to.throw Error, 'duplicate item for wool' + + it 'adds an item indexes by its slug', -> + modVersion.addItem new Item name:'Wool' + modVersion.items.wool.name.should.equal 'Wool' + + describe 'compareTo', -> + + it 'lists required mods first', -> + minecraft = new ModVersion modName:'Minecraft', modVersion:'1.7.10' + modVersion.compareTo(minecraft).should.equal +1 + minecraft.compareTo(modVersion).should.equal -1 + + it 'sorts by name second', -> + buildcraft = new ModVersion modName:'Buildcraft', modVersion:'3.0' + modVersion.compareTo(buildcraft).should.equal +1 + buildcraft.compareTo(modVersion).should.equal -1 + + describe 'findItemByName', -> + + it 'locates items by slugified name', -> + modVersion.addItem new Item name:'Crafting Table' + modVersion.findItemByName('Crafting Table').slug.should.equal 'crafting_table' + + describe 'gatherRecipeNames', -> + + it 'skips names already found', -> + modVersion.addItem new Item name:'Wool' + modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + names = modVersion.gatherRecipeNames {wool:true} + names.wool.should.be.true + names.oak_wood_planks.value.should.equal 'Oak Wood Planks' + + it 'only includes craftable items', -> + modVersion.addItem new Item name:'Wool' + modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + names = modVersion.gatherRecipeNames() + _.keys(names).should.eql ['oak_wood_planks'] + + it 'computes the proper value and label', -> + modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + names = modVersion.gatherRecipeNames() + names.oak_wood_planks.value.should.equal 'Oak Wood Planks' + names.oak_wood_planks.label.should.equal 'Oak Wood Planks (from Test 0.0)' + + describe 'hasRecipe', -> + + it 'returns false for an unknown item', -> + modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + modVersion.hasRecipe('Pineapple Upside-Down Cake').should.be.false + + it 'returns false for a un-craftable item', -> + modVersion.addItem new Item name:'Wool' + modVersion.hasRecipe('Wool').should.be.false + + it 'returns true for a craftable item', -> + modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + modVersion.hasRecipe('Oak Wood Planks').should.be.true diff --git a/test/mod_version_parser.test.coffee b/test/mod_version_parser.test.coffee index 514e1bcd9..8fbe127aa 100644 --- a/test/mod_version_parser.test.coffee +++ b/test/mod_version_parser.test.coffee @@ -1,11 +1,13 @@ ### -# Crafting Guide - v1.test.coffee -# -# Copyright (c) 2014 by Redwood Labs -# All rights reserved. +Crafting Guide - v1.test.coffee + +Copyright (c) 2014 by Redwood Labs +All rights reserved. ### -RecipeBookParser = require '../src/scripts/models/mod_version_parser' +Item = require '../src/scripts/models/item' +ModVersion = require '../src/scripts/models/mod_version' +ModVersionParser = require '../src/scripts/models/mod_version_parser' ######################################################################################################################## @@ -13,48 +15,91 @@ parser = null ######################################################################################################################## -describe 'RecipeBookParser', -> +describe 'ModVersionParser', -> describe "V1", -> - before -> parser = new RecipeBookParser.V1 + before -> parser = new ModVersionParser.V1 + + describe '_parseItemList', -> + + beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0' + + it 'can parse an empty list', -> + parser._parseItemList [] + _.keys(parser.modVersion.items).should.eql [] + + it 'can add new items', -> + parser._parseItemList ['Crafting Table', 'Furnace'] + _.keys(parser.modVersion.items).should.eql ['crafting_table', 'furnace'] + + it 'can find existing items', -> + parser.modVersion.addItem new Item name:'Furnace' + parser._parseItemList ['Crafting Table', 'Furnace'] + _.keys(parser.modVersion.items).should.eql ['furnace', 'crafting_table'] describe '_parseModVersion', -> it 'requires a mod_name', -> - data = version:1, mod_version:'1.0', recipes:[] + data = version:1, mod_version:'1.0', items:[] expect(-> parser._parseModVersion data).to.throw Error, 'mod_name is required' it 'requires a mod_version', -> - data = version:1, mod_name:'Empty', recipes:[] + data = version:1, mod_name:'Empty', items:[] expect(-> parser._parseModVersion data).to.throw Error, 'mod_version is required' - it 'can parse an empty recipe book', -> + it 'can parse an empty modVersion', -> data = version: 1 mod_name: 'Empty' mod_version: '1.0' recipes: [] - book = parser._parseModVersion data - book.modName.should.equal 'Empty' - book.modVersion.should.equal '1.0' + modVersion = parser._parseModVersion data + modVersion.modName.should.equal 'Empty' + modVersion.modVersion.should.equal '1.0' - it 'can parse a non-empty recipe book', -> + it 'can parse a non-empty mod version', -> data = version: 1 mod_name: 'Minecraft' mod_version: '1.7.10' recipes: [ - { input:'sugar cane', output:'sugar' } - { input:[[3, 'wool'], [3, 'planks']], tools:'crafting table', output:'bed' } + { input:'Sugar Cane', output:'Sugar' } + { input:[[3, 'Wool'], [3, 'Planks']], tools:'Crafting Table', output:'Bed' } ] - book = parser._parseModVersion data - book.modName.should.equal 'Minecraft' - book.modVersion.should.equal '1.7.10' - (r.name for r in book.recipes).sort().should.eql ['bed', 'sugar'] + modVersion = parser._parseModVersion data + modVersion.modName.should.equal 'Minecraft' + modVersion.modVersion.should.equal '1.7.10' + slugs = (slug for slug, item of modVersion.items).sort() + slugs.should.eql ['bed', 'crafting_table', 'planks', 'sugar', 'sugar_cane', 'wool'] + + describe '_parseRawMaterials', -> + + beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0' + + it 'skips the section when missing', -> + parser._parseRawMaterials null + _.keys(parser.modVersion._items).length.should.equal 0 + + it 'skips the section when empty', -> + parser._parseRawMaterials [] + _.keys(parser.modVersion._items).length.should.equal 0 + + it 'adds items marked as gatherable', -> + parser._parseRawMaterials ['Wool'] + parser.modVersion.items['wool'].isGatherable.should.be.true + + it 'marks an existing item as gatherable', -> + parser.modVersion.addItem new Item name:'Wool' + parser.modVersion.items['wool'].isGatherable.should.be.false + parser._parseRawMaterials ['Wool'] + parser.modVersion.items['wool'].isGatherable.should.be.true + describe '_parseRecipe', -> + beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0' + it 'requires output to be defined', -> parser._errorLocation = 'boat' expect(-> parser._parseRecipe input:'wool').to.throw Error, 'boat is missing output' @@ -68,58 +113,62 @@ describe 'RecipeBookParser', -> input: [[3, 'planks'], [3, 'wool']] tools: 'crafting table' recipe = parser._parseRecipe data - (i.name for i in recipe.output).should.eql ['bed'] - (i.name for i in recipe.input).sort().should.eql ['planks', 'wool'] - (i.name for i in recipe.tools).should.eql ['crafting table'] + (stack.name for stack in recipe.output).should.eql ['bed'] + (stack.name for stack in recipe.input).sort().should.eql ['planks', 'wool'] + (item.name for item in recipe.tools).should.eql ['crafting table'] it 'can parse a recipe without tools', -> recipe = parser._parseRecipe output:'sugar', input:'sugar cane' - (i.name for i in recipe.output).should.eql ['sugar'] - (i.name for i in recipe.input).sort().should.eql ['sugar cane'] - (i.name for i in recipe.tools).should.eql [] + (stack.name for stack in recipe.output).should.eql ['sugar'] + (stack.name for stack in recipe.input).sort().should.eql ['sugar cane'] + (stack.name for stack in recipe.tools).should.eql [] - describe '_parseItemList', -> + describe '_parseStack', -> - it 'can promote a single item to a list', -> - list = parser._parseItemList 'boat' - (i.name for i in list).should.eql ['boat'] - - it 'can require a list to be non-empty', -> - parser._errorLocation = 'boat' - options = field:'output', canBeEmpty:false - expect(-> parser._parseItemList [], options).to.throw Error, 'output for boat cannot be empty' - - it 'can allow an empty list', -> - list = parser._parseItemList [], canBeEmpty:true - list.length.should.equal 0 - - it 'can parse a non-empty list', -> - list = parser._parseItemList [[3, 'plank'], [3, 'wool']] - (i.name for i in list).sort().should.eql ['plank', 'wool'] - - describe '_parseItem', -> + beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0' it 'requires the array to have at least one element', -> parser._errorLocation = 'boat' options = index:1, field:'output' - expect(-> parser._parseItem([], options)).to.throw Error, + expect(-> parser._parseStack([], options)).to.throw Error, "output element 1 for boat must have at least one element" it 'can fill in a missing number', -> - item = parser._parseItem 'boat' + item = parser._parseStack 'boat' item.name.should.equal 'boat' item.quantity.should.equal 1 - item2 = parser._parseItem ['boat'] + item2 = parser._parseStack ['boat'] item2.name.should.equal 'boat' item2.quantity.should.equal 1 it 'requires the data to start with a number', -> parser._errorLocation = 'boat' options = index:1, field:'output' - expect(-> parser._parseItem(['2', 'book'], options)).to.throw Error, + expect(-> parser._parseStack(['2', 'wool'], options)).to.throw Error, "output element 1 for boat must start with a number" it 'can parse a basic item', -> - item = parser._parseItem [2, 'book'] - item.constructor.name.should.equal 'Item' + stack = parser._parseStack [2, 'wool'] + stack.constructor.name.should.equal 'Stack' + + describe '_parseStackList', -> + + beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0' + + it 'can promote a single item to a list', -> + list = parser._parseStackList 'boat' + (i.name for i in list).should.eql ['boat'] + + it 'can require a list to be non-empty', -> + parser._errorLocation = 'boat' + options = field:'output', canBeEmpty:false + expect(-> parser._parseStackList [], options).to.throw Error, 'output for boat cannot be empty' + + it 'can allow an empty list', -> + list = parser._parseStackList [], canBeEmpty:true + list.length.should.equal 0 + + it 'can parse a non-empty list', -> + list = parser._parseStackList [[3, 'plank'], [3, 'wool']] + (i.name for i in list).sort().should.eql ['plank', 'wool'] diff --git a/test/test.coffee b/test/test.coffee index 418cf77c8..f73e3071b 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -20,6 +20,8 @@ global.should = chai.should() Logger = require '../src/scripts/logger' global.logger = new Logger level:Logger.TRACE +require '../src/scripts/underscore_mixins' + # Test Registry ######################################################################################################## mocha.setup 'bdd' @@ -27,6 +29,8 @@ mocha.setup 'bdd' require './crafting_plan.test' require './inventory.test' require './inventory_parser.test' +require './mod_pack.test' +require './mod_version.test' require './mod_version_parser.test' mocha.checkLeaks()