This commit is contained in:
Andrew Miner
2015-01-02 23:03:06 -08:00
parent e389c01468
commit b642c51486
23 changed files with 848 additions and 438 deletions
+6 -4
View File
@@ -4,7 +4,9 @@ global._ = require 'underscore'
global.Backbone = require 'backbone' global.Backbone = require 'backbone'
fs = require 'fs' 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' text = fs.readFileSync sourceFileName, 'UTF-8'
data = JSON.parse text data = JSON.parse text
parser = new RecipeBookParser parser = new ModVersionParser
recipeBook = parser.parse data modVersion = parser.parse data
text = parser.unparse recipeBook text = parser.unparse modVersion
if targetFileName is '-' if targetFileName is '-'
console.log text console.log text
+4
View File
@@ -764,6 +764,10 @@
"output": [[16, "Light Gray Stained Glass Pane"]], "output": [[16, "Light Gray Stained Glass Pane"]],
"input": [[6, "Light Gray Stained Glass"]], "input": [[6, "Light Gray Stained Glass"]],
"tools": "Crafting Table" "tools": "Crafting Table"
}, {
"output": [[8, "Light Gray Stained Glass"]],
"input": [[8, "Glass"], "Light Gray Dye"],
"tools": "Crafting Table"
}, { }, {
"output": "Light Gray Wool", "output": "Light Gray Wool",
"input": ["Light Gray Dye", "Wool"] "input": ["Light Gray Dye", "Wool"]
@@ -30,7 +30,7 @@ module.exports = class CraftingTableController extends BaseController
onModPackChanged: -> onModPackChanged: ->
return unless @rendered return unless @rendered
@model.modPack.enableBooksForRecipe @$nameField.val() @model.modPack.enableModsForItem @$nameField.val()
@_updateNameAutocomplete() @_updateNameAutocomplete()
@_craft() @_craft()
@@ -134,7 +134,7 @@ module.exports = class CraftingTableController extends BaseController
onChanged = => @onNameFieldChanged() onChanged = => @onNameFieldChanged()
@$nameField.autocomplete @$nameField.autocomplete
source: @model.modPack.gatherNames() source: @model.modPack.gatherRecipeNames()
delay: 0 delay: 0
minLength: 0 minLength: 0
change: onChanged change: onChanged
@@ -15,7 +15,7 @@ module.exports = class ModPackController extends BaseController
constructor: (options={})-> constructor: (options={})->
if not options.model? then throw new Error "options.model is required" if not options.model? then throw new Error "options.model is required"
@_bookControllers = [] @_modVersionControllers = []
options.templateName = 'mod_pack' options.templateName = 'mod_pack'
super options super options
@@ -23,8 +23,8 @@ module.exports = class ModPackController extends BaseController
# BaseController Overrides ##################################################################### # BaseController Overrides #####################################################################
onWillRender: -> onWillRender: ->
if @model.books.length is 0 if @model.modVersions.length is 0
@model.loadAllBooks DefaultBookUrls @model.loadAllModVersions DefaultBookUrls
onDidRender: -> onDidRender: ->
@$table = @$('table') @$table = @$('table')
@@ -34,12 +34,12 @@ module.exports = class ModPackController extends BaseController
@$('table tr:not(:last-child)').remove() @$('table tr:not(:last-child)').remove()
return unless @model? return unless @model?
@_bookControllers = [] @_modVersionControllers = []
for i in [@model.books.length-1..0] by -1 for i in [@model.modVersions.length-1..0] by -1
book = @model.books[i] modVersion = @model.modVersions[i]
controller = new ModVersionController model:book controller = new ModVersionController model:modVersion
controller.render() controller.render()
@_bookControllers.push controller @_modVersionControllers.push controller
@$table.prepend controller.$el @$table.prepend controller.$el
super super
+6 -4
View File
@@ -1,10 +1,12 @@
### ###
# Crafting Guide - main.coffee Crafting Guide - main.coffee
#
# Copyright (c) 2014 by Redwood Labs Copyright (c) 2014 by Redwood Labs
# All rights reserved. All rights reserved.
### ###
require './underscore_mixins'
views = require './views' views = require './views'
Logger = require './logger' Logger = require './logger'
CraftingGuideRouter = require './crafting_guide_router' CraftingGuideRouter = require './crafting_guide_router'
+46 -35
View File
@@ -29,11 +29,18 @@ module.exports = class CraftingPlan
return this return this
craft: (name, quantity=1, have=null)-> 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() @clear()
@result.addInventory(have) if have? @result.addInventory(have) if have?
@_expected.add name, quantity @_expected.add item, quantity
@_pending = @_expected.clone() @_pending = @_expected.clone()
while not @_pending.isEmpty while not @_pending.isEmpty
@_processPending() @_processPending()
@@ -47,50 +54,54 @@ module.exports = class CraftingPlan
# Private Methods ############################################################################## # Private Methods ##############################################################################
_processPending: -> _processPending: ->
targetItem = @_pending.pop() targetStack = @_pending.pop()
targetItem = targetStack.item
logger.verbose "processing item: #{targetItem}, craftable? #{targetItem.isCraftable}"
return unless targetItem? return unless targetItem?
return if (not targetItem.isCraftable) or targetItem.isGatherable
return if @modPack.isRawMaterial targetItem.name recipe = targetItem.recipes[0]
logger.verbose "recipe: #{recipe}"
recipes = @modPack.gatherRecipes targetItem.name
return if not recipes.length > 0
recipe = recipes[0]
if @includingTools if @includingTools
for tool in recipe.tools for toolItem in recipe.tools
totalExpected = @result.quantityOf(tool.name) + @_expected.quantityOf(tool.name) totalExpected = @result.quantityOf(toolItem.slug) + @_expected.quantityOf(toolItem.slug)
if totalExpected < tool.quantity if totalExpected < 1
@_pending.add tool.name, tool.quantity @_pending.add toolItem
@_expected.add tool.name, tool.quantity @_expected.add toolItem
while @_totalQuantityOf(targetItem.name) < @_expected.quantityOf(targetItem.name) while @_totalQuantityOf(targetItem.slug) < @_expected.quantityOf(targetItem.slug)
@steps.push recipe @steps.push recipe
for item in recipe.input for stack in recipe.input
@_processInputItem item @_processInputStack stack
for item in recipe.output for stack in recipe.output
@_processOutputItem item @_processOutputStack stack
_processInputItem: (item)-> _processInputStack: (stack)->
quantityAvailable = @result.quantityOf item.name slug = stack.item.slug
quantityUsed = Math.min quantityAvailable, item.quantity quantityAvailable = @result.quantityOf slug
quantityNeeded = item.quantity - quantityUsed 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 @result.remove slug, quantityUsed
@_pending.add item.name, quantityNeeded @_pending.add stack.item, quantityNeeded
@need.add item.name, quantityNeeded @need.add stack.item, quantityNeeded
_processOutputItem: (item)-> _processOutputStack: (stack)->
quantityMissing = @need.quantityOf item.name slug = stack.item.slug
quantityUsed = Math.min quantityMissing, item.quantity quantityMissing = @need.quantityOf slug
quantityLeft = item.quantity - quantityUsed 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 @make.add stack.item, stack.quantity
@need.remove item.name, quantityUsed @need.remove slug, quantityUsed
@result.add item.name, quantityLeft @result.add stack.item, quantityLeft
_totalQuantityOf: (slug)->
_totalQuantityOf: (name)-> return @result.quantityOf(slug) - @need.quantityOf(slug)
return @result.quantityOf(name) - @need.quantityOf(name)
+6 -6
View File
@@ -1,8 +1,8 @@
### ###
# Crafting Guide - crafting_table.coffee Crafting Guide - crafting_table.coffee
#
# Copyright (c) 2014 by Redwood Labs Copyright (c) 2014 by Redwood Labs
# All rights reserved. All rights reserved.
### ###
BaseModel = require './base_model' BaseModel = require './base_model'
@@ -30,10 +30,10 @@ module.exports = class CraftingTable extends BaseModel
@plan = null @plan = null
return 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}" logger.verbose "calculating build plan for #{@quantity} #{@name}#{toolPhrase} with inventory: #{@have}"
@modPack.enableBooksForRecipe @name @modPack.enableModsForItem @name
plan = new CraftingPlan @modPack, @includingTools plan = new CraftingPlan @modPack, @includingTools
plan.includingTools = @includingTools plan.includingTools = @includingTools
+48 -48
View File
@@ -7,7 +7,7 @@
BaseModel = require './base_model' BaseModel = require './base_model'
{Event} = require '../constants' {Event} = require '../constants'
Item = require './item' Stack = require './stack'
######################################################################################################################## ########################################################################################################################
@@ -17,91 +17,91 @@ module.exports = class Inventory extends BaseModel
super attributes, options super attributes, options
@clear() @clear()
Object.defineProperty this, 'isEmpty', get:-> @_names.length is 0 Object.defineProperty @prototype, 'isEmpty', get:-> @_slugs.length is 0
# Public Methods ############################################################################### # Public Methods ###############################################################################
add: (name, quantity=1)-> add: (item, quantity=1)->
return if quantity is 0 return if quantity is 0
item = @_items[name] stack = @_stacks[item.slug]
if not item? if not stack?
item = new Item name:name, quantity:quantity stack = new Stack item:item, quantity:quantity
@_items[name] = item @_stacks[item.slug] = stack
@_names.push name @_slugs.push item.slug
@_names.sort() @_slugs.sort()
else else
item.quantity += quantity stack.quantity += quantity
@trigger Event.add, this, name, quantity @trigger Event.add, this, item, quantity
@trigger Event.change, this @trigger Event.change, this
return this return this
addInventory: (inventory)-> addInventory: (inventory)->
inventory.each (item)=> @add item.name, item.quantity inventory.each (stack)=> @add stack.item, stack.quantity
return this return this
clear: -> clear: ->
@_items = {} @_stacks = {}
@_names = [] @_slugs = []
clone: -> clone: ->
inventory = new Inventory inventory = new Inventory
@each (item)-> inventory.add item.name, item.quantity @each (stack)-> inventory.add stack.item, stack.quantity
return inventory return inventory
each: (onItem)-> each: (onItem)->
for name in @_names for slug in @_slugs
item = @_items[name] stack = @_stacks[slug]
onItem item onItem stack
hasAtLeast: (name, quantity=1)-> hasAtLeast: (slug, quantity=1)->
if quantity is 0 then return true if quantity is 0 then return true
item = @_items[name] stack = @_stacks[slug]
return false unless item? return false unless stack?
return item.quantity >= quantity return stack.quantity >= quantity
pop: -> pop: ->
name = @_names.pop() slug = @_slugs.pop()
return null unless name? return null unless slug?
item = @_items[name] stack = @_stacks[slug]
delete @_items[name] delete @_stacks[slug]
@trigger Event.remove, this, item.name, item.quantity @trigger Event.remove, this, stack.item, stack.quantity
@trigger Event.change, this @trigger Event.change, this
return item return stack
quantityOf: (name)-> quantityOf: (slug)->
item = @_items[name] stack = @_stacks[slug]
return 0 unless item? return 0 unless stack?
return item.quantity return stack.quantity
remove: (name, quantity=1)-> remove: (slug, quantity=1)->
return if quantity is 0 return if quantity is 0
item = @_items[name] stack = @_stacks[slug]
if not item? then throw new Error "cannot remove #{name} since it is not in this inventory" if not stack? then throw new Error "cannot remove #{slug} since it is not in this inventory"
if item.quantity < quantity if stack.quantity < quantity
throw new Error "cannot remove #{quantity} #{name} because there is only #{item.quantity} in this inventory" throw new Error "cannot remove #{quantity} #{slug} because there is only #{stack.quantity} in this inventory"
item.quantity -= quantity stack.quantity -= quantity
if item.quantity is 0 if stack.quantity is 0
delete @_items[name] delete @_stacks[slug]
@_names = _(@_names).without name @_slugs = _(@_slugs).without slug
@trigger Event.remove, this, name, quantity @trigger Event.remove, this, slug, quantity
@trigger Event.change, this @trigger Event.change, this
return this return this
toList: -> toList: ->
result = [] result = []
@each (item)-> @each (stack)->
if item.quantity > 1 if stack.quantity > 1
result.push [item.quantity, item.name] result.push [stack.quantity, stack.item.slug]
else else
result.push item.name result.push stack.item.slug
return result return result
# Object Overrides ############################################################################# # Object Overrides #############################################################################
@@ -110,9 +110,9 @@ module.exports = class Inventory extends BaseModel
result = [@constructor.name, " (", @cid, ") { items: ["] result = [@constructor.name, " (", @cid, ") { items: ["]
needsDelimiter = false needsDelimiter = false
@each (item)-> @each (stack)->
if needsDelimiter then result.push ', ' if needsDelimiter then result.push ', '
result.push item.toString() result.push stack.toString()
needsDelimiter = true needsDelimiter = true
result.push ']' result.push ']'
+6 -5
View File
@@ -1,11 +1,12 @@
### ###
# Crafting Guide - inventory_parser.coffee Crafting Guide - inventory_parser.coffee
#
# Copyright (c) 2014 by Redwood Labs Copyright (c) 2014 by Redwood Labs
# All rights reserved. All rights reserved.
### ###
Inventory = require './inventory' Inventory = require './inventory'
Item = require './item'
######################################################################################################################## ########################################################################################################################
@@ -24,6 +25,6 @@ module.exports = class InventoryParser
quantity = if match? then parseInt(match[1]) else 1 quantity = if match? then parseInt(match[1]) else 1
if name.length > 0 if name.length > 0
inventory.add name, quantity inventory.add new Item(name:name), quantity
return inventory return inventory
+32 -22
View File
@@ -14,38 +14,48 @@ module.exports = class Item extends BaseModel
@DEFAULT_STACK_SIZE = 64 @DEFAULT_STACK_SIZE = 64
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
attributes.name ?= '' if not attributes.name? then throw new Error 'attributes.name is required'
attributes.quantity ?= 1
attributes.isGatherable ?= false
attributes.recipes ?= []
attributes.slug ?= _.slugify attributes.name
attributes.stackSize ?= Item.DEFAULT_STACK_SIZE attributes.stackSize ?= Item.DEFAULT_STACK_SIZE
super attributes, options super attributes, options
Object.defineProperty this, 'stackQuantity', get:@getStackQuantity Object.defineProperty @prototype, 'isCraftable', get:-> @recipes.length > 0
# Public Methods ############################################################################### # Public Methods ###############################################################################
canMerge: (item)-> addRecipe: (recipe)->
return item.name is @name 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)-> @recipes.push recipe
if not @canMerge(item) then throw new Error "cannot merge #{item} into #{this}"
@quantity += item.quantity
# Property Methods ############################################################################# compareTo: (that)->
if this.name isnt that.name
getStackQuantity: -> return if this.name < that.name then -1 else +1
count = 0 return 0
extra = @quantity
while extra > @stackSize
extra -= @stackSize
count += 1
return count:count, extra:extra
# Object Overrides ############################################################################# # Object Overrides #############################################################################
toString: -> 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 if @stackSize isnt Item.DEFAULT_STACK_SIZE
result += ", stackSize:#{@stackSize}" result.push ', stackSize:'; result.push @stackSize
result += ' }'
return result if @recipes.length > 0
result.push ', recipes:'
result.push @recipes.length
result.push ' items'
result.push '}'
return result.join ''
+44 -69
View File
@@ -9,32 +9,43 @@ BaseModel = require './base_model'
{Event} = require '../constants' {Event} = require '../constants'
ModVersionParser = require './mod_version_parser' ModVersionParser = require './mod_version_parser'
{RequiredMods} = require '../constants' {RequiredMods} = require '../constants'
util = require 'util'
######################################################################################################################## ########################################################################################################################
module.exports = class ModPack extends BaseModel module.exports = class ModPack extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
attributes.books ?= [] attributes.modVersions ?= []
super attributes, options super attributes, options
@_parser = new ModVersionParser @_parser = new ModVersionParser
# Public Methods ############################################################################### # Public Methods ###############################################################################
enableBooksForRecipe: (name)-> enableModsForItem: (name)->
for book in @books for modVersion in @modVersions
continue if book.enabled continue if modVersion.enabled
if book.hasRecipe name if modVersion.hasRecipe name
book.enabled = true 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 options.includeDisabled ?= false
nameData = {} nameData = {}
for book in @books for modVersion in @modVersions
continue unless book.enabled or options.includeDisabled continue unless modVersion.enabled or options.includeDisabled
book.gatherNames nameData modVersion.gatherRecipeNames nameData
result = [] result = []
names = _.keys(nameData).sort() names = _.keys(nameData).sort()
@@ -42,76 +53,56 @@ module.exports = class ModPack extends BaseModel
result.push nameData[name] result.push nameData[name]
return result 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={})-> hasRecipe: (name, options={})->
options.includeDisabled ?= false options.includeDisabled ?= false
for book in @books for modVersion in @modVersions
continue unless book.enabled or options.includeDisabled continue unless modVersion.enabled or options.includeDisabled
return true if book.hasRecipe name return true if modVersion.hasRecipe name
return false return false
isRawMaterial: (name, options={})-> loadModVersion: (url)->
options.includeDisabled ?= false
for book in @books
continue unless book.enabled or options.includeDisabled
return true if book.isRawMaterial name
return false
loadBook: (url)->
w.promise (resolve, reject)=> w.promise (resolve, reject)=>
@trigger Event.load.started, this, url @trigger Event.load.started, this, url
$.ajax $.ajax
url: url url: url
dataType: 'json' dataType: 'json'
success: (data, status, xhr)=> success: (data, status, xhr)=>
resolve @onBookLoaded(url, data, status, xhr) resolve @onModVersionLoaded(url, data, status, xhr)
error: (xhr, status, error)=> error: (xhr, status, error)=>
reject @onBookLoadFailed(url, error, status, xhr) reject @onModVersionLoadFailed(url, error, status, xhr)
loadBookData: (data)-> loadModVersionData: (data)->
book = @_parser.parse data modVersion = @_parser.parse data
@books.push book @modVersions.push modVersion
@_sortBooks() @modVersions.sort (a, b)-> a.compareTo b
book.on Event.change, => @trigger Event.change, this modVersion.on Event.change, => @trigger Event.change, this
return book return modVersion
loadAllBooks: (urlList)-> loadAllModVersions: (urlList)->
promises = (@loadBook(url) for url in urlList) promises = (@loadModVersion(url) for url in urlList)
return w.settle promises return w.settle promises
# Event Methods ################################################################################ # Event Methods ################################################################################
onBookLoaded: (url, data, status, xhr)-> onModVersionLoaded: (url, data, status, xhr)->
try try
book = @loadBookData data modVersion = @loadModVersionData data
logger.info "loaded recipe book from #{url}: #{book}" logger.info "loaded ModVersion from #{url}: #{modVersion}"
@trigger Event.load.succeeded, this, book @trigger Event.load.succeeded, this, modVersion
@trigger Event.load.finished, this @trigger Event.load.finished, this
@trigger Event.change, this @trigger Event.change, this
return book return modVersion
catch e 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 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.failed, this, error.message
@trigger Event.load.finished, this @trigger Event.load.finished, this
return error return error
@@ -119,20 +110,4 @@ module.exports = class ModPack extends BaseModel
# Object Overrides ############################################################################# # Object Overrides #############################################################################
toString: -> toString: ->
return "ModPack (#{@cid}) {books:#{@books.length} items}" return "ModPack (#{@cid}) {modVersions:#{@modVersions.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
+34 -20
View File
@@ -17,34 +17,48 @@ module.exports = class ModVersion extends BaseModel
if _.isEmpty(attributes.modVersion) then throw new Error 'modVersion cannot be empty' if _.isEmpty(attributes.modVersion) then throw new Error 'modVersion cannot be empty'
attributes.description ?= '' attributes.description ?= ''
attributes.rawMaterials ?= [] attributes.items ?= {}
attributes.recipes ?= []
attributes.enabled ?= attributes.modName in RequiredMods attributes.enabled ?= attributes.modName in RequiredMods
super attributes, options super attributes, options
# Public Methods ############################################################################### # Public Methods ###############################################################################
gatherNames: (result)-> addItem: (item)->
for recipe in @recipes if @items[item.slug]? then throw new Error "duplicate item for #{item.slug}"
continue if result[recipe.name] @items[item.slug] = item
result[recipe.name] = value:recipe.name, label:"#{recipe.name} (from #{@modName} #{@modVersion})" 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 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)-> hasRecipe: (name)->
for recipe in @recipes item = @findItemByName name
return true if recipe.name is name return false unless item?
return false return item.recipes.length > 0
# Object Overrides ############################################################################# # Object Overrides #############################################################################
@@ -53,4 +67,4 @@ module.exports = class ModVersion extends BaseModel
enabled:#{@enabled}, enabled:#{@enabled},
modName:#{@modName}, modName:#{@modName},
modVersion:#{@modVersion}, modVersion:#{@modVersion},
recipes:#{@recipes.length} items}" items:#{_.keys(@items).length} items}"
+140 -85
View File
@@ -6,8 +6,10 @@ All rights reserved.
### ###
Item = require './item' Item = require './item'
Recipe = require './recipe'
ModVersion = require './mod_version' ModVersion = require './mod_version'
Recipe = require './recipe'
Stack = require './stack'
util = require 'util'
######################################################################################################################## ########################################################################################################################
@@ -20,21 +22,21 @@ module.exports = class ModVersionParser
'1': new V1 '1': new V1
parse: (data)-> 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' if not data.version? then throw new Error 'version is required'
parser = @_parsers["#{data.version}"] 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 return parser.parse data
unparse: (ModVersion, version=ModVersionParser.CURRENT_VERSION)-> unparse: (modVersion, version=ModVersionParser.CURRENT_VERSION)->
if not ModVersion? then throw new Error 'recipe book is required' if not modVersion? then throw new Error 'modVersion is required'
parser = @_parsers["#{version}"] parser = @_parsers["#{version}"]
if not parser? then throw new Error "version #{version} is not supported" 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)-> parse: (data)->
return @_parseModVersion data return @_parseModVersion data
unparse: (ModVersion)-> unparse: (modVersion)->
return @_unparseModVersion ModVersion return @_unparseModVersion modVersion
# Private Methods ############################################################################## # Private Methods ##############################################################################
_parseModVersion: (data)-> _findOrCreateItem: (name)->
if not data? then throw new Error 'recipe book data is missing' item = @modVersion.findItemByName name
if not data.version? then throw new Error 'version is required' if not item?
if not data.mod_name? then throw new Error 'mod_name is required' item = new Item name:name
if not data.mod_version? then throw new Error 'mod_version is required' @modVersion.addItem item
if not _.isArray(data.recipes) then throw new Error 'recipes must be an array' return item
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
_parseItemList: (data, options={})-> _parseItemList: (data, options={})->
if not data? then throw new Error "#{@_errorLocation} must have an #{options.field} field" if not data? then throw new Error "#{@_errorLocation} must have an #{options.field} field"
if not _.isArray(data) then data = [data] 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 = [] result = []
for index in [0...data.length] for name in data
itemData = data[index] item = @_findOrCreateItem name
result.push @_parseItem itemData, field:options.field, index:index result.push item
return result 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}" errorBase = "#{options.field} element #{options.index} for #{@_errorLocation}"
if not data? then throw new Error "#{errorBase} is missing" if not data? then throw new Error "#{errorBase} is missing"
@@ -112,39 +129,64 @@ module.exports.V1 = class V1
if data.length isnt 2 then throw new Error "#{errorBase} must have at least one element" 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" 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 = []
result.push '{\n' result.push '{\n'
result.push ' "version": 1,\n' result.push ' "version": 1,\n'
result.push ' "mod_name": "' + ModVersion.modName + '",\n' result.push ' "mod_name": "' + modVersion.modName + '",\n'
result.push ' "mod_version": "' + ModVersion.modVersion + '",\n' result.push ' "mod_version": "' + modVersion.modVersion + '",\n'
if ModVersion.description.length > 0 if modVersion.description.length > 0
result.push ' "description": "' + ModVersion.description + '",\n' 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' result.push ' "raw_materials": [\n'
firstItem = true firstItem = true
materials = ModVersion.rawMaterials.slice() for material in rawMaterials
materials.sort()
for material in materials
if not firstItem then result.push ',\n' if not firstItem then result.push ',\n'
result.push ' "' + material + '"' result.push ' "' + material + '"'
firstItem = false firstItem = false
result.push '\n ],\n' result.push '\n ],\n'
result.push ' "recipes": [\n' result.push ' "recipes": [\n'
recipes = ModVersion.recipes.slice()
recipes.sort (a, b)-> items = (item for slug, item of modVersion.items when item.isCraftable)
if a.name isnt b.name items.sort (a, b)-> a.compareTo b
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
firstItem = true firstItem = true
for recipe in recipes for item in items
for recipe in item.recipes
result.push if firstItem then ' {\n' else ' }, {\n' result.push if firstItem then ' {\n' else ' }, {\n'
@_unparseRecipe recipe, result @_unparseRecipe recipe, result
firstItem = false firstItem = false
@@ -157,12 +199,12 @@ module.exports.V1 = class V1
_unparseRecipe: (recipe, result=[])-> _unparseRecipe: (recipe, result=[])->
result.push ' "output": ' result.push ' "output": '
@_unparseItemList recipe.output, result, sort:false @_unparseStackList recipe.output, result, sort:false
if recipe.input.length > 0 if recipe.input.length > 0
result.push ',\n' result.push ',\n'
result.push ' "input": ' result.push ' "input": '
@_unparseItemList recipe.input, result @_unparseStackList recipe.input, result
if recipe.tools.length > 0 if recipe.tools.length > 0
result.push ',\n' result.push ',\n'
@@ -172,38 +214,51 @@ module.exports.V1 = class V1
result.push '\n' result.push '\n'
return result return result
_unparseItemList: (itemList, result, options={})-> _unparseStackList: (stackList, result, options={})->
options.sort ?= true options.sort ?= true
if itemList.length is 0 if stackList.length is 0
result.push '[]' result.push '[]'
else if itemList.length is 1 else if stackList.length is 1
item = itemList[0] stack = stackList[0]
if item.quantity is 1 if stack.quantity is 1
result.push '"' + item.name + '"' result.push '"' + stack.name + '"'
else else
result.push '[[' + item.quantity + ', "' + item.name + '"]]' result.push '[[' + stack.quantity + ', "' + stack.name + '"]]'
else else
result.push '[' result.push '['
items = itemList.slice() stacks = stackList.slice()
if options.sort if options.sort
items.sort (a, b)-> stacks.sort (a, b)->
if a.quantity isnt b.quantity if a.quantity isnt b.quantity
return if a.quantity > b.quantity then -1 else +1 return if a.quantity > b.quantity then -1 else +1
if a.name isnt b.name if a.item.name isnt b.item.name
return if a.name < b.name then -1 else +1 return if a.item.name < b.item.name then -1 else +1
return 0 return 0
firstItem = true firstItem = true
for item in items for stack in stacks
result.push ', ' if not firstItem result.push ', ' if not firstItem
if item.quantity is 1 if stack.quantity is 1
result.push '"' + item.name + '"' result.push '"' + stack.name + '"'
else else
result.push '[' + item.quantity + ', "' + item.name + '"]' result.push '[' + stack.quantity + ', "' + stack.name + '"]'
firstItem = false firstItem = false
result.push ']' result.push ']'
return result 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
+19 -16
View File
@@ -12,26 +12,29 @@ BaseModel = require './base_model'
module.exports = class Recipe extends BaseModel module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
if not attributes.output? then throw new Error "attributes.output is required" if not attributes.input? then throw new Error 'attributes.input is required'
attributes.input ?= [] 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 ?= [] attributes.tools ?= []
super attributes, options super attributes, options
Object.defineProperty @prototype, 'name', get:-> @output[0].name Object.defineProperty @prototype, 'name', get:-> @output[0].item.name
# Public Methods ############################################################################### # Public Methods ###############################################################################
make: (inventory, missing)-> make: (inventory, missing)->
for item in @input for stack in @input
needed = item.quantity item = stack.item
needed = stack.quantity
while needed > 0 while needed > 0
if inventory.hasAtLeast item.name if inventory.hasAtLeast item.slug
inventory.remove item.name inventory.remove item.slug
else else
missing.add item.name missing.add item.slug
for item in @output for stack in @output
inventory.add item.name, item.quantity inventory.add stack.item, stack.quantity
return this return this
@@ -42,26 +45,26 @@ module.exports = class Recipe extends BaseModel
result.push ", input:[" result.push ", input:["
needsDelimiter = false needsDelimiter = false
for inputItem in @input for stack in @input
if needsDelimiter then result.push ', ' if needsDelimiter then result.push ', '
result.push inputItem.toString() result.push stack.toString()
needsDelimiter = true needsDelimiter = true
result.push ']' result.push ']'
result.push ", output:[" result.push ", output:["
needsDelimiter = false needsDelimiter = false
for outputItem in @output for stack in @output
if needsDelimiter then result.push ', ' if needsDelimiter then result.push ', '
result.push outputItem.toString() result.push stack.toString()
needsDelimiter = true needsDelimiter = true
result.push ']' result.push ']'
if @tools.length > 0 if @tools.length > 0
result.push ", tools:[" result.push ", tools:["
needsDelimiter = false needsDelimiter = false
for tool in @tools for stack in @tools
if needsDelimiter then result.push ', ' if needsDelimiter then result.push ', '
result.push tool.toString() result.push stack.toString()
needsDelimiter = true needsDelimiter = true
result.push ']' result.push ']'
+49
View File
@@ -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}"
+18
View File
@@ -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
+22 -20
View File
@@ -1,8 +1,8 @@
### ###
# Crafting Guide - crafting_plan.test.coffee Crafting Guide - crafting_plan.test.coffee
#
# Copyright (c) 2014 by Redwood Labs Copyright (c) 2014 by Redwood Labs
# All rights reserved. All rights reserved.
### ###
ModPack = require '../src/scripts/models/mod_pack' ModPack = require '../src/scripts/models/mod_pack'
@@ -10,15 +10,15 @@ CraftingPlan = require '../src/scripts/models/crafting_plan'
######################################################################################################################## ########################################################################################################################
catalog = plan = null modPack = plan = null
######################################################################################################################## ########################################################################################################################
describe 'CraftingPlan', -> describe 'CraftingPlan', ->
beforeEach -> beforeEach ->
catalog = new ModPack modPack = new ModPack
catalog.loadBookData { modPack.loadModVersionData {
version: 1 version: 1
mod_name: 'Minecraft' mod_name: 'Minecraft'
mod_version: '1.7.10' mod_version: '1.7.10'
@@ -31,8 +31,10 @@ describe 'CraftingPlan', ->
{ input:[[2, 'Iron Ingot'], 'Stick'], tools:'Crafting Table', output:'Iron Sword' } { 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', -> describe 'craft', ->
@@ -40,40 +42,40 @@ describe 'CraftingPlan', ->
it 'can craft a single step recipe', -> it 'can craft a single step recipe', ->
plan.craft 'Oak Plank' plan.craft 'Oak Plank'
plan.need.toList().should.eql ['Oak Log'] plan.need.toList().should.eql ['oak_log']
plan.result.toList().should.eql [[4, 'Oak Plank']] plan.result.toList().should.eql [[4, 'oak_plank']]
it 'can craft a multi-step recipe', -> it 'can craft a multi-step recipe', ->
plan.craft 'Crafting Table' plan.craft 'Crafting Table'
plan.need.toList().should.eql ['Oak Log'] plan.need.toList().should.eql ['oak_log']
plan.result.toList().should.eql ['Crafting Table'] plan.result.toList().should.eql ['crafting_table']
it 'can craft a multi-step recipe using tools', -> it 'can craft a multi-step recipe using tools', ->
plan.craft 'Furnace' plan.craft 'Furnace'
plan.need.toList().should.eql [[8, 'Cobblestone']] plan.need.toList().should.eql [[8, 'cobblestone']]
plan.result.toList().should.eql ['Furnace'] plan.result.toList().should.eql ['furnace']
it 'can craft a multi-step recipe re-using tools', -> it 'can craft a multi-step recipe re-using tools', ->
plan.craft 'Iron Sword' plan.craft 'Iron Sword'
plan.need.toList().should.eql [[2, 'Iron Ore'], 'Oak Log', [2, '{furnace fuel}']] 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']] plan.result.toList().should.eql ['iron_sword', [2, 'oak_plank'], [3, 'stick']]
describe 'with building tools', -> describe 'with building tools', ->
it 'can craft a multi-step recipe using tools', -> it 'can craft a multi-step recipe using tools', ->
plan.includingTools = true plan.includingTools = true
plan.craft 'Furnace' plan.craft 'Furnace'
plan.need.toList().should.eql [[8, 'Cobblestone'], 'Oak Log'] plan.need.toList().should.eql [[8, 'cobblestone'], 'oak_log']
plan.result.toList().should.eql ['Crafting Table', 'Furnace'] plan.result.toList().should.eql ['crafting_table', 'furnace']
it 'can craft a multi-step recipe re-using tools', -> it 'can craft a multi-step recipe re-using tools', ->
plan.includingTools = true plan.includingTools = true
plan.craft 'Iron Sword' plan.craft 'Iron Sword'
plan.need.toList().should.eql [ 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 [ 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', -> describe 'using existing inventory', ->
+29 -29
View File
@@ -20,30 +20,30 @@ describe 'Inventory', ->
beforeEach -> beforeEach ->
inventory = new Inventory inventory = new Inventory
inventory.add 'wool', 4 inventory.add new Item(name:'Wool'), 4
inventory.add 'string', 20 inventory.add new Item(name:'String'), 20
inventory.add 'boat' inventory.add new Item(name:'Boat')
describe 'add', -> describe 'add', ->
it 'can add to an empty inventory', -> it 'can add to an empty inventory', ->
inventory.add 'iron ingot', 4 inventory.add new Item(name:'Iron Ingot'), 4
item = inventory._items['iron ingot'] stack = inventory._stacks['iron_ingot']
item.constructor.name.should.equal 'Item' stack.constructor.name.should.equal 'Stack'
item.name.should.equal 'iron ingot' stack.name.should.equal 'Iron Ingot'
item.quantity.should.equal 4 stack.quantity.should.equal 4
it 'can augment quantity of existing items', -> 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']] inventory.toList().should.eql ['boat', [20, 'string'], [6, 'wool']]
it 'can add zero quantity', -> 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']] inventory.toList().should.eql ['boat', [20, 'string'], [4, 'wool']]
it 'emits the proper events', -> it 'emits the proper events', ->
events = new EventRecorder inventory 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] events.names.should.eql [Event.add, Event.change]
describe 'addInventory', -> describe 'addInventory', ->
@@ -51,11 +51,11 @@ describe 'Inventory', ->
it 'can add to an empty inventory', -> it 'can add to an empty inventory', ->
newInventory = new Inventory newInventory = new Inventory
newInventory.addInventory 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', -> it 'can add a mix of new and existing items', ->
newInventory = new Inventory newInventory = new Inventory
newInventory.add 'string', 2 newInventory.add new Item(name:'String'), 2
newInventory.addInventory inventory newInventory.addInventory inventory
newInventory.toList().should.eql ['boat', [22, 'string'], [4, 'wool']] newInventory.toList().should.eql ['boat', [22, 'string'], [4, 'wool']]
@@ -64,12 +64,12 @@ describe 'Inventory', ->
it 'creates an empty inventory from an empty inventory', -> it 'creates an empty inventory from an empty inventory', ->
a = new Inventory a = new Inventory
b = a.clone() b = a.clone()
b._names.should.eql [] b._slugs.should.eql []
it 'faithfully copies an existing inventory', -> it 'faithfully copies an existing inventory', ->
copy = inventory.clone() copy = inventory.clone()
copy._names.should.eql ['boat', 'string', 'wool'] logger.debug "copy.toList(): #{copy.toList()}"
(item.quantity for name, item of copy._items).should.eql [1, 20, 4] copy.toList().should.eql ['boat', [20, 'string'], [4, 'wool']]
describe 'each', -> describe 'each', ->
@@ -81,17 +81,17 @@ describe 'Inventory', ->
it 'works when items have only been added', -> it 'works when items have only been added', ->
result = [] result = []
inventory.each (item)-> result.push item.name inventory.each (stack)-> result.push stack.name
result.should.eql ['boat', 'string', 'wool'] result.should.eql ['Boat', 'String', 'Wool']
it 'works when items have been augmented', -> it 'works when items have been augmented', ->
inventory.add 'iron ingot' inventory.add new Item name:'Iron Ingot'
inventory.add 'boat' inventory.add new Item name:'Boat'
inventory.add 'wool', 2 inventory.add new Item(name:'Wool'), 2
result = [] result = []
inventory.each (item)-> result.push item.name inventory.each (stack)-> result.push stack.name
result.should.eql ['boat', 'iron ingot', 'string', 'wool'] result.should.eql ['Boat', 'Iron Ingot', 'String', 'Wool']
describe 'hasAtLeast', -> describe 'hasAtLeast', ->
@@ -101,7 +101,7 @@ describe 'Inventory', ->
it 'always returns true for zero quantity', -> it 'always returns true for zero quantity', ->
inventory.hasAtLeast('chicken', 0).should.be.true 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', -> it 'works for a quantity above 1', ->
inventory.hasAtLeast('wool', 3).should.be.true inventory.hasAtLeast('wool', 3).should.be.true
@@ -116,9 +116,9 @@ describe 'Inventory', ->
expect(result).to.be.null expect(result).to.be.null
it 'completely removes the last item', -> it 'completely removes the last item', ->
result = inventory.pop() stack = inventory.pop()
result.name.should.equal 'wool' stack.name.should.equal 'Wool'
result.quantity.should.equal 4 stack.quantity.should.equal 4
inventory.toList().should.eql ['boat', [20, 'string']] inventory.toList().should.eql ['boat', [20, 'string']]
it 'triggers the right events', -> it 'triggers the right events', ->
@@ -138,11 +138,11 @@ describe 'Inventory', ->
it 'removes a single item by default', -> it 'removes a single item by default', ->
inventory.remove 'wool' inventory.remove 'wool'
inventory._items.wool.quantity.should.equal 3 inventory._stacks.wool.quantity.should.equal 3
it 'removes a quantity above 1', -> it 'removes a quantity above 1', ->
inventory.remove 'wool', 3 inventory.remove 'wool', 3
inventory._items.wool.quantity.should.equal 1 inventory._stacks.wool.quantity.should.equal 1
it 'emits the proper events', -> it 'emits the proper events', ->
events = new EventRecorder inventory events = new EventRecorder inventory
+8 -7
View File
@@ -7,6 +7,7 @@
Inventory = require '../src/scripts/models/inventory' Inventory = require '../src/scripts/models/inventory'
InventoryParser = require '../src/scripts/models/inventory_parser' 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', -> it 'returns an empty Inventory for an empty string', ->
result = parser.parse '' result = parser.parse ''
result._names.length.should.equal 0 result.toList().should.eql []
it 'can parse a single item without quantity', -> it 'can parse a single item without quantity', ->
result = parser.parse 'wool' result = parser.parse 'Wool'
result._items.wool.quantity.should.equal 1 result.toList().should.eql ['wool']
it 'can parse a single item with quantity', -> it 'can parse a single item with quantity', ->
result = parser.parse '4 wool' 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', -> 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']] result.toList().should.eql ['boat', [10, 'string'], [4, 'wool']]
it 're-uses the given inventory object', -> it 're-uses the given inventory object', ->
inventory = new Inventory inventory = new Inventory
inventory.add 'string', 8 inventory.add new Item(name:'String'), 8
result = parser.parse '4 wool', inventory result = parser.parse '4 Wool', inventory
result.toList().should.eql [[8, 'string'], [4, 'wool']] result.toList().should.eql [[8, 'string'], [4, 'wool']]
+115
View File
@@ -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
+95
View File
@@ -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
+100 -51
View File
@@ -1,11 +1,13 @@
### ###
# Crafting Guide - v1.test.coffee Crafting Guide - v1.test.coffee
#
# Copyright (c) 2014 by Redwood Labs Copyright (c) 2014 by Redwood Labs
# All rights reserved. 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", -> 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', -> describe '_parseModVersion', ->
it 'requires a mod_name', -> 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' expect(-> parser._parseModVersion data).to.throw Error, 'mod_name is required'
it 'requires a mod_version', -> 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' 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 = data =
version: 1 version: 1
mod_name: 'Empty' mod_name: 'Empty'
mod_version: '1.0' mod_version: '1.0'
recipes: [] recipes: []
book = parser._parseModVersion data modVersion = parser._parseModVersion data
book.modName.should.equal 'Empty' modVersion.modName.should.equal 'Empty'
book.modVersion.should.equal '1.0' modVersion.modVersion.should.equal '1.0'
it 'can parse a non-empty recipe book', -> it 'can parse a non-empty mod version', ->
data = data =
version: 1 version: 1
mod_name: 'Minecraft' mod_name: 'Minecraft'
mod_version: '1.7.10' mod_version: '1.7.10'
recipes: [ recipes: [
{ input:'sugar cane', output:'sugar' } { input:'Sugar Cane', output:'Sugar' }
{ input:[[3, 'wool'], [3, 'planks']], tools:'crafting table', output:'bed' } { input:[[3, 'Wool'], [3, 'Planks']], tools:'Crafting Table', output:'Bed' }
] ]
book = parser._parseModVersion data modVersion = parser._parseModVersion data
book.modName.should.equal 'Minecraft' modVersion.modName.should.equal 'Minecraft'
book.modVersion.should.equal '1.7.10' modVersion.modVersion.should.equal '1.7.10'
(r.name for r in book.recipes).sort().should.eql ['bed', 'sugar'] 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', -> describe '_parseRecipe', ->
beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0'
it 'requires output to be defined', -> it 'requires output to be defined', ->
parser._errorLocation = 'boat' parser._errorLocation = 'boat'
expect(-> parser._parseRecipe input:'wool').to.throw Error, 'boat is missing output' expect(-> parser._parseRecipe input:'wool').to.throw Error, 'boat is missing output'
@@ -68,58 +113,62 @@ describe 'RecipeBookParser', ->
input: [[3, 'planks'], [3, 'wool']] input: [[3, 'planks'], [3, 'wool']]
tools: 'crafting table' tools: 'crafting table'
recipe = parser._parseRecipe data recipe = parser._parseRecipe data
(i.name for i in recipe.output).should.eql ['bed'] (stack.name for stack in recipe.output).should.eql ['bed']
(i.name for i in recipe.input).sort().should.eql ['planks', 'wool'] (stack.name for stack in recipe.input).sort().should.eql ['planks', 'wool']
(i.name for i in recipe.tools).should.eql ['crafting table'] (item.name for item in recipe.tools).should.eql ['crafting table']
it 'can parse a recipe without tools', -> it 'can parse a recipe without tools', ->
recipe = parser._parseRecipe output:'sugar', input:'sugar cane' recipe = parser._parseRecipe output:'sugar', input:'sugar cane'
(i.name for i in recipe.output).should.eql ['sugar'] (stack.name for stack in recipe.output).should.eql ['sugar']
(i.name for i in recipe.input).sort().should.eql ['sugar cane'] (stack.name for stack in recipe.input).sort().should.eql ['sugar cane']
(i.name for i in recipe.tools).should.eql [] (stack.name for stack in recipe.tools).should.eql []
describe '_parseItemList', -> describe '_parseStack', ->
it 'can promote a single item to a list', -> beforeEach -> parser.modVersion = new ModVersion modName:'Test', modVersion:'0.0'
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', ->
it 'requires the array to have at least one element', -> it 'requires the array to have at least one element', ->
parser._errorLocation = 'boat' parser._errorLocation = 'boat'
options = index:1, field:'output' 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" "output element 1 for boat must have at least one element"
it 'can fill in a missing number', -> it 'can fill in a missing number', ->
item = parser._parseItem 'boat' item = parser._parseStack 'boat'
item.name.should.equal 'boat' item.name.should.equal 'boat'
item.quantity.should.equal 1 item.quantity.should.equal 1
item2 = parser._parseItem ['boat'] item2 = parser._parseStack ['boat']
item2.name.should.equal 'boat' item2.name.should.equal 'boat'
item2.quantity.should.equal 1 item2.quantity.should.equal 1
it 'requires the data to start with a number', -> it 'requires the data to start with a number', ->
parser._errorLocation = 'boat' parser._errorLocation = 'boat'
options = index:1, field:'output' 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" "output element 1 for boat must start with a number"
it 'can parse a basic item', -> it 'can parse a basic item', ->
item = parser._parseItem [2, 'book'] stack = parser._parseStack [2, 'wool']
item.constructor.name.should.equal 'Item' 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']
+4
View File
@@ -20,6 +20,8 @@ global.should = chai.should()
Logger = require '../src/scripts/logger' Logger = require '../src/scripts/logger'
global.logger = new Logger level:Logger.TRACE global.logger = new Logger level:Logger.TRACE
require '../src/scripts/underscore_mixins'
# Test Registry ######################################################################################################## # Test Registry ########################################################################################################
mocha.setup 'bdd' mocha.setup 'bdd'
@@ -27,6 +29,8 @@ mocha.setup 'bdd'
require './crafting_plan.test' require './crafting_plan.test'
require './inventory.test' require './inventory.test'
require './inventory_parser.test' require './inventory_parser.test'
require './mod_pack.test'
require './mod_version.test'
require './mod_version_parser.test' require './mod_version_parser.test'
mocha.checkLeaks() mocha.checkLeaks()