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
+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]