diff --git a/src/coffee/models/multiblock.coffee b/src/coffee/models/multiblock.coffee new file mode 100644 index 000000000..0923c9ae1 --- /dev/null +++ b/src/coffee/models/multiblock.coffee @@ -0,0 +1,75 @@ +### +Crafting Guide - multiblock.coffee + +Copyright (c) 2014-2015 by Redwood Labs +All rights reserved. +### + +BaseModel = require './base_model' +SimpleInventory = require './simple_inventory' + +######################################################################################################################## + +module.exports = class Multiblock extends BaseModel + + constructor: (attributes={}, options={})-> + if not attributes.input? then throw new Error 'attributes.input is required' + if not attributes.layers? then throw new Error 'attributes.layers is required' + if attributes.layers.length < 1 then throw new Error 'attributes.layers.length must be >= 1' + super attributes, options + + @_analyzePattern() + + # Public Methods ############################################################################### + + getStackAt: (x, y, z)-> + value = @_stackCache[y]?[z]?[x] + return null if value is undefined + return value + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + depth: + get: -> @_depth + + height: + get: -> @_height + + width: + get: -> @_width + + # Private Methods ############################################################################## + + _analyzeLayer: (layer, stackCacheLayer)-> + rows = layer.split ' ' + @_depth = Math.max @_depth, rows.length + + for row in rows + @_width = Math.max @_width, row.length + stackCacheRow = [] + stackCacheLayer.push stackCacheRow + @_analyzeRow row, stackCacheRow + + _analyzePattern: -> + @_depth = @_height = @_width = 0 + @_inventory = new SimpleInventory + @_stackCache = [] + + for layer in @layers + stackCacheLayer = [] + @_stackCache.push stackCacheLayer + @_analyzeLayer layer, stackCacheLayer + + @_height = @layers.length + + _analyzeRow: (row, stackCacheRow)-> + for cell, x in row.split '' + index = parseInt cell + stack = null + + if not _.isNaN index + stack = @input[index] + @_inventory.add stack.itemSlug, stack.quantity + + stackCacheRow.push stack diff --git a/src/coffee/models/parser_versions/mod_version_parser_v1.coffee b/src/coffee/models/parser_versions/mod_version_parser_v1.coffee index 25201af73..0df79f147 100644 --- a/src/coffee/models/parser_versions/mod_version_parser_v1.coffee +++ b/src/coffee/models/parser_versions/mod_version_parser_v1.coffee @@ -5,13 +5,14 @@ Copyright (c) 2014-2015 by Redwood Labs All rights reserved. ### +_ = require 'underscore' CommandParserVersionBase = require './command_parser_version_base' Item = require '../item' ItemSlug = require '../item_slug' ModVersion = require '../mod_version' +Multiblock = require '../multiblock' Recipe = require '../recipe' Stack = require '../simple_stack' -_ = require 'underscore' {StringBuilder} = require 'crafting-guide-common' ######################################################################################################################## @@ -70,12 +71,29 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase @_recipeData.ignoreDuringCrafting = (value is 'yes') - _command_input: (inputNames...)-> - if not @_recipeData? then throw new Error 'cannot declare "input" before "recipe"' - if @_recipeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"' + _command_input: (stackDescriptions...)-> + activeData = if @_recipeData? then @_recipeData else @_multiblockData - for name in inputNames - @_recipeData.input.push @_parseStack name + if not activeData? then throw new Error 'cannot declare "input" before "recipe" or "multiblock"' + if activeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"' + + for stackDescription in stackDescriptions + activeData.input.push @_parseStack stackDescription + + _command_layer: (layerText)-> + if not @_multiblockData? then throw new Error 'cannot declare "layer" before "multiblock"' + if not layerText? then throw new Error 'cannot have an empty layer' + if layerText.length is 0 then throw new Error 'cannot have an empty layer' + + @_multiblockData.layers.push layerText + + _command_multiblock: -> + if not @_itemData? then throw new Error 'cannot declare "multiblock" before "item"' + if @_itemData.multiblockData? then throw new Error 'duplicate declaration of "multiblock"' + + @_recipeData = null + @_multiblockData = input:[], layers:[], line:@_lineNumber + @_itemData.multiblockData = @_multiblockData _command_onlyIf: (condition)-> words = condition.split ' ' @@ -112,6 +130,7 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase _command_recipe: -> if not @_itemData? then throw new Error 'cannot delcare "recipe" before "item"' + @_multiblockData = null @_recipeData = line:@_lineNumber, input:[], output:[{quantity:1, name:@_itemData.name}], tools:[] @_itemData.recipes ?= [] @_itemData.recipes.push @_recipeData @@ -171,11 +190,23 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase itemData.slug = ItemSlug.slugify itemData.name modVersion.registerName itemData.slug, itemData.name + if itemData.multiblockData + item.multiblock = @_handleErrors @_buildMultiblock, modVersion, item, itemData.multiblockData + for recipeData in itemData.recipes @_handleErrors @_buildRecipe, modVersion, itemData, recipeData return item + _buildMultiblock: (modVersion, item, multiblockData)-> + @_lineNumber = multiblockData.line + if multiblockData.layers.length is 0 then throw new Error '"multiblock" requires at least one "layer"' + if multiblockData.input.length is 0 then throw new Error '"multiblock" requires at least one "input"' + + input = @_buildStackList modVersion, multiblockData.input + multiblock = new Multiblock input:input, layers:multiblockData.layers + return multiblock + _buildRecipe: (modVersion, itemData, recipeData)-> @_lineNumber = recipeData.line if recipeData.input.length is 0 then throw new Error 'the "input" declaration is required' diff --git a/test/multiblock.test.coffee b/test/multiblock.test.coffee new file mode 100644 index 000000000..168635c93 --- /dev/null +++ b/test/multiblock.test.coffee @@ -0,0 +1,85 @@ +### +Crafting Guide - multiblock.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +ItemSlug = require '../src/coffee/models/item_slug' +Mod = require '../src/coffee/models/multiblock' +Multiblock = require '../src/coffee/models/multiblock' +Stack = require '../src/coffee/models/stack' + +######################################################################################################################## + +describe 'multiblock.coffee', -> + + describe 'with a single block type', -> + + beforeEach -> + @input = [ new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1 ] + + it 'correctly parses a 1x1x1 cube', -> + model = new Multiblock input:@input, layers:['0'] + model.depth.should.equal 1 + model.height.should.equal 1 + model.width.should.equal 1 + model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone' + + it 'correctly parses a solid 3x3x3 cube', -> + model = new Multiblock input:@input, layers:['000 000 000', '000 000 000', '000 000 000'] + model.depth.should.equal 3 + model.height.should.equal 3 + model.width.should.equal 3 + + for x in [0..2] + for y in [0..2] + for z in [0..2] + model.getStackAt(x, y, z).toString().should.equal '1 cobblestone' + + it 'correctly parses a hollow 3x3x3 cube', -> + model = new Multiblock input:@input, layers:['000 000 000', '000 0.0 000', '000 000 000'] + model.depth.should.equal 3 + model.height.should.equal 3 + model.width.should.equal 3 + + for x in [0..2] + for y in [0..2] + for z in [0..2] + if x isnt 1 or y isnt 1 or z isnt 1 + model.getStackAt(x, y, z).toString().should.equal '1 cobblestone' + else + expect(model.getStackAt(x, y, z)).to.be.null + + it 'correctly parses a 3x2x3 pyramid', -> + model = new Multiblock input:@input, layers:['000 000 000', '.. .0 ..'] + model.depth.should.equal 3 + model.height.should.equal 2 + model.width.should.equal 3 + + for y in [0..1] + for z in [0..2] + for x in [0..2] + if y is 1 and (x isnt 1 or z isnt 1) + expect(model.getStackAt(x, y, z)).to.be.null + else + model.getStackAt(x, y, z).toString().should.equal '1 cobblestone' + + describe 'with multiple block types', -> + + beforeEach -> + @input = [ + new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1 + new Stack itemSlug:ItemSlug.slugify('stone'), quantity:1 + new Stack itemSlug:ItemSlug.slugify('oak wood'), quantity:1 + ] + + it 'correctly parses a 1x3x1 column of different types', -> + model = new Multiblock input:@input, layers:['0', '1', '2'] + model.depth.should.equal 1 + model.height.should.equal 3 + model.width.should.equal 1 + + model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone' + model.getStackAt(0, 1, 0).toString().should.equal '1 stone' + model.getStackAt(0, 2, 0).toString().should.equal '1 oak_wood' diff --git a/test/parser_versions/mod_version_parser_v1.test.coffee b/test/parser_versions/mod_version_parser_v1.test.coffee index 545e5a4fd..01a48499d 100644 --- a/test/parser_versions/mod_version_parser_v1.test.coffee +++ b/test/parser_versions/mod_version_parser_v1.test.coffee @@ -63,6 +63,42 @@ describe 'mod_version_parser_v1.coffee', -> func = -> parser.parse 'gatherable: yes; item: Alpha Bravo; gatherable: yes' expect(func).to.throw Error, '"gatherable" before "item"' + describe 'Multiblock', -> + + it 'adds a "multiblock" when present', -> + modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo; layer: 0' + item = modVersion.findItemByName 'Alpha' + item.multiblock.height.should.equal 1 + + it 'requires "item" be declared before "multiblock"', -> + func = -> parser.parse "multiblock:" + expect(func).to.throw Error, '"multiblock" before "item"' + + it 'prohibits multiple "multiblock" commands per item"', -> + func = -> parser.parse "item: Alpha; multiblock:; multiblock:" + expect(func).to.throw Error, 'duplicate' + + it 'prohibits multiblocks with no inputs', -> + func = -> parser.parse "item: Alpha; multiblock:; layer: 000" + expect(func).to.throw Error, 'at least one "input"' + + describe 'layer', -> + + it 'allows multiple "layer" commands', -> + modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo, Charlie; layer: 01 10; layer: 10 01' + item = modVersion.findItemByName 'Alpha' + item.multiblock.depth.should.equal 2 + item.multiblock.height.should.equal 2 + item.multiblock.width.should.equal 2 + + it 'prohibits empty layers', -> + func = -> parser.parse 'item: Alpha; multiblock:; input: Bravo; layer:; layer: 00 00' + expect(func).to.throw Error, 'empty layer' + + it 'prohibits multiblocks with no layers', -> + func = -> parser.parse "item: Alpha; multiblock:; input: Bravo" + expect(func).to.throw Error, 'at least one "layer"' + describe 'Recipe', -> beforeEach -> baseText = 'item: Charlie; ' diff --git a/test/test_helper.coffee b/test/test_helper.coffee index d4b7bbd07..1741653a7 100644 --- a/test/test_helper.coffee +++ b/test/test_helper.coffee @@ -15,6 +15,7 @@ global._ = require 'underscore' global.Backbone = require 'backbone' global.assert = chai.assert global.expect = chai.expect +global.inspect = require('util').inspect global.should = chai.should() global.sinon = require 'sinon' global.util = require 'util'