Added CraftingTable* components and UI

This commit is contained in:
Andrew Miner
2014-12-23 20:26:36 -08:00
parent b05fc4c5c9
commit 95c15abe10
30 changed files with 866 additions and 252 deletions
+6 -3
View File
@@ -98,9 +98,9 @@ module.exports = (grunt)->
images: images:
files: ['./src/images/**/*'] files: ['./src/images/**/*']
tasks: ['copy:images'] tasks: ['copy:images']
browserify: source:
files: ['./src/scripts/**/*.coffee', './test/**/*.coffee'] files: ['./src/scripts/**/*.coffee']
tasks: ['browserify'] tasks: ['browserify:main']
pages: pages:
files: ['./src/pages/**/*.jade'] files: ['./src/pages/**/*.jade']
tasks: ['jade:pages'] tasks: ['jade:pages']
@@ -110,6 +110,9 @@ module.exports = (grunt)->
sass: sass:
files: ['./src/css/**/*.scss'] files: ['./src/css/**/*.scss']
tasks: ['sass', 'copy:styles'] tasks: ['sass', 'copy:styles']
tests:
files: ['./src/scripts/**/*.coffee', './test/**/*.coffee']
tasks: ['browserify:test']
grunt.registerTask 'default', 'build' grunt.registerTask 'default', 'build'
+59 -18
View File
@@ -8,17 +8,6 @@ All rights reserved.
@import 'mixins'; @import 'mixins';
@import 'reset'; @import 'reset';
// Constants ///////////////////////////////////////////////////////////////////////////////////////////////////////////
$color-background: #AAC9FF;
$color-bedrock: #0A0A0A;
$color-content: rgba(230, 230, 230, 0.90);
$color-brick-dark: #7A7A7A;
$color-brick-light: #D9D9D9;
$color-footer: #3D1D09;
$color-footer-text: white;
$color-dirt: #906647;
// Fonts /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Fonts ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
@font-face { @font-face {
@@ -28,6 +17,29 @@ $color-dirt: #906647;
src: url('/fonts/press_start.ttf') format('truetype'); src: url('/fonts/press_start.ttf') format('truetype');
} }
// Constants ///////////////////////////////////////////////////////////////////////////////////////////////////////////
$color-background: #AAC9FF;
$color-bedrock: #0A0A0A;
$color-brick-dark: #7A7A7A;
$color-brick-light: #D9D9D9;
$color-content: rgba(230, 230, 230, 0.90);
$color-dirt: #906647;
$color-footer: #3D1D09;
$color-footer-text: white;
$color-gray-faint: #F2F2F2;
$color-gray-light: #CCCCCC;
$color-table-background: white;
$font-family-header: 'PressStart';
$font-family-normal: sans-serif;
$font-family-input: monospace;
$font-size-small: 1.1em;
$font-size-normal: 1.3em;
$font-size-large: 2.0em;
$font-size-x-large: 3.0em;
// Common ////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Common //////////////////////////////////////////////////////////////////////////////////////////////////////////////
body { body {
@@ -47,8 +59,8 @@ h1 {
} }
p { p {
font-family: PressStart; font-family: $font-family-header;
font-size: 3em; font-size: $font-size-x-large;
position: relative; top: -0.5em; position: relative; top: -0.5em;
} }
} }
@@ -59,18 +71,40 @@ h2 {
* { display: inline; } * { display: inline; }
img { img {
position: relative; height: 6em; width: 6em; position: relative; height: 4em; width: 4em;
margin-right: 1em; margin-right: 1em;
vertical-align: bottom; vertical-align: bottom;
} }
p { p {
font-family: PressStart; font-family: $font-family-header;
font-size: 2em; font-size: $font-size-large;
position: relative; top: -0.25em; position: relative; top: -0.25em;
} }
} }
h3 {
font-family: $font-family-normal;
font-weight: bold;
p { font-size: $font-size-normal; }
}
p {
font-family: $font-family-normal;
font-size: $font-size-normal;
}
input {
font-family: $font-family-input;
font-size: $font-size-normal;
}
label {
font-family: $font-family-normal;
font-size: $font-size-normal;
}
.centered { .centered {
text-align: center; text-align: center;
} }
@@ -84,6 +118,13 @@ h2 {
width: 100%; width: 100%;
} }
.page > div {
position: relative; width: 100%;
padding: 2em 2em 0 2em;
&:first-child { padding-top: 0; }
}
// Views /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Views ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
.view__footer { .view__footer {
@@ -111,7 +152,7 @@ h2 {
p { p {
font-family: sans-serif; font-family: sans-serif;
font-size: 1.25em; font-size: $font-size-small;
padding-bottom: 1.0em; padding-bottom: 1.0em;
&:last-child { &:last-child {
@@ -123,7 +164,7 @@ h2 {
padding-bottom: 1.0em; padding-bottom: 1.0em;
p { p {
font-family: PressStart; font-family: $font-family-header;
font-size: 1.5em; font-size: 1.5em;
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,dialog,div,dl,dt,em,embed,fieldset,figcaption,figure,font,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,meter,nav,object,ol,output,p,pre,progress,q,rp,rt,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video,xmp { a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,dialog,div,dl,dt,em,embed,fieldset,figcaption,figure,font,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,meter,nav,object,ol,output,p,pre,progress,q,rp,rt,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,textarea,tfoot,th,thead,time,tr,tt,u,ul,var,video,xmp {
border: 0; border: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
+45
View File
@@ -0,0 +1,45 @@
/*
Crafting Table - crafting_table.scss
Copyright (c) 2014 by Redwood Labs
All rights reserved.
*/
.view__crafting_table {
.recipe_selector {
background: $color-table-background;
padding: 1em;
position: relative; width: 100%;
& > * {
display: inline;
margin-left: 0.5em;
&:first-child { margin-left: 0; }
}
input[type="checkbox"] + label { margin-left: 0; }
input[name="name"] { width: 60%; }
}
h3 {
margin-bottom: 0.5em;
}
table {
background: white;
width: 100%;
td { padding: 0 1em 1em 1em; width: 33%; }
}
textarea, ul {
border: 0.1em solid $color-gray-light;
width: 100%;
height: 100%;
min-height: 6em;
padding: 0.2em;
}
}
+1
View File
@@ -5,4 +5,5 @@ Copyright (c) 2014 by Redwood Labs
All rights reserved. All rights reserved.
*/ */
@import 'crafting_table';
@import 'recipe_catalog'; @import 'recipe_catalog';
+4 -7
View File
@@ -6,9 +6,6 @@ All rights reserved.
*/ */
.view__recipe_catalog { .view__recipe_catalog {
position: relative; width: 100%;
padding: 0 2em;
button { button {
@include center-vertical; @include center-vertical;
position: absolute; right: 1em; position: absolute; right: 1em;
@@ -19,12 +16,12 @@ All rights reserved.
table { table {
background: white; background: white;
margin: 0 auto; margin: 0 auto;
width: 98%; width: 100%;
td { td {
height: 3em; padding: 0 1em; position: relative; vertical-align: middle; position: relative; height: 2 * $font-size-normal;
padding: 0 1em;
p { font-size: 1.5em; } vertical-align: middle;
} }
td:nth-child(1) { width: 30%; } td:nth-child(1) { width: 30%; }
+1
View File
@@ -30,4 +30,5 @@
.centered .centered
iframe(src="http://ghbtns.com/github-btn.html?user=andrewminer&repo=crafting-guide&type=fork&size=large", iframe(src="http://ghbtns.com/github-btn.html?user=andrewminer&repo=crafting-guide&type=fork&size=large",
allowtransparency="true", frameborder="0", scrolling="0", width="100", height="32") allowtransparency="true", frameborder="0", scrolling="0", width="100", height="32")
a(href="/test.html")
.divider.bottom .divider.bottom
+1
View File
@@ -7,6 +7,7 @@
.view__header .view__header
h1 h1
a(href="/")
img(src="images/workbench_front.png") img(src="images/workbench_front.png")
p Crafting Guide p Crafting Guide
.divider.bottom .divider.bottom
+7 -5
View File
@@ -16,12 +16,14 @@ Opacity.hidden = 1e-6
Opacity.shown = 1 Opacity.shown = 1
exports.Event = Event = {} exports.Event = Event = {}
Event.change = 'change' Event.add = 'add' # collection, item...
Event.change = 'change' # model
Event.load = {} Event.load = {}
Event.load.started = 'book:load:started' # controller, url Event.load.started = 'load:started' # controller, url
Event.load.succeeded = 'book:load:succeeded' # controller, book Event.load.succeeded = 'load:succeeded' # controller, book
Event.load.failed = 'book:load:failed' # controller, error message Event.load.failed = 'load:failed' # controller, error message
Event.load.finished = 'book:load:finished' # controller Event.load.finished = 'load:finished' # controller
Event.remove = 'remove' # collection, item...
exports.DefaultBookUrls = DefaultBookUrls = [ exports.DefaultBookUrls = DefaultBookUrls = [
'/data/vanilla.json' '/data/vanilla.json'
@@ -19,6 +19,7 @@ module.exports = class BaseController extends Backbone.View
@_parent = options.parent @_parent = options.parent
@_children = [] @_children = []
Object.defineProperty this, 'rendered', get:-> return @_rendered
@_loadTemplate options.templateName @_loadTemplate options.templateName
super options super options
@@ -0,0 +1,76 @@
###
# Crafting Guide - crafting_table_controller.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
BaseController = require './base_controller'
InventoryParser = require '../models/inventory_parser'
########################################################################################################################
module.exports = class CraftingTableController extends BaseController
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.templateName = 'crafting_table'
super options
@_parser = new InventoryParser
@_name = null
@_quantity = null
@_includingTools = null
# Event Methods ################################################################################
onHaveFieldChanged: ->
return unless @rendered
@model.have.clear()
@_parser.parse @$haveField.val(), @model.have
@_recalculate()
onIncludingToolsFieldChanged: ->
return unless @rendered
@_includingTools = @$('input[name="including_tools"]:checked').length > 0
@_recalculate()
onNameFieldChanged: ->
return unless @rendered
@_name = @$nameField.val()
@_recalculate()
onQuantityFieldChanged: ->
return unless @rendered
@_quantity = parseInt @$quantityField.find(":selected").val()
@_recalculate()
# BaseController Overrides #####################################################################
onDidRender: ->
@$haveField = @$('textarea[name="have"]')
@$includingToolsField = @$('input[name="including_tools"]')
@$nameField = @$('input[name="name"]')
@$quantityField = @$('select[name="quantity"]')
super
@onHaveFieldChanged()
@onIncludingToolsFieldChanged()
@onNameFieldChanged()
@onQuantityFieldChanged()
# Backbone.View Overrides ######################################################################
events:
'input textarea[name="have"]': 'onHaveFieldChanged'
'input input[name="name"]': 'onNameFieldChanged'
'input select[name="quantity"]': 'onQuantityFieldChanged'
'change input[name="including_tools"]': 'onIncludingToolsFieldChanged'
# Private Methods ##############################################################################
_recalculate: ->
return if not @_name? or @_name.length is 0
@model.craft @_name, @_quantity
@@ -6,6 +6,7 @@
### ###
BaseController = require './base_controller' BaseController = require './base_controller'
CraftingTableController = require './crafting_table_controller'
LandingPage = require '../models/landing_page' LandingPage = require '../models/landing_page'
RecipeCatalogController = require './recipe_catalog_controller' RecipeCatalogController = require './recipe_catalog_controller'
@@ -21,5 +22,6 @@ module.exports = class LandingPageController extends BaseController
# BaseController Overrides ##################################################################### # BaseController Overrides #####################################################################
onDidRender: -> onDidRender: ->
@recipeBooksController = @addChild RecipeCatalogController, '.view__recipe_catalog', model:@model.catalog @catalogController = @addChild RecipeCatalogController, '.view__recipe_catalog', model:@model.catalog
@tableController = @addChild CraftingTableController, '.view__crafting_table', model:@model.table
super super
+2 -2
View File
@@ -45,8 +45,8 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
controller.render() controller.render()
$pageContent = $('.page') $pageContent = $('.page')
$pageContent.empty() controller.$el.addClass 'page'
$pageContent.append controller.$el $pageContent.replaceWith controller.$el
controller.$el.fadeIn showDuration, -> controller.$el.fadeIn showDuration, ->
controller.onDidShow() controller.onDidShow()
+27
View File
@@ -0,0 +1,27 @@
###
# Crafting Guide - crafting_table.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
BaseModel = require './base_model'
Inventory = require './inventory'
########################################################################################################################
module.exports = class CraftingTable extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.catalog? then throw new Error "attributes.catalog is required"
super attributes, options
@have = new Inventory
@make = new Inventory
@get = new Inventory
# Public Methods ###############################################################################
craft: (name, quantity=1, includingTools=false)->
toolPhrase = if includingTools then ' includingTools' else ''
logger.verbose "calculating build plan for #{quantity} #{name}#{toolPhrase} with inventory: #{@have}"
+79
View File
@@ -0,0 +1,79 @@
###
# Crafting Guide - inventory.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Item = require './item'
########################################################################################################################
module.exports = class Inventory extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
@clear()
# Public Methods ###############################################################################
add: (name, quantity=1)->
item = @_items[name]
if not item?
item = new Item name:name, quantity:quantity
@_items[name] = item
@_names.push name
else
item.quantity += quantity
@trigger Event.add, this, name, quantity
@trigger Event.change, this
return this
clear: ->
@_items = {}
@_names = []
each: (onItem)->
for name in @_names
item = @_items[name]
onItem item
hasAtLeast: (name, quantity=1)->
if quantity is 0 then return true
item = @_items[name]
return false unless item?
return item.quantity >= quantity
remove: (name, quantity=1)->
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"
item.quantity -= quantity
if item.quantity is 0
delete @_items[name]
@_names = _(@_names).without name
@trigger Event.remove, this, name, quantity
@trigger Event.change, this
return this
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") { items: ["]
needsDelimiter = false
for name, item of @_items
if needsDelimiter then result.push ', '
result.push item.toString()
needsDelimiter = true
result.push ']'
result.push '}'
return result.join ''
@@ -0,0 +1,29 @@
###
# Crafting Guide - inventory_parser.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
Inventory = require './inventory'
########################################################################################################################
module.exports = class InventoryParser
@ITEM_REGEX = /^([0-9]+)(.*)$/
parse: (data, inventory=null)->
inventory ?= new Inventory
return inventory if not data? or data.length is 0
lines = data.split '\n'
for line in lines
match = InventoryParser.ITEM_REGEX.exec line
name = if match? then match[2].trim() else line
quantity = if match? then parseInt(match[1]) else 1
if name.length > 0
inventory.add name, quantity
return inventory
+38
View File
@@ -11,9 +11,47 @@ BaseModel = require './base_model'
module.exports = class Item extends BaseModel module.exports = class Item extends BaseModel
@DEFAULT_STACK_SIZE = 64
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
attributes.name ?= '' attributes.name ?= ''
attributes.quantity ?= 1 attributes.quantity ?= 1
attributes.stackSize ?= Item.DEFAULT_STACK_SIZE
super attributes, options super attributes, options
Object.defineProperty this, 'stackQuantity', get:@getStackQuantity
# Public Methods ############################################################################### # Public Methods ###############################################################################
canMerge: (item)->
return item.name is @name
merge: (item)->
if not @canMerge(item) then throw new Error "cannot merge #{item} into #{this}"
@quantity += item.quantity
toFormatHash: ->
return result =
name: @name
quantity: @quantity
stackQuantity: @stackQuantity
# Property Methods #############################################################################
getStackQuantity: ->
count = 0
extra = @quantity
while extra > @stackSize
extra -= @stackSize
count += 1
return count:count extra:extra
# Object Overrides #############################################################################
toString: ->
result = "#{@constructor.name} (#{@cid}) { name:\"#{@name}\", quantity:#{@quantity}"
if @stackSize isnt Item.DEFAULT_STACK_SIZE
result += ", stackSize:#{@stackSize}"
result += ' }'
return result
+2
View File
@@ -6,6 +6,7 @@
### ###
BaseModel = require './base_model' BaseModel = require './base_model'
CraftingTable = require './crafting_table'
RecipeCatalog = require './recipe_catalog' RecipeCatalog = require './recipe_catalog'
######################################################################################################################## ########################################################################################################################
@@ -14,4 +15,5 @@ module.exports = class LandingPage extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
attributes.catalog ?= new RecipeCatalog attributes.catalog ?= new RecipeCatalog
attributes.table ?= new CraftingTable catalog:attributes.catalog
super attributes, options super attributes, options
@@ -1,81 +0,0 @@
###
# Crafting Guide - v1.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
Item = require '../item'
Recipe = require '../recipe'
RecipeBook = require '../recipe_book'
########################################################################################################################
module.exports = class V1
constructor: ->
@_errorLocation = 'the header information'
parse: (data)->
return @_parseRecipeBook data
# Private Methods ##############################################################################
_parseRecipeBook: (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 RecipeBook version:data.version, modName:data.mod_name, modVersion:data.mod_version
book.description = data.description or ''
for index in [0...data.recipes.length]
@_errorLocation = "recipe #{index + 1}"
recipeData = data.recipes[index]
book.recipes.push @_parseRecipe recipeData
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"
if not data.input? then throw new Error "#{@_errorLocation} is missing input"
output = @_parseItemList data.output, field:'output', canBeEmpty:false
@_errorLocation = "recipe for output[0].name"
data.tools ?= []
input = @_parseItemList data.input, field:'input', canBeEmpty:false
tools = @_parseItemList data.tools, field:'tools', canBeEmpty:true
return new Recipe input:input, output:output, tools:tools
_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
return result
_parseItem: (data, options={})->
errorBase = "#{options.field} element #{options.index} for #{@_errorLocation}"
if not data? then throw new Error "#{errorBase} is missing"
if _.isString(data) then data = [1, data]
if not _.isArray(data) then throw new Error "#{errorBase} must be an array"
if data.length is 1 then data.unshift 1
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]
+36
View File
@@ -12,6 +12,42 @@ 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.tools ?= []
super attributes, options super attributes, options
Object.defineProperty this, 'name', get:-> @output[0].name Object.defineProperty this, 'name', get:-> @output[0].name
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") { name:", @name]
result.push ", input:["
needsDelimiter = false
for inputItem in @input
if needsDelimiter then result.push ', '
result.push inputItem.toString()
needsDelimiter = false
result.push ']'
result.push ", output:["
needsDelimiter = false
for outputItem in @output
if needsDelimiter then result.push ', '
result.push outputItem.toString()
needsDelimiter = false
result.push ']'
if @tools.length > 0
result.push ", tools:["
needsDelimiter = false
for tool in @tools
if needsDelimiter then result.push ', '
result.push tool.toString()
needsDelimiter = false
result.push ']'
result.push '}'
return result.join ''
+73 -1
View File
@@ -5,7 +5,9 @@
# All rights reserved. # All rights reserved.
### ###
V1 = require './parser_versions/v1' Item = require './item'
Recipe = require './recipe'
RecipeBook = require './recipe_book'
######################################################################################################################## ########################################################################################################################
@@ -24,3 +26,73 @@ module.exports = class RecipeBookParser
throw new Error "cannot parse version #{data.version} recipe books" throw new Error "cannot parse version #{data.version} recipe books"
return parser.parse data return parser.parse data
########################################################################################################################
module.exports.V1 = class V1
constructor: ->
@_errorLocation = 'the header information'
parse: (data)->
return @_parseRecipeBook data
# Private Methods ##############################################################################
_parseRecipeBook: (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 RecipeBook version:data.version, modName:data.mod_name, modVersion:data.mod_version
book.description = data.description or ''
for index in [0...data.recipes.length]
@_errorLocation = "recipe #{index + 1}"
recipeData = data.recipes[index]
book.recipes.push @_parseRecipe recipeData
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"
if not data.input? then throw new Error "#{@_errorLocation} is missing input"
output = @_parseItemList data.output, field:'output', canBeEmpty:false
@_errorLocation = "recipe for output[0].name"
data.tools ?= []
input = @_parseItemList data.input, field:'input', canBeEmpty:false
tools = @_parseItemList data.tools, field:'tools', canBeEmpty:true
return new Recipe input:input, output:output, tools:tools
_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
return result
_parseItem: (data, options={})->
errorBase = "#{options.field} element #{options.index} for #{@_errorLocation}"
if not data? then throw new Error "#{errorBase} is missing"
if _.isString(data) then data = [1, data]
if not _.isArray(data) then throw new Error "#{errorBase} must be an array"
if data.length is 1 then data.unshift 1
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]
+10 -1
View File
@@ -2,13 +2,22 @@ var jade = jade || require('jade').runtime;
this["JST"] = this["JST"] || {}; this["JST"] = this["JST"] || {};
this["JST"]["crafting_table"] = function template(locals) {
var buf = [];
var jade_mixins = {};
var jade_interp;
var jade_indent = [];
buf.push("\n<div class=\"view__crafting_table\">\n <h2><img src=\"/images/workbench_top.png\"/>\n <p>Crafting Table</p>\n </h2>\n <div class=\"recipe_selector\">\n <p>I want to make</p>\n <select name=\"quantity\">\n <option value=\"1\">1</option>\n <option value=\"2\">2</option>\n <option value=\"4\">4</option>\n <option value=\"8\">8</option>\n <option value=\"16\">16</option>\n <option value=\"64\">64</option>\n </select>\n <input name=\"name\" placeholder=\"enter an item name\"/>\n <label>\n <input name=\"including_tools\" type=\"checkbox\"/>including tools\n </label>\n </div>\n <table>\n <tr>\n <td class=\"have\">\n <h3>\n <p>You have...</p>\n </h3>\n <textarea name=\"have\" placeholder=\"enter what you have, for example:\n\n3 wool\n4 iron ingot\"></textarea>\n </td>\n <td class=\"make\">\n <h3>\n <p>You'll make...</p>\n </h3>\n <ul></ul>\n </td>\n <td class=\"get\">\n <h3>\n <p>You'll get...</p>\n </h3>\n <ul></ul>\n </td>\n </tr>\n </table>\n</div>");;return buf.join("");
};
this["JST"]["landing_page"] = function template(locals) { this["JST"]["landing_page"] = function template(locals) {
var buf = []; var buf = [];
var jade_mixins = {}; var jade_mixins = {};
var jade_interp; var jade_interp;
var jade_indent = []; var jade_indent = [];
buf.push("\n<div class=\"view__landing_page\">\n <div class=\"view__recipe_catalog\"></div>\n <div class=\"view__crafter\"></div>\n</div>");;return buf.join(""); buf.push("\n<div class=\"view__landing_page\">\n <div class=\"view__recipe_catalog\"></div>\n <div class=\"view__crafting_table\"></div>\n</div>");;return buf.join("");
}; };
this["JST"]["recipe_book"] = function template(locals) { this["JST"]["recipe_book"] = function template(locals) {
+37
View File
@@ -0,0 +1,37 @@
//-
//- Crafting Guide - crafting_table.jade
//-
//- Copyright (c) 2014 by Redwood Labs
//- All rights reserved.
//-
.view__crafting_table
h2
img(src="/images/workbench_top.png")
p Crafting Table
.recipe_selector
p I want to make
select(name="quantity")
option(value="1") 1
option(value="2") 2
option(value="4") 4
option(value="8") 8
option(value="16") 16
option(value="64") 64
input(name="name", placeholder="enter an item name")
label
input(name="including_tools", type="checkbox")
| including tools
table
tr
td.have
h3: p You have...
textarea(name="have", placeholder="enter what you have, for example:\n\n3 wool\n4 iron ingot")
td.make
h3: p You'll make...
ul
td.get
h3: p You'll get...
ul
+1 -1
View File
@@ -7,4 +7,4 @@
.view__landing_page .view__landing_page
.view__recipe_catalog .view__recipe_catalog
.view__crafter .view__crafting_table
+28
View File
@@ -0,0 +1,28 @@
###
# Crafting Guide - event_recorder.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
util = require 'util'
########################################################################################################################
module.exports = class EventRecorder
constructor: (model)->
if not model? then throw new Error 'model is required'
@model = model
@events = []
Object.defineProperty this, 'names', get:-> e.event for e in @events
@model.on 'all', (event, model, args...)=>
logger.verbose "#{model?.constructor?.name}(#{model?.cid}) emitted #{event}
with args: #{util.inspect(args)}"
@events.push id:model?.cid, event:event, args:args
reset: ->
@events = []
+115
View File
@@ -0,0 +1,115 @@
###
# Crafting Guide - inventory.test.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
{Event} = require '../src/scripts/constants'
EventRecorder = require './event_recorder'
Inventory = require '../src/scripts/models/inventory'
Item = require '../src/scripts/models/item'
########################################################################################################################
inventory = null
########################################################################################################################
describe 'Inventory', ->
beforeEach ->
inventory = new Inventory
inventory.add 'wool', 4
inventory.add 'string', 20
inventory.add '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
it 'can augment quantity of existing items', ->
inventory.add 'wool', 2
inventory._items.wool.quantity.should.equal 6
it 'can add zero quantity', ->
inventory.add 'wool', 0
inventory._items.wool.quantity.should.equal 4
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.add 'iron ingot', 10
events.names.should.eql [Event.add, Event.change]
describe 'each', ->
it 'works with an empty inventory', ->
inventory = new Inventory
result = []
inventory.each (item)-> result.push item.name
result.should.eql []
it 'works when items have only been added', ->
result = []
inventory.each (item)-> result.push item.name
result.should.eql ['wool', 'string', 'boat']
it 'works when items have been augmented', ->
inventory.add 'iron ingot'
inventory.add 'boat'
inventory.add 'wool', 2
result = []
inventory.each (item)-> result.push item.name
result.should.eql ['wool', 'string', 'boat', 'iron ingot']
it 'moves items to the end when removed and re-added', ->
inventory.remove 'wool', 4
inventory.add 'wool'
result = []
inventory.each (item)-> result.push item.name
result.should.eql ['string', 'boat', 'wool']
describe 'hasAtLeast', ->
it 'works when the item is completely absent', ->
answer = inventory.hasAtLeast 'chicken', 1
answer.should.be.false
it 'always returns true for zero quantity', ->
inventory.hasAtLeast('chicken', 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
inventory.hasAtLeast('wool', 4).should.be.true
inventory.hasAtLeast('wool', 5).should.be.false
describe 'remove', ->
it 'throws when the item is absent', ->
expect(-> inventory.remove('chicken')).to.throw Error,
'cannot remove chicken since it is not in this inventory'
it 'throws when the item has insufficient quantity', ->
expect(-> inventory.remove('wool', 10)).to.throw Error,
'cannot remove 10 wool because there is only 4 in this inventory'
it 'removes a single item by default', ->
inventory.remove 'wool'
inventory._items.wool.quantity.should.equal 3
it 'removes a quantity above 1', ->
inventory.remove 'wool', 3
inventory._items.wool.quantity.should.equal 1
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.remove 'wool'
events.names.should.eql [Event.remove, Event.change]
+46
View File
@@ -0,0 +1,46 @@
###
# Crafting Guide - inventory_parser.test.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
Inventory = require '../src/scripts/models/inventory'
InventoryParser = require '../src/scripts/models/inventory_parser'
########################################################################################################################
parser = null
########################################################################################################################
describe 'InventoryParser', ->
beforeEach -> parser = new InventoryParser
it 'returns an empty Inventory for an empty string', ->
result = parser.parse ''
result._names.length.should.equal 0
it 'can parse a single item without quantity', ->
result = parser.parse 'wool'
result._items.wool.quantity.should.equal 1
it 'can parse a single item with quantity', ->
result = parser.parse '4 wool'
result._items.wool.quantity.should.equal 4
it 'can parse multiple mixed-type items', ->
result = parser.parse '4 wool\n10 string\nboat\n\n'
result._items.wool.quantity.should.equal 4
result._items.string.quantity.should.equal 10
result._items.boat.quantity.should.equal 1
result._names.should.eql ['wool', 'string', 'boat']
it 're-uses the given inventory object', ->
inventory = new Inventory
inventory.add 'string', 8
result = parser.parse '4 wool', inventory
inventory._items.string.quantity.should.equal 8
inventory._items.wool.quantity.should.equal 4
inventory._names.should.eql ['string', 'wool']
-124
View File
@@ -1,124 +0,0 @@
###
# Crafting Guide - v1.test.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
V1 = require '../../src/scripts/models/parser_versions/v1'
########################################################################################################################
parser = null
########################################################################################################################
describe 'RecipeBookParser.V1', ->
before -> parser = new V1
describe '_parseRecipeBook', ->
it 'requires a mod_name', ->
data = version:1, mod_version:'1.0', recipes:[]
expect(-> parser._parseRecipeBook data).to.throw Error, 'mod_name is required'
it 'requires a mod_version', ->
data = version:1, mod_name:'Empty', recipes:[]
expect(-> parser._parseRecipeBook data).to.throw Error, 'mod_version is required'
it 'can parse an empty recipe book', ->
data =
version: 1
mod_name: 'Empty'
mod_version: '1.0'
recipes: []
book = parser._parseRecipeBook data
book.modName.should.equal 'Empty'
book.modVersion.should.equal '1.0'
it 'can parse a non-empty recipe book', ->
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' }
]
book = parser._parseRecipeBook 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']
describe '_parseRecipe', ->
it 'requires output to be defined', ->
parser._errorLocation = 'boat'
expect(-> parser._parseRecipe input:'wool').to.throw Error, 'boat is missing output'
it 'requires input to be defined', ->
parser._errorLocation = 'boat'
expect(-> parser._parseRecipe output:'wool').to.throw Error, 'boat is missing input'
it 'can parse a regular recipe', ->
data =
output: 'bed'
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']
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 []
describe '_parseItemList', ->
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', ->
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,
"output element 1 for boat must have at least one element"
it 'can fill in a missing number', ->
item = parser._parseItem 'boat'
item.name.should.equal 'boat'
item.quantity.should.equal 1
item2 = parser._parseItem ['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,
"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'
+126
View File
@@ -0,0 +1,126 @@
###
# Crafting Guide - v1.test.coffee
#
# Copyright (c) 2014 by Redwood Labs
# All rights reserved.
###
RecipeBookParser = require '../src/scripts/models/recipe_book_parser'
########################################################################################################################
parser = null
########################################################################################################################
describe 'RecipeBookParser', ->
describe "V1", ->
before -> parser = new RecipeBookParser.V1
describe '_parseRecipeBook', ->
it 'requires a mod_name', ->
data = version:1, mod_version:'1.0', recipes:[]
expect(-> parser._parseRecipeBook data).to.throw Error, 'mod_name is required'
it 'requires a mod_version', ->
data = version:1, mod_name:'Empty', recipes:[]
expect(-> parser._parseRecipeBook data).to.throw Error, 'mod_version is required'
it 'can parse an empty recipe book', ->
data =
version: 1
mod_name: 'Empty'
mod_version: '1.0'
recipes: []
book = parser._parseRecipeBook data
book.modName.should.equal 'Empty'
book.modVersion.should.equal '1.0'
it 'can parse a non-empty recipe book', ->
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' }
]
book = parser._parseRecipeBook 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']
describe '_parseRecipe', ->
it 'requires output to be defined', ->
parser._errorLocation = 'boat'
expect(-> parser._parseRecipe input:'wool').to.throw Error, 'boat is missing output'
it 'requires input to be defined', ->
parser._errorLocation = 'boat'
expect(-> parser._parseRecipe output:'wool').to.throw Error, 'boat is missing input'
it 'can parse a regular recipe', ->
data =
output: 'bed'
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']
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 []
describe '_parseItemList', ->
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', ->
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,
"output element 1 for boat must have at least one element"
it 'can fill in a missing number', ->
item = parser._parseItem 'boat'
item.name.should.equal 'boat'
item.quantity.should.equal 1
item2 = parser._parseItem ['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,
"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'
+6 -1
View File
@@ -17,11 +17,16 @@ global.assert = chai.assert
global.expect = chai.expect global.expect = chai.expect
global.should = chai.should() global.should = chai.should()
Logger = require '../src/scripts/logger'
global.logger = new Logger level:Logger.TRACE
# Test Registry ######################################################################################################## # Test Registry ########################################################################################################
mocha.setup 'bdd' mocha.setup 'bdd'
require './parser_versions/v1.test' require './inventory.test'
require './inventory_parser.test'
require './recipe_book_parser.test'
mocha.checkLeaks() mocha.checkLeaks()
mocha.run() mocha.run()