Restructure static files into a single directory

This commit is contained in:
Andrew Miner
2015-03-08 15:05:24 -07:00
parent 997b37ab7d
commit 0a6c185965
5053 changed files with 383 additions and 175 deletions
+70
View File
@@ -0,0 +1,70 @@
###
Crafting Guide - constants.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
# Minecraft must be first
exports.DefaultMods = [
'minecraft',
'applied_energistics_2',
'buildcraft',
'enderio',
'industrial_craft_2',
'railcraft',
'thermal_expansion',
]
exports.Duration = Duration = {}
Duration.snap = 200
Duration.fast = Duration.snap * 2
Duration.normal = Duration.fast * 2
Duration.slow = Duration.normal * 2
exports.Event = Event = {}
Event.add = 'add' # collection, item...
Event.change = 'change' # model
Event.load = {}
Event.load.started = 'load:started' # controller, url
Event.load.succeeded = 'load:succeeded' # controller, book
Event.load.failed = 'load:failed' # controller, error message
Event.load.finished = 'load:finished' # controller
Event.remove = 'remove' # collection, item...
Event.request = 'request' # model
Event.route = 'route'
Event.sort = 'sort'
Event.sync = 'sync' # model, response
Event.transitionEnd = 'webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend'
exports.Key = Key = {}
Key.Return = 13
exports.Opacity = Opacity = {}
Opacity.hidden = 1e-6
Opacity.shown = 1
exports.RequiredMods = [ 'minecraft' ]
exports.ModelState = ModelState = {}
ModelState.unloaded = 'unloaded'
ModelState.loading = 'loading'
ModelState.loaded = 'loaded'
ModelState.failed = 'failed'
exports.Text = Text = {}
Text.title = 'Crafting Guide for Minecraft | The Ultimate Step-by-Step Tutorial for Making Anything in Minecraft'
exports.Url = Url = {}
Url.crafting = _.template "/craft/<%= inventoryText %>"
Url.itemIcon = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/icon.png"
Url.item = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/"
Url.mod = _.template "/browse/<%= modSlug %>/"
Url.modData = _.template "/data/<%= modSlug %>/mod.cg"
Url.modIcon = _.template "/browse/<%= modSlug %>/icon.png"
Url.modVersion = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg"
exports.UrlParam = UrlParam = {}
UrlParam.quantity = 'count'
UrlParam.recipe = 'recipeName'
UrlParam.includingTools = 'tools'
@@ -0,0 +1,124 @@
###
Crafting Guide - base_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
views = require '../views'
########################################################################################################################
module.exports = class BaseController extends Backbone.View
constructor: (options={})->
@tryRefresh = _.debounce @_tryRefresh, 100
Object.defineProperty this, 'model', get:@getModel, set:@setModel
@_model = null
@_rendered = false
@_parent = options.parent
@_children = []
Object.defineProperty this, 'rendered', get:-> return @_rendered
@_loadTemplate options.templateName
super options
# Public Methods ###############################################################################
addChild: (Controller, atSelector, options={})->
options.el = @$(atSelector)[0]
options.parent = this
child = new Controller options
child.render()
@_children.push child
return child
refresh: ->
logger.verbose => "#{this} refreshing"
routeLinkClick: (event)->
href = $(event.currentTarget).attr 'href'
router.navigate href, trigger:true
return false
# Event Methods ################################################################################
onDidModelChange: ->
@tryRefresh()
onDidModelSync: ->
@tryRefresh()
onWillRender: -> # do nothing
onDidRender: ->
@tryRefresh()
onWillShow: -> # do nothing
onDidShow: -> # do nothing
onWillChangeModel: (oldModel, newModel)->
if oldModel?.on?
@stopListening oldModel
if newModel?.on?
@listenTo newModel, 'sync', (e)=> @onDidModelSync e
@listenTo newModel, 'change', (e)=> @onDidModelChange e
return true
# Property Methods #############################################################################
getModel: ->
return @_model
setModel: (newModel)->
return if @model is newModel
return unless @onWillChangeModel @_model, newModel
@_model = newModel
@tryRefresh()
# Backbone.View Overrides ######################################################################
events: ->
return {}
render: (options={})->
return this unless not @_rendered or options.force
data = (@model?.toHash? and @model.toHash()) or @model or {}
if not @_template?
logger.error => "Default render called for #{@constructor.name} without a template"
return this
logger.verbose => "#{this} rendering with data: #{data}"
@onWillRender()
$oldEl = @$el
$newEl = Backbone.$(@_template(data))
if $oldEl
$oldEl.replaceWith $newEl
$newEl.addClass $oldEl.attr 'class'
@setElement $newEl
@_rendered = true
@onDidRender()
return this
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name}.#{@cid}"
# Private Methods ##############################################################################
_loadTemplate: (templateName)->
if templateName?
@_template = views[templateName]
_tryRefresh: ->
return unless @_rendered
@refresh()
@@ -0,0 +1,58 @@
###
Crafting Guide - browse_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
PageController = require './page_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
ModController = require './mod_controller'
########################################################################################################################
module.exports = class BrowsePageController extends PageController
constructor: (options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'browse_page'
super options
@modPack = options.modPack
@modPack.on Event.change, => @tryRefresh()
# PageController Overrides #####################################################################
getTitle: ->
return 'Browse'
# BaseController Overrides #####################################################################
onDidRender: ->
@$modContainer = @$('.mods')
super
refresh: ->
@_controllers ?= []
controllerIndex = 0
@modPack.eachMod (mod)=>
return unless mod.enabled
controller = @_controllers[controllerIndex]
if not controller?
controller = new ModController model:mod
controller.render()
@$modContainer.append controller.$el
else
controller.model = mod
controllerIndex += 1
while @_controllers.length > controllerIndex
controller = @_controllers.pop()
controller.$el.addClass 'removing'
controller.$el.one Event.transitionEnd -> controller.$el.remove()
super
@@ -0,0 +1,33 @@
###
Crafting Guide - configure_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
PageController = require './page_controller'
ModPackController = require './mod_pack_controller'
########################################################################################################################
module.exports = class ConfigurePageController extends PageController
constructor: (options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
if not options.storage? then throw new Error 'options.storage is required'
options.templateName = 'configure_page'
super options
@modPack = options.modPack
@storage = options.storage
# PageController Overrides #####################################################################
getTitle: ->
return "Configure"
# BaseController Overrides #####################################################################
onDidRender: ->
@modPackController = @addChild ModPackController, '.view__mod_pack', model:@modPack, storage:@storage
super
@@ -0,0 +1,113 @@
###
Crafting Guide - craft_page_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
CraftingTableController = require './crafting_table_controller'
CraftPage = require '../models/craft_page'
{Event} = require '../constants'
ImageLoader = require './image_loader'
InventoryController = require './inventory_controller'
ModPackController = require './mod_pack_controller'
NameFinder = require '../models/name_finder'
PageController = require './page_controller'
Storage = require '../models/storage'
{Text} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class CraftPageController extends PageController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.model ?= new CraftPage modPack:options.modPack
options.storage ?= new Storage storage:window.localStorage
options.templateName = 'craft_page'
super options
@imageLoader = options.imageLoader
@storage = options.storage
# Event Methods ################################################################################
onToolsBoxToggled: ->
@model.plan.includingTools = @$('.includeTools:checked').length isnt 0
# PageController Overrides #####################################################################
getTitle: ->
return 'Craft'
# BaseController Overrides #####################################################################
onWillRender: ->
@storage.register 'crafting-plan', @model.plan, 'includingTools'
@model.plan.have.clear()
@model.plan.have.parse @storage.load('crafting-plan:have')
super
onDidRender: ->
@wantController = @addChild InventoryController, '.want',
editable: true
icon: '/images/fishing_rod.png'
imageLoader: @imageLoader
model: @model.plan.want
modPack: @model.modPack
onChange: => @_updateLocation()
title: 'Items you want'
@haveController = @addChild InventoryController, '.have',
editable: true,
imageLoader: @imageLoader
model: @model.plan.have
modPack: @model.modPack
onChange: => @_saveHaveInventory()
nameFinder: new NameFinder @model.modPack, includeGatherable:true
title: 'Items you have'
@needController = @addChild InventoryController, '.need',
editable: false
icon: '/images/boots.png'
imageLoader: @imageLoader
model: @model.plan.need
modPack: @model.modPack
title: "Items you'll need"
@craftingTableController = @addChild CraftingTableController, '.view__crafting_table',
imageLoader: @imageLoader
model: @model.table
modPack: @model.modPack
@$('.want .toolbar').append '<label><input class="includeTools" type="checkbox"> include tools</label>'
@$includeToolsBox = @$('.includeTools')
super
refresh: ->
if @model.plan.includingTools
@$includeToolsBox.attr 'checked', 'checked'
else
@$includeToolsBox.removeAttr 'checked'
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'change .includeTools': 'onToolsBoxToggled'
# Private Methods ##############################################################################
_saveHaveInventory: ->
@storage.store 'crafting-plan:have', @model.plan.have.unparse()
_updateLocation: ->
text = @model.plan.want.unparse()
url = Url.crafting inventoryText:text
router.navigate url
@@ -0,0 +1,74 @@
###
Crafting Guide - crafting_grid_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
ImageLoader = require './image_loader'
########################################################################################################################
module.exports = class CraftingGridController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'crafting_grid'
super options
@_imageLoader = options.imageLoader
@_modPack = options.modPack
@_slotCount = 9
@_modPack.on Event.change, => @tryRefresh()
# BaseController Methods #######################################################################
onDidRender: ->
@slots = []
for el in @$('td')
$el = $(el)
@slots.push a:$el.find('a'), img:$el.find('img')
super
refresh: ->
for index in [0...@slots.length]
slot = @slots[index]
slot.a.addClass 'empty'
slot.a.removeAttr 'href'
slot.img.attr 'src', '/images/empty.png'
slot.img.removeAttr 'alt'
display = @_getItemDisplayAt index
if display?
slot.a.removeClass 'empty'
slot.a.attr 'href', display.itemUrl
slot.a.attr 'title', display.itemName
@_imageLoader.load display.iconUrl, slot.img
slot.img.attr 'alt', display.itemName
@$el.tooltip show:{delay:Duration.snap, duration:Duration.fast}
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click td a': 'routeLinkClick'
# Private Methods ##############################################################################
_getItemDisplayAt: (slot)->
if slot >= @_slotCount then throw new Error "slot (#{slot}) must be less than #{@_slotCount}"
return null unless @model?
itemSlug = @model.getItemSlugAt slot
return null unless itemSlug?
itemDisplay = @_modPack.findItemDisplay itemSlug
return itemDisplay
@@ -0,0 +1,94 @@
###
Crafting Guide - crafting_table_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
ImageLoader = require './image_loader'
MinimalRecipeController = require './minimal_recipe_controller'
########################################################################################################################
module.exports = class CraftingTableController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'crafting_table'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
# Event Methods ################################################################################
onNextClicked: ->
@model.stepIndex += 1
onPrevClicked: ->
@model.stepIndex -= 1
onReportProblem: ->
itemList = @model.plan.want.unparse()
toolsMessage = if @model.plan.includingTools then '(including tools)' else ''
message = "When I was on step #{@model.stepIndex + 1} of making:
\n\n#{itemList}#{toolsMessage}\n\nI noticed that...\n"
global.feedbackController.enterFeedback message
# BaseController Overrides #####################################################################
onDidRender: ->
@recipeController = @addChild MinimalRecipeController, '.view__minimal_recipe',
imageLoader: @imageLoader
modPack: @modPack
@$next = @$('.next')
@$prev = @$('.prev')
@$problemControl = @$('.problem')
@$title = @$('h2 p')
@$tool = @$('.tool p')
@$multiplier = $('<p class="multiplier"></p>')
@$('.output').append @$multiplier
@defaultTitle = @$title.html()
super
refresh: ->
@$prev.removeClass 'enabled'
@$next.removeClass 'enabled'
if @model.hasSteps
if @model.hasPrevStep then @$prev.addClass 'enabled'
if @model.hasNextStep then @$next.addClass 'enabled'
@$title.html "Step #{@model.stepIndex + 1} of #{@model.stepCount}"
else
@$title.html @defaultTitle
currentStep = @model.currentStep
@recipeController.model = currentStep?.recipe
if currentStep?.multiplier > 1
@$multiplier.html "×#{currentStep.multiplier}"
else
@$multiplier.html ''
if not (@model.hasSteps and global.feedbackController?)
@$problemControl.hide duration:Duration.snap
else
@$problemControl.show duration:Duration.snap
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click .next': 'onNextClicked'
'click .prev': 'onPrevClicked'
'click .problem a': 'onReportProblem'
@@ -0,0 +1,112 @@
###
Crafting Guide - feedback_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
EmailClient = require '../models/email_client'
{Key} = require '../constants'
########################################################################################################################
module.exports = class FeedbackController extends BaseController
constructor: (options={})->
options.model ?= new EmailClient
options.templateName = 'feedback'
super options
Object.defineProperty this, 'isOpen', get:-> @$el.offset().left is 0
# Public Methods ###############################################################################
enterFeedback: (message)->
promise = w(true)
if not @isOpen then promise = @onToggle()
promise.then => @$commentField.val message
# Event Methods ################################################################################
onSendClicked: ->
return unless @$commentField.val().length > 0
@$sendButton.attr 'disabled', 'disabled'
message =
subject:'Crafting Guide Feedback'
body:
"""
url: #{window.location.href}
name: #{@$nameField.val()}
email: #{@$emailField.val()}
comment:
#{@$commentField.val()}
"""
@model.send(message)
.then =>
@onToggle()
@$error.slideUp duration:Duration.normal
.catch (error)=>
@$error.slideDown duration:Duration.normal
.finally =>
@$sendButton.removeAttr 'disabled'
onTextChanged: (event)->
if @$commentField.val().length > 0
@$sendButton.removeAttr 'disabled'
else
@$sendButton.attr 'disabled', 'disabled'
onToggle: ->
w.promise (resolve)=>
@$screen.off 'click'
if @isOpen
@$screen.css display:'none'
@$el.animate {left:-@$el.outerWidth()}, duration:Duration.fast, complete:=>
@$commentField.val ''
@$('input, textarea').blur()
resolve()
else
@$screen.on 'click', => @onToggle()
@$screen.css display:'block'
@$el.animate {left:0}, duration:Duration.fast, complete:=>
if @$nameField.val().length is 0
@$nameField.focus()
else if @$emailField.val().length is 0
@$emailField.focus()
else
@$commentField.focus()
resolve()
# BaseController Overrides #####################################################################
onDidRender: ->
@$screen = $('.view__screen')
@$commentField = @$('textarea[name="comment"]')
@$emailField = @$('input[name="email"]')
@$error = @$('.error')
@$nameField = @$('input[name="name"]')
@$sendButton = @$('button[name="send"]')
windowHeight = window.innerHeight
@$el.delay(Duration.slow).animate {left:-@$el.outerWidth()}, Duration.normal
@onTextChanged()
super
# Backbone.View Methods ########################################################################
events: ->
return _.extend super,
'click .label': 'onToggle'
'click button': 'onSendClicked'
'input textarea': 'onTextChanged'
'keyup textarea': 'onTextChanged'
@@ -0,0 +1,95 @@
###
Crafting Guide - full_recipe_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
CraftingGridController = require './crafting_grid_controller'
{Duration} = require '../constants'
ImageLoader = require './image_loader'
Inventory = require '../models/inventory'
InventoryTableController = require './inventory_table_controller'
StringBuilder = require '../models/string_builder'
########################################################################################################################
module.exports = class FullRecipeController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'full_recipe'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
# BaseController Overrides #####################################################################
onDidRender: ->
@gridController = @addChild CraftingGridController, '.view__crafting_grid',
modPack: @modPack
imageLoader: @imageLoader
@inputController = @addChild InventoryTableController, '.input .view__inventory_table',
editable: false
imageLoader: @imageLoader
model: new Inventory
modPack: @modPack
@outputController = @addChild InventoryTableController, '.output .view__inventory_table',
editable: false
imageLoader: @imageLoader
model: new Inventory
modPack: @modPack
@$toolContainer = @$('.tool')
super
refresh: ->
@gridController.model = @model
@$el.tooltip show:{delay:Duration.normal, duration:Duration.normal}
@_refreshInputs()
@_refreshOutputs()
@_refreshTools()
super
# Backbone.View Methods ########################################################################
events: ->
return _.extend super,
'click a': 'routeLinkClick'
# Private Methods ##############################################################################
_refreshInputs: ->
inputs = @inputController.model
inputs.clear()
if @model?
for stack in @model.input
inputs.add stack.itemSlug, stack.quantity
_refreshOutputs: ->
outputs = @outputController.model
outputs.clear()
if @model?
for stack in @model.output
outputs.add stack.itemSlug, stack.quantity
_refreshTools: ->
@$toolContainer.empty()
return unless @model?
builder = new StringBuilder
builder.loop @model.tools, delimiter:', ', onEach:(b, stack)=>
display = @modPack.findItemDisplay stack.itemSlug
b.push "<a href=\"#{display.itemUrl}\">#{display.itemName}</a>"
@$toolContainer.html builder.toString()
@@ -0,0 +1,60 @@
###
Crafting Guide - header_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
########################################################################################################################
module.exports = class HeaderController extends BaseController
constructor: (options={})->
super options
# Event Methods ################################################################################
onLogoClicked: ->
router.navigate '/', trigger:true
return false
onNavItemClicked: (event)->
return true unless $(event.currentTarget).attr('href')?
router.navigate $(event.currentTarget).attr('href'), trigger:true
return false
# BaseController Overrides #####################################################################
render: ->
# Overriding render because the header is already part of the stock page layout, and we
# don't need to replace it here.
@_rendered = true
@$navLinks = ($(el) for el in @$('.navBar a'))
zIndex = @$navLinks.length + 100
for $navLink in @$navLinks
$navLink.css 'z-index', zIndex--
refresh: ->
for $navLink in @$navLinks
linkPage = $navLink.data('page')
if @model is linkPage
$navLink.addClass 'selected'
else
$navLink.removeClass 'selected'
if @model.indexOf(linkPage) isnt -1
$navLink.find('.dot').css 'opacity': 1;
else
$navLink.find('.dot').css 'opacity': 0;
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a.logo': 'onLogoClicked'
'click .navBar a': 'onNavItemClicked'
@@ -0,0 +1,22 @@
###
Crafting Guide - home_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
PageController = require './page_controller'
########################################################################################################################
module.exports = class HomeController extends PageController
constructor: (options={})->
options.templateName = 'home_page'
super options
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a': 'routeLinkClick'
+105
View File
@@ -0,0 +1,105 @@
###
Crafting Guide - image_loader.coffee
Copyright (c) 2014 by Redwood Labs
All rights reserved.
###
{Duration} = require '../constants'
########################################################################################################################
module.exports = class ImageLoader
constructor: (options={})->
options.defaultUrl ?= null
if not options.defaultUrl?
options.onLoading ?= -> @hide()
options.onLoad ?= -> @show()
@defaultUrl = options.defaultUrl
@onLoading = options.onLoading
@onLoad = options.onLoad
@_images = {}
if @defaultUrl
@_defaultImage = new Image
@_defaultImage.src = @defaultUrl
# Class Methods ################################################################################
@load: (imageUrl, $el, options={}) ->
loader = new ImageLoader options
loader.load $el, options
return loader
# Public Methods ###############################################################################
isLoaded: (imageUrl)->
data = @_images[imageUrl]
return false unless data?
return data.isLoaded
load: (imageUrl, $el)->
return if not $el
data = @preload imageUrl
if $el.attr('src')?
return if $el.attr('src').indexOf(imageUrl) isnt -1
if @onLoading? then @onLoading.call $el
$el.data 'isLoading', true
$el.data 'isLoaded', false
if data.isLoaded
@_loadImageIntoElement data.imageUrl, $el
else
if @defaultUrl?
$el.attr 'src', @defaultUrl
else
$el.removeAttr 'src'
if data.elements.indexOf($el) is -1 then data.elements.push $el
return this
preload: (imageUrl)->
data = @_images[imageUrl]
if not data?
data = imageUrl:imageUrl, elements:[], image:new Image, isLoaded:false
@_images[imageUrl] = data
data.image = new Image
data.image.onload = => @_onImageLoaded data
data.image.onerror = => @_onImageError data
data.image.src = imageUrl
return data
# Private Methods ##############################################################################
_onImageLoaded: (data)->
for $el in data.elements
@_loadImageIntoElement data.imageUrl, $el
data.elements = []
data.isLoaded = true
_onImageError: (data)->
if @defaultUrl?
data.imageUrl = @defaultUrl
for $el in data.elements
@_loadImageIntoElement @defaultUrl, $el
data.elements = []
data.isLoaded = true
_loadImageIntoElement: (imageUrl, $el)->
$el.data 'isLoading', false
$el.data 'isLoaded', true
$el.attr 'src', imageUrl
if @onLoad? then @onLoad.call $el
@@ -0,0 +1,181 @@
###
Crafting Guide - inventory_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
{Key} = require '../constants'
ImageLoader = require './image_loader'
NameFinder = require '../models/name_finder'
StackController = require './stack_controller'
########################################################################################################################
module.exports = class InventoryController extends BaseController
@MAX_QUANTITY = 9999
@ONLY_DIGITS = /^[0-9]*$/
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
@editable = options.editable ?= true
@icon = options.icon ?= '/images/chest_front.png'
@imageLoader = options.imageLoader
@modPack = options.modPack
@nameFinder = options.nameFinder ?= new NameFinder options.modPack
@onChange = options.onChange ?= -> # do nothing
@title = options.title ?= 'Inventory'
options.templateName = 'inventory'
super options
@_stackControllers = []
@listenTo @modPack, Event.change, => @refresh()
# Event Methods ################################################################################
onAddButtonClicked: ->
if @$nameField.val().trim().length is 0
@$nameField.focus()
return
item = @modPack.findItemByName @$nameField.val()
return unless item?
@model.add item.slug, 1
@$nameField.val ''
@$scrollbox.scrollTop @$scrollbox.prop 'scrollHeight'
@$nameField.autocomplete 'close'
@onChange()
onClearButtonClicked: ->
@model.clear()
@onChange()
onItemSelected: ->
func = =>
@onNameFieldChanged()
@onAddButtonClicked()
@$nameField.blur()
setTimeout func, 10 # needed to allow the autocomplete to finish
return true
onNameFieldBlur: ->
item = @modPack.findItemByName @$nameField.val()
@$nameField.val if item? then item.name else ''
@onNameFieldChanged()
onNameFieldChanged: ->
@_refreshButtonState()
onNameFieldFocused: ->
@$nameField.val ''
@$nameField.autocomplete 'search'
onNameFieldKeyUp: (event)->
if event.which is Key.Return
@onAddButtonClicked()
# BaseController Overrides #####################################################################
onDidRender: ->
@$addButton = @$('button[name="add"]')
@$clearButton = @$('button[name="clear"]')
@$icon = @$('.icon')
@$editPanel = @$('.edit')
@$nameField = @$('input[name="name"]')
@$scrollbox = @$('.scrollbox')
@$table = @$('table')
@$toolbar = @$('.toolbar')
@$title = @$('h2 p')
super
refresh: ->
@$editPanel.css display:(if @editable then 'table-row' else 'none')
@$toolbar.css display:(if @editable then 'block' else 'none')
@$scrollbox.css bottom:(if @editable then @$toolbar.height() else '0')
@$icon.attr 'src', @icon
@$title.html @title
@_refreshStacks()
@_refreshNameAutocomplete()
@_refreshButtonState()
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'blur input[name="name"]': 'onNameFieldBlur'
'click button[name="add"]': 'onAddButtonClicked'
'click button[name="clear"]': 'onClearButtonClicked'
'focus input[name="name"]': 'onNameFieldFocused'
'input input[name="name"]': 'onNameFieldChanged'
'keyup input[name="name"]': 'onNameFieldKeyUp'
# Private Methods ##############################################################################
_refreshButtonState: ->
if @model.isEmpty then @$clearButton.attr('disabled', 'disabled') else @$clearButton.removeAttr('disabled')
noText = @$nameField.val().trim().length is 0
itemValid = @modPack.findItemByName(@$nameField.val())?
disable = not (itemValid or noText)
if disable then @$addButton.attr('disabled', 'disabled') else @$addButton.removeAttr('disabled')
_refreshNameAutocomplete: ->
onChanged = => @onNameFieldChanged()
onSelected = => @onItemSelected()
@$nameField.autocomplete
source: (request, callback)=> callback @nameFinder.search request.term
delay: 0
minLength: 0
change: onChanged
close: onChanged
select: onSelected
_refreshStacks: ->
@_stackControllers ?= []
index = 0
$lastRow = @$table.find 'tr:last-child'
@model.each (stack)=>
controller = @_stackControllers[index]
if not controller?
controller = new StackController
editable: @editable
imageLoader: @imageLoader
model: stack
modPack: @modPack
onChange: @onChange
onRemove: if not @editable then null else (stack)=> @_removeStack(stack)
controller.render()
controller.$el.hide()
controller.$el.insertBefore $lastRow
controller.$el.slideDown duration:Duration.fast
@_stackControllers.push controller
else
controller.model = stack
index += 1
while @_stackControllers.length > index
controller = @_stackControllers.pop()
controller.$el.fadeOut duration:Duration.fast, complete:-> @remove()
_removeStack: (stack)->
@model.remove stack.itemSlug, stack.quantity
@@ -0,0 +1,184 @@
###
Crafting Guide - inventory_table_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
ImageLoader = require './image_loader'
ItemSlug = require '../models/item_slug'
{Key} = require '../constants'
NameFinder = require '../models/name_finder'
StackController = require './stack_controller'
########################################################################################################################
module.exports = class InventoryTableController extends BaseController
@ONLY_DIGITS = /^[0-9]*$/
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
@editable = options.editable ?= true
@imageLoader = options.imageLoader
@modPack = options.modPack
@nameFinder = options.nameFinder ?= new NameFinder options.modPack
@onChange = options.onChange ?= -> # do nothing
options.templateName = 'inventory_table'
super options
@_stackControllers = []
@listenTo @modPack, 'change', => @refresh()
# Event Methods ################################################################################
onAddButtonClicked: ->
name = @modpack.findItemByName @$nameField.val()
return unless item?
@model.add item.slug, parseInt(@$quantityField.val())
@$nameField.val ''
@$quantityField.val '1'
@$scrollbox.scrollTop @$scrollbox.prop 'scrollHeight'
@$nameField.autocomplete 'close'
@onChange()
onClearButtonClicked: ->
@model.clear()
@onChange()
onItemSelected: ->
func = =>
@onNameFieldChanged()
@onAddButtonClicked()
@$nameField.blur()
setTimeout func, 10 # needed to allow the autocomplete to finish
return true
onNameFieldBlur: ->
item = @modPack.findItemByName @$nameField.val()
@$nameField.val if item? then item.name else ''
@onNameFieldChanged()
onNameFieldChanged: ->
item = @modPack.findItemByName @$nameField.val()
@_updateButtonState()
onNameFieldFocused: ->
@$nameField.val ''
@$nameField.autocomplete('search')
onNameFieldKeyUp: (event)->
if event.which is Key.Return
@onAddButtonClicked()
onQuantityFieldBlur: ->
value = @$quantityField.val().replace /[^0-9]/g, ''
if value.length is 0 then value = '1'
value = Math.min value, 64
@$quantityField.val value
@onQuantityFieldChanged()
onQuantityFieldChanged: ->
if not @$quantityField.val().match /^[0-9]*$/
@$quantityField.addClass 'error', 0
@$quantityField.addClass 'error-new', 0
@$quantityField.removeClass 'error-new', Duration.slow
@$quantityField.focus()
return
@$quantityField.removeClass 'error', Duration.normal
@$quantityField.removeClass 'error-new', Duration.normal
@_updateButtonState()
onQuantityFieldFocused: ->
@$quantityField.val ''
# BaseController Overrides #####################################################################
onDidRender: ->
@$addButton = @$('button[name="add"]')
@$clearButton = @$('button[name="clear"]')
@$editPanel = @$('.edit')
@$nameField = @$('input[name="name"]')
@$quantityField = @$('input[name="quantity"]')
@$scrollbox = @$('.scrollbox')
@$table = @$('table')
@$toolbar = @$('.toolbar')
super
refresh: ->
@$editPanel.css display:(if @editable then 'table-row' else 'none')
@$toolbar.css display:(if @editable then 'block' else 'none')
@$scrollbox.css bottom:(if @editable then @$toolbar.height() else '0')
if _.isEmpty(@$quantityField.val()) then @$quantityField.val '1'
@$table.find('tr:not(:last-child)').remove()
$lastRow = @$table.find 'tr:last-child'
@_stackControllers = []
@model.each (stack)=>
controller = new StackController
editable: @editable
imageLoader: @imageLoader
model: stack
modPack: @modPack
onRemove: if not @editable then null else (stack)=> @_removeStack(stack)
controller.render()
controller.$el.insertBefore $lastRow
@_stackControllers.push controller
@_updateNameAutocomplete()
@_updateButtonState()
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'blur input[name="name"]': 'onNameFieldBlur'
'blur input[name="quantity"]': 'onQuantityFieldBlur'
'click button[name="add"]': 'onAddButtonClicked'
'click button[name="clear"]': 'onClearButtonClicked'
'focus input[name="name"]': 'onNameFieldFocused'
'focus input[name="quantity"]': 'onQuantityFieldFocused'
'input input[name="name"]': 'onNameFieldChanged'
'input input[name="quantity"]': 'onQuantityFieldChanged'
'keyup input[name="name"]': 'onNameFieldKeyUp'
# Private Methods ##############################################################################
_removeStack: (stack)->
@model.remove stack.itemSlug, stack.quantity
_updateNameAutocomplete: ->
onChanged = => @onNameFieldChanged()
onSelected = => @onItemSelected()
@$nameField.autocomplete
source: (request, callback)=> callback @nameFinder.search request.term
delay: 0
minLength: 0
change: onChanged
close: onChanged
select: onSelected
_updateButtonState: ->
if @model.isEmpty then @$clearButton.attr('disabled', 'disabled') else @$clearButton.removeAttr('disabled')
itemValid = @modPack.findItemByName(@$nameField.val())?
quantityValid = @$quantityField.val().match(InventoryTableController.ONLY_DIGITS)
disable = not (itemValid and quantityValid)
if disable then @$addButton.attr('disabled', 'disabled') else @$addButton.removeAttr('disabled')
@@ -0,0 +1,43 @@
###
Crafting Guide - item_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
########################################################################################################################
module.exports = class ItemController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'item'
super options
@_imageLoader = options.imageLoader
@_modPack = options.modPack
# BaseController Overrides #####################################################################
onDidRender: ->
@$icon = @$('img')
@$name = @$('.itemName')
@$nameLink = @$('a')
super
refresh: ->
display = @_modPack.findItemDisplay @model.slug
@_imageLoader.load display.iconUrl, @$icon
@$name.html display.itemName
@$nameLink.attr 'href', display.itemUrl
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a': 'routeLinkClick'
@@ -0,0 +1,84 @@
###
Crafting Guide - item_group_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
Item = require '../models/item'
ItemController = require './item_controller'
########################################################################################################################
module.exports = class ItemGroupController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.title ?= ''
options.templateName = 'item_group'
super options
@_delayStep = 20
@_imageLoader = options.imageLoader
@_itemControllers = []
@_modPack = options.modPack
@_title = options.title
Object.defineProperties this,
title: {get:@getTitle, set:@setTitle}
# Property Methods #############################################################################
getTitle: ->
return @_title
setTitle: (title)->
@_title = title
@tryRefresh()
# BaseController Overrides #####################################################################
onDidRender: ->
@$title = @$('h2')
@$items = @$('.panel')
super
refresh: ->
@$title.html @_title
@_refreshItems()
super
# Private Methods ##############################################################################
_createItemController: (item, delay)->
controller = new ItemController imageLoader:@_imageLoader, model:item, modPack:@_modPack
@_itemControllers.push controller
attachController = =>
controller.render()
@$items.append controller.$el
_.delay attachController, delay
_refreshItems: ->
controllerIndex = 0
delay = 0
if @model?
for item in @model
controller = @_itemControllers[controllerIndex]
if not controller?
@_createItemController item, delay
delay += @_delayStep
else
controller.model = item
controller.refresh()
controllerIndex += 1
while @_itemControllers.length > controllerIndex
controller = @_itemControllers.pop()
controller.$el.slideUp duration:Duration.normal, complete:-> @remove()
@@ -0,0 +1,174 @@
###
Crafting Guide - item_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
{Duration} = require '../constants'
{Event} = require '../constants'
FullRecipeController = require './full_recipe_controller'
ImageLoader = require './image_loader'
Item = require '../models/item'
ItemGroupController = require './item_group_controller'
ItemPage = require '../models/item_page'
ItemSlug = require '../models/item_slug'
PageController = require './page_controller'
{Text} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ItemPageController extends PageController
constructor: (options={})->
if not options.itemSlug? then throw new Error 'options.itemSlug is required'
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.model ?= new ItemPage modPack:options.modPack
options.templateName ?= 'item_page'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
@_itemSlug = options.itemSlug
@modPack.on Event.change, => @tryRefresh()
# PageController Overrides #####################################################################
getTitle: ->
return @model.item?.name
# BaseController Overrides #####################################################################
onDidRender: ->
@_usedAsToolToMakeController = @addChild ItemGroupController, '.usedAsToolToMake .view__item_group',
imageLoader: @imageLoader
modPack: @modPack
@_similarItemsController = @addChild ItemGroupController, '.similar .view__item_group',
imageLoader: @imageLoader
modPack: @modPack
@_usedToMakeController = @addChild ItemGroupController, '.usedToMake .view__item_group',
imageLoader: @imageLoader
modPack: @modPack
@$usedAsToolToMakeSection = @$('.usedAsToolToMake')
@$byline = @$('.byline')
@$bylineLink = @$('.byline a')
@$craftingPlanLink = @$('a.craftingPlan')
@$name = @$('h1.name')
@$recipeContainer = @$('.recipes .panel')
@$recipesSection = @$('.recipes')
@$similarSection = @$('.similar')
@$titleImage = @$('.titleImage img')
@$usedToMakeSection = @$('.usedToMake')
super
refresh: ->
@_resolveItemSlug()
if @model.item?
display = @modPack.findItemDisplay @model.item.slug
@$craftingPlanLink.attr href:display.craftingUrl
@$craftingPlanLink.fadeIn duration:Duration.fast
@imageLoader.load display.iconUrl, @$titleImage
@$name.html display.itemName
@$el.slideDown duration:Duration.normal
else
@$el.slideUp duration:Duration.normal
@_refreshByline()
@_refreshRecipes()
@_refreshSimilarItems()
@_refreshUsedAsToolToMake()
@_refreshUsedToMake()
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a.craftingPlan': 'routeLinkClick'
'click .byline a': 'routeLinkClick'
# Private Methods ##############################################################################
_refreshUsedAsToolToMake: ->
@_usedAsToolToMakeController.title = 'Used as Tool to Make'
@_usedAsToolToMakeController.model = @model.findToolForRecipes()
if @_usedAsToolToMakeController.model?
@$usedAsToolToMakeSection.slideDown duration:Duration.normal
else
@$usedAsToolToMakeSection.slideUp duration:Duration.normal
_refreshByline: ->
mod = @model.item?.modVersion?.mod
if mod?.name?.length > 0
@$bylineLink.attr 'href', Url.mod modSlug:mod.slug
@$bylineLink.html mod.name
@$byline.fadeIn duration:Duration.fast
else
@$byline.fadeOut duration:Duration.fast
_refreshUsedToMake: ->
@_usedToMakeController.title = 'Used to Make'
@_usedToMakeController.model = @model.findComponentInItems()
if @_usedToMakeController.model?
@$usedToMakeSection.slideDown duration:Duration.normal
else
@$usedToMakeSection.slideUp duration:Duration.normal
_refreshRecipes: ->
@_recipeControllers ?= []
index = 0
recipes = @model.findRecipes()
if recipes?
@$recipesSection.slideDown duration:Duration.normal
for recipe in @model.findRecipes()
controller = @_recipeControllers[index]
if not controller?
controller = new FullRecipeController imageLoader:@imageLoader, modPack:@modPack, model:recipe
@_recipeControllers.push controller
controller.render()
@$recipeContainer.append controller.$el
else
controller.model = recipe
index++
else
@$recipesSection.slideUp duration:Duration.normal
while @_recipeControllers.length > index
controller = @_recipeControllers.pop()
controller.$el.slideUp duration:Duration.normal, complete:-> controller.$el.remove()
_refreshSimilarItems: ->
group = @model.item?.group
if group? and group isnt Item.Group.Other
@_similarItemsController.title = "Other #{group}"
@_similarItemsController.model = @model.findSimilarItems()
else
@_similarItemsController.model = null
if @_similarItemsController.model?
@$similarSection.slideDown duration:Duration.normal
else
@$similarSection.slideUp duration:Duration.normal
_resolveItemSlug: ->
item = @modPack.findItem @_itemSlug, includeDisabled:false
if item? and not ItemSlug.equal item.slug, @_itemSlug
router.navigate Url.item(modSlug:item.slug.mod, itemSlug:item.slug.item), trigger:true
return
@model.item = item
@@ -0,0 +1,80 @@
###
Crafting Guide - minimal_recipe_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
CraftingGridController = require './crafting_grid_controller'
{Duration} = require '../constants'
ImageLoader = require './image_loader'
StringBuilder = require '../models/string_builder'
########################################################################################################################
module.exports = class MinimalRecipeController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'minimal_recipe'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
# BaseController Overrides #####################################################################
onDidRender: ->
@gridController = @addChild CraftingGridController, '.view__crafting_grid',
modPack: @modPack
imageLoader: @imageLoader
@$outputImg = @$('.output img')
@$outputLink = @$('.output a')
@$outputQuantity = @$('.quantity')
@$toolContainer = @$('.tool')
super
refresh: ->
@gridController.model = @model
@$outputImg.attr 'src', '/images/empty.png'
@$outputImg.removeAttr 'alt'
@$outputLink.removeAttr 'href'
@$outputQuantity.html ''
if @model?
outputStack = @model.output[0]
if outputStack?
display = @modPack.findItemDisplay outputStack.itemSlug
@$outputLink.attr 'href', display.itemUrl
@$outputLink.attr 'title', display.itemName
@$outputImg.attr 'alt', display.itemName
@$outputQuantity.html outputStack.quantity if outputStack.quantity > 1
@imageLoader.load display.iconUrl, @$outputImg
@$el.tooltip show:{delay:Duration.snap, duration:Duration.fast}
@_refreshTools()
super
# Backbone.View Methods ########################################################################
events: ->
return _.extend super,
'click a': 'routeLinkClick'
# Private Methods ##############################################################################
_refreshTools: ->
@$toolContainer.empty()
return unless @model?
builder = new StringBuilder
builder.loop @model.tools, delimiter:', ', onEach:(b, stack)=>
display = @modPack.findItemDisplay stack.itemSlug
b.push "<a href=\"#{display.itemUrl}\">#{display.itemName}</a>"
@$toolContainer.html builder.toString()
@@ -0,0 +1,43 @@
###
Crafting Guide - mod_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ModController extends BaseController
constructor: (options={})->
options.templateName = 'mod'
super options
# BaseController Overrides #####################################################################
onDidRender: ->
@$link = @$('a')
@$logo = @$('.logo')
@$name = @$('.name p')
@$description = @$('.description p')
super
refresh: ->
if @model?
@$el.removeClass 'empty'
@$link.attr 'href', Url.mod modSlug:@model.slug
@$logo.attr 'src', Url.modIcon modSlug:@model.slug
@$name.html @model.name
@$description.html @model.description
else
@$el.addClass 'empty'
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a': 'routeLinkClick'
@@ -0,0 +1,77 @@
###
Crafting Guide - mod_pack_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{DefaultMods} = require '../constants'
{Duration} = require '../constants'
Mod = require '../models/mod'
ModSelectorController = require './mod_selector_controller'
########################################################################################################################
module.exports = class ModPackController extends BaseController
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.templateName = 'mod_pack'
super options
@_controllers = []
@storage = options.storage
# Event Methods ################################################################################
onSuggestModClicked: ->
return unless global.feedbackController?
global.feedbackController.enterFeedback 'Please add mod:\n\n'
# BaseController Overrides #####################################################################
onDidRender: ->
@$mods = @$('.mods')
@$toolbar = @$('.toolbar')
super
refresh: ->
if not @model?
@_controllers = []
@$('table tr').remove()
return
index = 0
mods = @model.getMods()
while index < Math.min @_controllers.length, mods.length
controller = @_controllers[index]
controller.model = mods[index]
index++
while @_controllers.length < mods.length
controller = new ModSelectorController model:mods[index], storage:@storage
controller.render()
@_controllers.push controller
controller.$el.hide duration:0
@$mods.append controller.$el
controller.$el.slideDown duration:Duration.normal
index++
while @_controllers.length > mods.length
controller = @_controllers.pop()
controller.$el.slideUp duration:Duration.normal, complete:-> controller.$el.remove()
if global.feedbackController?
@$toolbar.show duration:0
else
@$toolbar.hide duration:0
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click button[name="suggestMod"]': 'onSuggestModClicked'
@@ -0,0 +1,153 @@
###
Crafting Guide - mod_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
{Duration} = require '../constants'
Item = require '../models/item'
ItemGroupController = require './item_group_controller'
Mod = require '../models/mod'
ModPack = require '../models/mod_pack'
PageController = require './page_controller'
{Text} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ModPageController extends PageController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.delayStep ?= 10
options.templateName = 'mod_page'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
@_delayStep = options.delayStep
@_effectiveModVersion = null
@_groupControllers = []
Object.defineProperties this,
effectiveModVersion: {get:@_getEffectiveModVersion}
# Event Methods ################################################################################
onVersionChanged: ->
version = @$versionSelector.val()
modVersion = @model.getModVersion version
@_effectiveModVersion = modVersion
if modVersion? then modVersion.fetch()
@refresh()
# PageController Overrides #####################################################################
getTitle: ->
return @model?.name
# BaseController Overrides #####################################################################
onDidRender: ->
@$name = @$('.name')
@$byline = @$('.byline p')
@$description = @$('.description p')
@$documentationLink = @$('.documentation')
@$downloadLink = @$('.download')
@$homePageLink = @$('.homePage')
@$groupContainer = @$('.itemGroups')
@$titleImage = @$('.titleImage img')
@$versionSelector = @$('select.version')
super
refresh: ->
if @model?
@$name.html @model.name
@$byline.html "by #{@model.author}"
@$titleImage.attr 'src', Url.modIcon modSlug:@model.slug
@$description.html @model.description
@$el.slideDown duration:Duration.normal
else
@$el.slideUp duration:Duration.normal
@_refreshLink @$homePageLink, @model.homePageUrl
@_refreshLink @$documentationLink, @model.documentationUrl
@_refreshLink @$downloadLink, @model.downloadUrl
@_refreshItemGroups()
@_refreshVersions()
super
# Backbone.View Methods ########################################################################
events:
'change select.version': 'onVersionChanged'
# Private Methods ##############################################################################
_getEffectiveModVersion: ->
return @_effectiveModVersion if @_effectiveModVersion?
return null unless @model?
modVersion = @model.activeModVersion
modVersion ?= @model.getModVersion Mod.Version.Latest
modVersion.fetch() if modVersion?
return modVersion
_refreshItemGroups: ->
groupIndex = 0
modVersion = @effectiveModVersion
if modVersion?
modVersion.eachGroup (group)=>
controller = @_groupControllers[groupIndex]
items = modVersion.allItemsInGroup group
if not controller?
title = if group is Item.Group.Other then 'Items' else group
controller = new ItemGroupController
imageLoader: @imageLoader
model: items
modPack: @modPack
title: title
controller.render()
@$groupContainer.append controller.$el
@_groupControllers[groupIndex] = controller
else
controller.modVersion = modVersion
controller.model = items
controller.refresh()
groupIndex++
while @_groupControllers.length > groupIndex + 1
controller = @_groupControllers.pop()
controller.$el.slideUp duration:Duration.normal, -> @remove()
_refreshLink: ($link, url)->
if url?
$link.slideDown duration:Duration.normal
$link.attr 'href', url
else
$link.slideUp duration:Duration.normal
_refreshVersions: ->
@$versionSelector.empty()
return unless @model?
effectiveModVersion = @effectiveModVersion
versionCount = 0
@model.eachModVersion (modVersion)=>
option = $("<option value=\"#{modVersion.version}\">#{modVersion.version}</option>")
if modVersion is effectiveModVersion
option.attr 'selected', 'selected'
@$versionSelector.append option
versionCount++
if versionCount <= 1
@$versionSelector.attr 'disabled', 'disabled'
else
@$versionSelector.removeAttr 'disabled'
@@ -0,0 +1,70 @@
###
Crafting Guide - mod_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Event} = require '../constants'
Mod = require '../models/mod'
{RequiredMods} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ModSelectorController extends BaseController
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.templateName = 'mod_selector'
super options
@_storage = options.storage
# Event Methods ################################################################################
onVersionChanged: ->
@model.activeVersion = @$version.val()
# BaseController Overrides #####################################################################
onDidRender: ->
@$version = @$('select')
@$nameLink = @$('.name a')
@$nameText = @$('.name p')
@$description = @$('.description p')
super
refresh: ->
if @model.activeVersion is Mod.Version.None
@$el.addClass 'disabled'
else
@$el.removeClass 'disabled'
@$version.empty()
if not (@model.slug in RequiredMods)
option = $("<option value=\"none\">Disabled</option>")
if @model.activeVersion is Mod.Version.None
option.attr 'selected', 'selected'
@$version.append option
@model.eachModVersion (modVersion)=>
option = $("<option value=\"#{modVersion.version}\">#{modVersion.version}</option>")
if modVersion is @model.activeModVersion
option.attr 'selected', 'selected'
@$version.append option
@$nameLink.attr 'href', Url.mod(modSlug:@model.slug)
@$nameText.html @model.name
@$description.html @model.description
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'change select': 'onVersionChanged'
'click a': 'routeLinkClick'
@@ -0,0 +1,37 @@
###
Crafting Guide - page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Text} = require '../constants'
########################################################################################################################
module.exports = class PageController extends BaseController
constructor: (options={})->
super options
# Public Methods ###############################################################################
getTitle: ->
# subclasses should override this to return the page-specific portion of the title
return null
# BaseController Overrides #####################################################################
refresh: ->
title = @getTitle()
title = if title? then title.trim() else ''
if title.length > 0
title += " | #{Text.title}"
else
title = Text.title
$('title').html title
super
@@ -0,0 +1,25 @@
###
Crafting Guide - recipe_step_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
########################################################################################################################
module.exports = class RecipeStepController extends BaseController
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'recipe_step'
super options
# BaseController Overrides #####################################################################
onDidRender: ->
@$slotImages = (@$slots.push $(el) for el in @$('.table-slot img'))
@$output = @$('.output')
super
@@ -0,0 +1,125 @@
###
Crafting Guide - stack_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
{Duration} = require '../constants'
{Event} = require '../constants'
ImageLoader = require './image_loader'
{Key} = require '../constants'
########################################################################################################################
module.exports = class StackController extends BaseController
@MAX_QUANTITY = 9999
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.editable ?= false
options.onChange ?= -> # do nothing
options.onRemove ?= (stack)-> # do nothing
options.templateName = 'stack'
super options
@editable = options.editable
@modPack = options.modPack
@onChange = options.onChange
@onRemove = options.onRemove
@_imageLoader = options.imageLoader
@modPack.on Event.change, => @tryRefresh()
# Event Methods ################################################################################
onQuantityFieldBlur: ->
quantityText = @$quantityField.val().trim()
if quantityText.length is 0
quantity = @_priorValue
else if not quantityText.match /^[0-9]*$/
quantity = 1
else
quantity = parseInt quantityText, 10
if _.isNaN quantity then quantity = 1
quantity = Math.min quantity, StackController.MAX_QUANTITY
quantity = Math.max 1, quantity
if quantity?
@$quantityField.val "#{quantity}"
@$quantityField.removeClass 'error', Duration.snap
@$quantityField.removeClass 'error-new', Duration.snap
@model.quantity = quantity
@onChange()
onQuantityFieldChanged: ->
quantityText = @$quantityField.val().trim()
if not quantityText.match /^[0-9]*$/
@$quantityField.addClass 'error', 0
@$quantityField.addClass 'error-new', 0
@$quantityField.removeClass 'error-new', Duration.fast
else
@$quantityField.removeClass 'error', Duration.snap
@$quantityField.removeClass 'error-new', Duration.snap
onQuantityFieldFocused: ->
if @editable
@_priorValue = @model.quantity
@$quantityField.val ''
else
@$quantityField.blur()
onQuantityKeyUp: (event)->
if event.which is Key.Return
@$quantityField.blur()
onRemoveClicked: ->
@onRemove @model
@onChange()
# BaseController Overrides #####################################################################
onDidRender: ->
@$action = @$('.action')
@$image = @$('.icon img')
@$nameLink = @$('.name a')
@$quantityField = @$('.quantity input')
@$removeButton = @$('button.remove')
super
refresh: ->
display = @modPack.findItemDisplay @model.itemSlug
@_imageLoader.load display.iconUrl, @$image
@$nameLink.html display.itemName
@$nameLink.attr 'href', display.itemUrl
@$quantityField.val @model.quantity
if @editable
@$quantityField.removeAttr 'readonly'
@$quantityField.addClass 'editable'
else
@$quantityField.attr 'readonly', 'readonly'
@$quantityField.removeClass 'editable'
@$action.css display:(if @editable then 'table-cell' else 'none')
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'blur .quantity input': 'onQuantityFieldBlur'
'click button.remove': 'onRemoveClicked'
'click .name a': 'routeLinkClick'
'focus .quantity input': 'onQuantityFieldFocused'
'input .quantity input': 'onQuantityFieldChanged'
'keyup .quantity input': 'onQuantityKeyUp'
+178
View File
@@ -0,0 +1,178 @@
###
Crafting Guide - router.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BrowsePageController = require './controllers/browse_page_controller'
CraftPageController = require './controllers/craft_page_controller'
ConfigurePageController = require './controllers/configure_page_controller'
{DefaultMods} = require './constants'
{Duration} = require './constants'
{Event} = require './constants'
HeaderController = require './controllers/header_controller'
ItemPageController = require './controllers/item_page_controller'
ItemSlug = require './models/item_slug'
ImageLoader = require './controllers/image_loader'
Mod = require './models/mod'
ModPack = require './models/mod_pack'
ModPageController = require './controllers/mod_page_controller'
{Opacity} = require './constants'
Storage = require './models/storage'
{Url} = require './constants'
UrlParams = require './url_params'
HomePageController = require './controllers/home_page_controller'
########################################################################################################################
module.exports = class CraftingGuideRouter extends Backbone.Router
constructor: (options={})->
@_page = null
@_pageControllers = {}
@_lastReported = null
super options
@imageLoader = new ImageLoader defaultUrl:'/images/unknown.png'
@modPack = new ModPack
@storage = new Storage storage:window.localStorage
@_defaultOptions = imageLoader:@imageLoader, modPack:@modPack, storage:@storage
@headerController = new HeaderController el:'.view__header'
@headerController.render()
# Public Methods ###############################################################################
loadDefaultModPack: ->
makeResponder = (m)-> return ->
m.activeModVersion.fetch() if m.activeModVersion?
for modSlug in DefaultMods
mod = new Mod slug:modSlug
mod.on Event.change + ':activeModVersion', makeResponder mod
@storage.register "mod:#{mod.slug}", mod, 'activeVersion'
mod.fetch()
@modPack.addMod mod
# Backbone.Router Overrides ####################################################################
navigate: ->
super
@_recordPageView()
routes:
'(/)': 'route__home'
'browse(/)': 'route__browse'
'browse/:modSlug(/)': 'route__browseMod'
'browse/:modSlug/:itemSlug(/)': 'route__browseModItem'
'configure(/)': 'route__configure'
'craft(/)': 'route__craft'
'craft/:text': 'route__craft'
'item/:itemSlug': 'deprecated__item'
'crafting/(:text)': 'deprecated__crafting'
'mod/:modSlug': 'deprecated__mod'
'mod/:modSlug/:itemSlug': 'deprecated__modItem'
# Route Methods ################################################################################
route__home: ->
params = new UrlParams recipeName:{type:'string'}, count:{type:'integer'}
if params.recipeName?
@deprecated__v1_root params
return
@_setPage 'home', new HomePageController _.extend {}, @_defaultOptions
route__browse: ->
@_setPage 'browse', new BrowsePageController _.extend {}, @_defaultOptions
route__browseMod: (modSlug)->
controller = new ModPageController _.extend {}, @_defaultOptions
controller.model = @modPack.getMod modSlug
@_setPage 'browseMod', controller
route__browseModItem: (modSlug, itemSlug)->
slug = new ItemSlug modSlug, itemSlug
controller = new ItemPageController _.extend {itemSlug:slug}, @_defaultOptions
@_setPage 'browseModItem', controller
route__configure: ->
@_setPage 'configure', new ConfigurePageController _.extend {}, @_defaultOptions
route__craft: (text)->
controller = new CraftPageController _.extend {}, @_defaultOptions
controller.model.params = inventoryText:text
@_setPage 'craft', controller
# Deprecated Route Methods #####################################################################
deprecated__crafting: (text)->
controller = new CraftPageController _.extend {}, @_defaultOptions
controller.model.params = inventoryText:text
@_setPage 'crafting', controller
deprecated__item: (itemSlug)->
controller = new ItemPageController _.extend {itemSlug:ItemSlug.slugify(itemSlug)}, @_defaultOptions
@_setPage 'item', controller
deprecated__modItem: (modSlug, itemSlug)->
slug = new ItemSlug modSlug, itemSlug
controller = new ItemPageController _.extend {itemSlug:slug}, @_defaultOptions
@_setPage 'item', controller
deprecated__mod: (modSlug)->
controller = new ModPageController _.extend {}, @_defaultOptions
controller.model = @modPack.getMod modSlug
@_setPage 'mod', controller
deprecated__v1_root: (params)->
text = ''
if params.recipeName?
if params.count? and params.count > 1
text = "#{params.count}.#{_.slugify(params.recipeName)}"
else
text = _.slugify params.recipeName
@navigate Url.crafting(inventoryText:text), trigger:true
# Private Methods ##############################################################################
_recordPageView: ->
pathname = window.location.pathname
if global.env is 'production' and ga?
logger.info -> "Recording GA page view: #{pathname}"
ga 'send', 'pageview', pathname
else
logger.info -> "Suppressing GA page view: #{pathname}"
_setPage: (page, controller)->
return if @_controller is controller
logger.info -> "changing to page controller: #{controller.constructor.name}"
@headerController.model = page
showDuration = Duration.normal
show = =>
@_page = page
@_controller = controller
controller.onWillShow()
controller.render()
$pageContent = $('.page')
controller.$el.addClass 'page'
$pageContent.replaceWith controller.$el
controller.$el.slideDown showDuration, ->
controller.onDidShow()
if @_controller?
showDuration = showDuration / 2
@_controller.$el.slideUp showDuration, show
else
show()
+98
View File
@@ -0,0 +1,98 @@
###
Crafting Guide - logger.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class Logger
@TRACE = {name:'TRACE ', value:0}
@DEBUG = {name:'DEBUG ', value:1}
@VERBOSE = {name:'VERBOSE', value:2}
@INFO = {name:'INFO ', value:3}
@WARNING = {name:'WARNING', value:4}
@ERROR = {name:'ERROR ', value:5}
@FATAL = {name:'FATAL ', value:6}
ALL_LEVELS = [@TRACE, @DEBUG, @VERBOSE, @INFO, @WARNING, @ERROR, @FATAL]
constructor: (options={})->
options.level ?= Logger.FATAL
@formatText = options.format
@formatText ?= "<%= timestamp %> | <%= level %> | <%= indent %><%= message %>"
@level = @_parseLevel options
@_format = _.template @formatText
@_indent = ''
# Public Methods ###############################################################################
indent: ->
@_indent += ' '
log: (level, message)->
return unless level.value >= @level.value
message = message() if _.isFunction message
entry = {timestamp:new Date(), level:level, message:message, indent:@_indent}
entry.level ?= @level
lines = @_formatEntry entry
if entry.level.value < Logger.WARNING.value
console.log(line) for line in lines
else
console.error(line) for line in lines
outdent: ->
@_indent = @_indent[0...@_indent.length - 4]
# Log Methods ##################################################################################
trace: (message)-> @log Logger.TRACE, message
debug: (message)-> @log Logger.DEBUG, message
verbose: (message)-> @log Logger.VERBOSE, message
info: (message)-> @log Logger.INFO, message
warning: (message)-> @log Logger.WARNING, message
error: (message)->
message = "#{message.stack}" if message.stack?
@log Logger.ERROR, message
fatal: (message)-> @log Logger.FATAL, message
# Private Methods ##############################################################################
_formatEntry: (entry, lines=[])->
message = entry.message.replace /\\n/g, '\n'
for line in message.split '\n'
result = []
result.push @_format
timestamp: "#{entry.timestamp}"
level: entry.level.name
message: line
indent: entry.indent
lines.push result.join ''
return lines
_parseLevel: (options)->
return Logger.FATAL unless _(options).has 'level'
level = options.level
if not level?
candidates = []
else if _.isString level
candidates = (l for l in ALL_LEVELS when l.name.trim().toLowerCase() is level.trim().toLowerCase())
else if _.isNumber level
candidates = (l for l in ALL_LEVELS when l.value is level)
else if level?
candidates = (l for l in ALL_LEVELS when l is level)
throw new Error "invalid level: #{level}" unless candidates.length > 0
return candidates[0]
+45
View File
@@ -0,0 +1,45 @@
###
Crafting Guide - main.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
require './underscore_mixins'
require './polyfill'
views = require './views'
FeedbackController = require './controllers/feedback_controller'
Logger = require './logger'
CraftingGuideRouter = require './crafting_guide_router'
########################################################################################################################
if typeof(global) is 'undefined'
window.global = window
global = window.global
global.logger = new Logger
switch window.location.hostname
when 'localhost'
global.env = 'development'
logger.level = Logger.DEBUG
when 'new.crafting-guide.com'
global.env = 'staging'
logger.level = Logger.VERBOSE
when 'crafting-guide.com'
global.env = 'production'
logger.level = Logger.INFO
global.router = new CraftingGuideRouter
global.util = require 'util'
global.views = views
global.feedbackController = new FeedbackController el:'.view__feedback'
feedbackController.render()
global.router.loadDefaultModPack()
logger.info -> "CraftingGuide is ready"
Backbone.history.start pushState:true
+91
View File
@@ -0,0 +1,91 @@
###
Crafting Guide - base_model.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
{Event} = require '../constants'
{ModelState} = require '../constants'
########################################################################################################################
module.exports = class BaseModel extends Backbone.Model
constructor: (attributes={}, options={})->
options.logEvents ?= true
super attributes, options
makeGetter = (name)-> return -> @get name
makeSetter = (name)-> return (value)-> @set name, value
for name, value of attributes
continue if name is 'id'
Object.defineProperty this, name, get:makeGetter(name), set:makeSetter(name)
@logEvents = options.logEvents or false
@state = ModelState.unloaded
@loading = null
Object.defineProperties this,
isUnloaded: { get:-> @state is ModelState.unloaded }
isLoading: { get:-> @state is ModelState.loading }
isLoaded: { get:-> @state is ModelState.loaded }
isError: { get:-> @state is ModelState.error }
# Event Methods ################################################################################
onLoadSucceeded: (text, status, xhr)->
try
@set @parse text
@state = ModelState.loaded
@trigger Event.change, this
@trigger Event.sync, this
logger.info => "#{@constructor.name}.#{@cid} loaded successfully"
catch e
logger.error -> "A parsing error occured: #{e.stack}"
@onLoadFailed e.message, 'parsing failed', xhr
onLoadFailed: (error, status, xhr)->
@state = ModelState.error
logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}"
@trigger Event.error, this, error
# Backbone.Model Overrides #####################################################################
fetch: (options={})->
options.force ?= false
return if (@isLoading or @isLoaded) and not options.force
url = @url()
logger.info => "#{@constructor.name}.#{@cid} reading from url: #{url}"
@state = ModelState.loading
@trigger Event.request, this
@loading = w.promise (resolve, reject)=>
$.ajax
url: url
dataType: 'text'
success: (text, status, xhr)=> resolve @onLoadSucceeded text, status, xhr
error: (xhr, status, error)=> reject @onLoadFailed error, status, xhr
@loading.catch -> # do nothing. prevents unhandled promise warnings
return @loading
parse: (text)->
return JSON.parse text
sync: (method, model)->
throw new Error "#{@constructor.name}.#{@cid} is not permitted to #{method}"
trigger: (name, model, args...)->
if @logEvents
argText = ("#{arg}"[0..50] for arg in args).join ", "
logger.trace => "#{@constructor.name}.#{@cid} triggered event #{name} with args: #{argText}"
super
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name}.#{@cid}"
+47
View File
@@ -0,0 +1,47 @@
###
Crafting Guide - craft_page.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
CraftingPlan = require './crafting_plan'
CraftingTable = require './crafting_table'
{Event} = require '../constants'
Inventory = require './inventory'
ModPack = require './mod_pack'
########################################################################################################################
module.exports = class CraftPage extends BaseModel
constructor: (attributes={}, options={})->
attributes.modPack ?= new ModPack
attributes.params ?= null
attributes.plan ?= new CraftingPlan modPack:attributes.modPack
attributes.table ?= new CraftingTable plan:attributes.plan
super attributes, options
@modPack.on Event.change, => @_consumeParams()
@on Event.change + ':params', => @_consumeParams()
# Private Methods ##############################################################################
_consumeParams: ->
return unless @params?
@plan.want.clear()
if not @params.inventoryText?
@params = null
else
inventory = new Inventory
inventory.parse @params.inventoryText
inventory.each (stack)=>
item = @modPack.findItem stack.itemSlug, enableAsNeeded:true
return unless item? and item.isCraftable
@plan.want.add stack.itemSlug, stack.quantity
inventory.remove stack.itemSlug
if inventory.isEmpty then @params = null
+204
View File
@@ -0,0 +1,204 @@
###
Crafting Guide - crafting_plan.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Inventory = require './inventory'
ItemSlug = require './item_slug'
########################################################################################################################
module.exports = class CraftingPlan extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.modPack then throw new Error 'modPack is required'
attributes.includingTools ?= false
super attributes, options
@have = new Inventory modPack:@modPack
@want = new Inventory modPack:@modPack
@need = new Inventory modPack:@modPack
@result = new Inventory modPack:@modPack
recraft = _.debounce (=> @craft()), 100
for inventory in [@have, @want]
inventory.on 'change', recraft
@on Event.change + ':includingTools', recraft
@clear()
# Public Methods ###############################################################################
clear: (options={})->
@steps = []
@need.clear()
@result.clear()
@trigger 'change', this
return this
craft: ->
toolsMessage = if @includingTools then ' (including tools)' else ''
haveMessage = if @have.isEmpty then '' else " starting with #{@have.unparse()}"
logger.info => "crafting #{@want.unparse()}#{toolsMessage}#{haveMessage}"
@clear()
@have.localize()
@want.localize()
@result.addInventory @have
@steps = {}
@want.each (stack)=>
@_findSteps stack.itemSlug, {}, ignoreGatherable:true
item = @modPack.findItem stack.itemSlug
@need.add item.slug, stack.quantity
@steps = (step for recipeSlug, step of @steps)
@_resolveNeeds()
@_removeExtraSteps()
@result.addInventory @want
@need.trigger 'change', @need
@result.trigger 'change', @result
@trigger 'change', this
removeUncraftableItems: ->
toRemove = []
@want.each (stack)=>
item = @modPack.findItem stack.itemSlug
if not item? then toRemove.push stack.itemSlug
for itemSlug in toRemove
@want.remove itemSlug
# Event Methods ################################################################################
onIncludingToolsChanged: ->
@storage.setItem 'includingTools', "#{@includingTools}"
@craft()
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name} {
have:#{@have},
want:#{@want},
need:#{@need},
result:#{@result},
steps:#{@steps}
}"
# Private Methods ##############################################################################
_chooseRecipe: (item)->
recipes = @modPack.findRecipes item.slug
return null unless recipes? and recipes.length > 0
return recipes[0]
_findSteps: (itemSlug, parentSteps={})->
item = @modPack.findItem itemSlug
return unless item?
return unless item.isCraftable
ignoreGatherable = @want.hasAtLeast itemSlug, 1
if (not item.isGatherable) or ignoreGatherable
recipes = @modPack.findRecipes item.slug
recipes ?= []
if parentSteps[item.slug]?
logger.verbose -> "found cycle at #{item.slug}"
throw new Error 'invalid recipe path'
parentSteps[item.slug] = item
logger.verbose -> "exploring: #{item.slug}"
logger.indent()
currentSteps = _.clone @steps
foundValidRecipe = false
for i in [0...recipes.length] by 1
recipe = recipes[i]
logger.verbose -> "trying recipe #{i+1} of #{recipes.length}: #{recipe.slug}"
if @steps[recipe.slug]?
logger.verbose -> "already accepted this recipe"
foundValidRecipe = true
break
try
if @includingTools
for toolStack in recipe.tools
if not @_hasStep toolStack.itemSlug
@_findSteps toolStack.itemSlug, parentSteps
for inputStack in recipe.input
@_findSteps inputStack.itemSlug, parentSteps
logger.verbose -> "adding step for: #{recipe.slug}"
@steps[recipe.slug] = recipe:recipe, itemSlug:item.slug
foundValidRecipe = true
break
catch error
logger.verbose -> "recipe didn't work out: #{recipe.slug}"
if error.message isnt 'invalid recipe path' then throw error
@steps = _.clone currentSteps
delete parentSteps[item.slug]
logger.outdent()
if not (foundValidRecipe or item.isGatherable)
logger.verbose -> "could not find a valid recipe for #{item.slug}"
throw new Error 'invalid recipe path'
_hasStep: (itemSlug)->
for recipeSlug, step of @steps
return true if step.recipe.produces itemSlug
return false
_qualifyItemSlug: (itemSlug)->
item = @modPack.findItem itemSlug
return item.slug if item?
return itemSlug
_removeExtraSteps: ->
result = (step for step in @steps when step.multiplier > 0)
@steps = result
_resolveNeeds: ->
for i in [@steps.length-1..0] by -1
step = @steps[i]
recipe = step.recipe
step.multiplier = Math.ceil(@need.quantityOf(step.itemSlug) / recipe.output[0].quantity)
if @includingTools
for stack in recipe.tools
itemSlug = @_qualifyItemSlug stack.itemSlug
available = @result.quantityOf(itemSlug) + @need.quantityOf(itemSlug)
needed = Math.max 0, stack.quantity - available
@need.add itemSlug, needed
@result.add itemSlug, needed
for stack in recipe.input
itemSlug = @_qualifyItemSlug stack.itemSlug
needed = step.multiplier * stack.quantity
consumed = Math.min needed, @result.quantityOf itemSlug
remaining = needed - consumed
@result.remove itemSlug, consumed
@need.add itemSlug, remaining
for stack in recipe.output
itemSlug = @_qualifyItemSlug stack.itemSlug
created = stack.quantity * step.multiplier
consumed = Math.min created, @need.quantityOf itemSlug
remaining = created - consumed
@result.add itemSlug, remaining
@need.remove itemSlug, consumed
+72
View File
@@ -0,0 +1,72 @@
###
Crafting Guide - crafting_table.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
########################################################################################################################
module.exports = class CraftingTable extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.plan? then throw new Error "attributes.plan is required"
super attributes, options
@plan.on 'change', => @reset()
@_stepIndex = 0
Object.defineProperties this, {
currentStep: { get:@getCurrentStep }
hasNextStep: { get:@hasNextStep }
hasPrevStep: { get:@hasPrevStep }
hasSteps: { get:@hasSteps }
stepCount: { get:@getStepCount }
stepIndex: { get:@getStepIndex, set:@setStepIndex }
}
# Public Methods ###############################################################################
reset: ->
@stepIndex = 0
return this
# Property Methods #############################################################################
hasNextStep: ->
return @_stepIndex + 1 < @plan.steps.length
hasPrevStep: ->
return @_stepIndex > 0
hasSteps: ->
return @plan.steps.length > 0
getCurrentStep: ->
return @plan.steps[@_stepIndex]
getStepIndex: ->
return @_stepIndex
setStepIndex: (newStepIndex)->
oldStepIndex = @_stepIndex
newStepIndex = Math.max 0, Math.min @plan.steps.length - 1, newStepIndex
@_stepIndex = newStepIndex
@trigger Event.change + ':stepIndex', this, oldStepIndex, newStepIndex
@trigger Event.change, this
return this
getStepCount: ->
return 0 unless @plan.steps?
return @plan.steps.length
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name} (#{@cid}) { plan:#{@plan}, step:#{@_stepIndex} }"
+57
View File
@@ -0,0 +1,57 @@
###
Crafting Guide - email_client.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class EmailClient
constructor: ->
@baseUrl = 'https://mandrillapp.com:443/api/1.0'
@key = 'zaERWIuTVJaq0seCjjgVqw'
# Public Methods ###############################################################################
send: (options={})->
options.body ?= "(no body)"
options.fromAddress ?= "[email protected]"
options.fromName ?= "Crafting Guide Website"
options.subject ?= "(no subject)"
options.toAddress ?= "[email protected]"
options.toName ?= "Crafting Guide"
body =
key: @key
message:
from_email: options.fromAddress
from_name: options.fromName
subject: options.subject
text: options.body
to: [ email:options.toAddress, name:options.toName, type:'to' ]
logger.info -> "sending email: #{util.inspect(body)}"
w.promise (resolve, reject)=>
onSuccess = (data, status, request)->
logger.info -> "sending email result: #{util.inspect(data)}, status:#{status}"
data = if _.isArray data then data[0] else data
if data.status isnt "sent"
reject status:data.status, message:data.reject_reason
else
resolve status:data.status
onError = (request, status, error)->
logger.error -> "sending email failed: #{status}, error:#{error}"
reject status:status, message:error
$.ajax "#{@baseUrl}/messages/send.json",
cache: false
data: body
dataType: 'json'
error: onError
success: onSuccess
type: 'POST'
+196
View File
@@ -0,0 +1,196 @@
###
Crafting Guide - inventory.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
ItemSlug = require './item_slug'
{RequiredMods} = require '../constants'
Stack = require './stack'
########################################################################################################################
module.exports = class Inventory extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
attributes.modPack ?= null
@clear()
Object.defineProperties this,
isEmpty: { get:-> @_itemSlugs.length is 0 }
# Class Methods ################################################################################
@Delimiters =
Item: '.'
Stack: ':'
# Public Methods ###############################################################################
add: (itemSlug, quantity=1)->
@_add itemSlug, quantity
@trigger Event.add, this, itemSlug, quantity
@trigger Event.change, this
return this
addInventory: (inventory)->
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
@trigger Event.change, this
return this
clear: (options={})->
@_stacks = {}
@_itemSlugs = []
@trigger Event.change, this
clone: ->
inventory = new Inventory
inventory.addInventory this
return inventory
each: (callback)->
for itemSlug in @_itemSlugs
callback @_stacks[itemSlug]
getSlugs: ->
return @_itemSlugs[..]
hasAtLeast: (itemSlug, quantity=1)->
if quantity is 0 then return true
stack = @_stacks[itemSlug]
return false unless stack?
return stack.quantity >= quantity
localize: ->
if not @modPack? then throw new Error 'localize requires @modPack'
newSlugs = []
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
qualifiedSlug = @modPack.findItem(itemSlug)?.slug
if qualifiedSlug?
delete @_stacks[itemSlug]
newSlugs.push qualifiedSlug
@_stacks[qualifiedSlug] = stack
stack.itemSlug = qualifiedSlug
else
newSlugs.push itemSlug
@_itemSlugs = newSlugs
@_sort()
pop: ->
itemSlug = @_itemSlugs.pop()
return null unless itemSlug?
stack = @_stacks[itemSlug]
delete @_stacks[itemSlug]
@trigger Event.remove, this, stack.itemSlug, stack.quantity
@trigger Event.change, this
return stack
quantityOf: (itemSlug)->
stack = @_stacks[itemSlug]
return 0 unless stack?
return stack.quantity
remove: (itemSlug, quantity=null)->
return if quantity is 0
stack = @_stacks[itemSlug]
if not stack? then throw new Error "cannot remove #{itemSlug} since it is not in this inventory"
quantity ?= stack.quantity
if stack.quantity < quantity
throw new Error "cannot remove #{quantity}: only #{stack.quantity} #{itemSlug} in this inventory"
stack.quantity -= quantity
if stack.quantity is 0
@stopListening stack
delete @_stacks[itemSlug]
@_itemSlugs = _(@_itemSlugs).without itemSlug
@trigger Event.remove, this, itemSlug, quantity
@trigger Event.change, this
return this
# Parsing Methods ##############################################################################
parse: (data)->
return this if not data? or data.length is 0
stacks = data.split Inventory.Delimiters.Stack
for stackText in stacks
stackParts = stackText.split Inventory.Delimiters.Item
if stackParts.length is 2
quantity = parseInt stackParts[0], 10
itemSlug = ItemSlug.slugify stackParts[1]
else if stackParts.length is 1
quantity = 1
itemSlug = ItemSlug.slugify stackParts[0]
else
throw new Error "expected #{stackText} to have 0 or 1 parts"
if itemSlug.qualified.length > 0
@add itemSlug, quantity
return this
unparse: (options={})->
parts = []
@each (stack)=>
slugText = stack.itemSlug.item
if @modPack?
item = @modPack.findItem ItemSlug.slugify slugText
if item? and item.slug.qualified isnt stack.itemSlug.qualified
slugText = item.slug.qualified
if stack.quantity is 1
parts.push slugText
else
parts.push "#{stack.quantity}#{Inventory.Delimiters.Item}#{slugText}"
return parts.join Inventory.Delimiters.Stack
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") { items: ["]
needsDelimiter = false
@each (stack)->
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
result.push '}'
return result.join ''
# Private Methods ##############################################################################
_add: (itemSlug, quantity=1)->
return unless itemSlug?
return if quantity is 0
stack = @_stacks[itemSlug]
if not stack?
stack = new Stack itemSlug:itemSlug, quantity:quantity
@listenTo stack, Event.change, => @trigger Event.change, stack
@_stacks[itemSlug] = stack
@_itemSlugs.push itemSlug
@_sort()
else
stack.quantity += quantity
_sort: ->
@_itemSlugs.sort (a, b)-> ItemSlug.compare a, b
+73
View File
@@ -0,0 +1,73 @@
###
Crafting Guide - item.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
ItemSlug = require './item_slug'
Recipe = require './recipe'
StringBuilder = require './string_builder'
########################################################################################################################
module.exports = class Item extends BaseModel
@Group = Other:'Other'
constructor: (attributes={}, options={})->
if not attributes.name? then throw new Error 'attributes.name is required'
attributes.description ?= null
attributes.group ?= Item.Group.Other
attributes.isGatherable ?= false
attributes.modVersion ?= null
attributes.officialUrl ?= null
attributes.slug ?= ItemSlug.slugify attributes.name
attributes.videos ?= []
options.logEvents ?= false
super attributes, options
@on Event.change + ':modVersion', =>
@_isCraftable = null
@slug.mod = @modVersion?.modSlug
# Public Methods ###############################################################################
compareTo: (that)->
if this.slug isnt that.slug
return if this.slug < that.slug then -1 else +1
if this.name isnt that.name
return if this.name < that.name then -1 else +1
return 0
# Property Methods #############################################################################
getIsCraftable: ->
if not @_isCraftable?
@_isCraftable = false
if @modVersion?
@_isCraftable = @modVersion.hasRecipes @slug
return @_isCraftable
Object.defineProperties @prototype,
isCraftable: {get:@prototype.getIsCraftable}
# Object Overrides #############################################################################
toString: ->
builder = new StringBuilder
return builder
.push @constructor.name, ' (', @cid, ') { '
.push 'name:"', @name, '", '
.push 'isCraftable:', @isCraftable, ', '
.push 'isGatherable:', @isGatherable, ', '
.onlyIf (@group isnt Item.Group.Other), (b)=>
b.push 'group:"', @group, '", '
.push 'slug:"', @slug, '", '
.push '}'
.toString()
+57
View File
@@ -0,0 +1,57 @@
###
Crafting Guide - item_page.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
CraftingPlan = require './crafting_plan'
{Event} = require '../constants'
########################################################################################################################
module.exports = class ItemPage extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.modPack? then throw new Error 'attributes.modPack is required'
attributes.item ?= null
super attributes, options
# Property Methods #############################################################################
findComponentInItems: ->
return @_findRecipesMatching (recipe)=> recipe.requires @item.slug
findSimilarItems: ->
return null unless @item?.modVersion?
result = []
@item.modVersion.eachItemInGroup @item.group, (item)=>
result.push item
return null unless result.length > 0
return result
findRecipes: ->
return @modPack.findRecipes @item?.slug, [], alwaysFromOwningMod:true
findToolForRecipes: ->
return @_findRecipesMatching (recipe)=> recipe.requiresTool @item.slug
# Private Methods ##############################################################################
_findRecipesMatching: (callback)->
return null unless @item?
result = {}
@modPack.eachMod (mod)=>
mod.eachRecipe (recipe)=>
if callback(recipe)
outputItem = @modPack.findItem recipe.itemSlug, includeDisabled:true
result[outputItem.slug] = outputItem
result = _.values result
return null unless result.length > 0
return result.sort (a, b)-> a.compareTo b
+109
View File
@@ -0,0 +1,109 @@
###
Crafting Guide - item_slug.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
{RequiredMods} = require '../constants'
########################################################################################################################
module.exports = class ItemSlug
constructor: ->
@_item = @_mod = null
if arguments.length is 1
@item = arguments[0]
else if arguments.length is 2
@_mod = arguments[0]
@item = arguments[1]
else
throw new Error 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
# Class Methods ################################################################################
@compare: (a, b)->
aIsRequired = a.mod in RequiredMods
bIsRequired = b.mod in RequiredMods
if aIsRequired isnt bIsRequired
return -1 if aIsRequired
return +1 if bIsRequired
else if a.isQualified isnt b.isQualified
return -1 if a.isQualified
return +1 if b.isQualified
else if a.mod isnt b.mod
return if a.mod < b.mod then -1 else +1
else if a.item isnt b.item
return if a.item < b.item then -1 else +1
return 0
@equal: (a, b)->
return false unless a.mod is b.mod
return false unless a.item is b.item
return true
@slugify: (arg)->
return arg if arg?.constructor?.name is 'ItemSlug'
[modSlug, itemSlug] = _.decomposeSlug arg
itemSlug = _.slugify itemSlug
if modSlug?
return new ItemSlug modSlug, itemSlug
else
return new ItemSlug itemSlug
# Public Methods ###############################################################################
compareTo: (that)->
return ItemSlug.compare this, that
matches: (slug, options={exact:false})->
return false unless typeof(slug.matches) is 'function'
if slug.isQualified and this.isQualified
return slug.qualified is this.qualified
else
return slug.item is this.item
# Property Methods #############################################################################
getIsQualified: ->
return @_mod?
getItem: ->
return @_item
setItem: (item)->
if not item? then throw new Error 'item is required'
@_item = item
@mod = @mod # reset @_qualified
getMod: ->
return @_mod
setMod: (mod)->
@_mod = mod
@_qualified = if @_mod? then _.composeSlugs(@_mod, @_item) else @_item
getQualified: ->
return @_qualified
Object.defineProperties @prototype,
isQualified: { get:@prototype.getIsQualified }
mod: { get:@prototype.getMod, set:@prototype.setMod }
item: { get:@prototype.getItem, set:@prototype.setItem }
qualified: { get:@prototype.getQualified }
# Object Overrides #############################################################################
toString: ->
return @_qualified
valueOf: ->
return @_qualified.valueOf()
+186
View File
@@ -0,0 +1,186 @@
###
Crafting Guide - mod.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
{RequiredMods} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class Mod extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.slug? then throw new Error 'attributes.slug is required'
attributes.author ?= ''
attributes.description ?= ''
attributes.documentationUrl ?= null
attributes.downloadUrl ?= null
attributes.homePageUrl ?= null
attributes.modPack ?= null
attributes.name ?= ''
super attributes, options
@_activeModVersion = null
@_activeVersion = null
@_modVersions = []
Object.defineProperties this,
'activeModVersion': { get:-> @_activeModVersion }
'activeVersion': { get:@getActiveVersion, set:@setActiveVersion }
'enabled': { get:-> @_activeModVersion? }
# Class Methods ##################################################################################
@Version =
None: 'none'
Latest: 'latest'
# Public Methods #################################################################################
compareTo: (that)->
thisRequired = this.slug in RequiredMods
thatRequired = that.slug in RequiredMods
if thisRequired isnt thatRequired
return -1 if thisRequired
return +1 if thatRequired
else
if this.name isnt that.name
return if this.name < that.name then -1 else +1
return 0
# ModVersion Proxy Methods #####################################################################
eachItem: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachItem callback
eachName: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachName callback
eachRecipe: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachRecipe callback
findItem: (slug, options={})->
options.includeDisabled ?= false
options.enableAsNeeded ?= false
if not options.includeDisabled
return unless @_activeModVersion?
return @_activeModVersion.findItem slug
else
for modVersion in @_modVersions
modVersion.fetch()
item = modVersion.findItem slug
if item?
if options.enableAsNeeded then @setActiveVersion modVersion.version
return item
return null
findItemByName: (name)->
return unless @_activeModVersion?
@_activeModVersion.findItemByName name
findName: (itemSlug)->
return unless @_activeModVersion?
@_activeModVersion.findName itemSlug
findRecipes: (itemSlug, result=[], options={})->
options.alwaysFromOwningMod ?= false
if @_activeModVersion?
return @_activeModVersion.findRecipes itemSlug, result, options
else if options.alwaysFromOwningMod and itemSlug.mod is @slug
return @getModVersion(Mod.Version.Latest).findRecipes itemSlug, result, options
return null
# Property Methods #############################################################################
addModVersion: (modVersion)->
return unless modVersion?
return if @_modVersions.indexOf(modVersion) isnt -1
@_modVersions.push modVersion
@listenTo modVersion, Event.change, => @trigger Event.change, this
modVersion.mod = this
@trigger Event.add + ':modVersion', modVersion, this
@trigger Event.change + ':version', modVersion, this
@trigger Event.change, this
if not @activeVersion? then @activeVersion = modVersion.version
if modVersion.version is @_activeVersion then @_activateModVersion modVersion
return this
eachModVersion: (callback)->
for modVersion in @_modVersions
callback modVersion
getModVersion: (version)->
return null if version is Mod.Version.None
return @_modVersions[0] if version is Mod.Version.Latest
for modVersion in @_modVersions
return modVersion if modVersion.version is version
return null
getActiveVersion: ->
return @_activeVersion
setActiveVersion: (version)->
return if version is @_activeVersion
version ?= Mod.Version.None
if version is Mod.Version.Latest then version = _.last(@_modVersions).version
if version is Mod.Version.None
@_activeVersion = version
@_activateModVersion null
@trigger Event.change + ':activeVersion', this, @_activeVersion
@trigger Event.change, this
else
for modVersion in @_modVersions
if version is modVersion.version
@_activateModVersion modVersion
break
@_activeVersion = version
@trigger Event.change + ':activeVersion', this, @_activeVersion
@trigger Event.change, this
# Backbone.View Overrides ######################################################################
parse: (text)->
ModParser = require './mod_parser' # to avoid require cycles
@_parser ?= new ModParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return Url.modData modSlug:@slug
# Private Methods ##############################################################################
_activateModVersion: (modVersion)->
if @_activeModVersion? then @stopListening @_activeModVersion
@_activeModVersion = modVersion
@trigger Event.change + ':activeModVersion', this, @_activeModVersion
logger.verbose => "#{@slug} switched to version #{@_activeVersion}"
if @_activeModVersion?
@listenTo @_activeModVersion, 'all', -> @trigger.apply this, arguments
+131
View File
@@ -0,0 +1,131 @@
###
Crafting Guide - mod_pack.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{DefaultModVersions} = require '../constants'
{Event} = require '../constants'
ModVersionParser = require './mod_version_parser'
Recipe = require './recipe'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ModPack extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
@_mods = []
# Public Methods ###############################################################################
findItem: (itemSlug, options={})->
options.includeDisabled ?= false
if itemSlug.isQualified
mod = @getMod itemSlug.mod
if mod?
item = mod.findItem itemSlug, options
return item if item?
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
item = mod.findItem itemSlug, options
return item if item?
return null
findItemByName: (name, options={})->
options.enableAsNeeded ?= false
options.includeDisabled = true if options.enableAsNeeded
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
item = mod.findItemByName name, options
return item if item?
return null
findItemDisplay: (itemSlug)->
if not itemSlug? then throw new Error 'itemSlug is required'
result = {}
item = @findItem itemSlug, includeDisabled:true
if item?
result.modSlug = item.slug.mod
result.modVersion = item.modVersion.version
result.itemSlug = item.slug.item
result.itemName = item.name
else
result.modSlug = @_mods[0].slug
result.modVersion = @_mods[0].activeVersion
result.itemSlug = itemSlug.item
result.itemName = @findName itemSlug, includeDisabled:true
result.craftingUrl = Url.crafting inventoryText:itemSlug.item
result.iconUrl = Url.itemIcon result
result.itemUrl = Url.item result
return result
findName: (slug, options={})->
options.includeDisabled ?= false
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
name = mod.findName slug
return name if name
return null
findRecipes: (itemSlug, result=[], options={})->
options.alwaysFromOwningMod ?= false
return null unless itemSlug?
for mod in @_mods
if not mod.enabled
owningMod = itemSlug.isQualified and (itemSlug.mod is mod.slug)
continue unless owningMod and options.alwaysFromOwningMod
mod.findRecipes itemSlug, result, options
result.sort (a, b)-> Recipe.compareFor a, b, itemSlug
return if result.length > 0 then result else null
# Property Methods #############################################################################
addMod: (mod)->
if not mod? then throw new Error 'mod is required'
return if @_mods.indexOf(mod) isnt -1
mod.modPack = this
@_mods.push mod
@listenTo mod, Event.change, => @trigger Event.change, this
@trigger Event.add + ':mod', mod, this
@_mods.sort (a, b)-> a.compareTo b
@trigger Event.sort + ':mod', this
@trigger Event.change, this
return this
eachMod: (callback)->
for mod in @_mods
callback mod
getMod: (slug)->
for mod in @_mods
return mod if mod.slug is slug
return null
getMods: ->
return @_mods[..]
# Object Overrides #############################################################################
toString: ->
return "ModPack (#{@cid}) {modVersions:«#{@_mods.length} items»}"
+19
View File
@@ -0,0 +1,19 @@
###
Crafting Guide - mod_parser.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
VersionedParserBase = require './versioned_parser_base'
ModParserV1 = require './parser_versions/mod_parser_v1'
########################################################################################################################
module.exports = class ModParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ModParserV1 options
+181
View File
@@ -0,0 +1,181 @@
###
Crafting Guide - mod_version.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Item = require './item'
ItemSlug = require './item_slug'
Recipe = require './recipe'
{RequiredMods} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ModVersion extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.modSlug? then throw new Error 'attributes.modSlug is required'
if not attributes.version? then throw new Error 'attributes.version is required'
attributes.mod ?= null
super attributes, options
@_groups = {}
@_items = {}
@_names = {}
@_recipes = {}
@_slugs = []
# Public Methods ###############################################################################
compareTo: (that)->
if this.mod? and that.mod?
return this.mod.compareTo that.mod
if this.modSlug isnt that.modSlug
return if this.modSlug < that.modSlug then -1 else +1
return 0
sort: ->
@_slugs.sort (a, b)-> ItemSlug.compare a, b
# Item Methods #################################################################################
addItem: (item)->
if @findItem(item.slug)? then throw new Error "duplicate item for #{item.name}"
item.modVersion = this
@_items[item.slug.item] = item
@_groups[item.group] ?= {}
@_groups[item.group][item.slug.item] = item
@registerName item.slug, item.name
return this
allItemsInGroup: (group)->
result = []
@eachItemInGroup group, (item)-> result.push item
return null if result.length is 0
return result
eachItem: (callback)->
for slug in @_slugs
item = @findItem slug
continue unless item?
callback item
return this
eachItemInGroup: (group, callback)->
itemMap = @_groups[group]
return unless itemMap?
items = _.values(itemMap).sort (a, b)-> a.compareTo b
for item in items
callback item
findItem: (itemSlug)->
return @_items[itemSlug.item]
findItemByName: (name)->
for itemSlug, item of @_items
return item if item.name is name
return null
# Group Methods ################################################################################
eachGroup: (callback)->
groupNames = _.keys @_groups
groupNames.sort (a, b)->
if a is b then return 0
if a is Item.Group.Other then return -1
if b is Item.Group.Other then return +1
return if a < b then -1 else +1
for groupName in groupNames
callback groupName
# Name Methods #################################################################################
eachName: (callback)->
for slug in @_slugs
callback @_names[slug.item], slug
return this
findName: (itemSlug)->
return @_names[itemSlug.item]
registerName: (itemSlug, name)->
return if @_names[itemSlug.item]
@_names[itemSlug.item] = name
@_slugs.push itemSlug
return this
# Recipe Methods ###############################################################################
addRecipe: (recipe)->
return unless recipe?
recipe.modVersion = this
if @_recipes[recipe.slug]? then throw new Error "duplicate recipe: #{recipe.slug}"
@_recipes[recipe.slug] = recipe
return this
eachRecipe: (callback)->
recipes = _.values(@_recipes).sort (a, b)-> Recipe.compareFor a, b
for recipe in recipes
callback recipe
return this
findRecipes: (itemSlug, result=[], options={})->
options.onlyPrimary ?= false
for recipe in _.values @_recipes
if options.onlyPrimary
if recipe.itemSlug.matches itemSlug
result.push recipe
else
if recipe.produces itemSlug
result.push recipe
return result
findExternalRecipes: ->
result = {}
for k, recipe of @_recipes
continue if recipe.itemSlug.isQualified
recipeList = result[recipe.itemSlug]
if not recipeList then recipeList = result[recipe.itemSlug] = []
recipeList.push recipe
return result
hasRecipes: (itemSlug)->
for k, recipe of @_recipes
return true if recipe.produces itemSlug
return false
# Backbone.Model Overrides #####################################################################
parse: (text)->
ModVersionParser = require './mod_version_parser' # to avoid require cycles
@_parser ?= new ModVersionParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return Url.modVersion modSlug:@modSlug, modVersion:@version
# Object Overrides #############################################################################
toString: ->
return "ModVersion (#{@cid}) {
modSlug:#{@modSlug}, version:#{@version}, items:«#{@_slugs.length} items»
}"
@@ -0,0 +1,19 @@
###
Crafting Guide - mod_version_parser.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
VersionedParserBase = require './versioned_parser_base'
ModVersionParserV1 = require './parser_versions/mod_version_parser_v1'
########################################################################################################################
module.exports = class ModVersionParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ModVersionParserV1 options
+68
View File
@@ -0,0 +1,68 @@
###
Crafting Guide - name_finder.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class NameFinder
constructor: (modPack, options={})->
if not modPack? then throw new Error 'modPack is required'
options.includeGatherable ?= false
options.includeDisabledMods ?= false
options.limit ?= 25
@includeDisabledMods = options.includeDisabledMods
@includeGatherable = options.includeGatherable
@limit = options.limit
@modPack = modPack
@names = []
@_nameMap = {}
# Public Methods ###############################################################################
search: (nameHint='')->
nameHint = null if nameHint.trim() is ''
nameHint = nameHint.toLowerCase() if nameHint?
names = @_findNames nameHint
names.sort (a, b)->
c = a.mod.compareTo b.mod
if c isnt 0 then return c
return 0 if a.label is b.label
return if a.label < b.label then -1 else +1
names = names[0...@limit]
return names
# Private Methods ##############################################################################
_findNames: (nameHint=null)->
names = []
nameMap = {}
hintRegex = new RegExp(nameHint, 'i') if nameHint?
@modPack.eachMod (mod)=>
return unless mod.enabled or @includeDisabledMods
mod.eachName (name, itemSlug)=>
return if nameMap[name]?
if not @includeGatherable
item = mod.findItem itemSlug
return unless item? and item.isCraftable
scanName = "#{mod.name} : #{name}"
if hintRegex?
return unless hintRegex.test scanName
nameMap[name] = name
names.push value:name, label:scanName, mod:mod
return names
@@ -0,0 +1,130 @@
###
Crafting Guide - command_parser_version_base.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
StringBuilder = require '../../models/string_builder'
########################################################################################################################
module.exports = class CommandParserVersionBase
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.showAllErrors ?= false
@_model = options.model
@_showAllErrors = options.showAllErrors
# Class Methods ################################################################################
@COMMAND = /\ *([^:]*):?(.*)/
@COMMENT = /([^\\]?)#.*/
@simplify: (text)->
text = text.trim()
text = text.replace /\ */g , ' '
text = text.replace /\n/g, ';'
text = text.replace /; */g, ';'
text = text.replace /;;*/g, ';'
text = text.replace /: /g, ':'
return text
# Public Methods ###############################################################################
parse: (text)->
@_rawData = {}
@_lineNumber = 1
@_lines = text.split '\n'
@_lineNumber = 0
while @_lineNumber < @_lines.length
@_lineNumber += 1
commands = @_parseLine @_lines[@_lineNumber - 1]
for command in commands
@_handleErrors @_execute, command
@_handleErrors @_buildModel, @_rawData, @_model
return @_model
unparse: ->
builder = new StringBuilder context:@_model
@_unparseModel builder, @_model
return builder.toString()
# Subclass Methods #############################################################################
_buildModel: (rawData, model)->
throw new Error 'Subclasses must override this method'
_unparseModel: (builder, model)->
throw new Error 'Subclasses must override this method'
_command_schema: -> # do nothing
# Private Methods ##############################################################################
_execute: (command)->
method = this["_command_#{command.name}"]
if not method? then throw new Error "Unknown command: #{command.name}"
@_handleErrors method, command.args
_parseLine: (line)->
line = line.replace CommandParserVersionBase.COMMENT, '$1'
line = line.trim()
return [] if line.length is 0
hereDoc = @_parseHereDoc line
lineParts = (part.trim() for part in line.split(';'))
commands = []
for linePart in lineParts
continue if linePart.length is 0
match = CommandParserVersionBase.COMMAND.exec linePart
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
args = []
args = (s.trim() for s in match[2].split(',')) if match[2]?
args.push hereDoc if hereDoc
commands.push name:match[1], args:args
return commands
_handleErrors: (callback, args...)->
if args.length is 1 and _.isArray(args[0]) then args = args[0]
try
callback.apply this, args
catch e
e.message = "line #{@_lineNumber}: #{e.message}"
if not @_showAllErrors then throw e
logger.error -> e.message
_parseHereDoc: (line)->
hereDocIndex = line.indexOf '<<-'
return null unless hereDocIndex isnt -1
hereDocStopText = line[hereDocIndex+3...line.length]
hereDocLines = []
while true
@_lineNumber += 1
break if @_lineNumber >= @_lines.length
nextLine = @_lines[@_lineNumber-1]
break if nextLine.trim() is hereDocStopText
hereDocLines.push nextLine
shortestIndent = Number.MAX_VALUE
for line in hereDocLines
shortestIndent = Math.min line.match(/( *).*/)[1].length, shortestIndent
for i in [0...hereDocLines.length]
hereDocLines[i] = hereDocLines[i][shortestIndent..]
return null unless hereDocLines.length > 0
return hereDocLines.join '\n'
@@ -0,0 +1,45 @@
###
Crafting Guide - item_parser_v1.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
CommandParserVersionBase = require './command_parser_version_base'
########################################################################################################################
module.exports = class ItemParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildItem rawData, model
_unparseModel: (builder, model)->
@_unparseItem builder, model
# Command Methods ##############################################################################
_command_description: (textParts...)->
@_rawData.text ?= ''
@_rawData.text += textParts.join ', '
_command_officialUrl: (officialUrl)->
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
if officialUrl.length is 0 then throw new Error 'officialUrl cannot be empty'
@_rawData.officialUrl = officialUrl
_command_video: (youTubeId, name)->
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
if not name?.length then throw new Error 'video declaration requires a name'
@_rawData.videos ?= []
@_rawData.videos.push youTubeId:youTubeId, name:name
# Object Building Methods ######################################################################
_buildItem: (rawData, model)->
item.description = rawData.description if rawData.description
item.officialUrl = rawData.officialUrl if rawData.officialUrl
item.videos = rawData.videos if rawData.videos
@@ -0,0 +1,82 @@
###
Crafting Guide - mod_parser_v2.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
CommandParserVersionBase = require './command_parser_version_base'
Mod = require '../mod'
ModVersion = require '../mod_version'
########################################################################################################################
module.exports = class ModParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildMod rawData, model
_unparseModel: (builder, model)->
@_unparseMod builder, model
# Command Methods ##############################################################################
_command_author: (authorParts...)->
if @_rawData.author? then throw new Error 'duplicate declaration of "author"'
author = authorParts.join ''
if author.length is 0 then throw new Error '"author" cannot be empty, but may be omitted'
@_rawData.author = author
_command_description: (descriptionParts...)->
if @_rawData.description? then throw new Error 'duplicate declaration of "description"'
description = descriptionParts.join ', '
if description.length is 0 then throw new Error '"description" cannot be empty, but may be omitted'
@_rawData.description = description
_command_documentationUrl: (documentationUrl)->
if @_rawData.documentationUrl? then throw new Error 'duplicate declaration of "documentationUrl"'
if documentationUrl.length is 0 then throw new Error 'documentationUrl cannot be empty (omit it instead)'
@_rawData.documentationUrl = documentationUrl
_command_downloadUrl: (downloadUrl)->
if @_rawData.downloadUrl? then throw new Error 'duplicate declaration of "downloadUrl"'
if downloadUrl.length is 0 then throw new Error 'downloadUrl cannot be empty (omit it instead)'
@_rawData.downloadUrl = downloadUrl
_command_name: (name)->
if @_rawData.name? then throw new Error 'duplicate declaration of "name"'
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.name = name
_command_homePageUrl: (homePageUrl='')->
if @_rawData.homePageUrl? then throw new Error 'duplicate declaration of "homePageUrl"'
if homePageUrl.length is 0 then throw new Error 'homePageUrl cannot be empty'
@_rawData.homePageUrl = homePageUrl
_command_version: (version='')->
if version.length is 0 then throw new Error 'version cannot be empty'
@_rawData.versions ?= []
@_rawData.versions.push version
# Object Building Methods ######################################################################
_buildMod: (rawData, model)->
if not rawData.name? then throw new Error 'the "name" declaration is required'
if not rawData.homePageUrl? then throw new Error 'the "homePageUrl" declaration is required'
if not rawData.versions? then throw new Error 'at least one "version" declaration is required'
model.author = rawData.author if rawData.author?
model.description = rawData.description if rawData.description?
model.documentationUrl = rawData.documentationUrl if rawData.documentationUrl?
model.downloadUrl = rawData.downloadUrl if rawData.downloadUrl?
model.name = rawData.name
model.homePageUrl = rawData.homePageUrl
for version in rawData.versions
model.addModVersion new ModVersion modSlug:model.slug, version:version
@@ -0,0 +1,299 @@
###
Crafting Guide - mod_version_parser_v2.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
CommandParserVersionBase = require './command_parser_version_base'
Item = require '../item'
ItemSlug = require '../item_slug'
ModVersion = require '../mod_version'
Recipe = require '../recipe'
Stack = require '../simple_stack'
StringBuilder = require '../string_builder'
########################################################################################################################
module.exports = class ModVersionParserV1 extends CommandParserVersionBase
# Class Methods ################################################################################
@INTEGER = /[0-9]+/
@PATTERN = /^[0-9.]{3} ?[0-9.]{3} ?[0-9.]{3}$/
@STACK = /^([0-9]+) +(.*)$/
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildModVersion rawData, model
_unparseModel: (builder, model)->
@_unparseModVersion builder, model
# Command Methods ##############################################################################
_command_extras: (extraTerms...)->
if not @_recipeData? then throw new Error 'cannot declare "extras" before "recipe"'
if @_recipeData.extras? then throw new Error 'duplicate declaration of "extras"'
@_recipeData.extras = []
for term in extraTerms
match = ModVersionParserV1.STACK.exec term
if match?
@_recipeData.extras.push quantity:parseInt(match[1]), name:match[2]
else
@_recipeData.extras.push quantity:1, name:term
_command_gatherable: (gatherable)->
if not @_itemData? then throw new Error 'cannot declare "gatherable" before "item"'
if @_itemData.gatherable? then throw new Error 'duplicate declaration of "gatherable"'
if not (gatherable in ['yes', 'no']) then throw new Error 'gatherable must be either "yes" or "no"'
@_itemData.gatherable = (gatherable is 'yes')
_command_group: (group)->
if group.length is 0 then throw new Error 'a group name cannot be empty'
@_rawData.group = group
_command_item: (name='')->
if not name.length > 0 then throw new Error 'the item name cannot be empty'
@_itemData = name:name, line:@_lineNumber, group:@_rawData.group, type:'new'
@_rawData.items ?= {}
@_rawData.items[name] = @_itemData
@_recipeData = null
_command_input: (inputNames...)->
if not @_recipeData? then throw new Error 'cannot declare "input" before "recipe"'
if @_recipeData.input? then throw new Error 'duplicate declaration of "input"'
@_recipeData.input = []
for name in inputNames
if name.length is 0 then throw new Error 'input names cannot be empty'
@_recipeData.input.push name
_command_pattern: (pattern='')->
if not @_recipeData? then throw new Error 'cannot declare "pattern" before "recipe"'
if @_recipeData.pattern? then throw new Error 'duplicate declaration of "pattern"'
if not ModVersionParserV1.PATTERN.test pattern
throw new Error 'a pattern must have 9 digits using 0-9 for items and "." for an empty spot;
spaces are optional'
@_recipeData.pattern = pattern
_command_quantity: (quantity)->
if not @_recipeData? then throw new Error 'cannot declare "quantity" before "recipe"'
if @_recipeData.quantity? then throw new Error 'duplicate declaration of "quantity"'
if not ModVersionParserV1.INTEGER.test(quantity) then throw new Error 'quantity must be an integer'
@_recipeData.quantity = parseInt quantity
_command_recipe: ->
if not @_itemData? then throw new Error 'cannot delcare "recipe" before "item"'
@_recipeData = line:@_lineNumber
@_itemData.recipes ?= []
@_itemData.recipes.push @_recipeData
_command_tools: (toolNames...)->
if not @_recipeData? then throw new Error 'cannot declare "tools" before "recipe"'
if @_recipeData.tools? then throw new Error 'duplicate declaration of "tools"'
@_recipeData.tools = []
for name in toolNames
if name.length is 0 then throw new Error 'tool names cannot be empty'
@_recipeData.tools.push name
_command_update: (name='')->
if not name.length > 0 then throw new 'the item name cannot be empty'
@_itemData = name:name, line:@_lineNumber, type:'update'
@_recipeData = null
@_rawData.items ?= {}
@_rawData.items[name] = @_itemData
# Object Creation Methods ######################################################################
_buildModVersion: (modVersionData, modVersion)->
modVersionData.items ?= []
for itemName, itemData of modVersionData.items
@_handleErrors @_buildItem, modVersion, itemData
modVersion.sort()
return modVersion
_buildItem: (modVersion, itemData)->
@_lineNumber = itemData.line
itemData.gatherable ?= false
itemData.recipes ?= []
if itemData.type is 'new'
item = new Item name:itemData.name, isGatherable:itemData.gatherable, group:itemData.group
modVersion.addItem item
itemData.slug = item.slug
else
itemData.slug = ItemSlug.slugify itemData.name
modVersion.registerName itemData.slug, itemData.name
for recipeData in itemData.recipes
@_handleErrors @_buildRecipe, modVersion, itemData, recipeData
return item
_buildRecipe: (modVersion, itemData, recipeData)->
@_lineNumber = recipeData.line
if not recipeData.input? then throw new Error 'the "input" declaration is required'
if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required'
createSlug = (name)=>
item = @_rawData.items[name]
if item?
slug = new ItemSlug modVersion.modSlug, _.slugify name
else
slug = new ItemSlug _.slugify name
modVersion.registerName slug, name
return slug
recipeData.quantity ?= 1
recipeData.extras ?= []
recipeData.tools ?= []
inputStacks = []
for name in recipeData.input
inputSlug = createSlug name
inputStacks.push new Stack itemSlug:inputSlug, quantity:0
for c in recipeData.pattern
continue if c is '.'
continue if c is ' '
stack = inputStacks[parseInt(c)]
if not stack? then throw new Error "there is no input #{c} in this recipe"
stack.quantity += 1
for i in [0...inputStacks.length]
stack = inputStacks[i]
if stack.quantity is 0
name = modVersion.findName stack.itemSlug
throw new Error "#{name} is an input for this recipe, but it is not in the pattern"
outputStacks = [ new Stack itemSlug:itemData.slug, quantity:recipeData.quantity ]
for extraData in recipeData.extras
outputSlug = createSlug extraData.name
outputStacks.push new Stack itemSlug:outputSlug, quantity:extraData.quantity
toolStacks = []
for name in recipeData.tools
toolSlug = createSlug name
toolStacks.push new Stack itemSlug:toolSlug, quantity:1
recipe = new Recipe
input: inputStacks
output: outputStacks
pattern: recipeData.pattern
tools: toolStacks
modVersion.addRecipe recipe
return recipe
# Un-parsing Methods ###########################################################################
_unparseModVersion: (builder, modVersion)->
builder
.line 'schema: ', 1
.line()
modVersion.eachGroup (group)=>
@_unparseGroup builder, modVersion, group
externalRecipes = modVersion.findExternalRecipes()
keys = _.keys(externalRecipes).sort()
for itemSlugText in keys
recipeList = externalRecipes[itemSlugText]
builder
.line 'update: ', modVersion.findName ItemSlug.slugify(itemSlugText)
.indent()
.loop(recipeList, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r))
.outdent()
.line()
_unparseGroup: (builder, modVersion, group)->
if group isnt Item.Group.Other
builder
.line 'group: ', group
.line()
.indent()
modVersion.eachItemInGroup group, (item)=>
@_unparseItem builder, modVersion, item
builder.line()
if group isnt Item.Group.Other
builder.outdent()
_unparseItem: (builder, modVersion, item)->
recipes = modVersion.findRecipes item.slug, [], onlyPrimary:true
builder
.line 'item: ', item.name
.indent()
.onlyIf item.isGatherable, => builder.line 'gatherable: yes'
.onlyIf recipes.length > 0, =>
builder.loop recipes, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r)
.outdent()
_unparseRecipe: (builder, recipe)->
inputNames = (builder.context.findName(stack.itemSlug) for stack in recipe.input)
inputNames.sort()
patternMap = {'.', '.'}
for i in [0...recipe.input.length]
stack = recipe.input[i]
patternMap["#{i}"] = "#{inputNames.indexOf builder.context.findName(stack.itemSlug)}"
pattern = recipe.pattern or recipe.defaultPattern
newPattern = []
for c in pattern.split('')
newPattern.push patternMap[c]
newPattern = newPattern.join ''
newPattern = newPattern.replace /(...)(...)(...)/, '$1 $2 $3'
quantity = recipe.output[0].quantity
extraOutputs = recipe.output[0...recipe.output.length]
extraOutputs.shift()
builder
.line 'recipe:'
.indent()
.onlyIf extraOutputs.length > 0, =>
builder
.push 'extras: '
.call => @_unparseStackList builder, extraOutputs
.line()
.push 'input: '
.loop inputNames
.line()
.line 'pattern: ', newPattern
.onlyIf quantity > 1, => builder.line 'quantity: ', quantity
.onlyIf recipe.tools.length > 0, =>
builder
.push 'tools: '
.call => @_unparseStackList builder, recipe.tools
.line()
.outdent()
_unparseStackList: (builder, stackList)->
if stackList.length is 1 and stackList[0].quantity is 1
builder.push builder.context.findName(stackList[0].itemSlug)
else
builder.loop stackList, onEach:(b, stack)=>
builder
.onlyIf stack.quantity > 1, => builder.push stack.quantity, ' '
.push builder.context.findName stack.itemSlug
+191
View File
@@ -0,0 +1,191 @@
###
Crafting Guide - recipe.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Stack = require './stack'
StringBuilder = require './string_builder'
########################################################################################################################
module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.pattern? then throw new Error 'attributes.pattern is required'
if attributes.itemSlug? and not attributes.output?
attributes.output = [new Stack itemSlug:attributes.itemSlug, quantity:1]
else if attributes.output? and not attributes.itemSlug?
if attributes.output.length is 0 then throw new Error 'attributes.output cannot be empty'
attributes.itemSlug = attributes.output[0].itemSlug
else
throw new Error 'attributes.itemSlug or attributes.output is required'
attributes.pattern = @_parsePattern attributes.pattern
attributes.modVersion ?= null
attributes.tools ?= []
options.logEvents ?= false
super attributes, options
@on Event.change + ':modVersion', => @_slug = null
# Class Methods ################################################################################
@compareFor: (a, b, itemSlug)->
if itemSlug?
aValue = a.itemSlug.matches itemSlug
bValue = b.itemSlug.matches itemSlug
if aValue isnt bValue
return -1 if aValue
return +1 if bValue
aValue = a.getQuantityProducedOf itemSlug
bValue = b.getQuantityProducedOf itemSlug
if aValue isnt bValue
return if aValue > bValue then -1 else +1
aValue = a.getOutputCount()
bValue = b.getOutputCount()
if aValue isnt bValue
return if aValue > bValue then -1 else +1
aValue = a.tools.length
bValue = b.tools.length
if aValue isnt bValue
return if aValue < bValue then -1 else +1
aValue = a.getInputCount()
bValue = b.getInputCount()
if aValue isnt bValue
return if aValue < bValue then -1 else +1
return 0
# Public Methods ###############################################################################
getInputCount: ->
result = 0
for stack in @input
result += stack.quantity
return result
getItemSlugAt: (patternSlot)->
trueIndex = 0:0, 1:1, 2:2, 3:4, 4:5, 5:6, 6:8, 7:9, 8:10
patternDigit = @pattern[trueIndex[patternSlot]]
return null unless patternDigit?
return null unless patternDigit.match /[0-9]/
stack = @input[parseInt(patternDigit)]
return null unless stack?
return stack.itemSlug
getOutputCount: ->
result = 0
for stack in @output
result += stack.quantity
return result
getQuantityProducedOf: (itemSlug)->
for stack in @output
if stack.itemSlug.matches itemSlug
return stack.quantity
return 0
produces: (itemSlug)->
if not @_produces?
@_produces = {}
for stack in @output
@_produces[stack.itemSlug.qualified] = true
@_produces[stack.itemSlug.item] = true
return @_produces[itemSlug.qualified]
requires: (itemSlug)->
for stack in @input
if stack.itemSlug.matches itemSlug
return true
return false
requiresTool: (itemSlug)->
for stack in @tools
if stack.itemSlug.matches itemSlug
return true
# Property Methods #############################################################################
getSlug: ->
if not @_slug?
builder = new StringBuilder
builder
.loop(@input, delimiter:',', onEach:(b, stack)-> b.push stack.itemSlug.qualified)
.onlyIf @tools.length > 0, (b)=>
b.push(' + ').loop(@tools, delimiter:',', onEach:(b, stack)-> b.push stack.itemSlug.qualified)
.push(' => ')
.loop(@output, delimiter:',', onEach:(b, stack)-> b.push stack.itemSlug.qualified)
@_slug = builder.toString()
return @_slug
Object.defineProperties @prototype,
slug: {get:@prototype.getSlug}
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") { name:", @name]
result.push ", input:["
needsDelimiter = false
for stack in @input
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
result.push ", output:["
needsDelimiter = false
for stack in @output
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
if @tools.length > 0
result.push ", tools:["
needsDelimiter = false
for stack in @tools
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
result.push '}'
return result.join ''
# Private Methods ##############################################################################
_parsePattern: (pattern)->
return unless pattern?
pattern = pattern.replace /\ /g, ''
return if pattern.length is 0
pattern = pattern.replace /[^0-9]/g, '.'
array = pattern.split ''
array = array[0...9]
while array.length isnt 9
array.push '.'
pattern = array.join ''
pattern = pattern.replace /(...)(...)(...)/, '$1 $2 $3'
return pattern
+26
View File
@@ -0,0 +1,26 @@
###
Crafting Guide - recipe_step.coffee
Copyright (c) 2014 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
########################################################################################################################
module.exports = class RecipeStep extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.recipe? then throw new Error 'attributes.recipe is required'
attributes.multiple ?= 1
super attributes, options
@recipe.on 'change', =>
@trigger 'change:recipe'
@trigger 'change'
# Public Methods ###############################################################################
getItemAt: (index)->
return @recipe.getItemAt index
+22
View File
@@ -0,0 +1,22 @@
###
Crafting Guide - simple_stack.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class Stack
constructor: (attributes={}, options={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
attributes.quantity ?= 1
@itemSlug = attributes.itemSlug
@quantity = attributes.quantity
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
+23
View File
@@ -0,0 +1,23 @@
###
Crafting Guide - stack.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
########################################################################################################################
module.exports = class Stack extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
attributes.quantity ?= 1
options.logEvents ?= false
super attributes, options
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
+57
View File
@@ -0,0 +1,57 @@
###
Crafting Guide - storage.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class Storage
constructor: (options={})->
options.storage ?= window.sessionStorage
@storage = options.storage
@_models = {}
# Public Methods ###############################################################################
load: (key)->
value = @storage.getItem key
logger.verbose -> "loaded #{value} from #{key}"
return value
store: (key, value)->
@storage.setItem key, value
logger.verbose -> "stored #{value} into #{key}"
register: (key, model, properties...)->
modelData = @_models[key]
if not modelData? then modelData = @_models[key] = model:model, properties:{}
makeStoreMethod = (k, m, p)=> return => @_storeProperty(k, m, p)
for property in properties
continue if modelData.properties[property]?
modelData.properties[property] = property
@_loadProperty key, model, property
model.on "change:#{property}", makeStoreMethod(key, model, property)
unregister: (key, property)->
modelData = @_models[key]
return unless modelData?
delete modelData.properties[key]
# Private Methods ##############################################################################
_loadProperty: (key, model, property)->
value = @load "#{key}:#{property}"
value = JSON.parse(value) if value?
return unless value?
model[property] = value
_storeProperty: (key, model, property)->
value = JSON.stringify model[property]
@store "#{key}:#{property}", value
+122
View File
@@ -0,0 +1,122 @@
###
Crafting Guide - string_builder.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class StringBuilder
constructor: (options={})->
options.indent ?= 0
options.indentString ?= ' '
@context = options.context
@_initialIndent = options.indent
@_indent = options.indent
@_indentString = options.indentString
@_pieces = []
# Public Methods ###############################################################################
call: ->
args = (arg for arg in arguments)
callback = args.pop()
args.unshift this
return unless _.isFunction callback
callback.apply null, args
clear: ->
@_indent = @_initialIndent
@_pieces = []
return this
indent: ->
@_indent += 1
return this
line: (args...)->
@push.apply(this, args) if args.length > 0
@push '\n'
loop: (list, options={})->
options.start ?= ''
options.end ?= ''
options.indent ?= false
options.delimiter ?= ', '
options.onEach ?= (builder, item)-> builder.push item
@_pushText options.start
if options.indent then @indent()
isFirst = true
for item in list
if not isFirst then @push options.delimiter
isFirst = false
options.onEach this, item
if options.indent then @outdent()
@_pushText options.end
return this
onlyIf: (condition, callback=null)->
return this unless condition
callback ?= (builder)-> # do nothing
callback this
return this
outdent: ->
@_indent -= 1
return this
push: ->
for i in [0...arguments.length]
arg = arguments[i]
if _.isArray arg
@push.apply this, arg
else if _.isString arg
@_pushText arg
else
@_pushText "#{arg}"
return this
# Object Overrides #############################################################################
toString: ->
return @_pieces.join ''
# Private Methods ##############################################################################
_pushIndent: ->
for i in [0...@_indent]
@_pieces.push @_indentString
_pushText: (text)->
return if text.length is 0
index = 0
if @_pieces.length > 0
lastPiece = @_pieces[@_pieces.length-1]
if lastPiece[lastPiece.length-1] is '\n'
@_pushIndent()
while true
newLineAt = text.indexOf '\n', index
break if newLineAt is -1
@_pieces.push text[index..newLineAt]
index = newLineAt + 1
break if newLineAt is text.length - 1
@_pushIndent()
if text.length > index
@_pieces.push text[index...text.length]
@@ -0,0 +1,49 @@
###
Crafting Guide - versioned_parser_base.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class VersionedParserBase
constructor: (options={})->
@_parsers = @_createParsers options
@_currentSchema = _.chain(@_parsers).keys().last().value()
# Class Members ################################################################################
@SCHEMA = /schema: *([0-9]+)/
# Public Methods ###############################################################################
parse: (text)->
return unless text?
schema = @_identifySchema text
parser = @_parsers[schema]
if not parser? then throw new Error "schema version #{schema} is not supported"
parser.parse text
return @_model
unparse: (schema=null)->
schema ?= @_currentSchema
parser = @_parsers["#{schema}"]
if not parser? then throw new Error "version #{schema} is not supported"
return parser.unparse()
# Overridable Methods ##########################################################################
_createParsers: (options)->
throw new Error 'subclasses must override this method'
_identifySchema: (text)->
match = VersionedParserBase.SCHEMA.exec text
if not match? then throw new Error 'missing "schema" declaration'
return match[1]
+13
View File
@@ -0,0 +1,13 @@
###
Crafting Table - polyfill.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
if not Array.prototype.clear?
Object.defineProperty Array.prototype, 'clear', value:->
@splice 0, @length
return this
+84
View File
@@ -0,0 +1,84 @@
###
Crafting Guide - crafting_plan.test.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
CraftingPlan = require '../models/crafting_plan'
ItemSlug = require '../models/item_slug'
Mod = require '../models/mod'
ModPack = require '../models/mod_pack'
ModVersion = require '../models/mod_version'
########################################################################################################################
modPack = plan = null
########################################################################################################################
describe 'crafting_plan.coffee', ->
beforeEach ->
mod = new Mod name:'Minecraft', slug:'minecraft'
mod.addModVersion new ModVersion modSlug:mod.slug, version:'1.7.10'
mod.activeModVersion.parse """
schema:1
item:Oak Plank; recipe:; input:Oak Log; pattern:... .0. ...; quantity:4
item:Stick; recipe:; input:Oak Plank; pattern:... .0. .0.; quantity:4
item:Crafting Table; recipe:; input:Oak Plank; pattern:00. 00. ...
item:Furnace; recipe:; input:Cobblestone; pattern:000 0.0 000; tools:Crafting Table
item:Iron Ingot; recipe:; input:Iron Ore, furnace fuel; pattern:.0. ... .1.; tools:Furnace
item:Iron Sword; recipe:; input:Iron Ingot, Stick; pattern:.0. .0. .1.; tools:Crafting Table
"""
modPack = new ModPack
modPack.addMod mod
plan = new CraftingPlan modPack:modPack, includingTools:false
describe 'craft', ->
describe 'under the simplest conditions', ->
it 'can craft a single step recipe', ->
plan.want.add ItemSlug.slugify 'oak_plank'
plan.craft()
plan.need.unparse().should.equal 'oak_log'
plan.result.unparse().should.equal '4.oak_plank'
it 'can craft a multi-step recipe', ->
plan.want.add ItemSlug.slugify 'crafting_table'
plan.craft()
plan.need.unparse().should.equal 'oak_log'
plan.result.unparse().should.equal 'crafting_table'
it 'can craft a multi-step recipe using tools', ->
plan.want.add ItemSlug.slugify 'furnace'
plan.craft()
plan.need.unparse().should.equal '8.cobblestone'
plan.result.unparse().should.equal 'furnace'
it 'can craft a multi-step recipe re-using tools', ->
plan.want.add ItemSlug.slugify 'iron_sword'
plan.craft()
plan.need.unparse().should.equal '2.furnace_fuel:2.iron_ore:oak_log'
plan.result.unparse().should.equal 'iron_sword:2.oak_plank:3.stick'
describe 'with building tools', ->
it 'can craft a multi-step recipe using tools', ->
plan.includingTools = true
plan.want.add ItemSlug.slugify 'furnace'
plan.craft()
plan.need.unparse().should.equal '8.cobblestone:oak_log'
plan.result.unparse().should.equal 'crafting_table:furnace'
it 'can craft a multi-step recipe re-using tools', ->
plan.includingTools = true
plan.want.add ItemSlug.slugify 'iron_sword'
plan.craft()
plan.need.unparse().should.eql '8.cobblestone:2.furnace_fuel:2.iron_ore:2.oak_log'
plan.result.unparse().should.equal 'crafting_table:furnace:' +
'iron_sword:2.oak_plank:3.stick'
+28
View File
@@ -0,0 +1,28 @@
###
# Crafting Guide - event_recorder.coffee
#
# Copyright (c) 2014-2015 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 = []
+211
View File
@@ -0,0 +1,211 @@
###
# Crafting Guide - inventory.test.coffee
#
# Copyright (c) 2014-2015 by Redwood Labs
# All rights reserved.
###
{Event} = require '../constants'
EventRecorder = require './event_recorder'
Inventory = require '../models/inventory'
Item = require '../models/item'
ItemSlug = require '../models/item_slug'
########################################################################################################################
inventory = modPack = null
########################################################################################################################
describe 'inventory.coffee', ->
beforeEach ->
inventory = new Inventory {}, silent:false
inventory.add ItemSlug.slugify('wool'), 4
inventory.add ItemSlug.slugify('string'), 20
inventory.add ItemSlug.slugify('boat')
describe 'add', ->
it 'can add to an empty inventory', ->
inventory.add ItemSlug.slugify('iron_ingot'), 4
stack = inventory._stacks['iron_ingot']
stack.constructor.name.should.equal 'Stack'
stack.itemSlug.qualified.should.equal 'iron_ingot'
stack.quantity.should.equal 4
it 'can augment quantity of existing items', ->
inventory.add ItemSlug.slugify('wool'), 2
inventory.unparse().should.equal 'boat:20.string:6.wool'
it 'can add zero quantity', ->
inventory.add ItemSlug.slugify('wool'), 0
inventory.unparse().should.equal 'boat:20.string:4.wool'
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.add ItemSlug.slugify('iron_ingot'), 10
events.names.should.eql [Event.add, Event.change]
describe 'addInventory', ->
it 'can add to an empty inventory', ->
newInventory = new Inventory
newInventory.addInventory inventory
newInventory.unparse().should.equal 'boat:20.string:4.wool'
it 'can add a mix of new and existing items', ->
newInventory = new Inventory
newInventory.add ItemSlug.slugify('string'), 2
newInventory.addInventory inventory
newInventory.unparse().should.equal 'boat:22.string:4.wool'
describe 'clone', ->
it 'creates an empty inventory from an empty inventory', ->
a = new Inventory
b = a.clone()
b._itemSlugs.should.eql []
it 'faithfully copies an existing inventory', ->
copy = inventory.clone()
copy.unparse().should.equal 'boat:20.string:4.wool'
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 (stack)-> result.push stack.itemSlug.qualified
result.should.eql ['boat', 'string', 'wool']
it 'works when items have been augmented', ->
inventory.add ItemSlug.slugify 'iron_ingot'
inventory.add ItemSlug.slugify 'boat'
inventory.add ItemSlug.slugify('wool'), 2
result = []
inventory.each (stack)-> result.push stack.itemSlug.qualified
result.should.eql ['boat', 'iron_ingot', 'string', '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 'localize', ->
before ->
modPack =
modSlug:
wool: 'minecraft'
string: 'minecraft'
boat: 'minecraft'
stone_gear: 'buildcraft'
findItem: (slug)->
return slug:new ItemSlug @modSlug[slug.item], slug.item
it 'replaces item slugs with qualified slugs', ->
inventory.add ItemSlug.slugify 'stone_gear'
inventory.modPack = modPack
inventory.localize()
slugs = []
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
slugs.should.eql [
'minecraft__boat'
'minecraft__string'
'minecraft__wool'
'buildcraft__stone_gear'
]
it 'ignores qualified slugs', ->
inventory.add ItemSlug.slugify 'buildcraft__stone_gear'
inventory.modPack = modPack
inventory.localize()
slugs = []
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
slugs.should.eql [
'minecraft__boat'
'minecraft__string'
'minecraft__wool'
'buildcraft__stone_gear'
]
describe 'parse', ->
beforeEach ->
inventory = new Inventory {}, silent:false
it 'ignores an empty string', ->
result = inventory.parse ''
result.unparse().should.eql ''
it 'can parse a single item without quantity', ->
result = inventory.parse 'wool'
result.unparse().should.equal 'wool'
it 'can parse a single item with quantity', ->
result = inventory.parse '4.wool'
result.unparse().should.equal '4.wool'
it 'can parse multiple mixed-type items', ->
result = inventory.parse '4.wool:10.string:boat'
result.unparse().should.equal 'boat:10.string:4.wool'
describe 'pop', ->
it 'returns null for an empty inventory', ->
inventory = new Inventory
result = inventory.pop()
expect(result).to.be.null
it 'completely removes the last item', ->
stack = inventory.pop()
stack.itemSlug.qualified.should.equal 'wool'
stack.quantity.should.equal 4
inventory.unparse().should.equal 'boat:20.string'
it 'triggers the right events', ->
events = new EventRecorder inventory
result = inventory.pop()
events.names.should.eql [Event.remove, Event.change]
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: only 4 wool in this inventory'
it 'removes all items by default', ->
inventory.remove 'wool'
expect(inventory._stacks.wool).to.be.empty
it 'removes a quantity above 1', ->
inventory.remove 'wool', 3
inventory._stacks.wool.quantity.should.equal 1
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.remove 'wool'
events.names.should.eql [Event.change, Event.remove, Event.change]
+136
View File
@@ -0,0 +1,136 @@
###
Crafting Guide - item_slug.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
ItemSlug = require '../models/item_slug'
########################################################################################################################
describe 'item_slug.coffee', ->
describe 'constructor', ->
it 'can handle one argument', ->
slug = new ItemSlug 'alpha'
slug.item.should.equal 'alpha'
expect(slug.mod).to.be.null
slug.qualified.should.equal 'alpha'
it 'can handle two arguments', ->
slug = new ItemSlug 'alpha', 'bravo'
slug.mod.should.equal 'alpha'
slug.item.should.equal 'bravo'
slug.qualified.should.equal 'alpha__bravo'
it 'throws with zero arguments', ->
f = -> new ItemSlug
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
it 'throws with more arguments', ->
f = -> new ItemSlug 'alpha', 'bravo', 'charlie'
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
describe 'ItemSlug.compare', ->
it 'sorts qualified slugs first', ->
a = new ItemSlug 'alpha'
b = new ItemSlug 'bravo', 'charlie'
ItemSlug.compare(a, b).should.equal +1
ItemSlug.compare(b, a).should.equal -1
it 'sorts by mod when both are qualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'delta'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
it 'sorts by item when both are qualified in the same mod', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'bravo'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
it 'sorts by item when not qualified', ->
a = new ItemSlug 'alpha'
b = new ItemSlug 'bravo'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
describe 'ItemSlug.equal', ->
it 'requires both to have the same mod', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'alpha', 'charlie'
c = new ItemSlug 'alpha', 'bravo'
ItemSlug.equal(a, b).should.be.false
ItemSlug.equal(a, c).should.be.true
it 'requires both to have the same item', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'alpha', 'charlie'
c = new ItemSlug 'alpha', 'bravo'
ItemSlug.equal(a, b).should.be.false
ItemSlug.equal(a, c).should.be.true
describe 'ItemSlug.slugify', ->
it 'can slugify a pure name', ->
slug = ItemSlug.slugify 'Alpha Bravo (Charlie)'
slug.item.should.equal 'alpha_bravo_charlie'
expect(slug.mod).to.be.null
it 'can slugify a simple item slug', ->
slug = ItemSlug.slugify 'alpha_bravo_charlie'
slug.item.should.equal 'alpha_bravo_charlie'
expect(slug.mod).to.be.null
it 'can slugify a fully-qualified slug', ->
slug = ItemSlug.slugify 'alpha_bravo__charlie_delta'
slug.mod.should.equal 'alpha_bravo'
slug.item.should.equal 'charlie_delta'
describe 'matches', ->
it 'ignores mod when either is unqualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'bravo'
c = new ItemSlug 'charlie'
a.matches(b).should.be.true
a.matches(c).should.be.false
it 'observes differences in mod when all are qualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'bravo'
c = new ItemSlug 'delta', 'echo'
d = new ItemSlug 'alpha', 'bravo'
a.matches(b).should.be.false
a.matches(c).should.be.false
a.matches(d).should.be.true
describe 'isQualified', ->
it 'returns true only when the mod slug is set', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie'
a.isQualified.should.be.true
b.isQualified.should.be.false
describe '[]', ->
it 'allows slugs as a key', ->
slug = new ItemSlug 'alpha', 'bravo'
data = {}
data[slug] = 'foo'
data['alpha__bravo'].should.equal 'foo'
data[slug].should.equal 'foo'
+30
View File
@@ -0,0 +1,30 @@
###
Crafting Guide - mod.test.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
Mod = require '../models/mod'
########################################################################################################################
mod = null
########################################################################################################################
describe 'mod.coffee', ->
beforeEach -> mod = new Mod name:'Test', slug:'test'
describe 'compareTo', ->
it 'lists required mods first', ->
minecraft = new Mod name:'Minecraft', slug:'minecraft'
mod.compareTo(minecraft).should.equal +1
minecraft.compareTo(mod).should.equal -1
it 'sorts by name second', ->
buildcraft = new Mod name:'Buildcraft', slug:'buildcraft'
mod.compareTo(buildcraft).should.equal +1
buildcraft.compareTo(mod).should.equal -1
+103
View File
@@ -0,0 +1,103 @@
###
Crafting Guide - mod_pack.test.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
Item = require '../models/item'
ItemSlug = require '../models/item_slug'
Mod = require '../models/mod'
ModPack = require '../models/mod_pack'
ModVersion = require '../models/mod_version'
########################################################################################################################
buildcraft = industrialCraft = minecraft = modPack = null
########################################################################################################################
describe 'mod_pack.coffee', ->
beforeEach ->
minecraft = new Mod slug:'minecraft', name:'Minecraft'
minecraft.addModVersion new ModVersion modSlug:minecraft.slug, version:'1.7.10'
minecraft.activeModVersion.addItem new Item name:'Wool'
minecraft.activeModVersion.addItem new Item name:'Bed', recipes:['']
minecraft.activeModVersion.registerName ItemSlug.slugify('iron_chestplate'), 'Iron Chestplate'
buildcraft = new Mod slug:'buildcraft', name:'Buildcraft'
buildcraft.addModVersion new ModVersion modSlug:buildcraft.slug, version:'6.2.6'
buildcraft.activeModVersion.addItem new Item name:'Stone Gear', recipes:['']
buildcraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
buildcraft.activeVersion = Mod.Version.None
industrialCraft = new Mod slug:'industrial_craft', name:'Industrial Craft'
industrialCraft.addModVersion new ModVersion modSlug:industrialCraft.slug, version:'2.0'
industrialCraft.activeModVersion.addItem new Item name:'Resin'
industrialCraft.activeModVersion.addItem new Item name:'Rubber'
industrialCraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
industrialCraft.activeVersion = Mod.Version.None
modPack = new ModPack
modPack.addMod minecraft
modPack.addMod buildcraft
modPack.addMod industrialCraft
describe 'findItem', ->
it 'can find an item by partial slug', ->
item = modPack.findItem ItemSlug.slugify 'wool'
item.slug.qualified.should.equal 'minecraft__wool'
it 'can find an item by full slug', ->
item = modPack.findItem ItemSlug.slugify 'minecraft__wool'
item.name.should.equal 'Wool'
it 'can find an ambiguous item by full slug', ->
buildcraft.activeVersion = Mod.Version.Latest
industrialCraft.activeVersion = Mod.Version.Latest
item = modPack.findItem ItemSlug.slugify 'industrial_craft__wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Industrial Craft'
it 'can find an ambiguous item by partial slug', ->
buildcraft.activeVersion = Mod.Version.Latest
industrialCraft.activeVersion = Mod.Version.Latest
item = modPack.findItem ItemSlug.slugify 'wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Buildcraft'
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
describe 'findItemDisplay', ->
it 'returns all data for a regular Minecraft item', ->
display = modPack.findItemDisplay ItemSlug.slugify 'bed'
display.iconUrl.should.equal '/browse/minecraft/bed/icon.png'
display.itemUrl.should.equal '/browse/minecraft/bed/'
display.itemName.should.equal 'Bed'
display.modSlug.should.equal 'minecraft'
it 'returns all data for an item in an enabled mod', ->
buildcraft.activeVersion = '6.2.6'
display = modPack.findItemDisplay ItemSlug.slugify 'stone_gear'
display.iconUrl.should.equal '/browse/buildcraft/stone_gear/icon.png'
display.itemUrl.should.equal '/browse/buildcraft/stone_gear/'
display.itemName.should.equal 'Stone Gear'
display.modSlug.should.equal 'buildcraft'
it 'assumes an unfound item is from Minecraft', ->
display = modPack.findItemDisplay ItemSlug.slugify 'iron_chestplate'
display.iconUrl.should.equal '/browse/minecraft/iron_chestplate/icon.png'
display.itemUrl.should.equal '/browse/minecraft/iron_chestplate/'
display.itemName.should.equal 'Iron Chestplate'
display.modSlug.should.equal 'minecraft'
+96
View File
@@ -0,0 +1,96 @@
###
Crafting Guide - mod_version.test.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
Item = require '../models/item'
ItemSlug = require '../models/item_slug'
ModVersion = require '../models/mod_version'
########################################################################################################################
modVersion = null
########################################################################################################################
describe 'mod_version.coffee', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'0.0'
modVersion.addItem new Item name:'underscore', group:'punctuation'
modVersion.addItem new Item name:'bravo', group:'letter'
modVersion.addItem new Item name:'alpha', group:'letter'
modVersion.addItem new Item name:'one', group:'number'
modVersion.addItem new Item name:'two', group:'number'
describe 'constructor', ->
it 'requires a mod slug', ->
expect(-> new ModVersion version:'0.0').to.throw Error, 'attributes.modSlug is required'
it 'requires a mod version', ->
expect(-> new ModVersion modSlug:'test').to.throw Error, 'attributes.version is required'
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 indexed by its slug', ->
modVersion.addItem new Item name:'Wool'
modVersion._items.wool.name.should.equal 'Wool'
it 'sets the modVersion', ->
modVersion.addItem new Item name:'Wool'
modVersion._items.wool.modVersion.should.equal modVersion
describe 'eachGroup', ->
it 'returns all the groups in order', ->
groupNames = []
modVersion.eachGroup (groupName)-> groupNames.push groupName
groupNames.should.eql ['letter', 'number', 'punctuation']
describe 'eachItemInGroup', ->
it 'returns immediately for unknown group', ->
slugs = []
modVersion.eachItemInGroup 'foobar', (item)-> slugs.push item.slug.qualified
slugs.should.eql []
it 'calls callback for exactly the items in a group in order', ->
slugs = []
modVersion.eachItemInGroup 'letter', (item)-> slugs.push item.slug.qualified
slugs.should.eql ['test__alpha', 'test__bravo']
slugs = []
modVersion.eachItemInGroup 'number', (item)-> slugs.push item.slug.qualified
slugs.should.eql ['test__one', 'test__two']
describe 'findItemByName', ->
it 'locates items by slugified name', ->
modVersion.addItem new Item name:'Crafting Table'
modVersion.findItemByName('Crafting Table').slug.qualified.should.equal 'test__crafting_table'
describe 'findRecipes', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'1.0'
modVersion.parse """
schema:1
item: Cake
recipe:; input: Milk, Sugar, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
recipe:; input: Milk, Cocoa Beans, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
item: Bucket
recipe:; input: Iron Ingot; pattern: ... 0.0 .0.
"""
it 'finds all recipes which list item as output', ->
recipes = modVersion.findRecipes ItemSlug.slugify('Bucket')
(r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'cake', 'cake']
@@ -0,0 +1,45 @@
###
Crafting Guide - command_parser_version_base.test.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
CommandParserVersionBase = require '../../models/parser_versions/command_parser_version_base'
########################################################################################################################
parser = null
########################################################################################################################
describe 'command_parser_version_base.coffee', ->
beforeEach -> parser = new CommandParserVersionBase model:{}
describe '_parseHereDoc', ->
it 'returns null for non-heredoc lines', ->
result = parser._parseHereDoc 'foobar: baz'
expect(result).to.be.null
it 'identifies the right text for a real heredoc', ->
parser._lines = ['command: <<-END', 'alpha', 'bravo', 'charlie', 'END', 'command1: arg2']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result.should.equal 'alpha\nbravo\ncharlie'
it 'identifies an empty heredoc', ->
parser._lines = ['command: <<-END', 'END']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
expect(result).to.be.null
it 'trims smallest leading whitespace', ->
parser._lines = ['command: <<-END', ' alpha', ' bravo', ' charlie', 'END', 'command1: arg2']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result.should.equal 'alpha\n bravo\ncharlie'
@@ -0,0 +1,251 @@
###
Crafting Guide - mod_version_parser_v1.test.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
CommandParserVersionBase = require '../../models/parser_versions/command_parser_version_base'
ItemSlug = require '../../models/item_slug'
ModVersion = require '../../models/mod_version'
ModVersionParserV1 = require '../../models/parser_versions/mod_version_parser_v1'
########################################################################################################################
baseText = modVersion = parser = null
########################################################################################################################
describe 'mod_version_parser_v1.coffee', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'0.0'
parser = new ModVersionParserV1 model:modVersion
describe 'Item', ->
it 'allows multiple recipes', ->
recipes = "item: Charlie;
recipe:; input:Alpha; pattern:... .0. ...;
recipe:; input:Bravo; pattern:... 0.0 ...;"
modVersion = parser.parse recipes
recipes = modVersion.findRecipes ItemSlug.slugify 'charlie'
recipes[0].input[0].itemSlug.qualified.should.equal 'alpha'
recipes[1].input[0].itemSlug.qualified.should.equal 'bravo'
describe 'name', ->
it 'adds the name when present', ->
modVersion = parser.parse 'item: Charlie'
modVersion._items.charlie.name.should.equal 'Charlie'
it 'requires a non-empty name', ->
func = -> parser.parse 'item: \n'
expect(func).to.throw Error, 'cannot be empty'
describe 'gatherable', ->
it 'adds "gatherable" when present', ->
modVersion = parser.parse 'item: Alpha Bravo; gatherable: yes'
modVersion._items.alpha_bravo.isGatherable.should.be.true
it 'does not allow a duplicate "gatherable" declaration', ->
func = -> parser.parse 'item: Alpha Bravo; gatherable: yes; gatherable: yes'
expect(func).to.throw Error, 'duplicate'
it 'requires "gatherable" to be "yes" or "no"', ->
func = -> parser.parse 'item: Alpha Bravo; gatherable: true'
expect(func).to.throw Error, 'gatherable must be'
it 'does not allow "gatherable" before "item"', ->
func = -> parser.parse 'gatherable: yes; item: Alpha Bravo; gatherable: yes'
expect(func).to.throw Error, '"gatherable" before "item"'
describe 'Recipe', ->
beforeEach -> baseText = 'item: Charlie; '
describe 'input', ->
it 'adds "input" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo, Charlie; pattern: ... 012 ...'
slugs = (s.itemSlug.item for s in modVersion.findRecipes(ItemSlug.slugify('charlie'))[0].input)
slugs.should.eql ['alpha', 'bravo', 'charlie']
it 'requires an "input" declaration', ->
func = -> parser.parse baseText + 'recipe:; pattern: ... .0. ...'
expect(func).to.throw Error, 'the "input" declaration is required'
it 'does not allow a duplicate "input" declaration', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:....0....; input:Bravo'
expect(func).to.throw Error, 'duplicate declaration of "input"'
it 'does not allow "input" before "recipe"', ->
func = -> parser.parse baseText + 'input:Alpha, Bravo; recipe:; pattern:....0....'
expect(func).to.throw Error, 'cannot declare "input" before "recipe"'
it 'registers slugs for each input name', ->
modVersion = parser.parse baseText + 'recipe:; input:Delta, Echo, Foxtrot; pattern:...012...'
(s.item for s in modVersion._slugs).should.eql ['charlie', 'delta', 'echo', 'foxtrot']
describe 'pattern', ->
it 'adds "pattern" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:... .0. .1.'
modVersion.findRecipes(ItemSlug.slugify('charlie'))[0].pattern.should.equal '... .0. .1.'
it 'requires a "pattern" declaration', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo'
expect(func).to.throw Error, 'the "pattern" declaration is required'
it 'does not allow a duplicate "pattern" declaration', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:....0..1.; pattern:01.......'
expect(func).to.throw Error, 'duplicate declaration of "pattern"'
it 'requires pattern to be the right length', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:000'
expect(func).to.throw Error, 'a pattern must have'
it 'requires pattern to only use proper characters', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:abc def ghi'
expect(func).to.throw Error, 'a pattern must have'
it 'requires pattern to only refer to existing items', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:... 010 ...'
expect(func).to.throw Error, 'there is no input 1 in this recipe'
it 'requires all items to appear in the pattern', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: 000 0.0 000'
expect(func).to.throw Error, 'Bravo is an input'
it 'computes the input stack sizes from the pattern', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo, Charlie; pattern:111 .0. 2.2'
recipe = modVersion.findRecipes(ItemSlug.slugify('charlie'))[0]
recipe.input[0].quantity.should.equal 1
recipe.input[1].quantity.should.equal 3
recipe.input[2].quantity.should.equal 2
it 'does not allow "pattern" before "recipe"', ->
func = -> parser.parse baseText + 'pattern:... .0. ...; recipe:; inputs:Alpha'
expect(func).to.throw Error, 'cannot declare "pattern" before "recipe"'
describe 'quantity', ->
beforeEach ->
baseText = 'item: Charlie; recipe:; input:Alpha; pattern:...0.0...; '
it 'adds "quantity" when present', ->
modVersion = parser.parse baseText + 'quantity: 2'
modVersion.findRecipes(ItemSlug.slugify('charlie'))[0].output[0].quantity.should.equal 2
it 'does not allow a duplicate "quantity" declaration', ->
func = -> parser.parse baseText + 'quantity:1; quantity:2'
expect(func).to.throw Error, 'duplicate declaration of "quantity"'
it 'requires quantity to be an integer', ->
func = -> parser.parse baseText + 'quantity:ten'
expect(func).to.throw Error, 'quantity must be an integer'
it 'assumes a quantity of 1 by default', ->
modVersion = parser.parse baseText
modVersion.findRecipes(ItemSlug.slugify('charlie'))[0].output[0].quantity.should.equal 1
it 'does not allow "quantity" before recipe', ->
func = -> parser.parse 'item:Bravo; quantity:12; recipe:;'
expect(func).to.throw Error, 'cannot declare "quantity" before "recipe"'
describe 'output', ->
beforeEach ->
baseText = 'item: Delta; item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
it 'adds a single item as the default output', ->
modVersion = parser.parse baseText
stack = modVersion.findRecipes(ItemSlug.slugify('bravo'))[0].output[0]
stack.itemSlug.qualified.should.equal 'test__bravo'
stack.quantity.should.equal 1
it 'can add multiple extras with quantities', ->
modVersion = parser.parse baseText + 'extras:2 Delta, 4 Echo'
output = modVersion.findRecipes(ItemSlug.slugify('bravo'))[0].output
output[0].itemSlug.qualified.should.equal 'test__bravo'
output[0].quantity.should.equal 1
output[1].itemSlug.qualified.should.equal 'test__delta'
output[1].quantity.should.equal 2
output[2].itemSlug.qualified.should.equal 'echo'
output[2].quantity.should.equal 4
it 'does not allow "extras" before "recipe"', ->
func = -> parser.parse 'item:Bravo; extras:Charlie'
expect(func).to.throw Error, 'cannot declare "extras" before "recipe"'
it 'registers slugs for each output name', ->
modVersion = parser.parse baseText + 'extras:Delta, Echo'
(s.qualified for s in modVersion._slugs).should.eql [
'test__bravo', 'test__delta', 'charlie', 'echo'
]
it 'does not allow a duplicate "extras" declaration', ->
func = -> parser.parse baseText + 'extras:Echo; extras:Delta'
expect(func).to.throw Error, 'duplicate declaration of "extras"'
describe 'tools', ->
beforeEach ->
baseText = 'item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
it 'can add a single tool', ->
modVersion = parser.parse baseText + 'tools: Furnace'
modVersion.findRecipes(ItemSlug.slugify('bravo'))[0].tools[0].itemSlug.item.should.equal 'furnace'
it 'can add multiple tools', ->
modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace'
tools = modVersion.findRecipes(ItemSlug.slugify('bravo'))[0].tools
tools[0].itemSlug.item.should.equal 'crafting_table'
tools[1].itemSlug.item.should.equal 'furnace'
it 'registers slugs for each tool name', ->
modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace'
(s.item for s in modVersion._slugs).should.eql ['bravo', 'charlie', 'crafting_table', 'furnace']
it 'does not allow a duplicate "tools" declaration', ->
func = -> parser.parse baseText + 'tools:Crafting Table; tools:Furnace'
expect(func).to.throw Error, 'duplicate declaration of "tools"'
describe "unparsing", ->
beforeEach ->
baseText = """
schema: 1
group: Agriculture
item: Apple
item: Baked Potato
recipe:
input: Potato, furnace fuel
pattern: .0. ... .1.
tools: Furnace
group: Functional Blocks
item: Furnace
recipe:
input: Cobblestone
pattern: 000 0.0 000
tools: Crafting Table
update: Iron Ingot
recipe:
input: Iron Dust, furnace fuel
pattern: .0. ... .1.
tools: Furnace
"""
it 'can round-trip a data file', ->
text = parser.unparse parser.parse baseText
actual = CommandParserVersionBase.simplify text
expected = CommandParserVersionBase.simplify baseText
actual.should.equal expected
+86
View File
@@ -0,0 +1,86 @@
###
Crafting Guide - recipe.test.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
Item = require '../models/item'
ItemSlug = require '../models/item_slug'
Recipe = require '../models/recipe'
Stack = require '../models/stack'
########################################################################################################################
input = output = pattern = recipe = null
########################################################################################################################
describe 'recipe.coffee', ->
describe 'constructor', ->
beforeEach ->
input = [
new Stack(itemSlug:new ItemSlug('iron_gear')),
new Stack(itemSlug:new ItemSlug('gold_ingot'), quantity:4)
]
pattern = '.1. 101 .1.'
it 'requires input', ->
expect(-> new Recipe slug:'gold_gear', pattern:pattern).to.throw Error, 'attributes.input is required'
it 'requires a pattern', ->
expect(-> new Recipe slug:'gold_gear', input:input).to.throw Error, 'attributes.pattern is required'
it 'requires either outputs or a slug', ->
f = -> new Recipe input:input, pattern:pattern
expect(f).to.throw 'attributes.itemSlug or attributes.output is required'
it 'creates default output', ->
recipe = new Recipe itemSlug:ItemSlug.slugify('gold_gear'), input:input, pattern:pattern
recipe.output.length.should.equal 1
recipe.output[0].itemSlug.qualified.should.equal 'gold_gear'
recipe.output[0].quantity.should.equal 1
it 'assigns a default slug', ->
recipe = new Recipe input:input, pattern:pattern, output:[new Stack itemSlug:ItemSlug.slugify('gold_gear')]
recipe.itemSlug.qualified.should.equal 'gold_gear'
describe 'getItemSlugAt', ->
beforeEach ->
input = [
new Stack itemSlug:ItemSlug.slugify('iron_gear')
new Stack itemSlug:ItemSlug.slugify('gold_ingot'), quantity:4
]
recipe = new Recipe itemSlug:'gold_gear', input:input, pattern:'.1. 101 .1.'
it 'returns the proper item for an early slot', ->
recipe.getItemSlugAt(1).qualified.should.equal 'gold_ingot'
it 'returns the proper item for a late slot', ->
recipe.getItemSlugAt(4).qualified.should.equal 'iron_gear'
it 'returns null for an invalid slot', ->
expect(recipe.getItemSlugAt(12)).to.be.null
describe '_parsePattern', ->
beforeEach ->
recipe = new Recipe
itemSlug: 'oak_wood_planks',
input: [new Stack itemSlug:new ItemSlug('oak_wood')],
pattern:'... .0. ...'
it 'normalizes invalid characters', ->
recipe._parsePattern('$$0 #() 010').should.equal '..0 ... 010'
it 'removes extra characters', ->
recipe._parsePattern('000 000 000 000').should.equal '000 000 000'
it 'fills in missing characters', ->
recipe._parsePattern('000000').should.equal '000 000 ...'
it 'fills in spaces', ->
recipe._parsePattern('000000000').should.equal '000 000 000'
@@ -0,0 +1,84 @@
###
Crafting Guide - string_builder.test.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
StringBuilder = require '../models/string_builder'
########################################################################################################################
builder = null
########################################################################################################################
describe 'string_builder.coffee', ->
beforeEach -> builder = new StringBuilder
describe 'call', ->
it 'works with no arguments', ->
builder.call (b)-> b.push 'foo'
builder.toString().should.equal 'foo'
it 'works with multiple arguments', ->
builder.call 'foo', 'bar', 'baz', (builder, a, b, c)-> builder.loop [a, b, c]
builder.toString().should.equal 'foo, bar, baz'
describe 'loop', ->
it 'can make an empty list', ->
builder.loop [], start:'[', end:']'
builder.toString().should.equal '[]'
it 'can make a list with a single element', ->
builder.loop ['foo'], start:'[', end:']'
builder.toString().should.equal '[foo]'
it 'can make a list with many elements', ->
builder.loop ['foo', 'bar', 'baz'], start:'[', end:']'
builder.toString().should.equal '[foo, bar, baz]'
it 'can make a list with a custom callback', ->
builder.loop ['foo', 'bar', 'baz'], start:'[', end:']', onEach:(b, i)-> b.push "\"#{i}\""
builder.toString().should.equal '["foo", "bar", "baz"]'
it 'can use a custom delimiter', ->
builder.loop ['foo', 'bar', 'baz'], delimiter:'|'
builder.toString().should.equal 'foo|bar|baz'
it 'can indent content', ->
builder.loop ['foo', 'bar', 'baz'], start:'[\n', end:'\n]', delimiter:',\n', indent:true
builder.toString().should.equal '[\n foo,\n bar,\n baz\n]'
describe 'onlyIf', ->
it 'calls the callback on true', ->
builder.onlyIf true, (b)-> b.push 'foo'
builder.toString().should.equal 'foo'
describe 'push', ->
it 'can build a simple string', ->
builder.push('foo').push(' bar').push(' baz')
builder.toString().should.equal 'foo bar baz'
it 'can build a multi-line string', ->
builder.push('foo').push('\nbar\n').push('baz')
builder.toString().should.equal 'foo\nbar\nbaz'
it 'can build an indented multi-line string', ->
builder
.push 'foo\n'
.indent()
.push 'bar\n'
.outdent()
.push 'baz'
builder.toString().should.equal 'foo\n bar\nbaz'
it 'can pick apart multiple newlines in a single chunk', ->
builder.indent().push('foo\nbar\nbaz').outdent().push('\nbif')
builder.toString().should.equal 'foo\n bar\n baz\nbif'
+45
View File
@@ -0,0 +1,45 @@
###
# Crafting Guide - test.coffee
#
# Copyright (c) 2014-2015 by Redwood Labs
# All rights reserved.
###
# Test Set-up ##########################################################################################################
chai.use require 'sinon-chai'
chai.config.includeStack = true
if typeof(global) is 'undefined'
window.global = window
global.assert = chai.assert
global.expect = chai.expect
global.should = chai.should()
global.util = require 'util'
Logger = require '../logger'
global.logger = new Logger level:Logger.DEBUG
require '../polyfill'
require '../underscore_mixins'
# Test Registry ########################################################################################################
mocha.setup 'bdd'
# tests are roughly in order of how errors should be tackled
require './string_builder.test'
require './item_slug.test'
require './inventory.test'
require './recipe.test'
require './mod_version.test'
require './mod.test'
require './mod_pack.test'
require './parser_versions/command_parser_version_base.test'
require './parser_versions/mod_version_parser_v1.test'
require './crafting_plan.test'
mocha.checkLeaks()
mocha.globals ['LiveReload']
mocha.run()
+30
View File
@@ -0,0 +1,30 @@
###
Crafting Guide - underscore.coffee
Copyright (c) 2014-2015 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
composeSlugs: (part1, part2)->
return "#{part1}__#{part2}"
decomposeSlug: (slug)->
return [null, null] unless slug?
parts = slug.split '__'
if parts.length is 1
parts = [ null, parts[0] ]
return parts
+52
View File
@@ -0,0 +1,52 @@
###
Crafting Guide - url_params.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
url = require 'url'
########################################################################################################################
module.exports = class UrlParams
constructor: (parseMap, url=null)->
if not parseMap? then throw new Error 'parseMap is required'
url ?= window.location.href
@_parseMap = {}
for name, options of parseMap
options.type ?= 'string'
options.default ?= null
options.parse = this["_#{options.type}"]
if not options.parse? then throw new Error "#{options.type} is not a valid type"
@_parseMap[name] = options
@parse url
parse: (urlText)->
params = url.parse(urlText, true).query
for name, options of @_parseMap
value = params[name]
if value?
this[name] = options.parse value
else
this[name] = options.default
# Private Methods ##############################################################################
_boolean: (text)->
return true if text in ['true', 'yes']
return false
_integer: (text)->
return null unless text?
result = parseInt text
return null unless _.isNumber result
return result
_string: (text)->
return text
+157
View File
File diff suppressed because one or more lines are too long