This commit is contained in:
Andrew Miner
2016-12-27 15:25:33 -08:00
parent 7377559f7b
commit 69ca6b06b9
62 changed files with 2274 additions and 2286 deletions
+129
View File
@@ -0,0 +1,129 @@
#
# 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
@@ -0,0 +1,45 @@
#
# 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
@@ -0,0 +1,36 @@
#
# 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
@@ -0,0 +1,243 @@
#
# 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
@@ -0,0 +1,90 @@
#
# 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()
+234
View File
@@ -0,0 +1,234 @@
#
# 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
@@ -0,0 +1,202 @@
#
# 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
+247
View File
@@ -0,0 +1,247 @@
#
# 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
+23
View File
@@ -0,0 +1,23 @@
#
# 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}"
@@ -0,0 +1,33 @@
#
# 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
@@ -5,7 +5,7 @@
# All rights reserved.
#
Inventory = require "./inventory"
Inventory = require "../game/inventory"
{StringBuilder} = require "crafting-guide-common"
########################################################################################################################
@@ -20,8 +20,8 @@ module.exports = class CraftingPlan
@steps = attributes.steps
@want = attributes.want
@_consolidateSteps()
@_computeResources()
@_consolidateSteps()
# Properties ###################################################################################
@@ -7,8 +7,8 @@
CraftingPlan = require './crafting_plan'
CraftingPlanStep = require './crafting_plan_step'
Inventory = require './inventory'
fixtures = require './fixtures'
Inventory = require '../game/inventory'
fixtures = require '../fixtures'
########################################################################################################################
+8 -15
View File
@@ -15,7 +15,7 @@ module.exports = class Evaluation
@recipe = attributes.recipe if attributes.recipe?
@baseScore = attributes.baseScore
@_baseEvaluations = []
@_id = _.uniqueId "evaluation-"
@_includedTools = {}
@_toolScore = null
@@ -23,10 +23,6 @@ module.exports = class Evaluation
Object.defineProperties @prototype,
baseEvaluations:
get: -> return @_baseEvaluations
set: -> throw new Error "baseEvaluations cannot be assigned"
baseScore:
get: -> return @_baseScore
set: (baseScore)->
@@ -70,13 +66,15 @@ module.exports = class Evaluation
# Public Methods ###############################################################################
addBaseEvaluation: (evaluation)->
@_baseEvaluations.push evaluation
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
@@ -86,18 +84,13 @@ module.exports = class Evaluation
return @baseScore * multiplier + @toolScore
isToolIncluded: (item)->
return true if @_includedTools[item.id]?
for baseEvaluation in @_baseEvaluations
return true if baseEvaluation.isToolIncluded item
return false
return @_includedTools[item.id]?
# Object Overrides #############################################################################
toString: ->
obj = if @item? then @item else @recipe
return "#{@evaluator}=>#{obj}@#{@baseScore}"
return "#{@evaluator.constructor.name}:#{obj}@#{@baseScore}<#{@_id}>"
# Private Methods ##############################################################################
+14 -14
View File
@@ -34,8 +34,8 @@ module.exports = class Evaluator
recipeEvaluation = @_findBestRecipeEvaluationFor item
if recipeEvaluation?
evaluation.addBaseEvaluation recipeEvaluation
evaluation.baseScore = recipeEvaluation.baseScore
evaluation.addIncludedToolsFrom recipeEvaluation
else
@_computeGatherableItemScore item, evaluation
@@ -49,12 +49,13 @@ module.exports = class Evaluator
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
for id, toolItem of @evaluateItem(toolItem).includedTools
evaluation.addIncludedTool toolItem
evaluation.addIncludedToolsFrom @evaluateItem toolItem
return evaluation
@@ -79,24 +80,23 @@ module.exports = class Evaluator
# Overrideable Methods #########################################################################
_computeRecipeScore: (recipe, evaluation)->
throw new Error "#{@constructor.name} must override _computeRecipeScore"
_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 recipeMap in [item.recipes, item.recipesAsExtra]
for id, recipe of recipeMap
evaluation = @evaluateRecipe recipe
continue unless evaluation?.baseScore?
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
if not result? then result = evaluation
if evaluation.baseScore < result.baseScore then result = evaluation
return result
@@ -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}"
-93
View File
@@ -1,93 +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:
get: -> return @_displayName
set: (displayName)->
if not displayName? then throw new Error "displayName is required"
@_displayName = displayName
firstRecipe:
get: -> return recipe for id, recipe of @recipes
set: -> throw new Error "firstRecipe cannot be assigned"
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
isGatherable:
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:
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:
get: -> return @_mod.modPack
set: -> throw new Error "modPack cannot be replaced"
recipes:
get: -> return if @_hasPrimaryRecipe then @_recipesAsPrimary else @_recipesAsExtra
set: -> throw new Error "recipes cannot be assigned"
recipesAsPrimary:
get: -> return @_recipesAsPrimary
set: -> throw new Error "recipes cannot be assigned"
recipesAsExtra:
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] 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}>"
@@ -1,52 +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 = {}
@_oreDict = {}
# Property Methods #############################################################################
Object.defineProperties @prototype,
displayName:
get: -> return @_displayName
set: (displayName)->
if not displayName? then throw new Error "displayName is required"
if @_displayName is displayName then return
@_displayName = displayName
id:
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"
mods:
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
# Object Overrides #############################################################################
toString: ->
return "ModPack:#{@displayName}<#{@id}>"
@@ -5,14 +5,14 @@
# All rights reserved.
#
fixtures = require './fixtures'
Inventory = require './inventory'
fixtures = require '../fixtures'
Inventory = require '../game/inventory'
PlanBuilder = require './plan_builder'
ResourcesEvaluator = require './resources_evaluator'
########################################################################################################################
describe.only "PlanBuilder", ->
describe "PlanBuilder", ->
beforeEach ->
@planner = new PlanBuilder new ResourcesEvaluator
-148
View File
@@ -1,148 +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={})->
@id = attributes.id
@height = attributes.height
@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"
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
@_extras[stack.item.id] = stack
addTool: (item)->
return unless item
@_tools[item.id] = item
computeQuantityRequired: (item)->
result = 0
for row in [0...@height]
for col in [0...@width]
stack = @_inputGrid[row]?[col]
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: (row, col)->
return @_inputGrid[row]?[col]
setInputAt: (row, col, stack)->
@_height = Math.max @_height, row + 1
@_width = Math.max @_width, col + 1
@_inputGrid[row] ?= []
@_inputGrid[row][col] = stack
@_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}>"
@@ -16,29 +16,24 @@ module.exports = class ResourcesEvaluator extends Evaluator
_computeRecipeScore: (recipe, evaluation)->
evaluation.baseScore = 0
for row in [0...recipe.height]
for col in [0...recipe.width]
stack = recipe.getInputAt row, col
continue unless stack?
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
evaluation.addBaseEvaluation inputEvaluation
else
evaluation.baseScore = null
logger.outdent()
return
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
for id, toolItem of recipe.tools
toolEvaluation = @evaluateItem toolItem
evaluation.addBaseEvaluation toolEvaluation
evaluation.baseScore = evaluation.baseScore / recipe.output.quantity
_computeGatherableItemScore: (item, evaluation)->
@@ -6,7 +6,7 @@
#
ResourcesEvaluator = require './resources_evaluator'
fixtures = require './fixtures'
fixtures = require '../fixtures'
########################################################################################################################
@@ -123,3 +123,7 @@ describe "ResourcesEvaluator", ->
# 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'
-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}"
@@ -22,7 +22,6 @@ module.exports = class StepsEvaluator extends Evaluator
evaluation.score = null
return
evaluation.addBaseEvaluation inputEvaluation
evaluation.score = Math.min evaluation.score, inputEvaluation.score + 1
for id, item of recipe.tools
@@ -33,7 +32,6 @@ module.exports = class StepsEvaluator extends Evaluator
evaluation.score = null
return
evaluation.addBaseEvaluation evaluation
evaluation.addIncludedTool item
evaluation.score += toolEvaluation.score
@@ -5,11 +5,11 @@
# All rights reserved.
#
Item = require "./item"
Mod = require "./mod"
ModPack = require "./mod_pack"
Recipe = require "./recipe"
Stack = require "./stack"
Item = require "./game/item"
Mod = require "./game/mod"
ModPack = require "./game/mod_pack"
Recipe = require "./game/recipe"
Stack = require "./game/stack"
# Instance Creation Fixtures ###########################################################################################
@@ -43,7 +43,7 @@ exports.createStack = createStack = (attributes={})->
# Item Configuration Fixtures ##########################################################################################
exports.configureBucket = configureBucket = (mod)->
bucket = mod.items["bucket"]
bucket = mod.modPack.findItem "bucket"
if not bucket?
ironIngot = configureIronIngot mod
bucket = createItem mod:mod, id:"bucket", displayName:"Bucket"
@@ -52,13 +52,13 @@ exports.configureBucket = configureBucket = (mod)->
recipe = createRecipe output:createStack item:bucket
recipe.setInputAt 0, 0, createStack item:ironIngot
recipe.setInputAt 1, 1, createStack item:ironIngot
recipe.setInputAt 0, 2, createStack item:ironIngot
recipe.setInputAt 2, 0, createStack item:ironIngot
recipe.addTool craftingTable
return bucket
exports.configureCake = configureCake = (mod)->
cake = mod.items["cake"]
cake = mod.modPack.findItem "cake"
if not cake?
bucket = configureBucket mod
cake = createItem mod:mod, id:"cake", displayName:"Cake"
@@ -70,13 +70,13 @@ exports.configureCake = configureCake = (mod)->
recipe = createRecipe output:createStack item:cake
recipe.setInputAt 0, 0, createStack item:milkBucket
recipe.setInputAt 0, 1, createStack item:milkBucket
recipe.setInputAt 0, 2, createStack item:milkBucket
recipe.setInputAt 1, 0, createStack item:sugar
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 1, 2, createStack item:sugar
recipe.setInputAt 2, 0, createStack item:wheat
recipe.setInputAt 2, 1, createStack item:wheat
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
@@ -84,39 +84,39 @@ exports.configureCake = configureCake = (mod)->
return cake
exports.configureCoal = configureCoal = (mod)->
coal = mod.items["coal"]
coal = mod.modPack.findItem "coal"
if not coal?
coal = createItem mod:mod, id:"coal", displayName:"Coal", isGatherable:true
coal = createItem mod:mod, id:"coal", displayName:"Coal"
return coal
exports.configureCobblestone = configureCobblestone = (mod)->
cobblestone = mod.items["cobblestone"]
cobblestone = mod.modPack.findItem "cobblestone"
if not cobblestone?
cobblestone = createItem mod:mod, displayName:"Cobblestone", isGatherable:true
cobblestone = createItem mod:mod, displayName:"Cobblestone"
return cobblestone
exports.configureCraftingTable = configureCraftingTable = (mod)->
craftingTable = mod.items["crafting_table"]
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 0, 1, 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.items["egg"]
egg = mod.modPack.findItem "egg"
if not egg?
egg = createItem mod:mod, id:"egg", displayName:"Egg", isGatherable:true
egg = createItem mod:mod, id:"egg", displayName:"Egg"
return egg
exports.configureFurnace = configureFurnace = (mod)->
furnace = mod.items["furnace"]
furnace = mod.modPack.findItem "furnace"
if not furnace?
cobblestone = configureCobblestone mod
craftingTable = configureCraftingTable mod
@@ -124,19 +124,19 @@ exports.configureFurnace = configureFurnace = (mod)->
recipe = createRecipe output:createStack item:furnace
recipe.setInputAt 0, 0, createStack item:cobblestone
recipe.setInputAt 0, 1, createStack item:cobblestone
recipe.setInputAt 0, 2, createStack item:cobblestone
recipe.setInputAt 1, 0, createStack item:cobblestone
recipe.setInputAt 1, 2, 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.items["iron_ingot"]
ironIngot = mod.modPack.findItem "iron_ingot"
if not ironIngot?
coal = configureCoal mod
furnace = configureFurnace mod
@@ -144,23 +144,23 @@ exports.configureIronIngot = configureIronIngot = (mod)->
ironOre = configureIronOre mod
recipe = createRecipe output:createStack item:ironIngot, quantity:8
recipe.setInputAt 0, 1, createStack item:ironOre, quantity:8
recipe.setInputAt 2, 1, createStack item:coal
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.items["iron_block"]
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 row in [0..2]
for col in [0..2]
recipe.setInputAt row, col, createStack item:ironIngot
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
@@ -169,7 +169,7 @@ exports.configureIronBlock = configureIronBlock = (mod)->
return ironBlock
exports.configureIronSword = configureIronSword = (mod)->
ironSword = mod.items["iron_sword"]
ironSword = mod.modPack.findItem "iron_sword"
if not ironSword?
craftingTable = configureCraftingTable mod
ironIngot = configureIronIngot mod
@@ -177,15 +177,15 @@ exports.configureIronSword = configureIronSword = (mod)->
stick = configureStick mod
recipe = createRecipe output:createStack item:ironSword
recipe.setInputAt 0, 1, createStack item:ironIngot
recipe.setInputAt 1, 0, createStack item:ironIngot
recipe.setInputAt 1, 1, createStack item:ironIngot
recipe.setInputAt 2, 1, createStack item:stick
recipe.setInputAt 1, 2, createStack item:stick
recipe.addTool craftingTable
return ironSword
exports.configureIronShovel = configureIronShovel = (mod)->
ironShovel = mod.items["iron_shovel"]
ironShovel = mod.modPack.findItem "iron_shovel"
if not ironShovel?
craftingTable = configureCraftingTable mod
ironIngot = configureIronIngot mod
@@ -193,40 +193,40 @@ exports.configureIronShovel = configureIronShovel = (mod)->
stick = configureStick mod
recipe = createRecipe output:createStack item:ironShovel
recipe.setInputAt 0, 1, createStack item:ironIngot
recipe.setInputAt 1, 0, createStack item:ironIngot
recipe.setInputAt 1, 1, createStack item:stick
recipe.setInputAt 2, 1, createStack item:stick
recipe.setInputAt 1, 2, createStack item:stick
recipe.addTool craftingTable
return ironShovel
exports.configureIronOre = configureIronOre = (mod)->
ironOre = mod.items["iron_ore"]
ironOre = mod.modPack.findItem "iron_ore"
if not ironOre?
ironOre = createItem mod:mod, id:"iron_ore", displayName:"Iron Ore", isGatherable:true
ironOre = createItem mod:mod, id:"iron_ore", displayName:"Iron Ore"
return ironOre
exports.configureMilk = configureMilk = (mod)->
milk = mod.items["milk"]
milk = mod.modPack.findItem "milk"
if not milk?
milk = createItem mod:mod, id:"milk", displayName:"Milk", isGatherable:true
milk = createItem mod:mod, id:"milk", displayName:"Milk"
return milk
exports.configureMilkBucket = configureMilkBucket = (mod)->
milkBucket = mod.items["milk_bucket"]
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 0, 1, createStack item:milk
recipe.setInputAt 1, 0, createStack item:milk
recipe.setInputAt 1, 1, createStack item:bucket
return milkBucket
exports.configureOakPlank = configureOakPlank = (mod)->
oakPlanks = mod.items["oak_planks"]
oakPlanks = mod.modPack.findItem "oak_planks"
if not oakPlanks?
oakPlanks = createItem mod:mod, id:"oak_planks", displayName:"Oak Planks"
oakWood = configureOakWood mod
@@ -237,31 +237,37 @@ exports.configureOakPlank = configureOakPlank = (mod)->
return oakPlanks
exports.configureOakWood = configureOakWood = (mod)->
oakWood = mod.items["oak_wood"]
oakWood = mod.modPack.findItem "oak_wood"
if not oakWood?
oakWood = createItem mod:mod, id:"oak_wood", displayName:"Oak Wood", isGatherable:true
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.items["redstone_dust"]
redstoneDust = mod.modPack.findItem "redstone_dust"
if not redstoneDust?
redstoneDust = createItem mod:mod, id:"redstone_dust", displayName:"Redstone Dust", isGatherable:true
redstoneDust = createItem mod:mod, id:"redstone_dust", displayName:"Redstone Dust"
return redstoneDust
exports.configureStick = configureStick = (mod)->
stick = mod.items["stick"]
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 1, 0, createStack item:oakPlanks
recipe.setInputAt 0, 1, createStack item:oakPlanks
return stick
exports.configureSugar = configureSugar = (mod)->
sugar = mod.items["sugar"]
sugar = mod.modPack.findItem "sugar"
if not sugar?
sugar = createItem mod:mod, id:"sugar", displayName:"Sugar"
sugarCane = configureSugarCane mod
@@ -272,7 +278,7 @@ exports.configureSugar = configureSugar = (mod)->
return sugar
exports.configureSaw = configureSaw = (mod)->
saw = mod.items["saw"]
saw = mod.modPack.findItem "saw"
if not saw?
craftingTable = configureCraftingTable mod
ironBlock = configureIronBlock mod
@@ -284,13 +290,13 @@ exports.configureSaw = configureSaw = (mod)->
recipe = createRecipe output:createStack item:saw
recipe.setInputAt 0, 0, createStack item:oakPlank
recipe.setInputAt 0, 1, createStack item:ironIngot
recipe.setInputAt 0, 2, createStack item:oakPlank
recipe.setInputAt 1, 0, createStack item:oakPlank
recipe.setInputAt 1, 1, createStack item:ironBlock
recipe.setInputAt 1, 2, createStack item:oakPlank
recipe.setInputAt 1, 0, createStack item:ironIngot
recipe.setInputAt 2, 0, createStack item:oakPlank
recipe.setInputAt 2, 1, createStack item:redstoneDust
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
@@ -301,13 +307,13 @@ exports.configureSaw = configureSaw = (mod)->
return saw
exports.configureSugarCane = configureSugarCane = (mod)->
sugarCane = mod.items["sugar_cane"]
sugarCane = mod.modPack.findItem "sugar_cane"
if not sugarCane?
sugarCane = createItem mod:mod, id:"sugar_cane", displayName:"Sugar Cane", isGatherable:true
sugarCane = createItem mod:mod, id:"sugar_cane", displayName:"Sugar Cane"
return sugarCane
exports.configureWheat = configureWheat = (mod)->
wheat = mod.items["wheat"]
wheat = mod.modPack.findItem "wheat"
if not wheat?
wheat = createItem mod:mod, id:"wheat", displayName:"Wheat", isGatherable:true
wheat = createItem mod:mod, id:"wheat", displayName:"Wheat"
return wheat
+57 -210
View File
@@ -5,239 +5,86 @@
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Stack = require './stack'
Stack = require './stack'
########################################################################################################################
module.exports = class Inventory extends BaseModel
module.exports = class Inventory
constructor: (attributes={}, options={})->
super attributes, options
attributes.modPack ?= null
@clear()
constructor: (inventory=null)->
@_id = _.uniqueId "inventory-"
@_stacks = {}
if options.clone?
@addInventory options.clone
if inventory? then @merge inventory
# Class Methods ################################################################################
# Properties ###################################################################################
@Delimiters =
Item: '.'
Stack: ':'
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: (itemSlug, quantity=1, options={})->
return this unless quantity > 0
add: (item, quantity)->
return unless item?
return if quantity is 0
@_add itemSlug, quantity, options
@trigger c.event.add, this, itemSlug, quantity
@trigger c.event.change, this
return this
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
addInventory: (inventory)->
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
if @_stacks[item.id].quantity is 0
delete @_stacks[item.id]
@trigger c.event.change, this
return this
clear: (options={})->
clear: ->
@_stacks = {}
@_itemSlugs = []
@trigger c.event.change, this
contains: (item)->
return @_stacks[item.id]?
clone: ->
inventory = new Inventory
inventory.addInventory this
return inventory
getQuantity: (item)->
existingStack = @_stacks[item.id]
return 0 unless existingStack?
return existingStack.quantity
each: (callback)->
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
continue unless stack?
callback stack
merge: (inventory)->
for id, stack of inventory.stacks
@add stack.item, stack.quantity
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
remove: (item, quantity)->
@add item, -1 * quantity
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") {items: ["]
toString: (options={})->
options.full ?= false
needsDelimiter = false
@each (stack)->
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
if options.full
result = []
needsDelimiter = false
result.push '}'
return result.join ''
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
# Private Methods ##############################################################################
for stack in stackList
if needsDelimiter then result.push ", "
needsDelimiter = true
_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()
result.push stack.quantity
result.push " "
result.push stack.item.displayName
return result.join ""
else
stack.quantity += quantity
_sort: ->
@_itemSlugs.sort (a, b)-> ItemSlug.compare a, b
return "Inventory<#{@_id}>@#{(id for id, item of @_stacks).length}"
@@ -1,213 +0,0 @@
#
# Crafting Guide - inventory.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
EventRecorder = require '../event_recorder'
Inventory = require './inventory'
Item = require './item'
ItemSlug = require './item_slug'
########################################################################################################################
inventory = modPack = null
########################################################################################################################
describe 'inventory.coffee', ->
beforeEach ->
inventory = new Inventory {}, silent:false
inventory.add ItemSlug.slugify('wool'), 4
inventory.add ItemSlug.slugify('string'), 20
inventory.add ItemSlug.slugify('boat')
describe 'add', ->
it 'can add to an empty inventory', ->
inventory.add ItemSlug.slugify('iron_ingot'), 4
stack = inventory._stacks['iron_ingot']
stack.constructor.name.should.equal 'Stack'
stack.itemSlug.qualified.should.equal 'iron_ingot'
stack.quantity.should.equal 4
it 'can augment quantity of existing items', ->
inventory.add ItemSlug.slugify('wool'), 2
inventory.unparse().should.equal 'boat:20.string:6.wool'
it 'can add zero quantity', ->
inventory.add ItemSlug.slugify('wool'), 0
inventory.unparse().should.equal 'boat:20.string:4.wool'
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.add ItemSlug.slugify('iron_ingot'), 10
events.names.should.eql [c.event.add, c.event.change]
describe 'addInventory', ->
it 'can add to an empty inventory', ->
newInventory = new Inventory
newInventory.addInventory inventory
newInventory.unparse().should.equal 'boat:20.string:4.wool'
it 'can add a mix of new and existing items', ->
newInventory = new Inventory
newInventory.add ItemSlug.slugify('string'), 2
newInventory.addInventory inventory
newInventory.unparse().should.equal 'boat:22.string:4.wool'
describe 'clone', ->
it 'creates an empty inventory from an empty inventory', ->
a = new Inventory
b = a.clone()
b._itemSlugs.should.eql []
it 'faithfully copies an existing inventory', ->
copy = inventory.clone()
copy.unparse().should.equal 'boat:20.string:4.wool'
describe 'each', ->
it 'works with an empty inventory', ->
inventory = new Inventory
result = []
inventory.each (item)-> result.push item.name
result.should.eql []
it 'works when items have only been added', ->
result = []
inventory.each (stack)-> result.push stack.itemSlug.qualified
result.should.eql ['boat', 'string', 'wool']
it 'works when items have been augmented', ->
inventory.add ItemSlug.slugify 'iron_ingot'
inventory.add ItemSlug.slugify 'boat'
inventory.add ItemSlug.slugify('wool'), 2
result = []
inventory.each (stack)-> result.push stack.itemSlug.qualified
result.should.eql ['boat', 'iron_ingot', 'string', 'wool']
describe 'hasAtLeast', ->
it 'works when the item is completely absent', ->
answer = inventory.hasAtLeast 'chicken', 1
answer.should.be.false
it 'always returns true for zero quantity', ->
inventory.hasAtLeast('chicken', 0).should.be.true
inventory.hasAtLeast('wool', 0).should.be.true
it 'works for a quantity above 1', ->
inventory.hasAtLeast('wool', 3).should.be.true
inventory.hasAtLeast('wool', 4).should.be.true
inventory.hasAtLeast('wool', 5).should.be.false
describe 'localize', ->
before ->
modPack =
modSlug:
wool: 'minecraft'
string: 'minecraft'
boat: 'minecraft'
stone_gear: 'buildcraft'
findItem: (slug)->
return slug:new ItemSlug @modSlug[slug.item], slug.item
it 'replaces item slugs with qualified slugs', ->
inventory.add ItemSlug.slugify 'stone_gear'
inventory.modPack = modPack
inventory.localize()
slugs = []
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
slugs.should.eql [
'minecraft__boat'
'buildcraft__stone_gear'
'minecraft__string'
'minecraft__wool'
]
it 'ignores qualified slugs', ->
inventory.add ItemSlug.slugify 'buildcraft__stone_gear'
inventory.modPack = modPack
inventory.localize()
slugs = []
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
slugs.should.eql [
'minecraft__boat'
'buildcraft__stone_gear'
'minecraft__string'
'minecraft__wool'
]
describe 'parse', ->
beforeEach ->
inventory = new Inventory {}, silent:false
it 'ignores an empty string', ->
result = inventory.parse ''
result.unparse().should.eql ''
it 'can parse a single item without quantity', ->
result = inventory.parse 'wool'
result.unparse().should.equal 'wool'
it 'can parse a single item with quantity', ->
result = inventory.parse '4.wool'
result.unparse().should.equal '4.wool'
it 'can parse multiple mixed-type items', ->
result = inventory.parse '4.wool:10.string:boat'
result.unparse().should.equal 'boat:10.string:4.wool'
describe 'pop', ->
it 'returns null for an empty inventory', ->
inventory = new Inventory
result = inventory.pop()
expect(result).to.be.null
it 'completely removes the last item', ->
stack = inventory.pop()
stack.itemSlug.qualified.should.equal 'wool'
stack.quantity.should.equal 4
inventory.unparse().should.equal 'boat:20.string'
it 'triggers the right events', ->
events = new EventRecorder inventory
result = inventory.pop()
events.names.should.eql [c.event.remove, c.event.change]
describe 'remove', ->
it 'does nothing when the item is absent', ->
before = inventory.unparse()
inventory.remove 'foo'
after = inventory.unparse()
before.should.equal after
it 'throws when the item has insufficient quantity', ->
expect(-> inventory.remove('wool', 10)).to.throw Error,
'cannot remove 10: only 4 wool in this inventory'
it 'removes all items by default', ->
inventory.remove 'wool'
expect(inventory._stacks.wool).to.be.empty
it 'removes a quantity above 1', ->
inventory.remove 'wool', 3
inventory._stacks.wool.quantity.should.equal 1
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.remove 'wool'
events.names.should.eql [c.event.change, c.event.remove, c.event.change]
+73 -67
View File
@@ -5,86 +5,92 @@
# 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
module.exports = class Item
@Group = Other:'Other'
constructor: (attributes={})->
@id = attributes.id
@displayName = attributes.displayName
@isGatherable = attributes.isGatherable
@mod = attributes.mod
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()
@_hasPrimaryRecipe = false
@_recipesAsPrimary = {}
@_recipesAsExtra = {}
# 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 #####################################################################
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
parse: (text)->
ItemParser = require '../parsing/item_parser' # to avoid require cycles
@_parser ?= new ItemParser model:this
@_parser.parse text
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"
return null # prevent calling `set`
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
url: ->
return c.url.itemData modSlug:@slug.mod, itemSlug:@slug.item
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: ->
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()
return "Item:#{@displayName}<#{@id}>"
@@ -1,122 +0,0 @@
#
# Crafting Guide - item_slug.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require './item_slug'
########################################################################################################################
describe 'item_slug.coffee', ->
describe 'constructor', ->
it 'can handle one argument', ->
slug = new ItemSlug 'alpha'
slug.item.should.equal 'alpha'
expect(slug.mod).to.be.null
slug.qualified.should.equal 'alpha'
it 'can handle two arguments', ->
slug = new ItemSlug 'alpha', 'bravo'
slug.mod.should.equal 'alpha'
slug.item.should.equal 'bravo'
slug.qualified.should.equal 'alpha__bravo'
it 'throws with zero arguments', ->
f = -> new ItemSlug
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
it 'throws with more arguments', ->
f = -> new ItemSlug 'alpha', 'bravo', 'charlie'
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
describe 'ItemSlug.compare', ->
it 'sorts by item when both are qualified in the same mod', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'bravo'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
it 'sorts by item when not qualified', ->
a = new ItemSlug 'alpha'
b = new ItemSlug 'bravo'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
describe 'ItemSlug.equal', ->
it 'requires both to have the same mod', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'alpha', 'charlie'
c = new ItemSlug 'alpha', 'bravo'
ItemSlug.equal(a, b).should.be.false
ItemSlug.equal(a, c).should.be.true
it 'requires both to have the same item', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'alpha', 'charlie'
c = new ItemSlug 'alpha', 'bravo'
ItemSlug.equal(a, b).should.be.false
ItemSlug.equal(a, c).should.be.true
describe 'ItemSlug.slugify', ->
it 'can slugify a pure name', ->
slug = ItemSlug.slugify 'Alpha Bravo (Charlie)'
slug.item.should.equal 'alpha_bravo_charlie'
expect(slug.mod).to.be.null
it 'can slugify a simple item slug', ->
slug = ItemSlug.slugify 'alpha_bravo_charlie'
slug.item.should.equal 'alpha_bravo_charlie'
expect(slug.mod).to.be.null
it 'can slugify a fully-qualified slug', ->
slug = ItemSlug.slugify 'alpha_bravo__charlie_delta'
slug.mod.should.equal 'alpha_bravo'
slug.item.should.equal 'charlie_delta'
describe 'matches', ->
it 'ignores mod when either is unqualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'bravo'
c = new ItemSlug 'charlie'
a.matches(b).should.be.true
a.matches(c).should.be.false
it 'observes differences in mod when all are qualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'bravo'
c = new ItemSlug 'delta', 'echo'
d = new ItemSlug 'alpha', 'bravo'
a.matches(b).should.be.false
a.matches(c).should.be.false
a.matches(d).should.be.true
describe 'isQualified', ->
it 'returns true only when the mod slug is set', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie'
a.isQualified.should.be.true
b.isQualified.should.be.false
describe '[]', ->
it 'allows slugs as a key', ->
slug = new ItemSlug 'alpha', 'bravo'
data = {}
data[slug] = 'foo'
data['alpha__bravo'].should.equal 'foo'
data[slug].should.equal 'foo'
+40 -212
View File
@@ -5,230 +5,58 @@
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class Mod extends BaseModel
module.exports = class Mod
constructor: (attributes={}, options={})->
if not attributes.slug? then throw new Error 'attributes.slug is required'
constructor: (attributes={})->
@id = attributes.id
@displayName = attributes.displayName
@modPack = attributes.modPack
attributes.author ?= ''
attributes.description ?= ''
attributes.documentationUrl ?= null
attributes.downloadUrl ?= null
attributes.homePageUrl ?= null
attributes.modPack ?= null
attributes.name ?= ''
@_items = {}
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 #############################################################################
# Properties ###################################################################################
Object.defineProperties @prototype,
activeModVersion:
get: -> @_activeModVersion
displayName:
get: -> return @_displayName
set: (displayName)->
if not displayName? then throw new Error "displayName is required"
return if @_displayName is displayName
@_displayName = displayName
activeVersion:
get: ->
return @_activeVersion
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
set: (version)->
return if version is @_activeVersion
items:
get: -> return @_items
set: -> throw new Error "items cannot be replaced"
version ?= Mod.Version.None
if version is Mod.Version.Latest then version = _.last(@_modVersions).version
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
if version is Mod.Version.None
@_activeVersion = version
@_activateModVersion null
# Public Methods ###############################################################################
@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
addItem: (item)->
if not item? then return
if @_items[item.id] is item then return
@_items[item.id] = item
item.mod = this
@_activeVersion = version
@trigger c.event.change + ':activeVersion', this, @_activeVersion
@trigger c.event.change, this
# Object Overrides #############################################################################
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
toString: ->
return "Mod:#{@displayName}<#{@id}>"
-30
View File
@@ -1,30 +0,0 @@
#
# Crafting Guide - mod.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Mod = require './mod'
########################################################################################################################
mod = null
########################################################################################################################
describe 'mod.coffee', ->
beforeEach -> mod = new Mod name:'Test', slug:'test'
describe 'compareTo', ->
it 'lists required mods first', ->
minecraft = new Mod name:'Minecraft', slug:'minecraft'
mod.compareTo(minecraft).should.equal +1
minecraft.compareTo(mod).should.equal -1
it 'sorts by name second', ->
buildcraft = new Mod name:'Buildcraft', slug:'buildcraft'
mod.compareTo(buildcraft).should.equal +1
buildcraft.compareTo(mod).should.equal -1
+33 -176
View File
@@ -5,198 +5,55 @@
# 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
module.exports = class ModPack
constructor: (attributes={}, options={})->
super attributes, options
constructor: (attributes={})->
@id = attributes.id
@displayName = attributes.displayName
@_mods = []
@_cache = {}
@_mods = {}
@on c.event.change, => @_cache = {}
# Property Methods #############################################################################
# Item Methods #################################################################################
Object.defineProperties @prototype,
chooseRandomItem: ->
return null unless @_mods.length > 0
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
modIndex = Math.floor Math.random() * @_mods.length
return @_mods[modIndex].chooseRandomItem()
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
findItem: (itemSlug, options={})->
options.includeDisabled ?= false
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"
key = "#{itemSlug}-#{options.includeDisabled}"
@_cache.itemBySlug ?= {}
item = @_cache.itemBySlug[key]
return item if item?
# Public Methods ###############################################################################
if itemSlug.isQualified
mod = @getMod itemSlug.mod
if mod?
item = mod.findItem itemSlug, options
addMod: (mod)->
if not mod? then return
if @_mods[mod.id] is mod then return
@_mods[mod.id] = mod
mod.modPack = this
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
findItem: (itemId)->
for modId, mod of @mods
item = mod.items[itemId]
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
return "ModPack:#{@displayName}<#{@id}>"
-103
View File
@@ -1,103 +0,0 @@
#
# Crafting Guide - mod_pack.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require './item'
ItemSlug = require './item_slug'
Mod = require './mod'
ModPack = require './mod_pack'
ModVersion = require './mod_version'
########################################################################################################################
buildcraft = industrialCraft = minecraft = modPack = null
########################################################################################################################
describe 'mod_pack.coffee', ->
beforeEach ->
minecraft = new Mod slug:'minecraft', name:'Minecraft'
minecraft.addModVersion new ModVersion modSlug:minecraft.slug, version:'1.7.10'
minecraft.activeModVersion.addItem new Item name:'Wool'
minecraft.activeModVersion.addItem new Item name:'Bed', recipes:['']
minecraft.activeModVersion.registerName ItemSlug.slugify('iron_chestplate'), 'Iron Chestplate'
buildcraft = new Mod slug:'buildcraft', name:'Buildcraft'
buildcraft.addModVersion new ModVersion modSlug:buildcraft.slug, version:'6.2.6'
buildcraft.activeModVersion.addItem new Item name:'Stone Gear', recipes:['']
buildcraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
buildcraft.activeVersion = Mod.Version.None
industrialCraft = new Mod slug:'industrial_craft', name:'Industrial Craft'
industrialCraft.addModVersion new ModVersion modSlug:industrialCraft.slug, version:'2.0'
industrialCraft.activeModVersion.addItem new Item name:'Resin'
industrialCraft.activeModVersion.addItem new Item name:'Rubber'
industrialCraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
industrialCraft.activeVersion = Mod.Version.None
modPack = new ModPack
modPack.addMod minecraft
modPack.addMod buildcraft
modPack.addMod industrialCraft
describe 'findItem', ->
it 'can find an item by partial slug', ->
item = modPack.findItem ItemSlug.slugify 'wool'
item.slug.qualified.should.equal 'minecraft__wool'
it 'can find an item by full slug', ->
item = modPack.findItem ItemSlug.slugify 'minecraft__wool'
item.name.should.equal 'Wool'
it 'can find an ambiguous item by full slug', ->
buildcraft.activeVersion = Mod.Version.Latest
industrialCraft.activeVersion = Mod.Version.Latest
item = modPack.findItem ItemSlug.slugify 'industrial_craft__wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Industrial Craft'
it 'can find an ambiguous item by partial slug', ->
buildcraft.activeVersion = Mod.Version.Latest
industrialCraft.activeVersion = Mod.Version.Latest
item = modPack.findItem ItemSlug.slugify 'wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Buildcraft'
describe 'findItemByName', ->
it 'finds the requested item', ->
item = modPack.findItemByName 'Bed'
item.name.should.equal 'Bed'
it 'ignores disabled mod versions', ->
item = modPack.findItemByName 'Stone Gear'
expect(item).to.be.null
describe 'findItemDisplay', ->
it 'returns all data for a regular Minecraft item', ->
display = modPack.findItemDisplay ItemSlug.slugify 'bed'
display.iconUrl.should.equal '/data/minecraft/items/bed/icon.png'
display.itemUrl.should.equal '/browse/minecraft/bed/'
display.itemName.should.equal 'Bed'
display.modSlug.should.equal 'minecraft'
it 'returns all data for an item in an enabled mod', ->
buildcraft.activeVersion = '6.2.6'
display = modPack.findItemDisplay ItemSlug.slugify 'stone_gear'
display.iconUrl.should.equal '/data/buildcraft/items/stone_gear/icon.png'
display.itemUrl.should.equal '/browse/buildcraft/stone_gear/'
display.itemName.should.equal 'Stone Gear'
display.modSlug.should.equal 'buildcraft'
it 'assumes an unfound item is from Minecraft', ->
display = modPack.findItemDisplay ItemSlug.slugify 'iron_chestplate'
display.iconUrl.should.equal '/data/minecraft/items/iron_chestplate/icon.png'
display.itemUrl.should.equal '/browse/minecraft/iron_chestplate/'
display.itemName.should.equal 'Iron Chestplate'
display.modSlug.should.equal 'minecraft'
@@ -1,106 +0,0 @@
#
# Crafting Guide - mod_version.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require './item'
ItemSlug = require './item_slug'
ModVersion = require './mod_version'
########################################################################################################################
modVersion = null
########################################################################################################################
describe 'mod_version.coffee', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'0.0'
modVersion.addItem new Item name:'underscore', group:'punctuation'
modVersion.addItem new Item name:'bravo', group:'letter'
modVersion.addItem new Item name:'alpha', group:'letter'
modVersion.addItem new Item name:'one', group:'number'
modVersion.addItem new Item name:'two', group:'number'
describe 'constructor', ->
it 'requires a mod slug', ->
expect(-> new ModVersion version:'0.0').to.throw Error, 'attributes.modSlug is required'
it 'requires a mod version', ->
expect(-> new ModVersion modSlug:'test').to.throw Error, 'attributes.version is required'
describe 'addItem', ->
it 'refuses to add duplicates', ->
modVersion.addItem new Item name:'Wool'
expect(-> modVersion.addItem new Item name:'Wool').to.throw Error, 'duplicate item for Wool'
it 'adds an item indexed by its slug', ->
modVersion.addItem new Item name:'Wool'
modVersion._items.wool.name.should.equal 'Wool'
it 'sets the modVersion', ->
modVersion.addItem new Item name:'Wool'
modVersion._items.wool.modVersion.should.equal modVersion
describe 'eachGroup', ->
it 'returns all the groups in order', ->
groupNames = []
modVersion.eachGroup (groupName)-> groupNames.push groupName
groupNames.should.eql ['letter', 'number', 'punctuation']
describe 'eachItemInGroup', ->
it 'returns immediately for unknown group', ->
slugs = []
modVersion.eachItemInGroup 'foobar', (item)-> slugs.push item.slug.qualified
slugs.should.eql []
it 'calls callback for exactly the items in a group in order', ->
slugs = []
modVersion.eachItemInGroup 'letter', (item)-> slugs.push item.slug.qualified
slugs.should.eql ['test__alpha', 'test__bravo']
slugs = []
modVersion.eachItemInGroup 'number', (item)-> slugs.push item.slug.qualified
slugs.should.eql ['test__one', 'test__two']
describe 'findItemByName', ->
it 'locates items by slugified name', ->
modVersion.addItem new Item name:'Crafting Table'
modVersion.findItemByName('Crafting Table').slug.qualified.should.equal 'test__crafting_table'
describe 'findRecipes', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'1.0'
modVersion.parse """
schema:1
item: Cake
recipe:; input: Milk, Sugar, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
recipe:; input: Milk, Cocoa Beans, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice
item: Bucket
recipe:; input: Iron Ingot; pattern: ... 0.0 .0.
recipe:; input: Copper Ingot; pattern: ... 0.0 .0.; extras: Copper Nugget
"""
it 'finds all recipes which list item as output', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__bucket')
(r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'bucket']
it 'skip recipes whose conditions are not met', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__cake')
recipes.length.should.equal 2
it 'finds recipes for items which are only ever extras', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__copper_nugget')
recipes.length.should.equal 1
@@ -1,85 +0,0 @@
#
# Crafting Guide - multiblock.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require './item_slug'
Mod = require './mod'
Multiblock = require './multiblock'
Stack = require './stack'
########################################################################################################################
describe 'multiblock.coffee', ->
describe 'with a single block type', ->
beforeEach ->
@input = [ new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1 ]
it 'correctly parses a 1x1x1 cube', ->
model = new Multiblock input:@input, layers:['0']
model.depth.should.equal 1
model.height.should.equal 1
model.width.should.equal 1
model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone'
it 'correctly parses a solid 3x3x3 cube', ->
model = new Multiblock input:@input, layers:['000 000 000', '000 000 000', '000 000 000']
model.depth.should.equal 3
model.height.should.equal 3
model.width.should.equal 3
for x in [0..2]
for y in [0..2]
for z in [0..2]
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
it 'correctly parses a hollow 3x3x3 cube', ->
model = new Multiblock input:@input, layers:['000 000 000', '000 0.0 000', '000 000 000']
model.depth.should.equal 3
model.height.should.equal 3
model.width.should.equal 3
for x in [0..2]
for y in [0..2]
for z in [0..2]
if x isnt 1 or y isnt 1 or z isnt 1
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
else
expect(model.getStackAt(x, y, z)).to.be.null
it 'correctly parses a 3x2x3 pyramid', ->
model = new Multiblock input:@input, layers:['000 000 000', '.. .0 ..']
model.depth.should.equal 3
model.height.should.equal 2
model.width.should.equal 3
for y in [0..1]
for z in [0..2]
for x in [0..2]
if y is 1 and (x isnt 1 or z isnt 1)
expect(model.getStackAt(x, y, z)).to.be.null
else
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
describe 'with multiple block types', ->
beforeEach ->
@input = [
new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1
new Stack itemSlug:ItemSlug.slugify('stone'), quantity:1
new Stack itemSlug:ItemSlug.slugify('oak wood'), quantity:1
]
it 'correctly parses a 1x3x1 column of different types', ->
model = new Multiblock input:@input, layers:['0', '1', '2']
model.depth.should.equal 1
model.height.should.equal 3
model.width.should.equal 1
model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone'
model.getStackAt(0, 1, 0).toString().should.equal '1 stone'
model.getStackAt(0, 2, 0).toString().should.equal '1 oak_wood'
+146 -219
View File
@@ -5,243 +5,170 @@
# 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
module.exports = class Recipe
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'
constructor: (attributes={})->
@depth = attributes.depth
@height = attributes.height
@id = attributes.id
@output = attributes.output
@width = attributes.width
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'
@_extras = {}
@_inputs = {}
@_inputGrid = []
@_tools = {}
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 #############################################################################
# Properties ###################################################################################
Object.defineProperties @prototype,
slug:
get: ->
if not @_slug?
builder = new StringBuilder
delimiterNeeded = false
for stack in @input
if delimiterNeeded then builder.push ','
delimiterNeeded = true
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"
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
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
builder.push '>'
builder.push @pattern
builder.push '>'
for stack in @tools
builder.push stack.itemSlug.qualified
builder.push '>'
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"
delimiterNeeded = false
for stack in @output
if delimiterNeeded then builder.push ','
delimiterNeeded = true
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
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
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
@_slug = builder.toString()
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"
return @_slug
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: ->
result = [@constructor.name, " (", @cid, ") { name:", @name]
toString: (options={})->
options.full ?= false
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 ']'
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
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
return b.toString()
else
return "Recipe:#{@output}<#{@id}>"
+50 -68
View File
@@ -5,86 +5,68 @@
# All rights reserved.
#
Item = require './item'
ItemSlug = require './item_slug'
Recipe = require './recipe'
Stack = require './stack'
fixtures = require "../fixtures"
Item = require "./item"
Recipe = require "./recipe"
Stack = require "./stack"
########################################################################################################################
input = output = pattern = recipe = null
describe "Recipe", ->
########################################################################################################################
beforeEach ->
@mod = fixtures.createMod()
@stick = fixtures.configureStick @mod
@ironIngot = fixtures.configureIronIngot @mod
@obsidian = fixtures.configureObsidian @mod
describe 'recipe.coffee', ->
@ironSword = new Item id:"iron_sword", displayName:"Iron Sword", mod:@mod
@obsidianBox = new Item id:"obsidian_box", displayName:"Obsidian Box", mod:@mod
describe 'constructor', ->
describe "getting & setting inputs", ->
beforeEach ->
input = [
new Stack(itemSlug:new ItemSlug('iron_gear')),
new Stack(itemSlug:new ItemSlug('gold_ingot'), quantity:4)
]
pattern = '.1. 101 .1.'
describe "for 2D recipes", ->
it 'requires input', ->
expect(-> new Recipe slug:'gold_gear', pattern:pattern).to.throw Error, 'attributes.input is required'
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 'requires a pattern', ->
expect(-> new Recipe slug:'gold_gear', input:input).to.throw Error, 'attributes.pattern is required'
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 'requires either outputs or a slug', ->
f = -> new Recipe input:input, pattern:pattern
expect(f).to.throw 'attributes.itemSlug or attributes.output is required'
it "determines the correct dimentions", ->
@recipe.depth.should.equal 1
@recipe.height.should.equal 3
@recipe.width.should.equal 2
it 'creates default output', ->
recipe = new Recipe itemSlug:ItemSlug.slugify('gold_gear'), input:input, pattern:pattern
recipe.output.length.should.equal 1
recipe.output[0].itemSlug.qualified.should.equal 'gold_gear'
recipe.output[0].quantity.should.equal 1
describe "for 3D recipes", ->
it 'assigns a default slug', ->
recipe = new Recipe input:input, pattern:pattern, output:[new Stack itemSlug:ItemSlug.slugify('gold_gear')]
recipe.itemSlug.qualified.should.equal 'gold_gear'
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
describe 'getStackAtSlot', ->
@recipe.setInputAt x, y, z, new Stack item:@obsidian
beforeEach ->
input = [
new Stack itemSlug:ItemSlug.slugify('iron_gear')
new Stack itemSlug:ItemSlug.slugify('gold_ingot'), quantity:4
]
recipe = new Recipe itemSlug:'gold_gear', input:input, pattern:'.1. 101 .1.'
it "returns 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 'returns the proper item for an early slot', ->
stack = recipe.getStackAtSlot(1)
stack.itemSlug.qualified.should.equal 'gold_ingot'
stack.quantity.should.equal 4
it 'returns the proper item for a late slot', ->
stack = recipe.getStackAtSlot(4)
stack.itemSlug.qualified.should.equal 'iron_gear'
stack.quantity.should.equal 1
it 'returns null for an invalid slot', ->
expect(recipe.getStackAtSlot(12)).to.be.null
describe '_parsePattern', ->
beforeEach ->
recipe = new Recipe
itemSlug: 'oak_wood_planks',
input: [new Stack itemSlug:new ItemSlug('oak_wood')],
pattern:'... .0. ...'
it 'normalizes invalid characters', ->
recipe._parsePattern('$$0 #() 010').should.equal '..0 ... 010'
it 'removes extra characters', ->
recipe._parsePattern('000 000 000 000').should.equal '000 000 000'
it 'fills in missing characters', ->
recipe._parsePattern('000000').should.equal '000 000 ...'
it 'fills in spaces', ->
recipe._parsePattern('000000000').should.equal '000 000 000'
it "determines the correct dimentions", ->
@recipe.depth.should.equal 3
@recipe.height.should.equal 3
@recipe.width.should.equal 3
+28 -9
View File
@@ -5,19 +5,38 @@
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class Stack extends BaseModel
module.exports = class Stack
constructor: (attributes={}, options={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
attributes.quantity ?= 1
options.logEvents ?= false
super attributes, options
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 "#{@quantity} #{@itemSlug}"
return "Stack:#{@item}×#{@quantity}"
@@ -0,0 +1,124 @@
#
# 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
@@ -0,0 +1,101 @@
#
# 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 = {}
@@ -0,0 +1,164 @@
#
# 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
@@ -0,0 +1,55 @@
#
# Crafting Guide - mod_pack_store.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModPackJsonParser = require "../parsing/mod_pack_json_parser"
########################################################################################################################
module.exports = class ModPackStore
constructor: ->
@_data = {}
@_loading = {}
@_parser = new ModPackJsonParser
# Class Methods ################################################################################
Object.defineProperties ModPackStore,
instance:
get: ->
@_instance ?= new ModPackStore
return @_instance
set: ->
throw new Error "cannot assign instance"
# Public Methods ###############################################################################
get: (modPackId)->
return @_data[modPackId]
load: (modPackId)->
if not @_loading[modPackId]?
@_loading[modPackId] = w.promise (resolve, reject)->
url = c.url.modPackArchive modPackId:modPackId
onError = (xhr, status, message)=>
logger.error "failed to load mod pack #{modPackId}: #{status}#{message}"
reject new Error message
onSuccess = (data, status, xhr)=>
logger.info "laoded mod pack: #{modPackId}"
try
@_parser.reset()
modPack = @_parser.parse data, url
@_data[modPack.id] = modPack
resolve modPack
catch error
reject error
$.ajax dataType: "text", error: onError, success: onSuccess, url: url
return @_loading[modPackId]
+1 -18
View File
@@ -15,6 +15,7 @@ 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'
Router = require './router'
########################################################################################################################
@@ -30,7 +31,6 @@ 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,23 +41,6 @@ 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)=>
+4 -41
View File
@@ -29,46 +29,6 @@ adsense.skyscraper.margin = 24 # px
adsense.skyscraper.slotIds = ['7613920409', '9574673605', '3388539204']
adsense.skyscraper.width = 160 # px
exports.defaultMods = defaultMods = {}
defaultMods.minecraft = { defaultVersion: '1.7.10' } # Minecraft must be first
defaultMods.advanced_solar_panels = { defaultVersion: '3.5.1' }
defaultMods.agricraft = { defaultVersion: '1.4.6' }
defaultMods.applied_energistics_2 = { defaultVersion: 'rv1-stable-1' }
defaultMods.big_reactors = { defaultVersion: '0.4.2A2' }
defaultMods.buildcraft = { defaultVersion: '1.7.18' }
defaultMods.computercraft = { defaultVersion: '1.74' }
defaultMods.draconic_evolution = { defaultVersion: '1.0.2h' }
defaultMods.ender_storage = { defaultVersion: '1.4.5.29' }
defaultMods.enderio = { defaultVersion: '2.2.7.325' }
defaultMods.extra_cells = { defaultVersion: '2.2.73b129' }
defaultMods.extra_utilities = { defaultVersion: '1.2.2' }
defaultMods.forestry = { defaultVersion: '3.4.0.7' }
defaultMods.forge_multipart = { defaultVersion: '1.2.0.345' }
defaultMods.galacticraft = { defaultVersion: '3.0.12.404' }
defaultMods.hydraulicraft = { defaultVersion: '2.1.242' }
defaultMods.ic2_classic = { defaultVersion: 'none' }
defaultMods.industrial_craft_2 = { defaultVersion: '2.2.663' }
defaultMods.iron_chests = { defaultVersion: '6.0.62.742' }
defaultMods.jabba = { defaultVersion: '1.2.1a' }
defaultMods.logistics_pipes = { defaultVersion: '0.9.3.100' }
defaultMods.mekanism = { defaultVersion: '7.1.1.127' }
defaultMods.minefactory_reloaded = { defaultVersion: '2.8.0RC8-86' }
defaultMods.modular_powersuits = { defaultVersion: '0.11.0-300-thermal-expansion' }
defaultMods.opencomputers = { defaultVersion: '1.5.22' }
defaultMods.quantum_flux = { defaultVersion: '1.3.4' }
defaultMods.project_red = { defaultVersion: '4.5.16.77' }
defaultMods.redstone_arsenal = { defaultVersion: '9.5.0' }
defaultMods.railcraft = { defaultVersion: '9.5.0' }
defaultMods.simply_jetpacks = { defaultVersion: '1.4.1' }
defaultMods.solar_expansion = { defaultVersion: '1.6a' }
defaultMods.solar_flux = { defaultVersion: '0.5b' }
defaultMods.storage_drawers = { defaultVersion: '1.7.10-1.6.2' }
defaultMods.thermal_dynamics = { defaultVersion: '1.7.10r1.2.0' }
defaultMods.thermal_expansion = { defaultVersion: '1.7.10r4.1.4' }
defaultMods.thermal_foundation = { defaultVersion: '1.7.10r1.2.5' }
defaultMods.tinkers_construct = { defaultVersion: '1.7.10-1.8.8' }
exports.duration = duration = {}
duration.snap = 100
duration.fast = 200
@@ -119,6 +79,9 @@ login.clientIds =
'staging': '3d75ed772ce5004180d6'
'production': 'ce71be7f66926ff6ff38'
exports.modpack = modpack = {}
modpack.default = "crafting-guide-default"
exports.modelState = modelState = {}
modelState.unloaded = 'unloaded'
modelState.loading = 'loading'
@@ -186,7 +149,7 @@ url.login = _.template "/login"
url.mod = _.template "/browse/<%= modSlug %>/"
url.modData = _.template "/data/<%= modSlug %>/mod.cg"
url.modIcon = _.template "/data/<%= modSlug %>/icon.png"
url.modpackArchive = _.template "/data/modpack.cg"
url.modPackData = _.template "/data/<%= modPackId %>/modpack.json"
url.modVersionData = _.template "/data/<%= modSlug %>/versions/<%= modVersion %>/mod-version.cg"
url.root = _.template "/"
url.tutorial = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/"