Move model files to crafting-guide-common

This commit is contained in:
Andrew Miner
2017-05-02 18:34:53 -07:00
parent 69ca6b06b9
commit bd96bc6575
85 changed files with 134 additions and 5686 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ global.π = Math.PI
global.ε = 0.0001
global.w = require 'when'
{Logger} = require 'crafting-guide-common'
{Logger} = require('crafting-guide-common').util
global.logger = new Logger
Tracker = require './tracker'
@@ -63,7 +63,7 @@ storage = new Storage storage:global.localStorage
tracker.trackPageView()
{CraftingGuideClient} = require 'crafting-guide-common'
{CraftingGuideClient} = require('crafting-guide-common').api
client = _(new CraftingGuideClient(baseUrl:apiBaseUrl)).extend Backbone.Events
client.onStatusChanged = (client, oldStatus, newStatus)->
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
-129
View File
@@ -1,129 +0,0 @@
#
# Crafting Guide - base_model.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class BaseModel extends Backbone.Model
@_loadingQueue = []
@_isDraining = false
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)
@fileCache = options.fileCache or null
@loading = null
@logEvents = options.logEvents or false
@state = c.modelState.unloaded
Object.defineProperties this,
isUnloaded: { get:-> @state is c.modelState.unloaded }
isLoading: { get:-> @state is c.modelState.loading }
isLoaded: { get:-> @state is c.modelState.loaded }
isError: { get:-> @state is c.modelState.error }
# Event Methods ################################################################################
onLoadSucceeded: (text, status, xhr)->
try
@set @parse text
@state = c.modelState.loaded
@trigger c.event.change, this
@trigger c.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 = c.modelState.error
logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}"
@trigger c.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 = c.modelState.loading
@trigger c.event.request, this
loadFromServer = =>
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
if @fileCache?
@loading = @fileCache.loading.then =>
if @fileCache.hasFile url
return @_addToLoadingQueue @fileCache.getFile(url), 'success', {url:url}
else
@loading = loadFromServer()
else
@loading = loadFromServer()
@loading.catch (e)-> # do nothing
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}"
# Private Methods ##############################################################################
_addToLoadingQueue: (text, status, xhr)->
deferred = w.defer()
BaseModel._loadingQueue.push resolve:deferred.resolve, func:(=> @onLoadSucceeded text, status, xhr)
@_drainLoadingQueue()
return deferred.promise
_drainLoadingQueue: ->
return if @_isDraining
@_isDraining = true
drainDelay = 50
drain = =>
toLoad = BaseModel._loadingQueue.shift()
if not toLoad?
@_isDraining = false
else
toLoad.func()
toLoad.resolve(true)
_.delay drain, drainDelay
_.delay drain, drainDelay
-45
View File
@@ -1,45 +0,0 @@
#
# Crafting Guide - converter.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require "../models/game/item"
Mod = require "../models/game/mod"
ModPack = require "../models/game/mod_pack"
########################################################################################################################
module.exports = class Converter
# Public Methods ###############################################################################
convert: (id, displayName, oldModPack)->
modSlugToIdMap = {}
itemSlugToIdMap = {}
newModPack = new ModPack id:id, displayName:displayName
oldModPack.eachMod (oldMod)=>
newMod = new Mod id:_.uniqueId("mod-"), displayName:oldMod.name, modPack:newModPack
modSlugToIdMap[oldMod.slug.toString()] = newMod.id
oldMod.eachItem (oldItem)=>
newItem = new Item id:_.uniqueId("item-"), displayName:oldItem.name, mod:newMod
itemSlugToIdMap[oldItem.slug.toString()] = newItem.id
if oldItem.isGatherable?
newItem.isGatherable = oldItem.isGatherable
oldModPack.eachMod (oldMod)=>
newMod = newModPack.mods[modSlugToIdMap[oldMod.slug.toString()]]
return newModPack
# Private Methods ##############################################################################
_convertItem: (oldItem, newMod)->
_convertMod: (oldMod, newModPack)->
return newMod
@@ -1,36 +0,0 @@
#
# Crafting Guide - event_recorder.coffee
#
# Copyright © 2014-2016 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 = []
@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
# Public Methods ###############################################################################
reset: ->
@events = []
# Property Methods #############################################################################
getNames: ->
return (e.event for e in @events)
Object.defineProperties @prototype,
names: {get:@prototype.getNames}
-243
View File
@@ -1,243 +0,0 @@
#
# Crafting Guide - inventory.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Stack = require './stack'
########################################################################################################################
module.exports = class Inventory extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
attributes.modPack ?= null
@clear()
if options.clone?
@addInventory options.clone
# Class Methods ################################################################################
@Delimiters =
Item: '.'
Stack: ':'
# Public Methods ###############################################################################
add: (itemSlug, quantity=1, options={})->
return this unless quantity > 0
@_add itemSlug, quantity, options
@trigger c.event.add, this, itemSlug, quantity
@trigger c.event.change, this
return this
addInventory: (inventory)->
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
@trigger c.event.change, this
return this
clear: (options={})->
@_stacks = {}
@_itemSlugs = []
@trigger c.event.change, this
clone: ->
inventory = new Inventory
inventory.addInventory this
return inventory
each: (callback)->
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
continue unless stack?
callback stack
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'
changed = false
newSlugs = []
newStacks = []
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
continue unless stack?
qualifiedSlug = if itemSlug.isQualified then itemSlug else null
if not qualifiedSlug?
qualifiedSlug = @modPack.findItem(itemSlug)?.slug
changed = qualifiedSlug?
if qualifiedSlug?
newSlugs.push qualifiedSlug
newStacks.push new Stack itemSlug:qualifiedSlug, quantity:stack.quantity
else
newSlugs.push itemSlug
newStacks.push stack
if changed
for itemSlug, stack of @_stacks
@stopListening stack
@_itemSlugs = newSlugs
@_stacks = {}
for stack in newStacks
@_stacks[stack.itemSlug] = stack
@listenTo stack, c.event.change, => @trigger c.event.change, this
@_sort()
@trigger c.event.change, this
pop: ->
itemSlug = @_itemSlugs.pop()
return null unless itemSlug?
stack = @_stacks[itemSlug]
delete @_stacks[itemSlug]
@trigger c.event.remove, this, stack.itemSlug, stack.quantity
@trigger c.event.change, this
return stack
quantityOf: (itemSlug)->
stack = @_stacks[itemSlug]
return stack.quantity if stack
return 0
remove: (itemSlug, quantity=null)->
stack = @_stacks[itemSlug]
return this unless stack?
quantity ?= stack.quantity
return this unless quantity > 0
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 = (s for s in @_itemSlugs when not ItemSlug.equal(s, itemSlug))
@trigger c.event.remove, this, itemSlug, quantity
@trigger c.event.change, this
return this
toDescription: ->
return null if @isEmpty
return null unless @modPack?
item = @modPack.findItem @_itemSlugs[0]
extras = @_itemSlugs.length - 1
result = "#{item.name}"
if extras > 0 then result += " and #{extras} more..."
return result
# 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 = stack.itemSlug.qualified
if stack.quantity is 1
parts.push slugText
else
parts.push "#{stack.quantity}#{Inventory.Delimiters.Item}#{slugText}"
return parts.join Inventory.Delimiters.Stack
# Property Methods #############################################################################
Object.defineProperties @prototype,
isEmpty:
get: -> @_itemSlugs.length is 0
totalQuantity:
get: ->
total = 0
@each (stack)->
total += stack.quantity
return total
# 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, options={})->
options.insert ?= false
return unless itemSlug?
return unless quantity > 0
stack = @_stacks[itemSlug]
if not stack?
stack = new Stack itemSlug:itemSlug, quantity:quantity
@listenTo stack, c.event.change, => @trigger c.event.change, this
@_stacks[itemSlug] = stack
if options.insert
@_itemSlugs.unshift itemSlug
else
@_itemSlugs.push itemSlug
@_sort()
else
stack.quantity += quantity
_sort: ->
@_itemSlugs.sort (a, b)-> ItemSlug.compare a, b
-90
View File
@@ -1,90 +0,0 @@
#
# Crafting Guide - item.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Recipe = require './recipe'
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
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.ignoreDuringCrafting ?= false
attributes.isGatherable ?= false
attributes.modVersion ?= null
attributes.officialUrl ?= null
attributes.slug ?= ItemSlug.slugify attributes.name
attributes.videos ?= []
options.logEvents ?= false
super attributes, options
@on c.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
unparse: ->
ItemParser = require '../parsing/item_parser' # to avoid require cycles
@_parser ?= new ItemParser model:this
return @_parser.unparse()
# Property Methods #############################################################################
getIsCraftable: ->
if not @_isCraftable?
@_isCraftable = false
if @modVersion?
@_isCraftable = @modVersion.hasRecipes @slug
return @_isCraftable
Object.defineProperties @prototype,
isCraftable: {get:@prototype.getIsCraftable}
# Backbone.Model Overrides #####################################################################
parse: (text)->
ItemParser = require '../parsing/item_parser' # to avoid require cycles
@_parser ?= new ItemParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.itemData modSlug:@slug.mod, itemSlug:@slug.item
# 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()
@@ -1,96 +0,0 @@
#
# Crafting Guide - item_slug.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ItemSlug
constructor: ->
@_item = @_mod = null
if arguments.length is 1
parts = _.decomposeSlug arguments[0]
@_mod = _.slugify parts[0]
@item = _.slugify parts[1]
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)->
if a.item isnt b.item
return if a.item < b.item then -1 else +1
if a.mod isnt b.mod
return if a.mod < b.mod then -1 else +1
return 0
@equal: (a, b)->
return true if not a? and not b?
return false unless a? and 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 slug?
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 #############################################################################
Object.defineProperties @prototype,
isQualified:
get: -> @_mod?
item:
get: -> @_item
set: (newItem)->
if not newItem? then throw new Error 'item is required'
@_item = newItem
@mod = @mod # reset @_qualified
mod:
get: -> @_mod
set: (newMod)->
@_mod = newMod
@_qualified = if @_mod? then _.composeSlugs(@_mod, @_item) else @_item
qualified:
get: -> @_qualified
# Object Overrides #############################################################################
toString: ->
return @_qualified
valueOf: ->
return @_qualified.valueOf()
-234
View File
@@ -1,234 +0,0 @@
#
# Crafting Guide - mod.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
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 = []
@_tutorials = []
# Class Methods ##################################################################################
@Version: Version =
None: 'none'
Latest: 'latest'
# Public Methods #################################################################################
compareTo: (that)->
thisRequired = this.slug in c.requiredMods
thatRequired = that.slug in c.requiredMods
if thisRequired isnt thatRequired
return -1 if thisRequired
return +1 if thatRequired
else if this.slug isnt that.slug
return if this.slug < that.slug then -1 else +1
return 0
# Property Methods #############################################################################
Object.defineProperties @prototype,
activeModVersion:
get: -> @_activeModVersion
activeVersion:
get: ->
return @_activeVersion
set: (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 c.event.change + ':activeVersion', this, @_activeVersion
@trigger c.event.change, this
else
for modVersion in @_modVersions
if version is modVersion.version
@_activateModVersion modVersion
break
@_activeVersion = version
@trigger c.event.change + ':activeVersion', this, @_activeVersion
@trigger c.event.change, this
enabled:
get: -> @_activeModVersion?
modVersions:
get: -> @_modVersions[..]
tutorials:
get: -> @getAllTutorials()
# Item Methods #################################################################################
chooseRandomItem: ->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
return null unless effectiveModVersion?
return effectiveModVersion.chooseRandomItem()
eachItem: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachItem 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
# ModVersion Methods ###########################################################################
addModVersion: (modVersion)->
return unless modVersion?
return if @_modVersions.indexOf(modVersion) isnt -1
@_modVersions.push modVersion
@listenTo modVersion, c.event.change, => @trigger c.event.change, this
modVersion.fileCache = this.fileCache
modVersion.mod = this
@trigger c.event.add + ':modVersion', modVersion, this
@trigger c.event.change + ':version', modVersion, this
@trigger c.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
getAllModVersions: ->
return @_modVersions[..]
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
# Name Methods #################################################################################
eachName: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachName callback
findName: (itemSlug)->
return unless @_activeModVersion?
@_activeModVersion.findName itemSlug
# Recipe Methods ###############################################################################
eachRecipe: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachRecipe callback
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
# Tutorial Methods #############################################################################
addTutorial: (tutorial)->
return unless tutorial?
if @getTutorial(tutorial.slug)? then throw new Error "duplicate tutorial: #{tutorial.name}"
@_tutorials.push tutorial
tutorial.modSlug = @slug
getAllTutorials: ->
return @_tutorials[..]
getTutorial: (tutorialSlug)->
for tutorial in @_tutorials
return tutorial if tutorial.slug is tutorialSlug
return null
# Backbone.Model Overrides #####################################################################
parse: (text)->
ModParser = require '../parsing/mod_parser' # to avoid require cycles
@_parser ?= new ModParser model:this
@_parser.parse text
@_verifyActiveModVersion()
return null # prevent calling `set`
url: ->
return c.url.modData modSlug:@slug
# Private Methods ##############################################################################
_activateModVersion: (modVersion)->
if @_activeModVersion? then @stopListening @_activeModVersion
@_activeModVersion = modVersion
@trigger c.event.change + ':activeModVersion', this, @_activeModVersion
logger.verbose => "#{@slug} switched to version #{@_activeVersion}"
if @_activeModVersion?
@listenTo @_activeModVersion, 'all', -> @trigger.apply this, arguments
_verifyActiveModVersion: ->
if (@_activeVersion isnt Version.None) and (not @_activeModVersion?)
logger.warning => "#{@slug} no longer has a version #{@_activeVersion}, using latest instead"
@activeVersion = Version.Latest
-202
View File
@@ -1,202 +0,0 @@
#
# Crafting Guide - mod_pack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
Mod = require './mod'
ModVersionParser = require '../parsing/mod_version_parser'
Recipe = require './recipe'
SimpleInventory = require '../crafting/simple_inventory'
########################################################################################################################
module.exports = class ModPack extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
@_mods = []
@_cache = {}
@on c.event.change, => @_cache = {}
# Item Methods #################################################################################
chooseRandomItem: ->
return null unless @_mods.length > 0
modIndex = Math.floor Math.random() * @_mods.length
return @_mods[modIndex].chooseRandomItem()
findItem: (itemSlug, options={})->
options.includeDisabled ?= false
key = "#{itemSlug}-#{options.includeDisabled}"
@_cache.itemBySlug ?= {}
item = @_cache.itemBySlug[key]
return item if item?
if itemSlug.isQualified
mod = @getMod itemSlug.mod
if mod?
item = mod.findItem itemSlug, options
if not item?
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
item = mod.findItem itemSlug, options
break if item?
if item?
@_cache.itemBySlug[key] = item
return item
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 = {slug:itemSlug}
item = @findItem itemSlug, includeDisabled:true
if item?
result.itemName = item.name
result.itemSlug = item.slug.item
result.modSlug = item.slug.mod
result.modVersion = item.modVersion.version
else
result.itemName = @findName itemSlug, includeDisabled:true
result.itemSlug = itemSlug.item
result.modSlug = @_mods[0].slug
result.modVersion = @_mods[0].activeVersion
craftingUrlInventory = new SimpleInventory modPack:this
if item?.multiblock?
craftingUrlInventory.addInventory item.multiblock.inventory
else
craftingUrlInventory.add itemSlug
result.craftingUrl = c.url.crafting inventoryText:craftingUrlInventory.unparse()
result.iconUrl = c.url.itemIcon result
result.itemUrl = c.url.item result
result.modName = @getMod(result.modSlug).name
return result
qualifySlug: (itemSlug)->
return itemSlug if itemSlug.isQualified
item = @findItem itemSlug
return item.slug if item?
return itemSlug
# Mod 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, c.event.change, (modVersion)=> @_onModVersionLoaded modVersion
@trigger c.event.add + ':mod', mod, this
@_mods.sort (a, b)-> a.compareTo b
@trigger c.event.sort + ':mod', this
@trigger c.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
getAllMods: ->
return @_mods[..]
removeMod: (mod)->
index = @_mods.indexOf mod
return unless index >= 0
@_mods.splice index, 1
@trigger c.event.remove, this, mod.slug
@trigger c.event.change, this
# Name Methods #################################################################################
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
# Recipe Methods ###############################################################################
findRecipes: (itemSlug, options={})->
options.alwaysFromOwningMod ?= false
return null unless itemSlug?
key = "#{itemSlug}-#{options.alwaysFromOwningMod}"
@_cache.recipesBySlug ?= {}
result = @_cache.recipesBySlug[key]
return result if result?
result = []
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
@_cache.recipesBySlug[key] = result
return if result.length > 0 then result else null
# Object Overrides #############################################################################
toString: ->
return "ModPack (#{@cid}) {modVersions:«#{@_mods.length} items»}"
# Private Methods ##############################################################################
_onModVersionLoaded: (modVersion)->
mods = @getAllMods()
return true unless mods.length > 0
for mod in mods
if mod.isError
@removeMod mod
continue
modVersions = mod.getAllModVersions()
return true unless modVersions.length > 0
continue if mod.activeVersion is Mod.Version.None
activeModVersion = mod.activeModVersion
return true unless activeModVersion?
return true if activeModVersion.isUnloaded
return true if activeModVersion.isLoading
@trigger c.event.change, this
@trigger c.event.sync, this
@@ -1,200 +0,0 @@
#
# Crafting Guide - mod_version.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
Item = require './item'
ItemSlug = require './item_slug'
Recipe = require './recipe'
########################################################################################################################
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
chooseRandomItem: ->
itemIndex = Math.floor Math.random() * @_slugs.length
return @_slugs[itemIndex]
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 ################################################################################
getAllGroups: ->
result = []
@eachGroup (group)-> result.push group
return result
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
options.forCrafting ?= false
primaryRecipes = []
otherRecipes = []
for recipe in _.values @_recipes
continue unless recipe.isConditionSatisfied()
continue unless recipe.hasAllTools()
continue if options.forCrafting and recipe.ignoreDuringCrafting
if recipe.itemSlug.matches itemSlug
primaryRecipes.push recipe
else if recipe.produces itemSlug
otherRecipes.push recipe
for recipe in primaryRecipes
result.push recipe
if not options.onlyPrimary and result.length is 0
for recipe in otherRecipes
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 '../parsing/mod_version_parser' # to avoid require cycles
@_parser ?= new ModVersionParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.modVersionData modSlug:@modSlug, modVersion:@version
# Object Overrides #############################################################################
toString: ->
return "ModVersion (#{@cid}) {
modSlug:#{@modSlug}, version:#{@version}, items:«#{@_slugs.length} items»
}"
@@ -1,85 +0,0 @@
#
# Crafting Guide - multiblock.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
Inventory = require './inventory'
########################################################################################################################
module.exports = class Multiblock extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.layers? then throw new Error 'attributes.layers is required'
if attributes.layers.length < 1 then throw new Error 'attributes.layers.length must be >= 1'
super attributes, options
@_analyzePattern()
# Public Methods ###############################################################################
getStackAt: (x, y, z)->
value = @_stackCache[y]?[z]?[x]
return null if value is undefined
return value
getLayerInventory: (y)->
@_layerInventories ?= []
result = @_layerInventories[y]
if not result? then result = @_layerInventories[y] = new Inventory
return result
# Property Methods #############################################################################
Object.defineProperties @prototype,
depth:
get: -> @_depth
height:
get: -> @_height
inventory:
get: -> @_inventory
width:
get: -> @_width
# Private Methods ##############################################################################
_analyzeLayer: (layer, y, stackCacheLayer)->
rows = layer.split ' '
@_depth = Math.max @_depth, rows.length
for row in rows
@_width = Math.max @_width, row.length
stackCacheRow = []
stackCacheLayer.push stackCacheRow
@_analyzeRow row, stackCacheRow, @getLayerInventory(y)
_analyzePattern: ->
@_depth = @_height = @_width = 0
@_inventory = new Inventory
@_stackCache = []
for layer, y in @layers
stackCacheLayer = []
@_stackCache.push stackCacheLayer
@_analyzeLayer layer, y, stackCacheLayer
@_height = @layers.length
_analyzeRow: (row, stackCacheRow, layerInventory)->
for cell, x in row.split ''
index = parseInt cell
stack = null
if not _.isNaN index
stack = @input[index]
@_inventory.add stack.itemSlug, stack.quantity
layerInventory.add stack.itemSlug, stack.quantity
stackCacheRow.push stack
-247
View File
@@ -1,247 +0,0 @@
#
# Crafting Guide - recipe.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Stack = require './stack'
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
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.condition ?= null
attributes.ignoreDuringCrafting ?= false
attributes.modVersion ?= null
attributes.tools ?= []
options.logEvents ?= false
super attributes, options
@_computeQuantities attributes.pattern
@on c.event.change + ':modVersion', => @_slug = null
@on c.event.change + ':pattern', => @_patternCache = 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.getQuantityProduced itemSlug
bValue = b.getQuantityProduced itemSlug
if aValue isnt bValue
return if aValue > bValue then -1 else +1
return 0
# Public Methods ###############################################################################
getStackAtSlot: (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
getQuantityProduced: (itemSlug)->
total = 0
for stack in @output
if stack.itemSlug.matches itemSlug
total += stack.quantity
return total
getQuantityRequired: (itemSlug)->
total = 0
for stack, index in @input
if ItemSlug.equal stack.itemSlug, itemSlug
total += @_quantities[index] * stack.quantity
return total
hasAllTools: (modPack)->
modPack ?= @modVersion?.mod?.modPack
return true unless modPack?
for stack in @tools
return false unless modPack.findItem stack.itemSlug
return true
isConditionSatisfied: (modPack)->
return true unless @condition?
modPack ?= @modVersion?.mod?.modPack
result = false
if @condition.verb is 'item'
if modPack?.findItemByName(@condition.noun)?
result = true
else if @condition.verb is 'mod'
modPack.eachMod (mod)=>
if mod.name is @condition.noun
result = true
if @condition.inverted then result = not result
return result
isPassThroughFor: (itemSlug)->
return @getQuantityProduced(itemSlug) is @getQuantityRequired(itemSlug)
produces: (itemSlug)->
if not @_produces?
@_produces = {}
for stack in @output
actuallyProduces = not @isPassThroughFor stack.itemSlug
@_produces[stack.itemSlug.qualified] = actuallyProduces
result = @_produces[itemSlug.qualified] or @_produces[itemSlug.item]
return result
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 #############################################################################
Object.defineProperties @prototype,
slug:
get: ->
if not @_slug?
builder = new StringBuilder
delimiterNeeded = false
for stack in @input
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
builder.push '>'
builder.push @pattern
builder.push '>'
for stack in @tools
builder.push stack.itemSlug.qualified
builder.push '>'
delimiterNeeded = false
for stack in @output
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
@_slug = builder.toString()
return @_slug
# 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 @getQuantityRequired stack.itemSlug
result.push ' '
result.push stack.itemSlug
needsDelimiter = true
result.push ']'
result.push ", output:["
needsDelimiter = false
for stack in @output
if needsDelimiter then result.push ', '
result.push @getQuantityProduced stack.itemSlug
result.push ' '
result.push stack.itemSlug
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 ##############################################################################
_computeQuantities: (pattern)->
quantityMap = {}
index = 0
while index < pattern.length
c = pattern[index]
index += 1
continue if c is '.'
continue if c is ' '
if quantityMap[c]?
quantityMap[c] += 1
else
quantityMap[c] = 1
@_quantities = []
for i in [0...@input.length]
@_quantities.push quantityMap["#{i}"]
_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
@@ -1,33 +0,0 @@
#
# Crafting Guide - simple_stack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require './item_slug'
########################################################################################################################
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
# Class Methods ################################################################################
@compare: (a, b)->
if a? and not b? then return -1
if not a? and b? then return +1
if a.quantity isnt b.quantity
return if a.quantity > b.quantity then -1 else +1
return ItemSlug.compare a.itemSlug, b.itemSlug
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
-23
View File
@@ -1,23 +0,0 @@
#
# Crafting Guide - stack.coffee
#
# Copyright © 2014-2016 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}"
@@ -1,138 +0,0 @@
#
# Crafting Guide - command_parser_version_base.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
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
@errors = []
# 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
@errors = []
@_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
[line, 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 for s in match[2].split(',') when s.length > 0) if match[2]?
args = (s.trim() for s in args)
args = (s for s in args when s.length > 0)
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
@errors.push e
logger.error -> e.message
_parseHereDoc: (line)->
hereDocIndex = line.indexOf '<<-'
return [line, null] unless hereDocIndex isnt -1
hereDocStopText = line[hereDocIndex+3...line.length]
line = line[0...hereDocIndex]
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 hereDocLine in hereDocLines
continue if hereDocLine.trim().length is 0
shortestIndent = Math.min hereDocLine.match(/( *).*/)[1].length, shortestIndent
for i in [0...hereDocLines.length]
hereDocLines[i] = hereDocLines[i][shortestIndent..]
return [line, null] unless hereDocLines.length > 0
return [line, hereDocLines.join('\n')]
@@ -1,48 +0,0 @@
#
# Crafting Guide - command_parser_version_base.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './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[1]).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[0].should.equal 'command: '
result[1].should.equal 'alpha\nbravo\ncharlie'
it 'identifies an empty heredoc', ->
parser._lines = ['command: <<-END', 'END']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result[0].should.equal 'command: '
expect(result[1]).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[0].should.equal 'command: '
result[1].should.equal 'alpha\n bravo\n\ncharlie'
@@ -1,19 +0,0 @@
#
# Crafting Guide - item_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
ItemParserV1 = require './item_parser_v1'
########################################################################################################################
module.exports = class ItemParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ItemParserV1 options
@@ -1,72 +0,0 @@
#
# Crafting Guide - item_parser_v1.coffee
#
# Copyright © 2014-2016 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)->
builder.line 'schema: ', 1
builder.line()
@_unparseItem builder, model
# Command Methods ##############################################################################
_command_description: (textParts...)->
if not @_rawData.description?
@_rawData.description = ''
else
@_rawData.description += '\n'
@_rawData.description += textParts.join ', '
_command_officialUrl: (officialUrl)->
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
if not officialUrl? or (officialUrl.length is 0) then throw new Error 'officialUrl cannot be empty'
@_rawData.officialUrl = officialUrl
_command_video: (youTubeId, nameParts...)->
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
name = nameParts.join ', '
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)->
model.description = rawData.description if rawData.description?
model.officialUrl = rawData.officialUrl if rawData.officialUrl?
model.videos = rawData.videos if rawData.videos?
# Un-parsing Methods ###########################################################################
_unparseItem: (builder, model)->
if model.officialUrl?
builder.line 'officialUrl: ', model.officialUrl
builder.line()
if model.description?
if model.description.indexOf('\n') isnt -1
builder.line 'description: <<-END'
builder.line model.description
builder.line 'END'
else
builder.line 'description: ', model.description
builder.line()
for video in model.videos
builder.line 'video: ', video.youTubeId, ', ', video.name
builder.line()
@@ -1,99 +0,0 @@
#
# Crafting Guide - item_parser_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require '../game/item'
ItemParserV1 = require './item_parser_v1'
########################################################################################################################
baseText = item = parser = null
########################################################################################################################
describe 'item_parser_v1.coffee', ->
beforeEach ->
item = new Item name:'alpha'
parser = new ItemParserV1 model:item
describe 'officialUrl', ->
it 'may be omitted', ->
parser.parse 'schema: 1\nvideo: youtubeid, Video Alpha\ndescription: Bravo, Charlie'
expect(item.officialUrl).to.be.null
it 'is assigned properly when given', ->
parser.parse 'schema: 1\nofficialUrl: http://testurl.com'
item.officialUrl.should.equal 'http://testurl.com'
it 'does not allow duplicate declarations', ->
func = -> parser.parse 'schema: 1\nofficialUrl: http://testurl.com\nofficialUrl: http://testurl2.com'
expect(func).to.throw Error, 'duplicate'
it 'does not allow an empty value if given', ->
func = -> parser.parse 'schema: 1\nofficialUrl:'
expect(func).to.throw Error, 'empty'
describe 'description', ->
it 'may be omitted', ->
parser.parse 'schema: 1\nvideo: youTubeId, Video Alpha\nofficialUrl: http://testurl.com'
expect(item.description).to.be.null
it 'is assigned properly when given', ->
parser.parse 'schema: 1\ndescription: Alpha Bravo Charlie'
item.description.should.equal 'Alpha Bravo Charlie'
it 'can be a heredoc', ->
parser.parse 'schema: 1\ndescription: <<-END\nAlpha\nBravo\nCharlie\nEND'
item.description.should.equal 'Alpha\nBravo\nCharlie'
it 'concatenates multiple declarations', ->
parser.parse 'schema: 1\ndescription: Alpha\ndescription: Bravo'
item.description.should.equal 'Alpha\nBravo'
describe 'video', ->
it 'may be omitted', ->
parser.parse 'schema: 1\nofficialUrl: http://testurl.com\ndescription: Alpha Bravo Charlie'
item.videos.should.eql []
it 'is assigned properly when given', ->
parser.parse 'schema: 1\nvideo: youtubeid, Alpha Bravo'
item.videos[0].should.eql youTubeId:'youtubeid', name:'Alpha Bravo'
item.videos.length.should.equal 1
it 'may be included multiple times', ->
parser.parse 'schema: 1\nvideo: youtubeid1, Alpha\nvideo: youtubeid2, Bravo'
item.videos[0].should.eql youTubeId:'youtubeid1', name:'Alpha'
item.videos[1].should.eql youTubeId:'youtubeid2', name:'Bravo'
item.videos.length.should.equal 2
it 'requires a YouTubeId and name', ->
func = -> parser.parse 'schema: 1\nvideo: alpha'
expect(func).to.throw Error, 'requires a name'
describe 'unparsing', ->
it 'can round-trip a fully described item', ->
text = """
schema: 1
officialUrl: http://testurl.com
description: <<-END
Alpha
Bravo
END
video: youtubeid1, Alpha Bravo
video: youtubeid2, Charlie Delta
"""
parser.parse text
parser.unparse().should.equal text
@@ -1,19 +0,0 @@
#
# Crafting Guide - mod_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
ModParserV1 = require './mod_parser_v1'
########################################################################################################################
module.exports = class ModParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ModParserV1 options
@@ -1,94 +0,0 @@
#
# Crafting Guide - mod_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
Mod = require '../game/mod'
ModVersion = require '../game/mod_version'
Tutorial = require '../site/tutorial'
########################################################################################################################
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)->
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_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_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_tutorial: (nameParts...)->
name = nameParts.join(', ').trim()
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.tutorialNames ?= []
@_rawData.tutorialNames.push name
_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
if rawData.tutorialNames?
for tutorialName in rawData.tutorialNames
model.addTutorial new Tutorial name:tutorialName
for version in rawData.versions
model.addModVersion new ModVersion modSlug:model.slug, version:version
@@ -1,19 +0,0 @@
#
# Crafting Guide - mod_version_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
ModVersionParserV1 = require './mod_version_parser_v1'
########################################################################################################################
module.exports = class ModVersionParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ModVersionParserV1 options
@@ -1,380 +0,0 @@
#
# Crafting Guide - mod_version_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
Item = require '../game/item'
ItemSlug = require '../game/item_slug'
ModVersion = require '../game/mod_version'
Multiblock = require '../game/multiblock'
Recipe = require '../game/recipe'
Stack = require '../game/simple_stack'
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
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.output.length isnt 1 then throw new Error 'duplicate declaration of "extras"'
for term in extraTerms
@_recipeData.output.push @_parseStack 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_ignoreDuringCrafting: (value)->
if not @_recipeData? then throw new Error 'cannot declare "ignoreDuringCrafting" before "recipe"'
if @_recipeData.ignoreDuringCrafting? then throw new Error 'duplicate declaration of "ignoreDuringCrafting"'
if not (value in ['yes', 'no']) then throw new Error 'ignoreDuringCrafting must be either "yes" or "no"'
@_recipeData.ignoreDuringCrafting = (value is 'yes')
_command_input: (stackDescriptions...)->
activeData = if @_recipeData? then @_recipeData else @_multiblockData
if not activeData? then throw new Error 'cannot declare "input" before "recipe" or "multiblock"'
if activeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"'
for stackDescription in stackDescriptions
activeData.input.push @_parseStack stackDescription
_command_layer: (layerText)->
if not @_multiblockData? then throw new Error 'cannot declare "layer" before "multiblock"'
if not layerText? then throw new Error 'cannot have an empty layer'
if layerText.length is 0 then throw new Error 'cannot have an empty layer'
@_multiblockData.layers.push layerText
_command_multiblock: ->
if not @_itemData? then throw new Error 'cannot declare "multiblock" before "item"'
if @_itemData.multiblockData? then throw new Error 'duplicate declaration of "multiblock"'
@_recipeData = null
@_multiblockData = input:[], layers:[], line:@_lineNumber
@_itemData.multiblockData = @_multiblockData
_command_onlyIf: (condition)->
words = condition.split ' '
if words.length < 2 then throw new Error 'condition must include a verb followed by a noun'
inverted = false
if words[0] is 'not'
inverted = true
words.shift()
verb = words[0]
noun = words[1..].join ' '
if not (verb in ['item', 'mod']) then throw new Error "unknown verb: #{verb}"
@_recipeData.condition = verb:verb, noun:noun, inverted:inverted
_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 = quantity
@_recipeData.output[0].quantity = parseInt quantity
_command_recipe: ->
if not @_itemData? then throw new Error 'cannot delcare "recipe" before "item"'
@_multiblockData = null
@_recipeData = line:@_lineNumber, input:[], output:[{quantity:1, name:@_itemData.name}], tools:[]
@_itemData.recipes ?= []
@_itemData.recipes.push @_recipeData
_command_tools: (toolNames...)->
if not @_recipeData? then throw new Error 'cannot declare "tools" before "recipe"'
if @_recipeData.tools.length isnt 0 then throw new Error 'duplicate declaration of "tools"'
for name in toolNames
if name.length is 0 then throw new Error 'tool names cannot be empty'
@_recipeData.tools.push name:name, quantity:1
_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
# Parsing Helpers ##############################################################################
_parseStack: (stackText)->
match = ModVersionParserV1.STACK.exec stackText
if match?
return quantity:parseInt(match[1]), name:match[2]
else
return quantity:1, name:stackText
# 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.ignoreDuringCrafting ?= false
itemData.recipes ?= []
if itemData.type is 'new'
item = new Item
name: itemData.name,
ignoreDuringCrafting: itemData.ignoreDuringCrafting,
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
if itemData.multiblockData
item.multiblock = @_handleErrors @_buildMultiblock, modVersion, item, itemData.multiblockData
for recipeData in itemData.recipes
@_handleErrors @_buildRecipe, modVersion, itemData, recipeData
return item
_buildMultiblock: (modVersion, item, multiblockData)->
@_lineNumber = multiblockData.line
if multiblockData.layers.length is 0 then throw new Error '"multiblock" requires at least one "layer"'
if multiblockData.input.length is 0 then throw new Error '"multiblock" requires at least one "input"'
input = @_buildStackList modVersion, multiblockData.input
multiblock = new Multiblock input:input, layers:multiblockData.layers
return multiblock
_buildRecipe: (modVersion, itemData, recipeData)->
@_lineNumber = recipeData.line
if recipeData.input.length is 0 then throw new Error 'the "input" declaration is required'
if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required'
recipe = new Recipe
condition: recipeData.condition
ignoreDuringCrafting: recipeData.ignoreDuringCrafting
input: @_buildStackList modVersion, recipeData.input, recipeData.pattern
output: @_buildStackList modVersion, recipeData.output
pattern: recipeData.pattern
tools: @_buildStackList modVersion, recipeData.tools
modVersion.addRecipe recipe
return recipe
_buildStackList: (modVersion, data, pattern=null)->
createSlug = (name)=>
item = @_rawData.items[name]
if item? and item.type isnt 'update'
slug = new ItemSlug modVersion.modSlug, _.slugify name
else
slug = new ItemSlug name
modVersion.registerName slug, name
return slug
stacks = []
for stackData in data
itemSlug = createSlug stackData.name
stacks.push new Stack itemSlug:itemSlug, quantity:stackData.quantity
if pattern?
expectedIndexes = _.reduce [0...stacks.length], ((obj, i)-> obj[i] = true; return obj), {}
for c in pattern
continue if c is '.'
continue if c is ' '
delete expectedIndexes[c]
if not stacks[parseInt(c)]? then throw new Error "there is no item #{c} in this recipe"
unusedNames = _.map(_.keys(expectedIndexes), ((i)-> data[parseInt(i)].name))
if unusedNames.length > 1
throw new Error "#{unusedNames.join(', ')} are listed for this recipe, but do not appear in the pattern"
else if unusedNames.length is 1
throw new Error "#{unusedNames[0]} is listed for this recipe, but does not appear in the pattern"
return stacks
# 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 item.multiblock?, =>
builder
.indent()
.call => @_unparseMultiblock builder, item.multiblock
.outdent()
.onlyIf recipes.length > 0, =>
builder.loop recipes, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r)
.outdent()
_unparseMultiblock: (builder, multiblock)->
builder
.line 'multiblock:'
.indent()
.push "input: "
.call => @_unparseStackList builder, multiblock.input
.push ";\n"
.loop(multiblock.layers, delimiter:'', onEach:(builder, layer)=> builder.line "layer: #{layer}")
.outdent()
_unparseRecipe: (builder, recipe)->
inputStacks = recipe.input[..]
inputStacks.sort (a, b)-> Stack.compare a, b
inputNames = []
for stack in inputStacks
name = builder.context.findName stack.itemSlug
if stack.quantity > 1
inputNames.push "#{stack.quantity} #{name}"
else
inputNames.push name
patternMap = {'.', '.'}
for i in [0...recipe.input.length]
stack = recipe.input[i]
name = builder.context.findName(stack.itemSlug)
name = "#{stack.quantity} #{name}" if stack.quantity > 1
patternMap["#{i}"] = "#{inputNames.indexOf name}"
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 recipe.condition?, =>
builder.push 'onlyIf: '
.onlyIf recipe.condition.inverted, -> builder.push 'not '
.line recipe.condition.verb, ' ', recipe.condition.noun
.onlyIf extraOutputs.length > 0, =>
builder
.push 'extras: '
.call => @_unparseStackList builder, extraOutputs
.line()
.onlyIf recipe.ignoreDuringCrafting, => builder.line 'ignoreDuringCrafting: yes'
.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
@@ -1,344 +0,0 @@
#
# Crafting Guide - mod_version_parser_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
ItemSlug = require '../game/item_slug'
ModVersion = require '../game/mod_version'
ModVersionParserV1 = require './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 'test__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 'Multiblock', ->
it 'adds a "multiblock" when present', ->
modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo; layer: 0'
item = modVersion.findItemByName 'Alpha'
item.multiblock.height.should.equal 1
it 'requires "item" be declared before "multiblock"', ->
func = -> parser.parse "multiblock:"
expect(func).to.throw Error, '"multiblock" before "item"'
it 'prohibits multiple "multiblock" commands per item"', ->
func = -> parser.parse "item: Alpha; multiblock:; multiblock:"
expect(func).to.throw Error, 'duplicate'
it 'prohibits multiblocks with no inputs', ->
func = -> parser.parse "item: Alpha; multiblock:; layer: 000"
expect(func).to.throw Error, 'at least one "input"'
describe 'layer', ->
it 'allows multiple "layer" commands', ->
modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo, Charlie; layer: 01 10; layer: 10 01'
item = modVersion.findItemByName 'Alpha'
item.multiblock.depth.should.equal 2
item.multiblock.height.should.equal 2
item.multiblock.width.should.equal 2
it 'prohibits empty layers', ->
func = -> parser.parse 'item: Alpha; multiblock:; input: Bravo; layer:; layer: 00 00'
expect(func).to.throw Error, 'empty layer'
it 'prohibits multiblocks with no layers', ->
func = -> parser.parse "item: Alpha; multiblock:; input: Bravo"
expect(func).to.throw Error, 'at least one "layer"'
describe 'Recipe', ->
beforeEach -> baseText = 'item: Charlie; '
describe 'input', ->
it 'adds "input" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: ... 010 ...'
charlieSlug = ItemSlug.slugify('test__charlie')
slugs = (s.itemSlug.item for s in modVersion.findRecipes(charlieSlug)[0].input)
slugs.should.eql ['alpha', 'bravo']
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']
it 'correctly handles recipes which use the same input multiple times', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha; pattern:.0..0....'
recipe = _.values(modVersion._recipes)[0]
recipe.getQuantityRequired(ItemSlug.slugify('alpha')).should.equal 2
it 'allows a quantity for each input', ->
modVersion = parser.parse baseText + 'recipe:; input: 12 Delta, 3 Echo; pattern:... 0.1 ...'
recipe = _.values(modVersion._recipes)[0]
recipe.input[0].quantity.should.equal 12
recipe.input[0].itemSlug.qualified.should.equal 'delta'
recipe.input[1].quantity.should.equal 3
recipe.input[1].itemSlug.qualified.should.equal 'echo'
describe 'pattern', ->
it 'adds "pattern" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:... .0. .1.'
modVersion.findRecipes(ItemSlug.slugify('test__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 item 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 listed'
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('test__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('test__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 'onlyIf', ->
beforeEach ->
baseText = 'item: Charlie; recipe:; input:Alpha; pattern:...0.0...; '
it 'understands the "item" verb', ->
modVersion = parser.parse baseText + 'onlyIf: item Iron Ingot'
recipes = []
modVersion.eachRecipe (recipe)-> recipes.push recipe
recipe = recipes[0]
recipe.condition.should.eql verb:'item', noun:'Iron Ingot', inverted:false
it 'understands the "mod" verb', ->
modVersion = parser.parse baseText + 'onlyIf: mod BuildCraft'
recipes = []
modVersion.eachRecipe (recipe)-> recipes.push recipe
recipe = recipes[0]
recipe.condition.should.eql verb:'mod', noun:'BuildCraft', inverted:false
it 'understands inverting verbs', ->
modVersion = parser.parse baseText + 'onlyIf: not item Iron Ingot'
recipes = []
modVersion.eachRecipe (recipe)-> recipes.push recipe
recipe = recipes[0]
recipe.condition.should.eql verb:'item', noun:'Iron Ingot', inverted:true
it 'requires at least two words', ->
func = -> parser.parse baseText + 'onlyIf: item'
expect(func).to.throw Error, 'verb followed by a noun'
it 'only allows known verbs', ->
func = -> parser.parse baseText + 'onlyIf: foo Iron Ingot'
expect(func).to.throw Error, 'unknown verb'
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('test__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('test__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', 'charlie', 'test__delta', '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('test__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('test__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: furnace fuel, Potato
pattern: .1. ... .0.
tools: Furnace
item: (filled) Canned Food
recipe:
input: 4 (Empty) Tin Can, Apple
pattern: .1. .0. ...
item: Pyramid
multiblock:
input: Cobblestone
layer: 000 000 000
layer: ... .0. ...
group: Functional Blocks
item: Furnace
recipe:
input: Cobblestone
pattern: 000 0.0 000
tools: Crafting Table
update: Iron Ingot
recipe:
input: furnace fuel, Iron Dust
pattern: .1. ... .0.
tools: Furnace
recipe:
onlyIf: item Redstone Furnace
input: Iron Ore
pattern: ... .0. ...
tools: Redstone 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
@@ -1,19 +0,0 @@
#
# Crafting Guide - tutorial_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
TutorialParserV1 = require './tutorial_parser_v1'
########################################################################################################################
module.exports = class TutorialParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new TutorialParserV1 options
@@ -1,65 +0,0 @@
#
# Crafting Guide - tutorial_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
Tutorial = require '../site/tutorial'
########################################################################################################################
module.exports = class TutorialParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildTutorial rawData, model
_unparseModel: (builder, model)->
@_unparseTutorial builder, model
# Command Methods ##############################################################################
_command_content: (contentParts...)->
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
if @_rawData.currentSection.content? then throw new Error 'duplicate declaration of content'
content = contentParts.join(', ').trim()
if not content.length > 0 then throw new Error 'content cannot be empty'
@_rawData.currentSection.content = content
_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_section: (textParts...)->
@_rawData.sections ?= []
@_rawData.sections.push @_rawData.currentSection = {}
_command_title: (titleParts...)->
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
if @_rawData.currentSection.title? then throw new Error 'duplicate declaration of title'
title = titleParts.join(', ').trim()
if not title.length > 0 then throw new Error 'title cannot be empty'
@_rawData.currentSection.title = title
_command_video: (youTubeId, nameParts...)->
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
name = nameParts.join ', '
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 ######################################################################
_buildTutorial: (rawData, model)->
if not rawData.sections? then throw new Error 'the "section" declaration is required'
model.officialUrl = rawData.officialUrl
model.videos = rawData.videos
model.sections = rawData.sections
@@ -1,51 +0,0 @@
#
# Crafting Guide - versioned_parser_base.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class VersionedParserBase
constructor: (options={})->
@_parsers = @_createParsers options
@_currentSchema = _.chain(@_parsers).keys().last().value()
@errors = []
# 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
@errors = parser.errors
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]
@@ -1,33 +0,0 @@
#
# Crafting Guide - tutorial.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class Tutorial extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.name?.length > 0 then throw new Error "attributes.name cannot be empty"
attributes.modSlug ?= null
attributes.officialUrl ?= null
attributes.sections ?= []
attributes.slug ?= _.slugify attributes.name
attributes.videos ?= []
super attributes, options
# Backbone.Model Overrides #####################################################################
parse: (text)->
TutorialParser = require '../parsing/tutorial_parser' # to avoid require cycles
@_parser ?= new TutorialParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.tutorialData modSlug:@modSlug, tutorialSlug:@slug
-129
View File
@@ -1,129 +0,0 @@
#
# Crafting Guide - base_model.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class BaseModel extends Backbone.Model
@_loadingQueue = []
@_isDraining = false
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)
@fileCache = options.fileCache or null
@loading = null
@logEvents = options.logEvents or false
@state = c.modelState.unloaded
Object.defineProperties this,
isUnloaded: { get:-> @state is c.modelState.unloaded }
isLoading: { get:-> @state is c.modelState.loading }
isLoaded: { get:-> @state is c.modelState.loaded }
isError: { get:-> @state is c.modelState.error }
# Event Methods ################################################################################
onLoadSucceeded: (text, status, xhr)->
try
@set @parse text
@state = c.modelState.loaded
@trigger c.event.change, this
@trigger c.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 = c.modelState.error
logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}"
@trigger c.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 = c.modelState.loading
@trigger c.event.request, this
loadFromServer = =>
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
if @fileCache?
@loading = @fileCache.loading.then =>
if @fileCache.hasFile url
return @_addToLoadingQueue @fileCache.getFile(url), 'success', {url:url}
else
@loading = loadFromServer()
else
@loading = loadFromServer()
@loading.catch (e)-> # do nothing
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}"
# Private Methods ##############################################################################
_addToLoadingQueue: (text, status, xhr)->
deferred = w.defer()
BaseModel._loadingQueue.push resolve:deferred.resolve, func:(=> @onLoadSucceeded text, status, xhr)
@_drainLoadingQueue()
return deferred.promise
_drainLoadingQueue: ->
return if @_isDraining
@_isDraining = true
drainDelay = 50
drain = =>
toLoad = BaseModel._loadingQueue.shift()
if not toLoad?
@_isDraining = false
else
toLoad.func()
toLoad.resolve(true)
_.delay drain, drainDelay
_.delay drain, drainDelay
@@ -1,144 +0,0 @@
#
# Crafting Guide - crafting_plan.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Inventory = require "../game/inventory"
{StringBuilder} = require "crafting-guide-common"
########################################################################################################################
module.exports = class CraftingPlan
constructor: (attributes={})->
@_id = _.uniqueId "crafting-plan-"
@_make = null
@_need = null
@have = attributes.have
@steps = attributes.steps
@want = attributes.want
@_computeResources()
@_consolidateSteps()
# Properties ###################################################################################
Object.defineProperties @prototype,
have: # an Inventory specifying what the player already has
get: -> return @_have
set: (have)->
have ?= new Inventory
if @_have is have then return
if @_have? then throw new Error "have cannot be reassigned"
@_have = new Inventory have
id: # a string uniquely specifying this crafting plan
get: -> return @_id
set: -> throw new Error "id is not assignabled"
make: # an Inventory specifying what the results of executing the plan will be
get: -> return @_make
set: -> throw new Error "make cannot be assigned"
need: # an Inventory specifying what the player will need to gather before executing the plan
get: -> return @_need
set: -> throw new Error "need cannot be assigned"
want: # an Inventory specifying what the player wants to make
get: -> return @_want
set: (want)->
if not want? then throw new Error "want is required"
if want.isEmpty then throw new Error "want cannot be empty"
if @_want is want then return
if @_want? then throw new Error "want cannot be reassigned"
@_want = new Inventory want
steps: # an array of CraftingPlanSteps detailing the steps of the plan
get: -> return @_steps
set: (steps)->
steps ?= []
if steps is @_steps then return
if @_steps? then throw new Error "steps cannot be reassigned"
@_steps = steps
# Object Overrides #############################################################################
toString: (options={})->
options.full ?= false
if options.full
b = new StringBuilder
b.line "CraftingPlan<", @_id, ">"
b.indent()
b.line "have: ", @_have.toString full:true
b.line "want: ", @_want.toString full:true
b.line "need: ", @_need.toString full:true
b.line "steps:"
b.indent()
b.loop @_steps, delimiter:"\n", onEach:(b, step)->
b.push step.count, " × ", step.recipe.toString(full:true)
b.line()
b.outdent()
b.line "make: ", @_make.toString full:true
b.outdent()
return b.toString()
else
return "CraftingPlan:#{@_make.toString(full:true)}<#{@_id}>"
# Private Methods ##############################################################################
_computeResources: ->
need = new Inventory @_want
make = new Inventory @_have
steps = @_steps[..].reverse()
for step in steps
step.count = 0
for productStack in step.recipe.allProducts
continue unless need.contains productStack.item
productCount = Math.ceil need.getQuantity(productStack.item) / productStack.quantity
step.count = Math.max step.count, productCount
continue unless step.count > 0
for itemId, item of step.recipe.inputs
amountNeeded = step.count * step.recipe.computeQuantityRequired item
amountAvailable = make.getQuantity item
amountUsed = Math.min amountAvailable, amountNeeded
amountMissing = amountNeeded - amountUsed
make.remove item, amountUsed
need.add item, amountMissing
for productStack in step.recipe.allProducts
amountCreated = step.count * productStack.quantity
amountNeeded = need.getQuantity productStack.item
amountFulfilled = Math.min amountNeeded, amountCreated
amountSurplus = amountCreated - amountFulfilled
need.remove productStack.item, amountFulfilled
make.add productStack.item, amountSurplus
make.merge @_want
@_make = make
@_need = need
_consolidateSteps: ->
steps = []
stepsByRecipeId = {}
for step in @_steps
priorStep = stepsByRecipeId[step.recipe.id]
if priorStep?
priorStep.count += step.count
else if step.count > 0
steps.push step
stepsByRecipeId[step.recipe.id] = step
@_steps = steps
@@ -1,99 +0,0 @@
#
# Crafting Guide - crafting_plan.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingPlan = require './crafting_plan'
CraftingPlanStep = require './crafting_plan_step'
Inventory = require '../game/inventory'
fixtures = require '../fixtures'
########################################################################################################################
describe "CraftingPlan", ->
beforeEach ->
@mod = fixtures.createMod()
@want = new Inventory
@have = new Inventory
describe "with a single step plan", ->
beforeEach ->
@oakPlank = fixtures.configureOakPlank @mod
@want.add @oakPlank, 11
@plan = new CraftingPlan want:@want, steps:[
new CraftingPlanStep @oakPlank.firstRecipe
]
it "correctly computes the step counts", ->
@plan.steps[0].count.should.equal 3
it "correctly determines the inputs needed", ->
@plan.need.toString(full:true).should.equal "3 Oak Wood"
it "correctly computes the products created", ->
@plan.make.toString(full:true).should.equal "12 Oak Planks"
describe "with a multi-step plan", ->
beforeEach ->
@ironIngot = fixtures.configureIronIngot @mod
@ironSword = fixtures.configureIronSword @mod
@oakPlank = fixtures.configureOakPlank @mod
@stick = fixtures.configureStick @mod
@want.add @ironSword, 20
@plan = new CraftingPlan want:@want, steps:[
new CraftingPlanStep @oakPlank.firstRecipe
new CraftingPlanStep @stick.firstRecipe
new CraftingPlanStep @ironIngot.firstRecipe
new CraftingPlanStep @ironSword.firstRecipe
]
it "correctly computes the step counts", ->
@plan.steps[0].count.should.equal 3
@plan.steps[1].count.should.equal 5
@plan.steps[2].count.should.equal 5
@plan.steps[3].count.should.equal 20
it "correctly determines the inputs needed", ->
@plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore, 3 Oak Wood"
it "correctly computes the products created", ->
@plan.make.toString(full:true).should.equal "20 Iron Sword, 2 Oak Planks"
describe "with a plan which recycles some items", ->
beforeEach ->
@bucket = fixtures.configureBucket @mod
@cake = fixtures.configureCake @mod
@ironIngot = fixtures.configureIronIngot @mod
@milkBucket = fixtures.configureMilkBucket @mod
@sugar = fixtures.configureSugar @mod
@want.add @cake, 2
@plan = new CraftingPlan want:@want, steps:[
new CraftingPlanStep @ironIngot.firstRecipe
new CraftingPlanStep @bucket.firstRecipe
new CraftingPlanStep @milkBucket.firstRecipe
new CraftingPlanStep @sugar.firstRecipe
new CraftingPlanStep @cake.firstRecipe
]
it "correctly computes the step counts", ->
@plan.steps.length.should.equal 3
@plan.steps[0].count.should.equal 6
@plan.steps[1].count.should.equal 4
@plan.steps[2].count.should.equal 2
it "correctly determines the inputs needed", ->
@plan.need.toString(full:true).should.equal "2 Egg, 6 Milk, 4 Sugar Cane, 6 Wheat"
it "correctly computes the products created", ->
@plan.make.toString(full:true).should.equal "2 Cake"
@@ -1,39 +0,0 @@
#
# Crafting Guide - crafting_plan_step.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class CraftingPlanStep
constructor: (recipe, count=1)->
@_id = _.uniqueId "crafting-plan-step-"
@recipe = recipe
@count = count
# Properties ###################################################################################
Object.defineProperties @prototype,
recipe:
get: -> return @_recipe
set: (recipe)->
if not recipe? then throw new Error 'recipe is required'
if @_recipe is recipe then return
if @_recipe? then throw new Error 'recipe cannot be reassigned'
@_recipe = recipe
count:
get: -> return @_count
set: (count)->
count = parseInt "#{count}"
count = if Number.isNaN(count) then 0 else Math.max 0, count
@_count = count
# Object Overrides #############################################################################
toString: ->
return "CraftingPlanStep:#{@_recipe}×#{@_count}<#{@_id}>"
@@ -1,105 +0,0 @@
#
# Crafting Guide - evaluation.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class Evaluation
constructor: (attributes={})->
@evaluator = attributes.evaluator
@item = attributes.item if attributes.item?
@recipe = attributes.recipe if attributes.recipe?
@baseScore = attributes.baseScore
@_id = _.uniqueId "evaluation-"
@_includedTools = {}
@_toolScore = null
# Properties ###################################################################################
Object.defineProperties @prototype,
baseScore:
get: -> return @_baseScore
set: (baseScore)->
baseScore = parseFloat "#{baseScore}"
baseScore = if Number.isNaN(baseScore) then null else baseScore
@_baseScore = baseScore
evaluator:
get: -> return @_evaluator
set: (evaluator)->
if not evaluator? then throw new Error "evaluator is required"
if @_evaluator is evaluator then return
if @_evaluator? then throw new Error "evaluator cannot be reassigned"
@_evaluator = evaluator
includedTools:
get: -> return @_includedTools
set: -> throw new Error "includedTools cannot be assigned"
item:
get: -> return @_item
set: (item)->
if @_recipe? then throw new Error "this evaluation is for a recipe: cannot set an item"
if @_item is item then return
if not item? then throw new Error "item cannot be assigned null"
if @_item? then throw new Error "item cannot be reassigned"
@_item = item
recipe:
get: -> return @_recipe
set: (recipe)->
if @_item? then throw new Error "this evaluation is for an item: cannot set an recipe"
if @_recipe is recipe then return
if not recipe? then throw new Error "recipe cannot be assigned null"
if @_recipe? then throw new Error "recipe cannot be reassigned"
@_recipe = recipe
toolScore:
get: -> @_computeToolScore()
set: -> throw new Error "toolScore cannot be assigned"
# Public Methods ###############################################################################
addIncludedTool: (item)->
return if @_includedTools[item.id]?
@_includedTools[item.id] = item
@_toolScore = null
addIncludedToolsFrom: (evaluation)->
for id, toolItem of evaluation.includedTools
@addIncludedTool toolItem
computeTotalScore: (quantity=1)->
if @item?
multiplier = quantity
else if @recipe?
multiplier = Math.ceil 1.0 * quantity / @recipe.output.quantity
return @baseScore * multiplier + @toolScore
isToolIncluded: (item)->
return @_includedTools[item.id]?
# Object Overrides #############################################################################
toString: ->
obj = if @item? then @item else @recipe
return "#{@evaluator.constructor.name}:#{obj}@#{@baseScore}<#{@_id}>"
# Private Methods ##############################################################################
_computeToolScore: ->
if not @_toolScore?
@_toolScore = 0
for id, toolItem of @_includedTools
evaluation = @evaluator.evaluateItem toolItem
continue unless evaluation?.baseScore?
@_toolScore += evaluation.baseScore
return @_toolScore
-102
View File
@@ -1,102 +0,0 @@
#
# Crafting Guide - evaluator.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Evaluation = require './evaluation'
########################################################################################################################
module.exports = class Evaluator
constructor: ->
@_id = _.uniqueId "evaluator-"
@_evaluations = {}
# Property Methods #############################################################################
Object.defineProperties @prototype,
id:
get: -> return @_id
set: -> throw new Error "id cannot be assigned"
# Public Methods ###############################################################################
evaluateItem: (item)->
return null unless item?
evaluation = @_evaluations[item.id]
if not evaluation?
evaluation = @_evaluations[item.id] = new Evaluation evaluator:this, item:item
recipeEvaluation = @_findBestRecipeEvaluationFor item
if recipeEvaluation?
evaluation.baseScore = recipeEvaluation.baseScore
evaluation.addIncludedToolsFrom recipeEvaluation
else
@_computeGatherableItemScore item, evaluation
return evaluation
evaluateRecipe: (recipe)->
return null unless recipe?
evaluation = @_evaluations[recipe.id]
if not evaluation?
evaluation = @_evaluations[recipe.id] = new Evaluation evaluator:this, recipe:recipe
@_computeRecipeScore recipe, evaluation
for id, item of recipe.inputs
inputEvaluation = @evaluateItem item
evaluation.addIncludedToolsFrom inputEvaluation
for id, toolItem of recipe.tools
evaluation.addIncludedTool toolItem
evaluation.addIncludedToolsFrom @evaluateItem toolItem
return evaluation
getOrderedRecipes: (item, quantity=1)->
recipes = (recipe for recipeId, recipe of item.recipes)
recipes.sort (a, b)=>
scoreA = @evaluateRecipe(a).computeTotalScore quantity
scoreB = @evaluateRecipe(b).computeTotalScore quantity
if scoreA isnt scoreB
return if scoreA < scoreB then -1 else +1
return 0
return recipes
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name}<#{@id}>"
# Overrideable Methods #########################################################################
_computeGatherableItemScore: (item, evaluation)->
throw new Error "#{@constructor.name} must override _computeGatherableItemScore"
_computeRecipeScore: (recipe, evaluation)->
throw new Error "#{@constructor.name} must override _computeRecipeScore"
# Private Methods ##############################################################################
_findBestRecipeEvaluationFor: (item)->
return null if item.isGatherable
result = null
for id, recipe of item.recipes
evaluation = @evaluateRecipe recipe
continue unless evaluation?.baseScore?
if not result? then result = evaluation
if evaluation.baseScore < result.baseScore then result = evaluation
return result
@@ -1,92 +0,0 @@
#
# Crafting Guide - plan_builder.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingPlan = require "./crafting_plan"
CraftingPlanStep = require "./crafting_plan_step"
########################################################################################################################
module.exports = class PlanBuilder
constructor: (evaluator)->
@evaluator = evaluator
# Properties ###################################################################################
Object.defineProperties @prototype,
evaluator:
get: -> return @_evaluator
set: (evaluator)->
if not evaluator? then throw new Error "evaluator is required"
if @_evaluator is evaluator then return
if @_evaluator? then throw new Error "evaluator cannot be reassigned"
@_evaluator = evaluator
# Public Methods ###############################################################################
createPlan: (want, have)->
@_alreadyMaking = {}
@_recipesInUse = {}
stepList = []
for itemId, stack of want.stacks
steps = @_findStepsForItem stack.item, stack.quantity
return null unless steps?
stepList.push steps
plan = new CraftingPlan want:want, have:have, steps:_.flatten(stepList)
return plan
# Private Methods ##############################################################################
_findStepsForItem: (item, quantity=1)->
steps = null
if not @_alreadyMaking[item.id]
@_alreadyMaking[item.id] = true
recipes = @_evaluator.getOrderedRecipes item, quantity
if recipes.length is 0
steps = []
else
for recipe in recipes
continue if @_recipesInUse[recipe.id]?
steps = @_findStepsForRecipe recipe, quantity
break if steps?
delete @_alreadyMaking[item.id]
return steps
_findStepsForRecipe: (recipe, quantity=1)->
steps = null
if not @_recipesInUse[recipe.id]
@_recipesInUse[recipe.id] = true
invalidRecipe = false
for itemId, item of recipe.inputs
inputSteps = @_findStepsForItem item, quantity * recipe.computeQuantityRequired(item)
if not inputSteps?
invalidRecipe = true
break
steps ?= []
for step in inputSteps
steps.push step
if invalidRecipe
steps = null
else
steps.push new CraftingPlanStep recipe
delete @_recipesInUse[recipe.id]
return steps
@@ -1,158 +0,0 @@
#
# Crafting Guide - plan_builder.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
fixtures = require '../fixtures'
Inventory = require '../game/inventory'
PlanBuilder = require './plan_builder'
ResourcesEvaluator = require './resources_evaluator'
########################################################################################################################
describe "PlanBuilder", ->
beforeEach ->
@planner = new PlanBuilder new ResourcesEvaluator
@mod = fixtures.createMod()
@want = new Inventory
@have = new Inventory
describe "for a gatherable item, creates a plan that", ->
beforeEach ->
@oakWood = fixtures.configureOakWood @mod
@want.add @oakWood, 2
@plan = @planner.createPlan @want
it "has no steps at all", ->
@plan.steps.length.should.equal 0
it "demands the item itself as the input", ->
@plan.need.toString(full:true).should.equal "2 Oak Wood"
it "produces the item as the result", ->
@plan.make.toString(full:true).should.equal "2 Oak Wood"
describe "for a single item with a one-step recipe, it creates a plan that", ->
beforeEach ->
@oakPlank = fixtures.configureOakPlank @mod
@want.add @oakPlank, 63
@plan = @planner.createPlan @want
it "has the recipe as the only step", ->
@plan.steps.length.should.equal 1
@plan.steps[0].recipe.output.item.displayName.should.equal "Oak Planks"
it "demands the right inputs", ->
@plan.need.toString(full:true).should.equal "16 Oak Wood"
it "produces the right results", ->
@plan.make.toString(full:true).should.equal "64 Oak Planks"
describe "for a single item with a complex recipe, it creates a plan that", ->
beforeEach ->
@ironSword = fixtures.configureIronSword @mod
@want.add @ironSword, 2
@plan = @planner.createPlan @want
it "has the right steps", ->
@plan.steps.length.should.equal 4
@plan.steps[0].recipe.output.item.displayName.should.equal "Iron Ingot"
@plan.steps[1].recipe.output.item.displayName.should.equal "Oak Planks"
@plan.steps[2].recipe.output.item.displayName.should.equal "Stick"
@plan.steps[3].recipe.output.item.displayName.should.equal "Iron Sword"
it "demands the right inputs", ->
@plan.need.toString(full:true).should.equal "1 Coal, 8 Iron Ore, 1 Oak Wood"
it "produces the right results", ->
@plan.make.toString(full:true).should.equal "4 Iron Ingot, 2 Iron Sword, 2 Oak Planks, 2 Stick"
describe "for multiple items with complex recipes, it creates a plan that", ->
beforeEach ->
@ironSword = fixtures.configureIronSword @mod
@ironShovel = fixtures.configureIronShovel @mod
@want.add @ironSword, 3
@want.add @ironShovel, 3
@plan = @planner.createPlan @want
it "has consolidated the common steps", ->
(step.recipe.output.item.displayName for step in @plan.steps).should.eql [
"Iron Ingot", "Oak Planks", "Stick", "Iron Sword", "Iron Shovel"
]
it "requires the correct repetitions for each step", ->
(step.count for step in @plan.steps).should.eql [ 2, 2, 3, 3, 3 ]
it "demands the right inputs", ->
@plan.need.toString(full:true).should.equal "2 Coal, 16 Iron Ore, 2 Oak Wood"
it "produces the right results", ->
@plan.make.toString(full:true).should.equal(
"7 Iron Ingot, 3 Iron Shovel, 3 Iron Sword, 2 Oak Planks, 3 Stick"
)
describe "for an item which has a recursive recipe, it creates a plan that", ->
beforeEach ->
@ironBlock = fixtures.configureIronBlock @mod
@want.add @ironBlock, 4
@plan = @planner.createPlan @want
it "avoids the recursive options", ->
(step.recipe.output.item.displayName for step in @plan.steps).should.eql [
"Iron Ingot", "Iron Block"
]
it "requires the correct repetitions for each step", ->
(step.count for step in @plan.steps).should.eql [ 5, 4 ]
it "demands the right inputs", ->
@plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore"
it "produces the right results", ->
@plan.make.toString(full:true).should.equal(
"4 Iron Block, 4 Iron Ingot"
)
describe "when making enormous quantities of something simple", ->
beforeEach ->
@oakPlank = fixtures.configureOakPlank @mod
@want.add @oakPlank, Number.MAX_VALUE / 2
@start = Date.now()
@plan = @planner.createPlan @want
@duration = Date.now() - @start
it "doesn't take forever to create the plan", ->
@duration.should.be.lessThan 10
describe "when making an item which can be made with and without a tool, for a", ->
beforeEach ->
@oakPlank = fixtures.configureOakPlank @mod
@saw = fixtures.configureSaw @mod
describe "small batch, the plan", ->
beforeEach ->
@want.add @oakPlank, 2
@plan = @planner.createPlan @want
it "avoids the expensive tool", ->
(step.recipe.needsTools for step in @plan.steps).should.eql [ false ]
describe "large batch, the plan", ->
beforeEach ->
@want.add @oakPlank, 2048
@plan = @planner.createPlan @want
it "uses the tool for efficiency", ->
(step.recipe.needsTools for step in @plan.steps).should.eql [ true ]
@@ -1,40 +0,0 @@
#
# Crafting Guide - resources_evaluator.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Evaluator = require './evaluator'
########################################################################################################################
module.exports = class ResourcesEvaluator extends Evaluator
# Evaluator Overrides ##########################################################################
_computeRecipeScore: (recipe, evaluation)->
evaluation.baseScore = 0
for x in [0...recipe.width]
for y in [0...recipe.height]
for z in [0...recipe.depth]
stack = recipe.getInputAt x, y, z
continue unless stack?
inputEvaluation = @evaluateItem stack.item
if inputEvaluation?.baseScore?
evaluation.baseScore += inputEvaluation.baseScore * stack.quantity
else
evaluation.baseScore = null
return
for id, extraStack of recipe.extras
extraEvaluation = @evaluateItem extraStack.item
continue unless extraEvaluation?.baseScore?
evaluation.baseScore -= extraStack.quantity * extraEvaluation.baseScore
evaluation.baseScore = evaluation.baseScore / recipe.output.quantity
_computeGatherableItemScore: (item, evaluation)->
evaluation.baseScore = 1
@@ -1,129 +0,0 @@
#
# Crafting Guide - resources_evaluator.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ResourcesEvaluator = require './resources_evaluator'
fixtures = require '../fixtures'
########################################################################################################################
describe "ResourcesEvaluator", ->
beforeEach ->
@evaluator = new ResourcesEvaluator
@mod = fixtures.createMod()
describe 'evaluating a gatherable item', ->
beforeEach ->
@cobblestone = fixtures.configureCobblestone @mod
@evaluation = @evaluator.evaluateItem @cobblestone
it 'should return a score of 1 for each item', ->
# 1 cobblestone
@evaluation.computeTotalScore().should.equal 1
describe 'evaluating a first-level recipe', ->
beforeEach ->
@craftingTable = fixtures.configureCraftingTable @mod
@evaluation = @evaluator.evaluateItem @craftingTable
it 'should return a score which is the sum of all resources', ->
# 1 oak wood => 4 planks (1/4)
# 4 oak planks => 1 crafting table (4 * 1/4 = 1)
@evaluation.computeTotalScore().should.equal 1
describe 'evaluating a recipe which requires a tool', ->
beforeEach ->
@furnace = fixtures.configureFurnace @mod
@evaluation = @evaluator.evaluateItem @furnace
it 'includes the cost of the tool', ->
# 1 oak wood => 4 planks (1/4)
# 4 oak planks => 1 crafting table (4 * 1/4 = 1)
# 8 cobblestone =(crafting table)=> 1 furnace (8 + 1 = 9)
@evaluation.computeTotalScore().should.equal 9
describe 'evaluating a recipe where a tool requires a tool', ->
beforeEach ->
@ironIngot = fixtures.configureIronIngot @mod
@evaluation = @evaluator.evaluateItem @ironIngot
it 'should include both tools in the score', ->
# raw ingredients:
# 8 iron ore + 1 coal =(furnace)=> 8 iron ingots ((8 + 1) / 8 = 1.125)
# tools:
# 8 cobblestone =(crafting table)=> 1 furnace (8)
# 4 oak planks => 1 crafting table (4 * 1/4 = 1)
# 1 oak wood => 4 planks (1/4)
@evaluation.computeTotalScore().should.equal 10.125
describe 'evaluating a recipe which requires a tool multiple times', ->
beforeEach ->
@ironSword = fixtures.configureIronSword @mod
@evaluation = @evaluator.evaluateItem @ironSword
it 'should include the tool only once', ->
# raw ingredients:
# 1 oak wood ==> 4 planks (0.25)
# 2 oak planks ==> 4 sticks (0.125)
# 8 iron ore + 1 coal =(furnace)=> 8 iron ingots (1.125)
# 2 iron ingot + 1 stick =(crafting table)=> 1 iron sword (2.375)
#
# tools:
# 8 cobblestone =(crafting table)=> 1 furnace (8)
#
# 1 oak wood => 4 planks (0.25)
# 4 oak planks => 1 crafting table (1)
#
@evaluation.computeTotalScore().should.equal 11.375
describe 'evaluating a recipe which has a recursive recipe', ->
beforeEach ->
@ironBlock = fixtures.configureIronBlock @mod
@evaluation = @evaluator.evaluateItem @ironBlock
it 'should avoid the recursive recipes', ->
# raw ingredients:
# 8 iron ore + 1 coal =(furnace)=> 8 iron ingots ((8 + 1) / 8 = 1.125)
# 9 iron ingot =(crafting table)=> 1 iron block (10.125)
#
# tools:
# 8 cobblestone =(crafting table)=> 1 furnace (8)
# 4 oak planks => 1 crafting table (4 * 1/4 = 1)
# 1 oak wood => 4 planks (1/4)
@evaluation.computeTotalScore().should.equal 19.125
describe 'evaluating a recipe with multiple ouputs', ->
beforeEach ->
@cake = fixtures.configureCake @mod
@evaluation = @evaluator.evaluateItem @cake
it 'should discount the extra outputs', ->
# raw ingredients:
# 8 iron ore + 1 coal =(furnace)=> 8 iron ingot (1.125)
# 3 iron ingot =(crafting table)=> 1 bucket (3.375)
# 1 milk + 1 bucket ==> 1 milk bucket (4.375)
# 1 sugar cane ==> 1 sugar (1)
# 3 wheat + 1 egg + 2 sugar + 3 milk bucket =(crafting table)=> 1 cake, 3 buckets (9)
#
# furnace:
# 8 cobblestone =(crafting table)=> 1 furnace (8)
#
# crafting table
# 1 oak wood ==> 4 oak planks (0.25)
# 4 planks ==> 1 crafting table (1)
@evaluation.computeTotalScore().should.equal 18
describe 'evaluating a recipe which is only ever made as an extra', ->
it 'should still be able to come up with an evaluation'
@@ -1,41 +0,0 @@
#
# Crafting Guide - steps_evaluator.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Evaluator = require './evaluator'
########################################################################################################################
module.exports = class StepsEvaluator extends Evaluator
# Evaluator Overrides ##########################################################################
_computeRecipeScore: (recipe, evaluation)->
evaluation.score = 0
for id, stack of recipe.inputs
inputEvaluation = @evaluateItem stack.item
if not inputEvaluation?.score?
evaluation.score = null
return
evaluation.score = Math.min evaluation.score, inputEvaluation.score + 1
for id, item of recipe.tools
continue if evaluation.isToolIncluded item
toolEvaluation = @evaluateItem stack.item
if not toolEvaluation?.score?
evaluation.score = null
return
evaluation.addIncludedTool item
evaluation.score += toolEvaluation.score
return result
_computeGatherableItemScore: (item, evaluation)->
evaluation.score = 0
-36
View File
@@ -1,36 +0,0 @@
#
# Crafting Guide - event_recorder.coffee
#
# Copyright © 2014-2016 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 = []
@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
# Public Methods ###############################################################################
reset: ->
@events = []
# Property Methods #############################################################################
getNames: ->
return (e.event for e in @events)
Object.defineProperties @prototype,
names: {get:@prototype.getNames}
-319
View File
@@ -1,319 +0,0 @@
#
# Crafting Guide - fixtures.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require "./game/item"
Mod = require "./game/mod"
ModPack = require "./game/mod_pack"
Recipe = require "./game/recipe"
Stack = require "./game/stack"
# Instance Creation Fixtures ###########################################################################################
exports.createModPack = createModPack = (attributes={})->
attributes.id ?= _.uniqueId "mod-pack-"
attributes.displayName ?= "Test ModPack"
return new ModPack attributes
exports.createMod = createMod = (attributes={})->
attributes.modPack ?= createModPack()
attributes.id ?= _.uniqueId "mod-"
attributes.displayName ?= "Test Mod"
return new Mod attributes
exports.createItem = createItem = (attributes={})->
attributes.mod ?= createMod()
attributes.id ?= _.uniqueId "item-"
attributes.displayName ?= _.uniqueId "Test Item "
return new Item attributes
exports.createRecipe = createRecipe = (attributes={})->
attributes.id ?= _.uniqueId "recipe-"
attributes.output ?= createItem()
return new Recipe attributes
exports.createStack = createStack = (attributes={})->
attributes.item ?= createItem()
attributes.quantity ?= 1
return new Stack attributes
# Item Configuration Fixtures ##########################################################################################
exports.configureBucket = configureBucket = (mod)->
bucket = mod.modPack.findItem "bucket"
if not bucket?
ironIngot = configureIronIngot mod
bucket = createItem mod:mod, id:"bucket", displayName:"Bucket"
craftingTable = configureCraftingTable mod
recipe = createRecipe output:createStack item:bucket
recipe.setInputAt 0, 0, createStack item:ironIngot
recipe.setInputAt 1, 1, createStack item:ironIngot
recipe.setInputAt 2, 0, createStack item:ironIngot
recipe.addTool craftingTable
return bucket
exports.configureCake = configureCake = (mod)->
cake = mod.modPack.findItem "cake"
if not cake?
bucket = configureBucket mod
cake = createItem mod:mod, id:"cake", displayName:"Cake"
craftingTable = configureCraftingTable mod
egg = configureEgg mod
milkBucket = configureMilkBucket mod
sugar = configureSugar mod
wheat = configureWheat mod
recipe = createRecipe output:createStack item:cake
recipe.setInputAt 0, 0, createStack item:milkBucket
recipe.setInputAt 1, 0, createStack item:milkBucket
recipe.setInputAt 2, 0, createStack item:milkBucket
recipe.setInputAt 0, 1, createStack item:sugar
recipe.setInputAt 1, 1, createStack item:egg
recipe.setInputAt 2, 1, createStack item:sugar
recipe.setInputAt 0, 2, createStack item:wheat
recipe.setInputAt 1, 2, createStack item:wheat
recipe.setInputAt 2, 2, createStack item:wheat
recipe.addTool craftingTable
recipe.addExtra createStack item:bucket, quantity:3
return cake
exports.configureCoal = configureCoal = (mod)->
coal = mod.modPack.findItem "coal"
if not coal?
coal = createItem mod:mod, id:"coal", displayName:"Coal"
return coal
exports.configureCobblestone = configureCobblestone = (mod)->
cobblestone = mod.modPack.findItem "cobblestone"
if not cobblestone?
cobblestone = createItem mod:mod, displayName:"Cobblestone"
return cobblestone
exports.configureCraftingTable = configureCraftingTable = (mod)->
craftingTable = mod.modPack.findItem "crafting_table"
if not craftingTable?
craftingTable = createItem mod:mod, id:"crafting_table", displayName:"Crafting Table"
oakPlanks = configureOakPlank mod
recipe = createRecipe output:createStack item:craftingTable
recipe.setInputAt 0, 0, createStack item:oakPlanks
recipe.setInputAt 1, 0, createStack item:oakPlanks
recipe.setInputAt 0, 1, createStack item:oakPlanks
recipe.setInputAt 1, 1, createStack item:oakPlanks
return craftingTable
exports.configureEgg = configureEgg = (mod)->
egg = mod.modPack.findItem "egg"
if not egg?
egg = createItem mod:mod, id:"egg", displayName:"Egg"
return egg
exports.configureFurnace = configureFurnace = (mod)->
furnace = mod.modPack.findItem "furnace"
if not furnace?
cobblestone = configureCobblestone mod
craftingTable = configureCraftingTable mod
furnace = createItem mod:mod, displayName:"Furnace"
recipe = createRecipe output:createStack item:furnace
recipe.setInputAt 0, 0, createStack item:cobblestone
recipe.setInputAt 1, 0, createStack item:cobblestone
recipe.setInputAt 2, 0, createStack item:cobblestone
recipe.setInputAt 0, 1, createStack item:cobblestone
recipe.setInputAt 2, 1, createStack item:cobblestone
recipe.setInputAt 0, 2, createStack item:cobblestone
recipe.setInputAt 1, 2, createStack item:cobblestone
recipe.setInputAt 2, 2, createStack item:cobblestone
recipe.addTool craftingTable
return furnace
exports.configureIronIngot = configureIronIngot = (mod)->
ironIngot = mod.modPack.findItem "iron_ingot"
if not ironIngot?
coal = configureCoal mod
furnace = configureFurnace mod
ironIngot = createItem mod:mod, id:"iron_ingot", displayName:"Iron Ingot"
ironOre = configureIronOre mod
recipe = createRecipe output:createStack item:ironIngot, quantity:8
recipe.setInputAt 1, 0, createStack item:ironOre, quantity:8
recipe.setInputAt 1, 2, createStack item:coal
recipe.addTool furnace
return ironIngot
exports.configureIronBlock = configureIronBlock = (mod)->
ironBlock = mod.modPack.findItem "iron_block"
if not ironBlock?
craftingTable = configureCraftingTable mod
ironBlock = createItem mod:mod, id:"iron_block", displayName:"Iron Block"
ironIngot = configureIronIngot mod
recipe = createRecipe output:createStack item:ironBlock
for x in [0..2]
for y in [0..2]
recipe.setInputAt x, y, createStack item:ironIngot
recipe.addTool craftingTable
recipe = createRecipe output:createStack item:ironIngot, quantity:9
recipe.setInputAt 1, 1, createStack item:ironBlock
return ironBlock
exports.configureIronSword = configureIronSword = (mod)->
ironSword = mod.modPack.findItem "iron_sword"
if not ironSword?
craftingTable = configureCraftingTable mod
ironIngot = configureIronIngot mod
ironSword = createItem mod:mod, id:"iron_sword", displayName:"Iron Sword"
stick = configureStick mod
recipe = createRecipe output:createStack item:ironSword
recipe.setInputAt 1, 0, createStack item:ironIngot
recipe.setInputAt 1, 1, createStack item:ironIngot
recipe.setInputAt 1, 2, createStack item:stick
recipe.addTool craftingTable
return ironSword
exports.configureIronShovel = configureIronShovel = (mod)->
ironShovel = mod.modPack.findItem "iron_shovel"
if not ironShovel?
craftingTable = configureCraftingTable mod
ironIngot = configureIronIngot mod
ironShovel = createItem mod:mod, id:"iron_shovel", displayName:"Iron Shovel"
stick = configureStick mod
recipe = createRecipe output:createStack item:ironShovel
recipe.setInputAt 1, 0, createStack item:ironIngot
recipe.setInputAt 1, 1, createStack item:stick
recipe.setInputAt 1, 2, createStack item:stick
recipe.addTool craftingTable
return ironShovel
exports.configureIronOre = configureIronOre = (mod)->
ironOre = mod.modPack.findItem "iron_ore"
if not ironOre?
ironOre = createItem mod:mod, id:"iron_ore", displayName:"Iron Ore"
return ironOre
exports.configureMilk = configureMilk = (mod)->
milk = mod.modPack.findItem "milk"
if not milk?
milk = createItem mod:mod, id:"milk", displayName:"Milk"
return milk
exports.configureMilkBucket = configureMilkBucket = (mod)->
milkBucket = mod.modPack.findItem "milk_bucket"
if not milkBucket?
bucket = configureBucket mod
milk = configureMilk mod
milkBucket = createItem mod:mod, id:"milkBucket", displayName:"Milk Bucket"
recipe = createRecipe output:createStack item:milkBucket
recipe.setInputAt 1, 0, createStack item:milk
recipe.setInputAt 1, 1, createStack item:bucket
return milkBucket
exports.configureOakPlank = configureOakPlank = (mod)->
oakPlanks = mod.modPack.findItem "oak_planks"
if not oakPlanks?
oakPlanks = createItem mod:mod, id:"oak_planks", displayName:"Oak Planks"
oakWood = configureOakWood mod
recipe = createRecipe output:createStack item:oakPlanks, quantity:4
recipe.setInputAt 1, 1, createStack item:oakWood
return oakPlanks
exports.configureOakWood = configureOakWood = (mod)->
oakWood = mod.modPack.findItem "oak_wood"
if not oakWood?
oakWood = createItem mod:mod, id:"oak_wood", displayName:"Oak Wood"
return oakWood
exports.configureObsidian = configureObsidian = (mod)->
obsidian = mod.modPack.findItem "obsidian"
if not obsidian?
obsidian = createItem mod:mod, id:"obsidian", displayName:"Obsidian"
return obsidian
exports.configureRedstoneDust = configureRedstoneDust = (mod)->
redstoneDust = mod.modPack.findItem "redstone_dust"
if not redstoneDust?
redstoneDust = createItem mod:mod, id:"redstone_dust", displayName:"Redstone Dust"
return redstoneDust
exports.configureStick = configureStick = (mod)->
stick = mod.modPack.findItem "stick"
if not stick?
oakPlanks = configureOakPlank mod
stick = createItem mod:mod, id:"stick", displayName:"Stick"
recipe = createRecipe output:createStack item:stick, quantity:4
recipe.setInputAt 0, 0, createStack item:oakPlanks
recipe.setInputAt 0, 1, createStack item:oakPlanks
return stick
exports.configureSugar = configureSugar = (mod)->
sugar = mod.modPack.findItem "sugar"
if not sugar?
sugar = createItem mod:mod, id:"sugar", displayName:"Sugar"
sugarCane = configureSugarCane mod
recipe = createRecipe output:createStack item:sugar
recipe.setInputAt 1, 1, createStack item:sugarCane
return sugar
exports.configureSaw = configureSaw = (mod)->
saw = mod.modPack.findItem "saw"
if not saw?
craftingTable = configureCraftingTable mod
ironBlock = configureIronBlock mod
ironIngot = configureIronIngot mod
oakPlank = configureOakPlank mod
oakWood = configureOakWood mod
redstoneDust = configureRedstoneDust mod
saw = createItem mod:mod, id:"saw", displayName:"Saw"
recipe = createRecipe output:createStack item:saw
recipe.setInputAt 0, 0, createStack item:oakPlank
recipe.setInputAt 1, 0, createStack item:ironIngot
recipe.setInputAt 2, 0, createStack item:oakPlank
recipe.setInputAt 0, 1, createStack item:oakPlank
recipe.setInputAt 1, 1, createStack item:ironBlock
recipe.setInputAt 2, 1, createStack item:oakPlank
recipe.setInputAt 0, 2, createStack item:oakPlank
recipe.setInputAt 1, 2, createStack item:redstoneDust
recipe.setInputAt 2, 2, createStack item:oakPlank
recipe.addTool craftingTable
recipe = createRecipe output:createStack item:oakPlank, quantity:8
recipe.setInputAt 1, 1, createStack item:oakWood
recipe.addTool saw
return saw
exports.configureSugarCane = configureSugarCane = (mod)->
sugarCane = mod.modPack.findItem "sugar_cane"
if not sugarCane?
sugarCane = createItem mod:mod, id:"sugar_cane", displayName:"Sugar Cane"
return sugarCane
exports.configureWheat = configureWheat = (mod)->
wheat = mod.modPack.findItem "wheat"
if not wheat?
wheat = createItem mod:mod, id:"wheat", displayName:"Wheat"
return wheat
-90
View File
@@ -1,90 +0,0 @@
#
# Crafting Guide - inventory.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Stack = require './stack'
########################################################################################################################
module.exports = class Inventory
constructor: (inventory=null)->
@_id = _.uniqueId "inventory-"
@_stacks = {}
if inventory? then @merge inventory
# Properties ###################################################################################
Object.defineProperties @prototype,
isEmpty:
get: -> (id for id, stack of @_stacks).length is 0
stacks:
get: -> return @_stacks
set: -> throw new Error "stacks cannot be replaced"
# Public Methods ###############################################################################
add: (item, quantity)->
return unless item?
return if quantity is 0
existingStack = @_stacks[item.id]
if existingStack?
if existingStack.quantity + quantity < 0 then throw new Error "cannot have a negative quantity"
existingStack.quantity += quantity
else
if quantity < 0 then throw new Error "cannot have a negative quantity"
@_stacks[item.id] = new Stack item:item, quantity:quantity
if @_stacks[item.id].quantity is 0
delete @_stacks[item.id]
clear: ->
@_stacks = {}
contains: (item)->
return @_stacks[item.id]?
getQuantity: (item)->
existingStack = @_stacks[item.id]
return 0 unless existingStack?
return existingStack.quantity
merge: (inventory)->
for id, stack of inventory.stacks
@add stack.item, stack.quantity
remove: (item, quantity)->
@add item, -1 * quantity
# Object Overrides #############################################################################
toString: (options={})->
options.full ?= false
if options.full
result = []
needsDelimiter = false
stackList = (stack for itemId, stack of @_stacks)
stackList.sort (a, b)->
if a.item.displayName isnt b.item.displayName
return if a.item.displayName < b.item.displayName then -1 else +1
return 0
for stack in stackList
if needsDelimiter then result.push ", "
needsDelimiter = true
result.push stack.quantity
result.push " "
result.push stack.item.displayName
return result.join ""
else
return "Inventory<#{@_id}>@#{(id for id, item of @_stacks).length}"
-96
View File
@@ -1,96 +0,0 @@
#
# Crafting Guide - item.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class Item
constructor: (attributes={})->
@id = attributes.id
@displayName = attributes.displayName
@isGatherable = attributes.isGatherable
@mod = attributes.mod
@_hasPrimaryRecipe = false
@_recipesAsPrimary = {}
@_recipesAsExtra = {}
# Property Methods #############################################################################
Object.defineProperties @prototype,
displayName: # a string containing the user-facing name of this item
get: -> return @_displayName
set: (displayName)->
if not displayName? then throw new Error "displayName is required"
@_displayName = displayName
firstRecipe: # the first Recipe returned by iterating the `recipes` property
get: ->
recipeList = (recipe for id, recipe of @recipes)
return null unless recipeList.length > 0
return recipeList[0]
set: -> throw new Error "firstRecipe cannot be assigned"
id: # a string containing a unique identifier for this item
get: -> return @_id
set: (id)->
if not id? then throw new Error "id is required"
return if @_id is id
if @_id? then throw new Error "id cannot be reassigned"
@_id = id
isGatherable: # whether this item can be gathered directly without needing to be crafted
get: ->
return true if @_isGatherable is true
return false if (id for id, recipe of @_recipesAsPrimary).length > 0
return false if (id for id, recipe of @_recipesAsExtra).length > 0
return true
set: (isGatherable)->
@_isGatherable = null unless isGatherable?
@_isGatherable = !!isGatherable
mod: # the Mod which adds this item to the game
get: -> return @_mod
set: (mod)->
if not mod? then throw new Error "mod is required"
if @_mod is mod then return
if @_mod? then throw new Error "mod cannot be reassigned"
@_mod = mod
@_mod.addItem this
modPack: # the ModPack containing this item
get: -> return @_mod.modPack
set: -> throw new Error "modPack cannot be assigned"
recipes: # a hash of recipeId to Recipe containing `recipesAsPrimary` if not empty or else `recipesAsExtra`
get: -> return if @_hasPrimaryRecipe then @_recipesAsPrimary else @_recipesAsExtra
set: -> throw new Error "recipes cannot be assigned"
recipesAsPrimary: # a hash of recipeId to Recipe where this item is the primary output
get: -> return @_recipesAsPrimary
set: -> throw new Error "recipes cannot be assigned"
recipesAsExtra: # a hash of recipeId to Recipe where this item is an extra output
get: -> return @_recipesAsExtra
set: -> throw new Error "recipesAsExtra cannot be assigned"
# Public Recipes ###############################################################################
addRecipe: (recipe)->
if recipe.output.item is this
@_recipesAsPrimary[recipe.id] = recipe
@_hasPrimaryRecipe = true
else if recipe.extras[this.id]?.item is this
@_recipesAsExtra[recipe.id] = recipe
else
throw new Error "recipe<#{recipe.id}> does not produce this item<#{@id}>"
# Object Overrides #############################################################################
toString: ->
return "Item:#{@displayName}<#{@id}>"
-62
View File
@@ -1,62 +0,0 @@
#
# Crafting Guide - mod.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class Mod
constructor: (attributes={})->
@id = attributes.id
@displayName = attributes.displayName
@modPack = attributes.modPack
@_items = {}
# Properties ###################################################################################
Object.defineProperties @prototype,
displayName:
get: -> return @_displayName
set: (displayName)->
if not displayName? then throw new Error "displayName is required"
return if @_displayName is displayName
@_displayName = displayName
id:
get: -> return @_id
set: (id)->
if not id? then throw new Error "id is required"
return if @_id is id
if @_id? then throw new Error "id cannot be reassigned"
@_id = id
items:
get: -> return @_items
set: -> throw new Error "items cannot be replaced"
modPack:
get: -> return @_modPack
set: (modPack)->
if not modPack? then throw new Error "modPack is required"
if @_modPack is modPack then return
if @_modPack? then throw new Error "modPack cannot be reassigned"
@_modPack = modPack
@_modPack.addMod this
# Public Methods ###############################################################################
addItem: (item)->
if not item? then return
if @_items[item.id] is item then return
@_items[item.id] = item
item.mod = this
# Object Overrides #############################################################################
toString: ->
return "Mod:#{@displayName}<#{@id}>"
-59
View File
@@ -1,59 +0,0 @@
#
# Crafting Guide - mod_pack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ModPack
constructor: (attributes={})->
@id = attributes.id
@displayName = attributes.displayName
@_mods = {}
# Property Methods #############################################################################
Object.defineProperties @prototype,
displayName: # a string containing the user-displayable name of this ModPack
get: -> return @_displayName
set: (displayName)->
if not displayName? then throw new Error "displayName is required"
if @_displayName is displayName then return
@_displayName = displayName
id: # a string which uniquely identifies this ModPack
get: -> return @_id
set: (id)->
if not id? then throw new Error "id is required"
if @_id is id then return
if @_id? then throw new Error "id cannot be reassigned"
@_id = id
mods: # a hash of mod id to Mod containing all the mods which are part of this ModPack
get: -> return @_mods
set: -> throw new Error "mods cannot be replaced"
# Public Methods ###############################################################################
addMod: (mod)->
if not mod? then return
if @_mods[mod.id] is mod then return
@_mods[mod.id] = mod
mod.modPack = this
findItem: (itemId)->
for modId, mod of @mods
item = mod.items[itemId]
return item if item?
return null
# Object Overrides #############################################################################
toString: ->
return "ModPack:#{@displayName}<#{@id}>"
-174
View File
@@ -1,174 +0,0 @@
#
# Crafting Guide - recipe.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
module.exports = class Recipe
constructor: (attributes={})->
@depth = attributes.depth
@height = attributes.height
@id = attributes.id
@output = attributes.output
@width = attributes.width
@_extras = {}
@_inputs = {}
@_inputGrid = []
@_tools = {}
# Properties ###################################################################################
Object.defineProperties @prototype,
allProducts: # an array of Stacks starting the the primary output of this recipe
get: -> return [].concat @output, (stack for id, stack of @extras)
set: -> throw new Error "allProducts cannot be assigned"
depth: # an integer specifying the number of layers to this recipe
get: -> return @_depth
set: (depth)->
depth = parseInt "#{depth}"
depth = if Number.isNaN(depth) then 0 else Math.max(0, depth)
@_depth = depth
extras: # a hash of item id to Stack of all the non-primary outputs of this recipe
get: -> return @_extras
set: -> throw new Error "extras cannot be replaced"
id: # a string which uniquely identifies this recipe
get: -> return @_id
set: (id)->
if not id? then throw new Error "id is required"
if @_id is id then return
if @_id? then throw new Error "id cannot be reassigned"
@_id = id
height: # an integer specifying the number of rows needed by this recipe
get: -> return @_height
set: (height)->
height = parseInt "#{height}"
height = if Number.isNaN(height) then 0 else Math.max(0, height)
@_height = height
inputs: # a hash of item id to Item containing all the inputs to this recipe
get: -> return @_inputs
set: -> throw new Error "inputs cannot be replaced"
needsTools: # a boolean indicating whether this recipe requires a tool
get: -> return (id for id, toolItem of @_tools).length > 0
output: # a Stack specifying the primary output of this recipe
get: -> return @_output
set: (output)->
if not output? then throw new Error "output is required"
if @_output is output then return
if @_output? then throw new Error "output cannot be reassigned"
@_output = output
@_output.item.addRecipe this
modPack: # the ModPack to which this recipe belongs
get: -> return @_output.modPack
set: -> throw new Error "modPack cannot be replaced"
tools: # a hash of item id to Item of all the tools required for this recipe
get: -> return @_tools
set: -> throw new Error "tools cannot be assigned"
width: # an integer specifying the number of columns needed by this recipe
get: -> return @_width
set: (width)->
width = parseInt "#{width}"
width = if Number.isNaN(width) then 0 else Math.max(0, width)
@_width = width
# Public Methods ###############################################################################
addExtra: (stack)->
return unless stack
return if @_extras[stack.item.id] is stack
@_extras[stack.item.id] = stack
stack.item.addRecipe this
addTool: (item)->
return unless item
@_tools[item.id] = item
computeQuantityRequired: (item)->
result = 0
for x in [0...@width]
for y in [0...@height]
for z in [0...@depth]
stack = @getInputAt x, y, z
continue unless stack?
continue unless stack.item.id is item.id
result += stack.quantity
return result
computeQuantityProduced: (item)->
result = 0
if @_output.item.id is item.id
result += @_output.quantity
for itemId, stack of @_extras
continue unless itemId is item.id
result += stack.quantity
return result
getInputAt: ->
[x, y, z] = [0, 0, 0]
if arguments.length is 3
[x, y, z] = arguments
else
[x, y] = arguments
return @_inputGrid[x]?[y]?[z] or null
setInputAt: ->
[x, y, z, stack] = [0, 0, 0, null]
if arguments.length is 4
[x, y, z, stack] = arguments
else
[x, y, stack] = arguments
@_depth = Math.max @_depth, z + 1
@_height = Math.max @_height, y + 1
@_width = Math.max @_width, x + 1
@_inputGrid[x] ?= []
@_inputGrid[x][y] ?= []
@_inputGrid[x][y][z] = stack
if stack?.item? then @_inputs[stack.item.id] = stack.item
# Object Overrides #############################################################################
toString: (options={})->
options.full ?= false
if options.full
b = new StringBuilder
b.loop (item for id, item of @inputs), delimiter:" + ", onEach:(b, item)=>
b.push @computeQuantityRequired(item), " ", item.displayName
b.push " ="
b.onlyIf @needsTools, (b)=>
b.push "("
b.loop (toolItem for id, toolItem of @tools), onEach:(b, toolItem)-> b.push toolItem.displayName
b.push ")"
b.push "=> "
b.loop @allProducts, delimiter:" + ", onEach:(b, stack)=>
b.push stack.quantity, " ", stack.item.displayName
return b.toString()
else
return "Recipe:#{@output}<#{@id}>"
-72
View File
@@ -1,72 +0,0 @@
#
# Crafting Guide - recipe.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
fixtures = require "../fixtures"
Item = require "./item"
Recipe = require "./recipe"
Stack = require "./stack"
########################################################################################################################
describe "Recipe", ->
beforeEach ->
@mod = fixtures.createMod()
@stick = fixtures.configureStick @mod
@ironIngot = fixtures.configureIronIngot @mod
@obsidian = fixtures.configureObsidian @mod
@ironSword = new Item id:"iron_sword", displayName:"Iron Sword", mod:@mod
@obsidianBox = new Item id:"obsidian_box", displayName:"Obsidian Box", mod:@mod
describe "getting & setting inputs", ->
describe "for 2D recipes", ->
beforeEach ->
@recipe = new Recipe id:"test1", output:new Stack item:@ironSword
@recipe.setInputAt 1, 0, new Stack item:@ironIngot
@recipe.setInputAt 1, 1, new Stack item:@ironIngot
@recipe.setInputAt 1, 2, new Stack item:@stick
it "returns assigned values as expected", ->
expect(@recipe.getInputAt(0, 0)).to.equal null
@recipe.getInputAt(1, 0).item.displayName.should.equal "Iron Ingot"
@recipe.getInputAt(1, 1).item.displayName.should.equal "Iron Ingot"
@recipe.getInputAt(1, 2).item.displayName.should.equal "Stick"
expect(@recipe.getInputAt(2, 2)).to.equal null
it "determines the correct dimentions", ->
@recipe.depth.should.equal 1
@recipe.height.should.equal 3
@recipe.width.should.equal 2
describe "for 3D recipes", ->
beforeEach ->
@recipe = new Recipe id:"test2", output:new Stack item:@obsidianBox
for x in [0..2]
for y in [0..2]
for z in [0..2]
continue if x is 1 and y is 1
continue if x is 1 and z is 1
continue if y is 1 and z is 1
@recipe.setInputAt x, y, z, new Stack item:@obsidian
it "returns assigned values as expected", ->
@recipe.getInputAt(0, 0, 0).item.displayName.should.equal "Obsidian"
@recipe.getInputAt(2, 0, 0).item.displayName.should.equal "Obsidian"
@recipe.getInputAt(0, 2, 0).item.displayName.should.equal "Obsidian"
@recipe.getInputAt(2, 2, 2).item.displayName.should.equal "Obsidian"
expect(@recipe.getInputAt(1, 1, 1)).to.equal null
expect(@recipe.getInputAt(1, 1, 0)).to.equal null
it "determines the correct dimentions", ->
@recipe.depth.should.equal 3
@recipe.height.should.equal 3
@recipe.width.should.equal 3
-42
View File
@@ -1,42 +0,0 @@
#
# Crafting Guide - stack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class Stack
constructor: (attributes={})->
@item = attributes.item
@quantity = attributes.quantity
# Properties ###################################################################################
Object.defineProperties @prototype,
item:
get: -> return @_item
set: (item)->
if not item? then throw new Error "item is required"
if @_item is item then return
if @_item? then throw new Error "item cannot be reassigned"
@_item = item
modPack:
get: -> return @_item.modPack
set: -> throw new Error "modPack cannot be replaced"
quantity:
get: -> return @_quantity
set: (quantity)->
quantity = parseInt "#{quantity}"
quantity = if Number.isNaN(quantity) then 0 else Math.max(0, quantity)
@_quantity = quantity
# Object Overrides #############################################################################
toString: ->
return "Stack:#{@item}×#{@quantity}"
@@ -1,124 +0,0 @@
#
# Crafting Guide - mod_pack_json.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
fixtures = require "../fixtures"
ModPackJsonFormatter = require "./mod_pack_json_formatter"
ModPackJsonParser = require "./mod_pack_json_parser"
########################################################################################################################
describe "ModPackJsonParser & ModPackJsonFormatter", ->
beforeEach ->
@formatter = new ModPackJsonFormatter
@parser = new ModPackJsonParser
@modPack = fixtures.createModPack id:"alpha", displayName:"ALPHA"
@runTest = =>
@string1 = @formatter.format @modPack
@string2 = @formatter.format @parser.parse @string1
@result = @parser.parse @string2
describe "an empty modpack", ->
beforeEach -> @runTest()
it "can survive a round trip", ->
@string1.should.equal @string2
it "contains the modpack's own properties", ->
@result.id.should.equal @modPack.id
@result.displayName.should.equal @modPack.displayName
it "doesn't contain a mods list", ->
expect(@result.mods).to.beUndefined
describe "a modpack with a single mod", ->
beforeEach ->
@mod = fixtures.createMod modPack:@modPack, id:"bravo", displayName:"BRAVO"
describe "containing only a gatherable item", ->
beforeEach ->
@oakWood = fixtures.configureOakWood @mod
@runTest()
it "can survive a round trip", ->
@string1.should.equal @string2
it "contains the correct mod", ->
mod = @result.mods[@mod.id]
mod.displayName.should.equal @mod.displayName
it "contains the item", ->
item = @result.mods[@mod.id].items[@oakWood.id]
item.displayName.should.equal @oakWood.displayName
describe "containing a multi-step item & it's requirements", ->
beforeEach ->
@craftingTable = fixtures.configureCraftingTable @mod
@oakPlank = fixtures.configureOakPlank @mod
@runTest()
it "can survive a round trip", ->
@string1.should.equal @string2
it "contains oak planks", ->
item = @result.mods[@mod.id].items[@oakPlank.id]
item.displayName.should.equal @oakPlank.displayName
it "has the recipe for a crafting table", ->
recipe = @result.mods[@mod.id].items[@craftingTable.id].firstRecipe
recipe.getInputAt(0, 0).item.id.should.equal @oakPlank.id
recipe.getInputAt(0, 1).item.id.should.equal @oakPlank.id
recipe.getInputAt(1, 0).item.id.should.equal @oakPlank.id
recipe.getInputAt(1, 1).item.id.should.equal @oakPlank.id
recipe.output.quantity.should.equal 1
describe "containing a complex item which needs tools & it's requirements", ->
beforeEach ->
@cake = fixtures.configureCake @mod
@runTest()
it "can survive a round trip", ->
@string1.should.equal @string2
it "has the recipe for a cake", ->
recipe = @result.mods[@mod.id].items[@cake.id].firstRecipe
describe "a modpack with multiple mods", ->
beforeEach ->
@modA = fixtures.createMod modPack:@modPack, id:"bravo", displayName:"BRAVO"
@modB = fixtures.createMod modPack:@modPack, id:"charlie", displayName:"CHARLIE"
describe "where items are used in recipes crossing mods", ->
beforeEach ->
@stick = fixtures.configureStick @modA
@ironIngot = fixtures.configureIronIngot @modA
@ironSword = fixtures.configureIronSword @modB
@runTest()
it "can survive a round trip", ->
@string1.should.equal @string2
it "has each item in the correct mod", ->
@result.mods[@modA.id].items[@ironIngot.id].displayName.should.equal @ironIngot.displayName
@result.mods[@modB.id].items[@ironSword.id].displayName.should.equal @ironSword.displayName
it "has the correct recipe for an iron sword", ->
recipe = @result.mods[@modB.id].items[@ironSword.id].firstRecipe
recipe.output.item.id.should.equal @ironSword.id
recipe.output.quantity.should.equal 1
recipe.getInputAt(1, 0).item.id.should.equal @ironIngot.id
recipe.getInputAt(1, 1).item.id.should.equal @ironIngot.id
recipe.getInputAt(1, 2).item.id.should.equal @stick.id
@@ -1,101 +0,0 @@
#
# Crafting Guide - mod_pack_json_formatter.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ModPackJsonParser
constructor: ->
@_reset()
# Public Methods ###############################################################################
format: (modPack)->
@_reset()
return JSON.stringify @_formatModPack modPack
# Private Methods ##############################################################################
_formatItem: (item)->
result = {}
result.id = item.id
result.displayName = item.displayName
if item.isGatherable and item.firstRecipe?
result.gatherable = true
return result
_formatMod: (mod)->
result = {}
result.id = mod.id
result.displayName = mod.displayName
for itemId, item of mod.items
result.items ?= []
@_itemIndexById[item.id] = @_itemIndex++
result.items.push @_formatItem item
return result
_formatModPack: (modPack)->
result = {}
result.id = modPack.id
result.displayName = modPack.displayName
for modId, mod of modPack.mods
result.mods ?= []
result.mods.push @_formatMod mod
if result.mods?
for modResult in result.mods
for itemResult in modResult.items
item = modPack.mods[modResult.id].items[itemResult.id]
for recipeId, recipe of item.recipesAsPrimary
itemResult.recipes ?= []
itemResult.recipes.push @_formatRecipe recipe
return result
_formatRecipe: (recipe)->
result = {}
result.id = recipe.id
if recipe.output.quantity > 1
result.quantity = recipe.output.quantity
result.width = recipe.width
result.height = recipe.height
result.depth = recipe.depth if recipe.depth > 1
result.inputs = []
for x in [0...recipe.width]
for y in [0...recipe.height]
for z in [0...recipe.depth]
inputStack = @_formatStack recipe.getInputAt x, y, z
result.inputs.push inputStack
for itemId, stack of recipe.extras
result.extras ?= []
result.extras.push @_formatStack stack
for itemId, item of recipe.tools
result.tools ?= []
result.tools.push @_itemIndexById[itemId]
return result
_formatStack: (stack)->
return null unless stack?
itemIndex = @_itemIndexById[stack.item.id]
if stack.quantity is 1 then return itemIndex
return [itemIndex, stack.quantity]
_reset: ->
@_itemIndex = 0
@_itemIndexById = {}
@@ -1,164 +0,0 @@
#
# Crafting Guide - mod_pack_json_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require "../game/item"
Mod = require "../game/mod"
ModPack = require "../game/mod_pack"
Recipe = require "../game/recipe"
Stack = require "../game/stack"
########################################################################################################################
module.exports = class ModPackJsonParser
constructor: ->
@_reset()
# Public Methods ###############################################################################
parse: (arg, fileName=null)->
@_reset()
@_fileName = fileName
if _.isString arg
@_parseText arg
else
@_parseObject arg
return @_modPack
# Private Methods ##############################################################################
_parseInteger: (text, defaultValue)->
result = parseInt "#{text}"
result = if Number.isNaN result then defaultValue else result
return result
_parseText: (text)->
try
obj = JSON.parse text
catch error
@_throwError "could not parse JSON: #{error}"
@_parseObject obj
_parseObject: (obj)->
@_data = obj
@_parseModPack()
@_parseMods()
@_parseItems()
@_parseRecipes()
_parseModPack: ->
if not @_data? then @_throwError "there is no valid data"
if not @_data.id? then @_throwError "modPack requires an id"
if not @_data.displayName? then @_throwError "modPack requires a displayName"
@_modPack = new ModPack id:@_data.id, displayName:@_data.displayName
_parseMods: ->
return unless @_data.mods?
for modData, index in @_data.mods
@_location = "mods[#{index}]"
if not modData.id? then @_throwError "mod requires an id"
if not modData.displayName? then @_throwError "mod requires a displayName"
new Mod modPack:@_modPack, id:modData.id, displayName:modData.displayName
_parseItems: ->
return unless @_data.mods?
for modData in @_data.mods
continue unless modData.items?
mod = @_modPack.mods[modData.id]
for itemData, index in modData.items
@_location = "<#{mod.id}>.items[#{index}]"
if not itemData.id? then @_throwError "item requires an id"
if not itemData.displayName? then @_throwError "item requires a displayName"
item = new Item mod:mod, id:itemData.id, displayName:itemData.displayName
item.gatherable = itemData.gatherable if itemData.gatherable?
@_items.push item
_parseRecipes: ->
return unless @_data.mods?
for modData in @_data.mods
continue unless modData.items?
mod = @_modPack.mods[modData.id]
for itemData, index in modData.items
continue unless itemData.recipes?
item = mod.items[itemData.id]
for recipeData, index in itemData.recipes
@_location = "<#{itemData.id}>.recipes[#{index}]"
if not recipeData.id? then @_throwError "recipe requires id"
if not recipeData.inputs? then @_throwError "recipe requires inputs"
quantity = @_parseInteger recipeData.quantity, 1
outputStack = new Stack item:item, quantity:quantity
recipe = new Recipe id:recipeData.id, output:outputStack
depth = @_parseInteger recipeData.depth, 1
height = @_parseInteger recipeData.height, 3
width = @_parseInteger recipeData.width, 3
index = 0
for x in [0...width]
for y in [0...height]
for z in [0...depth]
stack = @_parseStack recipeData.inputs[index]
if stack? then recipe.setInputAt x, y, z, stack
index++
if recipeData.extras
for stackData in recipeData.extras
recipe.addExtra @_parseStack stackData
if recipeData.tools
for index in recipeData.tools
toolItem = @_items[index]
if not toolItem? then @_throwError "there is no item #{index}"
recipe.addTool toolItem
_parseStack: (stackData)->
return null unless stackData?
if _.isArray(stackData)
if stackData.length isnt 2 then @_throwError "input stacks must have an item index and a quantity"
index = stackData[0]
quantity = stackData[1]
else
index = stackData
quantity = 1
item = @_items[index]
if not item? then @_throwError "there is no item #{index}"
return new Stack item:item, quantity:quantity
_reset: ->
@_data = null
@_fileName = null
@_items = []
@_location = null
@_modPack = null
_throwError: (message, cause=null)->
if @_location? then message = "#{@_location}: #{message}"
if @_fileName? and @_location? then message = "@#{message}"
if @_fileName? then message = "#{@_fileName}#{message}"
if cause? then message = "#{message}: #{cause}"
error = new Error message
error.cause = cause if cause?
error.fileName = @_fileName if @_fileName?
error.location = @_location if @_location?
throw error
+6 -4
View File
@@ -5,10 +5,12 @@
# All rights reserved.
#
BaseModel = require '../base_model'
Craftsman = require '../crafting/craftsman'
Inventory = require '../game/inventory'
ModPack = require '../game/mod_pack'
CraftingGuideCommon = require "crafting-guide-common"
{BaseModel} = CraftingGuideCommon.deprecated
{Craftsman} = CraftingGuideCommon.deprecated.crafting
{Inventory} = CraftingGuideCommon.deprecated.game
{ModPack} = CraftingGuideCommon.deprecated.game
########################################################################################################################
+2 -1
View File
@@ -5,7 +5,8 @@
# All rights reserved.
#
BaseModel = require '../base_model'
{BaseModel} = require("crafting-guide-common").deprecated
w = require "when"
########################################################################################################################
+2
View File
@@ -5,6 +5,8 @@
# All rights reserved.
#
w = require "when"
########################################################################################################################
module.exports = class FileCache extends Backbone.Events
@@ -1,3 +1,9 @@
#
# Crafting Guide - file_cache.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
FileCache = require './file_cache'
+1 -1
View File
@@ -5,7 +5,7 @@
# All rights reserved.
#
BaseModel = require '../base_model'
{BaseModel} = require("crafting-guide-common").deprecated
########################################################################################################################
+1 -1
View File
@@ -5,7 +5,7 @@
# All rights reserved.
#
BaseModel = require '../base_model'
{BaseModel} = require("crafting-guide-common").deprecated
########################################################################################################################
+2 -2
View File
@@ -5,8 +5,8 @@
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require '../game/item_slug'
{BaseModel} = require('crafting-guide-common').deprecated
{ItemSlug} = require('crafting-guide-common').deprecated.game
########################################################################################################################
@@ -5,7 +5,7 @@
# All rights reserved.
#
BaseModel = require '../base_model'
{BaseModel} = require('crafting-guide-common').deprecated
MarkdownImage = require './markdown_image'
########################################################################################################################
-33
View File
@@ -1,33 +0,0 @@
#
# Crafting Guide - tutorial.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class Tutorial extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.name?.length > 0 then throw new Error "attributes.name cannot be empty"
attributes.modSlug ?= null
attributes.officialUrl ?= null
attributes.sections ?= []
attributes.slug ?= _.slugify attributes.name
attributes.videos ?= []
super attributes, options
# Backbone.Model Overrides #####################################################################
parse: (text)->
TutorialParser = require '../parsing/tutorial_parser' # to avoid require cycles
@_parser ?= new TutorialParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.tutorialData modSlug:@modSlug, tutorialSlug:@slug
@@ -6,6 +6,7 @@
#
ModPackJsonParser = require "../parsing/mod_pack_json_parser"
w = require "when"
########################################################################################################################
@@ -5,10 +5,10 @@
# All rights reserved.
#
{CraftingGuideClient} = require('crafting-guide-common').api
ModBallotController = require './mod_ballot/mod_ballot_controller'
ModTileController = require './mod_tile/mod_tile_controller'
PageController = require '../page_controller'
{CraftingGuideClient} = require 'crafting-guide-common'
########################################################################################################################
@@ -140,7 +140,7 @@ module.exports = class AdsenseController
adCount = @_computeAdCount @_computeAdType()
if adCount is 0
logger.verbose "Adsense is waiting for room to insert ads"
logger.trace "Adsense is waiting for room to insert ads"
@_waiting = true
_.delay (=> @_waiting = false; @_waitForPageReadiness()), c.adsense.readinessCheckInterval
else
@@ -8,6 +8,7 @@
BaseController = require '../../base_controller'
ItemSelector = require '../../../models/site/item_selector'
ElementController = require './element/element_controller'
w = require "when"
########################################################################################################################
@@ -8,7 +8,7 @@
BaseController = require '../../base_controller'
CraftingGridController = require '../crafting_grid/crafting_grid_controller'
SlotController = require '../slot/slot_controller'
{StringBuilder} = require 'crafting-guide-common'
{StringBuilder} = require('crafting-guide-common').util
########################################################################################################################
@@ -7,11 +7,11 @@
BaseController = require '../base_controller'
CraftPage = require '../../models/site/craft_page'
Craftsman = require '../../models/crafting/craftsman'
{Craftsman} = require('crafting-guide-common').deprecated.crafting
CraftsmanWorkingController = require './craftsman_working/craftsman_working_controller'
InventoryController = require '../common/inventory/inventory_controller'
PageController = require '../page_controller'
SimpleInventory = require '../../models/crafting/simple_inventory'
{SimpleInventory} = require('crafting-guide-common').deprecated.crafting
StepController = require './step/step_controller'
########################################################################################################################
@@ -64,6 +64,7 @@ module.exports = class CraftPageController extends PageController
sampleInventory.parse inventoryText
@model.craftsman.want.addInventory sampleInventory
@_scrollTo @$workingSection
@onWantInventoryChanged()
return false
@@ -6,7 +6,7 @@
#
BaseController = require '../../base_controller'
Craftsman = require '../../../models/crafting/craftsman'
{Craftsman} = require('crafting-guide-common').deprecated.crafting
########################################################################################################################
@@ -8,7 +8,7 @@
BaseController = require '../../base_controller'
InventoryController = require '../../common/inventory/inventory_controller'
RecipeController = require '../../common/recipe/recipe_controller'
SimpleInventory = require '../../../models/crafting/simple_inventory'
{SimpleInventory} = require('crafting-guide-common').deprecated.crafting
########################################################################################################################
@@ -7,6 +7,7 @@
BaseController = require '../base_controller'
EmailClient = require '../../models/site/email_client'
w = require "when"
########################################################################################################################
@@ -6,15 +6,16 @@
#
EditableFile = require '../../models/site/editable_file'
Item = require '../../models/game/item'
{Item} = require('crafting-guide-common').deprecated.game
ItemGroupController = require '../common/item_group/item_group_controller'
ItemPage = require '../../models/site/item_page'
ItemSlug = require '../../models/game/item_slug'
{ItemSlug} = require('crafting-guide-common').deprecated.game
MarkdownSectionController = require '../common/markdown_section/markdown_section_controller'
MultiblockViewerController = require './multiblock_viewer/multiblock_viewer_controller'
PageController = require '../page_controller'
RecipeDetailController = require './recipe_detail/recipe_detail_controller'
VideoController = require '../common/video/video_controller'
w = require "when"
########################################################################################################################
@@ -7,9 +7,9 @@
BaseController = require '../../base_controller'
CraftingGridController = require '../../common/crafting_grid/crafting_grid_controller'
Inventory = require '../../../models/game/inventory'
{Inventory} = require('crafting-guide-common').deprecated.game
InventoryController = require '../../common/inventory/inventory_controller'
{StringBuilder} = require 'crafting-guide-common'
{StringBuilder} = require('crafting-guide-common').util
########################################################################################################################
@@ -7,7 +7,7 @@
PageController = require '../page_controller'
GitHubUser = require '../../models/site/github_user'
{CraftingGuideClient} = require 'crafting-guide-common'
{CraftingGuideClient} = require('crafting-guide-common').api
########################################################################################################################
@@ -5,10 +5,10 @@
# All rights reserved.
#
PageController = require '../page_controller'
Item = require '../../models/game/item'
{Item} = require('crafting-guide-common').deprecated.game
ItemGroupController = require '../common/item_group/item_group_controller'
Mod = require '../../models/game/mod'
{Mod} = require('crafting-guide-common').deprecated.game
PageController = require '../page_controller'
TutorialController = require './tutorial/tutorial_controller'
########################################################################################################################
+9 -9
View File
@@ -5,15 +5,15 @@
# All rights reserved.
#
BrowsePageController = require './browse_page/browse_page_controller'
CraftPageController = require './craft_page/craft_page_controller'
ItemPageController = require './item_page/item_page_controller'
ItemSlug = require '../models/game/item_slug'
LoginPageController = require './login_page/login_page_controller'
ModPageController = require './mod_page/mod_page_controller'
NewsPageController = require './news_page/news_page_controller'
TutorialPageController = require './tutorial_page/tutorial_page_controller'
UrlParams = require './url_params'
BrowsePageController = require './browse_page/browse_page_controller'
CraftPageController = require './craft_page/craft_page_controller'
ItemPageController = require './item_page/item_page_controller'
{ItemSlug} = require('crafting-guide-common').deprecated.game
LoginPageController = require './login_page/login_page_controller'
ModPageController = require './mod_page/mod_page_controller'
NewsPageController = require './news_page/news_page_controller'
TutorialPageController = require './tutorial_page/tutorial_page_controller'
UrlParams = require './url_params'
########################################################################################################################
+20 -3
View File
@@ -13,9 +13,8 @@ FooterController = require './footer/footer_controller'
GitHubUser = require '../models/site/github_user'
HeaderController = require './header/header_controller'
ImageLoader = require './image_loader'
Mod = require '../models/game/mod'
ModPack = require '../models/game/mod_pack'
ModPackStore = require '../models/store/mod_pack_store'
{Mod} = require('crafting-guide-common').deprecated.game
{ModPack} = require('crafting-guide-common').deprecated.game
Router = require './router'
########################################################################################################################
@@ -31,6 +30,7 @@ module.exports = class SiteController extends BaseController
@client = options.client
@fileCache = new FileCache c.url.modpackArchive()
@imageLoader = new ImageLoader defaultUrl:'/images/unknown.png'
@modPack = new ModPack {}, fileCache:@fileCache
@router = new Router this
@storage = options.storage
@@ -41,6 +41,23 @@ module.exports = class SiteController extends BaseController
# Public Methods ###############################################################################
loadDefaultModPack: ->
makeResponder = (m)-> return ->
m.activeModVersion.fetch() if m.activeModVersion?
for modSlug, modData of c.defaultMods
mod = new Mod {slug:modSlug}, {fileCache:@fileCache}
mod.on c.event.change + ':activeModVersion', makeResponder mod
@storage.register "mod:#{mod.slug}", mod, 'activeVersion', modData.defaultVersion
mod.fetch()
@modPack.addMod mod
if global.env isnt 'prerender'
@modPack.once c.event.sync, =>
@$pageContent.removeClass 'hidden'
@$pageContentLoading.addClass 'hidden'
loadCurrentUser: ->
@client.getCurrentUser()
.then (response)=>
+2
View File
@@ -5,6 +5,8 @@
# All rights reserved.
#
w = require "when"
########################################################################################################################
module.exports = class Tracker