Qualify all item slugs to the mod which adds them #53

* Change default item URL to fall under /mod/<mod slug>/<item slug>
  but kept the old URL pattern around with the old behavior (i.e.,
  find the first item with that slug)
* Update Item to have a `qualifiedSlug` property which combines the
  item's own slug with its Mod's slug.  Updated a lot of code all
  over to use this in preference to the plain slug.
* Add a `localizeTo` method to Inventory to allow non-qualified slugs
  to be converted into qualified slugs within the inventory's stacks
* Update ModPack to work with either qualified or unqualified slugs
  for relevant methods
* Fix model state changes to happen before related events are
  triggered
* Add a debounce to the CraftingPlan object when triggering a recraft
  due to changes in related objects (esp. its inventories)
* Update the Inventory's sort to keep items sorted by mod before name
  with the Minecraft items sorted to the top
* Fix `ModPack.findItemByName` to actually recursively use the Mod's
  method instead of slugifying the given name and looking for the
  slug (since this won't necessarily work anymore).
* Ensure each Mod is assigned a reference to its containing ModPack
* Fix NameFinder to only consider an item non-gatherable if it is
  also craftable
* Update ModVersionParserV1 to assign qualified slugs to recipes
  for any items which are in the same mod (leaving any other slugs
  unqualified)
* Add Underscore mixins for composing and decomposing slugs
* Update old tests for the use of qualified slugs, and some new tests
  for the new methods added
This commit is contained in:
Andrew Miner
2015-02-10 17:56:02 -08:00
parent 246dbcafa9
commit 380a78ef61
28 changed files with 278 additions and 101 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ Text.title = 'Crafting Guide for Minecraft | The Ultimate Step-by-Step Tutorial
exports.Url = Url = {} exports.Url = Url = {}
Url.crafting = _.template "/crafting/<%= inventoryText %>" Url.crafting = _.template "/crafting/<%= inventoryText %>"
Url.itemIcon = _.template "/data/<%= modSlug %>/<%= modVersion %>/images/<%= slug %>.png" Url.itemIcon = _.template "/data/<%= modSlug %>/<%= modVersion %>/images/<%= slug %>.png"
Url.item = _.template "/item/<%= slug %>" Url.item = _.template "/mod/<%= modSlug %>/<%= slug %>"
Url.mod = _.template "/mod/<%= modSlug %>" Url.mod = _.template "/mod/<%= modSlug %>"
Url.modData = _.template "/data/<%= modSlug %>/mod.cg" Url.modData = _.template "/data/<%= modSlug %>/mod.cg"
Url.modVersion = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg" Url.modVersion = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg"
@@ -68,8 +68,8 @@ module.exports = class FullRecipeController extends BaseController
result = [] result = []
if @model? if @model?
for stack in @model.tools for stack in @model.tools
name = @modPack.findName stack.slug item = @modPack.findItem stack.slug
result.push name if name? result.push item.name if item?
return result return result
_refreshInputs: -> _refreshInputs: ->
@@ -28,7 +28,7 @@ module.exports = class ItemController extends BaseController
super super
refresh: -> refresh: ->
display = @_modPack.findItemDisplay @model.slug display = @_modPack.findItemDisplay @model.qualifiedSlug
@$icon.attr 'src', display.iconUrl @$icon.attr 'src', display.iconUrl
@$name.html display.itemName @$name.html display.itemName
@@ -57,7 +57,7 @@ module.exports = class ItemPageController extends BaseController
$('title').html if @model.item? then "#{@model.item.name} | #{Text.title}" else Text.title $('title').html if @model.item? then "#{@model.item.name} | #{Text.title}" else Text.title
@_resolveItemSlug() @_resolveItemSlug()
display = @_modPack.findItemDisplay @model.item?.slug display = @_modPack.findItemDisplay @model.item?.qualifiedSlug
if display? if display?
@$craftingPlanLink.attr href:display.craftingUrl @$craftingPlanLink.attr href:display.craftingUrl
@$craftingPlanLink.fadeIn duration:Duration.normal @$craftingPlanLink.fadeIn duration:Duration.normal
@@ -143,6 +143,4 @@ module.exports = class ItemPageController extends BaseController
@$similarContainer.fadeOut duration:Duration.normal @$similarContainer.fadeOut duration:Duration.normal
_resolveItemSlug: -> _resolveItemSlug: ->
oldItem = @model.item
@model.item = @_modPack.findItem @_itemSlug, includeDisabled:true @model.item = @_modPack.findItem @_itemSlug, includeDisabled:true
newItem = @model.item
@@ -72,6 +72,6 @@ module.exports = class MinimalRecipeController extends BaseController
result = [] result = []
if @model? if @model?
for stack in @model.tools for stack in @model.tools
name = @_modPack.findName stack.slug item = @_modPack.findItem stack.slug
result.push name if name? result.push item.name if item?
return result return result
+13 -7
View File
@@ -57,10 +57,11 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
@_recordPageView() @_recordPageView()
routes: routes:
'': 'root' '': 'root'
'item/:itemSlug': 'item' 'item/:itemSlug': 'item'
'crafting/(:text)': 'crafting' 'crafting/(:text)': 'crafting'
'mod/:modSlug': 'mod' 'mod/:modSlug': 'mod'
'mod/:modSlug/:itemSlug': 'modItem'
# Route Methods ################################################################################ # Route Methods ################################################################################
@@ -69,7 +70,12 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
controller.model.params = inventoryText:text controller.model.params = inventoryText:text
@_setPage 'crafting', controller @_setPage 'crafting', controller
item: (slug)-> item: (itemSlug)->
controller = new ItemPageController _.extend {itemSlug:itemSlug}, @_defaultOptions
@_setPage 'item', controller
modItem: (modSlug, itemSlug)->
slug = _.composeSlugs modSlug, itemSlug
controller = new ItemPageController _.extend {itemSlug:slug}, @_defaultOptions controller = new ItemPageController _.extend {itemSlug:slug}, @_defaultOptions
@_setPage 'item', controller @_setPage 'item', controller
@@ -84,9 +90,9 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
text = '' text = ''
if params.recipeName? if params.recipeName?
if params.count? if params.count?
text = "#{params.count}.#{params.recipeName}" text = "#{params.count}.#{_.slugify(params.recipeName)}"
else else
text = "#{params.recipeName}" text = _.slugify params.recipeName
@navigate Url.crafting(inventoryText:text), trigger:true @navigate Url.crafting(inventoryText:text), trigger:true
+1 -1
View File
@@ -24,7 +24,7 @@ global.logger = new Logger
switch window.location.hostname switch window.location.hostname
when 'localhost' when 'localhost'
global.env = 'development' global.env = 'development'
logger.level = Logger.INFO logger.level = Logger.DEBUG
when 'new.crafting-guide.com' when 'new.crafting-guide.com'
global.env = 'staging' global.env = 'staging'
logger.level = Logger.VERBOSE logger.level = Logger.VERBOSE
+4 -4
View File
@@ -25,10 +25,6 @@ module.exports = class BaseModel extends Backbone.Model
@logEvents = options.logEvents or false @logEvents = options.logEvents or false
@state = ModelState.unloaded @state = ModelState.unloaded
@on 'request', => @state = ModelState.loading
@on 'sync', => @state = ModelState.loaded
@on 'error', => @state = ModelState.error
@loading = null @loading = null
Object.defineProperties this, Object.defineProperties this,
@@ -42,6 +38,8 @@ module.exports = class BaseModel extends Backbone.Model
onLoadSucceeded: (text, status, xhr)-> onLoadSucceeded: (text, status, xhr)->
try try
@set @parse text @set @parse text
@state = ModelState.loaded
@trigger Event.change, this @trigger Event.change, this
@trigger Event.sync, this @trigger Event.sync, this
logger.info => "#{@constructor.name}.#{@cid} loaded successfully" logger.info => "#{@constructor.name}.#{@cid} loaded successfully"
@@ -50,6 +48,7 @@ module.exports = class BaseModel extends Backbone.Model
@onLoadFailed e.message, 'parsing failed', xhr @onLoadFailed e.message, 'parsing failed', xhr
onLoadFailed: (error, status, xhr)-> onLoadFailed: (error, status, xhr)->
@state = ModelState.error
logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}" logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}"
@trigger Event.error, this, error @trigger Event.error, this, error
@@ -62,6 +61,7 @@ module.exports = class BaseModel extends Backbone.Model
url = @url() url = @url()
logger.info => "#{@constructor.name}.#{@cid} reading from url: #{url}" logger.info => "#{@constructor.name}.#{@cid} reading from url: #{url}"
@state = ModelState.loading
@trigger Event.request, this @trigger Event.request, this
@loading = w.promise (resolve, reject)=> @loading = w.promise (resolve, reject)=>
$.ajax $.ajax
+2 -2
View File
@@ -14,7 +14,7 @@ ModPack = require './mod_pack'
######################################################################################################################## ########################################################################################################################
module.exports = class extends BaseModel module.exports = class CraftingPage extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
attributes.modPack ?= new ModPack attributes.modPack ?= new ModPack
@@ -40,7 +40,7 @@ module.exports = class extends BaseModel
inventory = @_parser.parse @params.inventoryText inventory = @_parser.parse @params.inventoryText
inventory.each (stack)=> inventory.each (stack)=>
item = @modPack.findItemByName stack.slug, enableAsNeeded:true item = @modPack.findItem stack.slug, enableAsNeeded:true
return unless item? and item.isCraftable return unless item? and item.isCraftable
@plan.want.add stack.slug, stack.quantity @plan.want.add stack.slug, stack.quantity
inventory.remove stack.slug inventory.remove stack.slug
+35 -23
View File
@@ -23,14 +23,14 @@ module.exports = class CraftingPlan extends BaseModel
@need = new Inventory @need = new Inventory
@result = new Inventory @result = new Inventory
recraft = _.debounce (=> @craft()), 100
for inventory in [@have, @want]
inventory.on 'change', recraft
@on Event.change + ':includingTools', recraft
@clear() @clear()
@have.on Event.change, => @craft()
@want.on Event.change, => @craft()
@modPack.on Event.change, => @craft()
@on Event.change + ':includingTools', => @craft()
# Public Methods ############################################################################### # Public Methods ###############################################################################
clear: (options={})-> clear: (options={})->
@@ -42,7 +42,12 @@ module.exports = class CraftingPlan extends BaseModel
return this return this
craft: -> craft: ->
toolsMessage = if @includingTools then ' (including tools)' else ''
logger.info => "crafting #{@want}#{toolsMessage} starting with #{@have}"
@clear() @clear()
@have.localizeTo @modPack
@want.localizeTo @modPack
@result.addInventory @have @result.addInventory @have
@@ -50,12 +55,14 @@ module.exports = class CraftingPlan extends BaseModel
@_reservedSteps = {} @_reservedSteps = {}
@want.each (stack)=> @want.each (stack)=>
@_findSteps stack.slug @_findSteps stack.slug
@need.add stack.slug, stack.quantity item = @modPack.findItem stack.slug
@need.add item.qualifiedSlug, stack.quantity
@_reservedSteps = null @_reservedSteps = null
@steps = _.values @steps @steps = _.values @steps
@_resolveNeeds() @_resolveNeeds()
@_removeExtraSteps() @_removeExtraSteps()
@result.addInventory @want @result.addInventory @want
@need.trigger 'change', @need @need.trigger 'change', @need
@@ -91,14 +98,13 @@ module.exports = class CraftingPlan extends BaseModel
# Private Methods ############################################################################## # Private Methods ##############################################################################
_addStep: (recipe)-> _addStep: (recipe)->
logger.verbose -> "adding step: #{recipe.slug}" logger.verbose -> "adding step: #{recipe.item.qualifiedSlug}"
@steps[recipe.slug] = recipe:recipe @steps[recipe.item.qualifiedSlug] = recipe:recipe
_chooseRecipe: (item)-> _chooseRecipe: (item)->
return item.getPrimaryRecipe() return item.getPrimaryRecipe()
_findSteps: (slug)-> _findSteps: (slug)->
logger.debug -> "finding steps for #{slug}"
item = @modPack.findItem slug item = @modPack.findItem slug
return unless item? return unless item?
return unless item.isCraftable return unless item.isCraftable
@@ -111,9 +117,8 @@ module.exports = class CraftingPlan extends BaseModel
if not @_hasStep toolStack.slug if not @_hasStep toolStack.slug
@_findSteps toolStack.slug @_findSteps toolStack.slug
return if @_hasStep item.slug return if @_hasStep item.qualifiedSlug
logger.debug -> "reserving: #{item.slug}" @_reservedSteps[item.qualifiedSlug] = recipe
@_reservedSteps[item.slug] = recipe
for inputStack in recipe.input for inputStack in recipe.input
@_findSteps inputStack.slug @_findSteps inputStack.slug
@@ -125,6 +130,11 @@ module.exports = class CraftingPlan extends BaseModel
return true if @_reservedSteps[slug]? return true if @_reservedSteps[slug]?
return false return false
_qualifyItemSlug: (slug)->
item = @modPack.findItem slug
return item.qualifiedSlug if item?
return slug
_removeExtraSteps: -> _removeExtraSteps: ->
result = (step for step in @steps when step.multiplier > 0) result = (step for step in @steps when step.multiplier > 0)
@steps = result @steps = result
@@ -134,29 +144,31 @@ module.exports = class CraftingPlan extends BaseModel
step = @steps[i] step = @steps[i]
recipe = step.recipe recipe = step.recipe
step.multiplier = Math.ceil(@need.quantityOf(recipe.slug) / step.recipe.output[0].quantity) step.multiplier = Math.ceil(@need.quantityOf(recipe.slug) / recipe.output[0].quantity)
if @includingTools if @includingTools
for stack in recipe.tools for stack in recipe.tools
slug = stack.slug slug = @_qualifyItemSlug stack.slug
available = @result.quantityOf(slug) + @need.quantityOf(slug) available = @result.quantityOf(slug) + @need.quantityOf(slug)
needed = Math.max 0, stack.quantity - available needed = Math.max 0, stack.quantity - available
@need.add stack.slug, needed @need.add slug, needed
@result.add stack.slug, needed @result.add slug, needed
for stack in recipe.input for stack in recipe.input
slug = @_qualifyItemSlug stack.slug
needed = step.multiplier * stack.quantity needed = step.multiplier * stack.quantity
consumed = Math.min needed, @result.quantityOf(stack.slug) consumed = Math.min needed, @result.quantityOf slug
remaining = needed - consumed remaining = needed - consumed
@result.remove stack.slug, consumed @result.remove slug, consumed
@need.add stack.slug, remaining @need.add slug, remaining
for stack in recipe.output for stack in recipe.output
slug = @_qualifyItemSlug stack.slug
created = stack.quantity * step.multiplier created = stack.quantity * step.multiplier
consumed = Math.min created, @need.quantityOf stack.slug consumed = Math.min created, @need.quantityOf slug
remaining = created - consumed remaining = created - consumed
@result.add stack.slug, remaining @result.add slug, remaining
@need.remove stack.slug, consumed @need.remove slug, consumed
+37 -4
View File
@@ -5,9 +5,10 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
BaseModel = require './base_model' BaseModel = require './base_model'
{Event} = require '../constants' {Event} = require '../constants'
Stack = require './stack' {RequiredMods} = require '../constants'
Stack = require './stack'
######################################################################################################################## ########################################################################################################################
@@ -59,6 +60,22 @@ module.exports = class Inventory extends BaseModel
return false unless stack? return false unless stack?
return stack.quantity >= quantity return stack.quantity >= quantity
localizeTo: (modPack)->
newSlugs = []
for slug in @_slugs
stack = @_stacks[slug]
qualifiedSlug = modPack.findItem(slug)?.qualifiedSlug
if qualifiedSlug?
delete @_stacks[slug]
newSlugs.push qualifiedSlug
@_stacks[qualifiedSlug] = stack
stack.slug = qualifiedSlug
else
throw new Error "could not find an item for: #{slug}"
@_slugs = newSlugs
@_sort()
pop: -> pop: ->
slug = @_slugs.pop() slug = @_slugs.pop()
return null unless slug? return null unless slug?
@@ -129,6 +146,22 @@ module.exports = class Inventory extends BaseModel
stack = new Stack slug:slug, quantity:quantity stack = new Stack slug:slug, quantity:quantity
@_stacks[slug] = stack @_stacks[slug] = stack
@_slugs.push slug @_slugs.push slug
@_slugs.sort() @_sort()
else else
stack.quantity += quantity stack.quantity += quantity
_sort: ->
@_slugs.sort (a, b)->
[modSlugA, itemSlugA] = _.decomposeSlug a
[modSlugB, itemSlugB] = _.decomposeSlug b
isRequiredA = modSlugA in RequiredMods
isRequiredB = modSlugB in RequiredMods
if isRequiredA isnt isRequiredB
return -1 if isRequiredA
return +1 if isRequiredB
else if modSlugA isnt modSlugB
return if modSlugA < modSlugB then -1 else +1
else if itemSlugA isnt itemSlugB
return if itemSlugA < itemSlugB then -1 else +1
return 0
+2 -2
View File
@@ -33,10 +33,10 @@ module.exports = class InventoryParser
stackParts = stackText.split InventoryParser.ITEM_DELIMITER stackParts = stackText.split InventoryParser.ITEM_DELIMITER
if stackParts.length is 2 if stackParts.length is 2
quantity = parseInt stackParts[0] quantity = parseInt stackParts[0]
slug = _.slugify stackParts[1] slug = stackParts[1]
else if stackParts.length is 1 else if stackParts.length is 1
quantity = 1 quantity = 1
slug = _.slugify stackParts[0] slug = stackParts[0]
else else
throw new Error "expected #{stackText} to have 0 or 1 parts" throw new Error "expected #{stackText} to have 0 or 1 parts"
+22 -4
View File
@@ -21,28 +21,33 @@ module.exports = class Item extends BaseModel
attributes.group ?= Item.Group.Other attributes.group ?= Item.Group.Other
attributes.isGatherable ?= false attributes.isGatherable ?= false
attributes.modVersion ?= null
attributes.slug ?= _.slugify attributes.name attributes.slug ?= _.slugify attributes.name
options.logEvents ?= false options.logEvents ?= false
super attributes, options super attributes, options
@_recipes = [] @_recipes = []
Object.defineProperties this, Object.defineProperties this,
isCraftable: { get:-> @_recipes.length > 0 } isCraftable: { get:-> @_recipes.length > 0 }
qualifiedSlug: { get:@getQualifiedSlug }
primaryRecipe: { get:@getPrimaryRecipe } primaryRecipe: { get:@getPrimaryRecipe }
@on Event.change + ':modVersion', => @_qualifiedSlug = null
# Public Methods ############################################################################### # Public Methods ###############################################################################
addRecipe: (recipe)-> addRecipe: (recipe)->
if recipe.slug isnt @slug then throw new Error "cannot add a recipe for #{recipe.slug} to #{@slug}" [modSlug, itemSlug] = _.decomposeSlug recipe.slug
if itemSlug isnt @slug then throw new Error "cannot add a recipe for #{recipe.slug} to #{@slug}"
recipe.item = this
@_recipes.push recipe @_recipes.push recipe
eachRecipe: (callback)-> eachRecipe: (callback)->
for recipe in @_recipes for recipe in @_recipes
callback recipe callback recipe
getPrimaryRecipe: ->
return @_recipes[0]
compareTo: (that)-> compareTo: (that)->
if this.slug isnt that.slug if this.slug isnt that.slug
return if this.slug < that.slug then -1 else +1 return if this.slug < that.slug then -1 else +1
@@ -50,6 +55,19 @@ module.exports = class Item extends BaseModel
return if this.name < that.name then -1 else +1 return if this.name < that.name then -1 else +1
return 0 return 0
# Property Methods #############################################################################
getQualifiedSlug: ->
return @slug if not @modVersion?
if not @_qualifiedSlug?
@_qualifiedSlug = _.composeSlugs @modVersion.modSlug, @slug
return @_qualifiedSlug
getPrimaryRecipe: ->
return @_recipes[0]
# Object Overrides ############################################################################# # Object Overrides #############################################################################
toString: -> toString: ->
+4 -4
View File
@@ -31,14 +31,14 @@ module.exports = class ItemPage extends BaseModel
findComponentInItems: -> findComponentInItems: ->
return null unless @item? return null unless @item?
itemSlug = @item.slug itemSlug = @item.qualifiedSlug
result = {} result = {}
@modPack.eachMod (mod)-> @modPack.eachMod (mod)->
mod.eachItem (item)-> mod.eachItem (item)->
item.eachRecipe (recipe)-> item.eachRecipe (recipe)->
for stack in recipe.input for stack in recipe.input
if stack.slug is itemSlug if stack.slug is itemSlug
result[item.slug] = item result[item.qualifiedSlug] = item
result = _.values(result).sort (a, b)-> a.compareTo b result = _.values(result).sort (a, b)-> a.compareTo b
return null unless result.length > 0 return null unless result.length > 0
@@ -56,7 +56,7 @@ module.exports = class ItemPage extends BaseModel
return result return result
findRecipes: -> findRecipes: ->
result = @modPack.findRecipes @item?.slug result = @modPack.findRecipes @item?.qualifiedSlug
return null unless result.length > 0 return null unless result.length > 0
return result return result
@@ -66,7 +66,7 @@ module.exports = class ItemPage extends BaseModel
@_plan.clear() @_plan.clear()
if @item? if @item?
@_plan.want.add @item.slug @_plan.want.add @item.qualifiedSlug
@_plan.craft() @_plan.craft()
if @_plan.steps.length > 0 if @_plan.steps.length > 0
+1
View File
@@ -21,6 +21,7 @@ module.exports = class Mod extends BaseModel
attributes.documentationUrl ?= null attributes.documentationUrl ?= null
attributes.downloadUrl ?= null attributes.downloadUrl ?= null
attributes.homePageUrl ?= null attributes.homePageUrl ?= null
attributes.modPack ?= null
attributes.name ?= '' attributes.name ?= ''
super attributes, options super attributes, options
+20 -8
View File
@@ -25,9 +25,16 @@ module.exports = class ModPack extends BaseModel
findItem: (slug, options={})-> findItem: (slug, options={})->
options.includeDisabled ?= false options.includeDisabled ?= false
[modSlug, itemSlug] = _.decomposeSlug slug
if modSlug?
mod = @getMod modSlug
if mod?
item = mod.findItem itemSlug, options
return item if item?
for mod in @_mods for mod in @_mods
continue unless mod.enabled or options.includeDisabled continue unless mod.enabled or options.includeDisabled
item = mod.findItem slug, options item = mod.findItem itemSlug, options
return item if item? return item if item?
return null return null
@@ -36,10 +43,9 @@ module.exports = class ModPack extends BaseModel
options.enableAsNeeded ?= false options.enableAsNeeded ?= false
options.includeDisabled = true if options.enableAsNeeded options.includeDisabled = true if options.enableAsNeeded
slug = _.slugify name
for mod in @_mods for mod in @_mods
continue unless mod.enabled or options.includeDisabled continue unless mod.enabled or options.includeDisabled
item = mod.findItem slug, options item = mod.findItemByName name, options
return item if item? return item if item?
return null return null
@@ -74,6 +80,14 @@ module.exports = class ModPack extends BaseModel
return null return null
findRecipes: (slug, result=[])-> findRecipes: (slug, result=[])->
[modSlug, itemSlug] = _.decomposeSlug slug
if modSlug?
mod = @getMod modSlug
if mod?
mod.findRecipes slug, result
return result if result.length > 0
for mod in @_mods for mod in @_mods
continue unless mod.enabled continue unless mod.enabled
mod.findRecipes slug, result mod.findRecipes slug, result
@@ -88,12 +102,9 @@ module.exports = class ModPack extends BaseModel
isValidName: (name)-> isValidName: (name)->
slug = _.slugify name slug = _.slugify name
for mod in @_mods existingName = @findName slug
continue unless mod.enabled
name = mod.findName slug
return true if name
return false return name is existingName
# Property Methods ############################################################################# # Property Methods #############################################################################
@@ -101,6 +112,7 @@ module.exports = class ModPack extends BaseModel
if not mod? then throw new Error 'mod is required' if not mod? then throw new Error 'mod is required'
return if @_mods.indexOf(mod) isnt -1 return if @_mods.indexOf(mod) isnt -1
mod.modPack = this
@_mods.push mod @_mods.push mod
@listenTo mod, Event.change, => @trigger Event.change, this @listenTo mod, Event.change, => @trigger Event.change, this
@trigger Event.add + ':mod', mod, this @trigger Event.add + ':mod', mod, this
+1
View File
@@ -19,6 +19,7 @@ module.exports = class ModVersion extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
if not attributes.modSlug? then throw new Error 'attributes.modSlug is required' if not attributes.modSlug? then throw new Error 'attributes.modSlug is required'
if not attributes.version? then throw new Error 'attributes.version is required' if not attributes.version? then throw new Error 'attributes.version is required'
attributes.mod ?= null
super attributes, options super attributes, options
@_groups = {} @_groups = {}
+1 -1
View File
@@ -67,7 +67,7 @@ module.exports = class NameFinder
item = mod.findItem slug item = mod.findItem slug
if not @includeGatherable if not @includeGatherable
return unless item? and (not item.isGatherable) return unless item? and item.isCraftable
scanName = "#{mod.name} : #{name}" scanName = "#{mod.name} : #{name}"
if nameHint? if nameHint?
@@ -61,8 +61,8 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
if not name.length > 0 then throw new Error 'the item name cannot be empty' if not name.length > 0 then throw new Error 'the item name cannot be empty'
@_itemData = name:name, line:@_lineNumber, group:@_rawData.group @_itemData = name:name, line:@_lineNumber, group:@_rawData.group
@_rawData.items ?= [] @_rawData.items ?= {}
@_rawData.items.push @_itemData @_rawData.items[name] = @_itemData
@_recipeData = null @_recipeData = null
@@ -112,7 +112,7 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
_buildModVersion: (modVersionData, modVersion)-> _buildModVersion: (modVersionData, modVersion)->
modVersionData.items ?= [] modVersionData.items ?= []
for itemData in modVersionData.items for itemName, itemData of modVersionData.items
@_handleErrors @_buildItem, modVersion, itemData @_handleErrors @_buildItem, modVersion, itemData
return modVersion return modVersion
@@ -135,6 +135,12 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
if not recipeData.input? then throw new Error 'the "input" declaration is required' if not recipeData.input? then throw new Error 'the "input" declaration is required'
if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required' if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required'
localizeSlug = (name, slug)=>
if @_rawData.items[name]?
return _.composeSlugs modVersion.modSlug, slug
else
return slug
recipeData.quantity ?= 1 recipeData.quantity ?= 1
recipeData.extras ?= [] recipeData.extras ?= []
recipeData.tools ?= [] recipeData.tools ?= []
@@ -143,6 +149,7 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
for name in recipeData.input for name in recipeData.input
slug = _.slugify name slug = _.slugify name
modVersion.registerSlug slug, name modVersion.registerSlug slug, name
slug = localizeSlug name, slug
inputStacks.push new Stack slug:slug, quantity:0 inputStacks.push new Stack slug:slug, quantity:0
for c in recipeData.pattern for c in recipeData.pattern
@@ -158,16 +165,18 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
name = modVersion.findName stack.slug name = modVersion.findName stack.slug
throw new Error "#{name} is an input for this recipe, but it is not in the pattern" throw new Error "#{name} is an input for this recipe, but it is not in the pattern"
outputStacks = [ new Stack slug:item.slug, quantity:recipeData.quantity ] outputStacks = [ new Stack slug:item.qualifiedSlug, quantity:recipeData.quantity ]
for extraData in recipeData.extras for extraData in recipeData.extras
slug = _.slugify extraData.name slug = _.slugify extraData.name
modVersion.registerSlug slug, extraData.name modVersion.registerSlug slug, extraData.name
slug = localizeSlug extraData.name, slug
outputStacks.push new Stack slug:slug, quantity:extraData.quantity outputStacks.push new Stack slug:slug, quantity:extraData.quantity
toolStacks = [] toolStacks = []
for name in recipeData.tools for name in recipeData.tools
slug = _.slugify name slug = _.slugify name
modVersion.registerSlug slug, name modVersion.registerSlug slug, name
slug = localizeSlug name, slug
toolStacks.push new Stack slug:slug, quantity:1 toolStacks.push new Stack slug:slug, quantity:1
attributes = attributes =
+10 -3
View File
@@ -15,8 +15,7 @@ module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
if attributes.item? if attributes.item?
attributes.name = attributes.item.name attributes.name = attributes.item.name
attributes.slug = attributes.item.slug attributes.output ?= [new Stack slug:attributes.item.qualifiedSlug, quantity:1]
attributes.output ?= [new Stack slug:attributes.item.slug, quantity:1]
if not attributes.name? then throw new Error 'attributes.name is required' if not attributes.name? then throw new Error 'attributes.name is required'
if not attributes.input? then throw new Error 'attributes.input is required' if not attributes.input? then throw new Error 'attributes.input is required'
@@ -25,11 +24,13 @@ module.exports = class Recipe extends BaseModel
attributes.item ?= null attributes.item ?= null
attributes.output ?= [new Stack slug:_.slugify(attributes.name), quantity:1] attributes.output ?= [new Stack slug:_.slugify(attributes.name), quantity:1]
attributes.pattern = @_parsePattern attributes.pattern attributes.pattern = @_parsePattern attributes.pattern
attributes.slug ?= attributes.output[0].slug
attributes.tools ?= [] attributes.tools ?= []
options.logEvents ?= false options.logEvents ?= false
super attributes, options super attributes, options
Object.defineProperties this,
slug: {get:@getSlug}
# Public Methods ############################################################################### # Public Methods ###############################################################################
getItemSlugAt: (patternSlot)-> getItemSlugAt: (patternSlot)->
@@ -48,6 +49,12 @@ module.exports = class Recipe extends BaseModel
return true if stack.slug is itemSlug return true if stack.slug is itemSlug
return false return false
# Property Methods #############################################################################
getSlug: ->
return @item.qualifiedSlug if @item?
return @output[0].slug
# Object Overrides ############################################################################# # Object Overrides #############################################################################
toString: -> toString: ->
+12
View File
@@ -16,3 +16,15 @@ _.mixin
result = result.replace /^_/, '' result = result.replace /^_/, ''
result = result.replace /_$/, '' result = result.replace /_$/, ''
return result return result
composeSlugs: (part1, part2)->
return "#{part1}__#{part2}"
decomposeSlug: (slug)->
return [null, null] unless slug?
parts = slug.split '__'
if parts.length is 1
parts = [ null, parts[0] ]
return parts
+14 -6
View File
@@ -44,25 +44,29 @@ describe 'crafting_plan.coffee', ->
plan.want.add 'oak_plank' plan.want.add 'oak_plank'
plan.craft() plan.craft()
plan.need.toList().should.eql ['oak_log'] plan.need.toList().should.eql ['oak_log']
plan.result.toList().should.eql [[4, 'oak_plank']] plan.result.toList().should.eql [[4, 'minecraft__oak_plank']]
it 'can craft a multi-step recipe', -> it 'can craft a multi-step recipe', ->
plan.want.add 'crafting_table' plan.want.add 'crafting_table'
plan.craft() plan.craft()
plan.need.toList().should.eql ['oak_log'] plan.need.toList().should.eql ['oak_log']
plan.result.toList().should.eql ['crafting_table'] plan.result.toList().should.eql ['minecraft__crafting_table']
it 'can craft a multi-step recipe using tools', -> it 'can craft a multi-step recipe using tools', ->
plan.want.add 'furnace' plan.want.add 'furnace'
plan.craft() plan.craft()
plan.need.toList().should.eql [[8, 'cobblestone']] plan.need.toList().should.eql [[8, 'cobblestone']]
plan.result.toList().should.eql ['furnace'] plan.result.toList().should.eql ['minecraft__furnace']
it 'can craft a multi-step recipe re-using tools', -> it 'can craft a multi-step recipe re-using tools', ->
plan.want.add 'iron_sword' plan.want.add 'iron_sword'
plan.craft() plan.craft()
plan.need.toList().should.eql [[2, 'furnace_fuel'], [2, 'iron_ore'], 'oak_log'] plan.need.toList().should.eql [[2, 'furnace_fuel'], [2, 'iron_ore'], 'oak_log']
plan.result.toList().should.eql ['iron_sword', [2, 'oak_plank'], [3, 'stick']] plan.result.toList().should.eql [
'minecraft__iron_sword',
[2, 'minecraft__oak_plank'],
[3, 'minecraft__stick']
]
describe 'with building tools', -> describe 'with building tools', ->
@@ -71,7 +75,7 @@ describe 'crafting_plan.coffee', ->
plan.want.add 'furnace' plan.want.add 'furnace'
plan.craft() plan.craft()
plan.need.toList().should.eql [[8, 'cobblestone'], 'oak_log'] plan.need.toList().should.eql [[8, 'cobblestone'], 'oak_log']
plan.result.toList().should.eql ['crafting_table', 'furnace'] plan.result.toList().should.eql ['minecraft__crafting_table', 'minecraft__furnace']
it 'can craft a multi-step recipe re-using tools', -> it 'can craft a multi-step recipe re-using tools', ->
plan.includingTools = true plan.includingTools = true
@@ -82,5 +86,9 @@ describe 'crafting_plan.coffee', ->
[8, 'cobblestone'], [2, 'furnace_fuel'], [2, 'iron_ore'], [2, 'oak_log'] [8, 'cobblestone'], [2, 'furnace_fuel'], [2, 'iron_ore'], [2, 'oak_log']
] ]
plan.result.toList().should.eql [ plan.result.toList().should.eql [
'crafting_table', 'furnace', 'iron_sword', [2, 'oak_plank'], [3, 'stick'] 'minecraft__crafting_table',
'minecraft__furnace',
'minecraft__iron_sword',
[2, 'minecraft__oak_plank'],
[3, 'minecraft__stick']
] ]
+34 -1
View File
@@ -12,7 +12,7 @@ Item = require '../src/scripts/models/item'
######################################################################################################################## ########################################################################################################################
inventory = null inventory = modPack = null
######################################################################################################################## ########################################################################################################################
@@ -107,6 +107,39 @@ describe 'inventory.coffee', ->
inventory.hasAtLeast('wool', 4).should.be.true inventory.hasAtLeast('wool', 4).should.be.true
inventory.hasAtLeast('wool', 5).should.be.false inventory.hasAtLeast('wool', 5).should.be.false
describe 'localizeTo', ->
before ->
modPack =
map:
wool: 'minecraft__wool'
string: 'minecraft__string'
boat: 'minecraft__boat'
stone_gear: 'buildcraft__stone_gear'
findItem: (slug)->
[modSlug, itemSlug] = _.decomposeSlug slug
return slug:itemSlug, qualifiedSlug:@map[itemSlug]
it 'replaces item slugs with qualified slugs', ->
inventory.add 'stone_gear'
inventory.localizeTo modPack
inventory.toList().should.eql [
'minecraft__boat',
[20, 'minecraft__string'],
[4, 'minecraft__wool'],
'buildcraft__stone_gear'
]
it 'ignores qualified slugs', ->
inventory.add 'buildcraft__stone_gear'
inventory.localizeTo modPack
inventory.toList().should.eql [
'minecraft__boat',
[20, 'minecraft__string'],
[4, 'minecraft__wool'],
'buildcraft__stone_gear'
]
describe 'pop', -> describe 'pop', ->
it 'returns null for an empty inventory', -> it 'returns null for an empty inventory', ->
+3 -3
View File
@@ -24,7 +24,7 @@ describe 'inventory_parser.coffee', ->
result.toList().should.eql [] result.toList().should.eql []
it 'can parse a single item without quantity', -> it 'can parse a single item without quantity', ->
result = parser.parse 'Wool' result = parser.parse 'wool'
result.toList().should.eql ['wool'] result.toList().should.eql ['wool']
it 'can parse a single item with quantity', -> it 'can parse a single item with quantity', ->
@@ -32,11 +32,11 @@ describe 'inventory_parser.coffee', ->
result.toList().should.eql [[4, 'wool']] result.toList().should.eql [[4, 'wool']]
it 'can parse multiple mixed-type items', -> it 'can parse multiple mixed-type items', ->
result = parser.parse '4.Wool:10.String:Boat' result = parser.parse '4.wool:10.string:boat'
result.toList().should.eql ['boat', [10, 'string'], [4, 'wool']] result.toList().should.eql ['boat', [10, 'string'], [4, 'wool']]
it 're-uses the given inventory object', -> it 're-uses the given inventory object', ->
inventory = new Inventory inventory = new Inventory
inventory.add 'string', 8 inventory.add 'string', 8
result = parser.parse '4.Wool', inventory result = parser.parse '4.wool', inventory
result.toList().should.eql [[8, 'string'], [4, 'wool']] result.toList().should.eql [[8, 'string'], [4, 'wool']]
+32 -7
View File
@@ -19,22 +19,23 @@ buildcraft = industrialCraft = minecraft = modPack = null
describe 'mod_pack.coffee', -> describe 'mod_pack.coffee', ->
beforeEach -> beforeEach ->
minecraft = new Mod slug:'minecraft' minecraft = new Mod slug:'minecraft', name:'Minecraft'
minecraft.addModVersion new ModVersion modSlug:minecraft.slug, version:'1.7.10' minecraft.addModVersion new ModVersion modSlug:minecraft.slug, version:'1.7.10'
minecraft.activeModVersion.addItem new Item name:'Wool' minecraft.activeModVersion.addItem new Item name:'Wool'
minecraft.activeModVersion.addItem new Item name:'Bed', recipes:[''] minecraft.activeModVersion.addItem new Item name:'Bed', recipes:['']
minecraft.activeModVersion.registerSlug 'iron_chestplate', 'Iron Chestplate' minecraft.activeModVersion.registerSlug 'iron_chestplate', 'Iron Chestplate'
buildcraft = new Mod slug:'buildcraft' buildcraft = new Mod slug:'buildcraft', name:'Buildcraft'
buildcraft.addModVersion new ModVersion modSlug:buildcraft.slug, version:'6.2.6' 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:'Stone Gear', recipes:['']
buildcraft.activeModVersion.addItem new Item name:'Bed', recipes:[''] buildcraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
buildcraft.activeVersion = Mod.Version.None buildcraft.activeVersion = Mod.Version.None
industrialCraft = new Mod slug:'industrial_craft' industrialCraft = new Mod slug:'industrial_craft', name:'Industrial Craft'
industrialCraft.addModVersion new ModVersion modSlug:industrialCraft.slug, version:'2.0' industrialCraft.addModVersion new ModVersion modSlug:industrialCraft.slug, version:'2.0'
industrialCraft.activeModVersion.addItem new Item name:'Resin' industrialCraft.activeModVersion.addItem new Item name:'Resin'
industrialCraft.activeModVersion.addItem new Item name:'Rubber' industrialCraft.activeModVersion.addItem new Item name:'Rubber'
industrialCraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
industrialCraft.activeVersion = Mod.Version.None industrialCraft.activeVersion = Mod.Version.None
modPack = new ModPack modPack = new ModPack
@@ -42,6 +43,30 @@ describe 'mod_pack.coffee', ->
modPack.addMod buildcraft modPack.addMod buildcraft
modPack.addMod industrialCraft modPack.addMod industrialCraft
describe 'findItem', ->
it 'can find an item by partial slug', ->
item = modPack.findItem 'wool'
item.qualifiedSlug.should.equal 'minecraft__wool'
it 'can find an item by full slug', ->
item = modPack.findItem '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 '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 'wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Buildcraft'
describe 'findItemByName', -> describe 'findItemByName', ->
it 'finds the requested item', -> it 'finds the requested item', ->
@@ -57,7 +82,7 @@ describe 'mod_pack.coffee', ->
it 'returns all data for a regular Minecraft item', -> it 'returns all data for a regular Minecraft item', ->
display = modPack.findItemDisplay 'bed' display = modPack.findItemDisplay 'bed'
display.iconUrl.should.equal '/data/minecraft/1.7.10/images/bed.png' display.iconUrl.should.equal '/data/minecraft/1.7.10/images/bed.png'
display.itemUrl.should.equal '/item/bed' display.itemUrl.should.equal '/mod/minecraft/bed'
display.itemName.should.equal 'Bed' display.itemName.should.equal 'Bed'
display.modSlug.should.equal 'minecraft' display.modSlug.should.equal 'minecraft'
@@ -65,13 +90,13 @@ describe 'mod_pack.coffee', ->
buildcraft.activeVersion = '6.2.6' buildcraft.activeVersion = '6.2.6'
display = modPack.findItemDisplay 'stone_gear' display = modPack.findItemDisplay 'stone_gear'
display.iconUrl.should.equal '/data/buildcraft/6.2.6/images/stone_gear.png' display.iconUrl.should.equal '/data/buildcraft/6.2.6/images/stone_gear.png'
display.itemUrl.should.equal '/item/stone_gear' display.itemUrl.should.equal '/mod/buildcraft/stone_gear'
display.itemName.should.equal 'Stone Gear' display.itemName.should.equal 'Stone Gear'
display.modSlug.should.equal 'buildcraft' display.modSlug.should.equal 'buildcraft'
it 'assumes an unfound item is from Minecraft', -> it 'assumes an unfound item is from Minecraft', ->
display = modPack.findItemDisplay 'iron_chestplate' display = modPack.findItemDisplay 'iron_chestplate'
display.iconUrl.should.equal '/data/minecraft/1.7.10/images/iron_chestplate.png' display.iconUrl.should.equal '/data/minecraft/1.7.10/images/iron_chestplate.png'
display.itemUrl.should.equal '/item/iron_chestplate' display.itemUrl.should.equal '/mod/minecraft/iron_chestplate'
display.itemName.should.equal 'Iron Chestplate' display.itemName.should.equal 'Iron Chestplate'
display.modSlug.should.equal 'minecraft' display.modSlug.should.equal 'minecraft'
+2 -2
View File
@@ -91,5 +91,5 @@ describe 'mod_version.coffee', ->
""" """
it 'finds all recipes which list item as output', -> it 'finds all recipes which list item as output', ->
recipes = modVersion.findRecipes 'bucket' recipes = modVersion.findRecipes 'test__bucket'
(r.output[0].slug for r in recipes).sort().should.eql ['bucket', 'cake', 'cake'] (r.output[0].slug for r in recipes).sort().should.eql ['test__bucket', 'test__cake', 'test__cake']
@@ -68,7 +68,7 @@ describe 'mod_version_parser_v1.coffee', ->
it 'adds "input" when present', -> it 'adds "input" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo, Charlie; pattern: ... 012 ...' modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo, Charlie; pattern: ... 012 ...'
slugs = (s.slug for s in modVersion._items.charlie._recipes[0].input) slugs = (s.slug for s in modVersion._items.charlie._recipes[0].input)
slugs.should.eql ['alpha', 'bravo', 'charlie'] slugs.should.eql ['alpha', 'bravo', 'test__charlie']
it 'requires an "input" declaration', -> it 'requires an "input" declaration', ->
func = -> parser.parse baseText + 'recipe:; pattern: ... .0. ...' func = -> parser.parse baseText + 'recipe:; pattern: ... .0. ...'
@@ -155,20 +155,20 @@ describe 'mod_version_parser_v1.coffee', ->
describe 'output', -> describe 'output', ->
beforeEach -> beforeEach ->
baseText = 'item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; ' baseText = 'item: Delta; item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
it 'adds a single item as the default output', -> it 'adds a single item as the default output', ->
modVersion = parser.parse baseText modVersion = parser.parse baseText
stack = modVersion._items.bravo._recipes[0].output[0] stack = modVersion._items.bravo._recipes[0].output[0]
stack.slug.should.equal 'bravo' stack.slug.should.equal 'test__bravo'
stack.quantity.should.equal 1 stack.quantity.should.equal 1
it 'can add multiple extras with quantities', -> it 'can add multiple extras with quantities', ->
modVersion = parser.parse baseText + 'extras:2 Delta, 4 Echo' modVersion = parser.parse baseText + 'extras:2 Delta, 4 Echo'
output = modVersion._items.bravo._recipes[0].output output = modVersion._items.bravo._recipes[0].output
output[0].slug.should.equal 'bravo' output[0].slug.should.equal 'test__bravo'
output[0].quantity.should.equal 1 output[0].quantity.should.equal 1
output[1].slug.should.equal 'delta' output[1].slug.should.equal 'test__delta'
output[1].quantity.should.equal 2 output[1].quantity.should.equal 2
output[2].slug.should.equal 'echo' output[2].slug.should.equal 'echo'
output[2].quantity.should.equal 4 output[2].quantity.should.equal 4
+3 -1
View File
@@ -5,6 +5,7 @@ Copyright (c) 2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
Item = require '../src/scripts/models/item'
Recipe = require '../src/scripts/models/recipe' Recipe = require '../src/scripts/models/recipe'
Stack = require '../src/scripts/models/stack' Stack = require '../src/scripts/models/stack'
@@ -32,7 +33,8 @@ describe 'recipe.coffee', ->
expect(-> new Recipe name:'Gold Gear', input:input).to.throw Error, 'attributes.pattern is required' expect(-> new Recipe name:'Gold Gear', input:input).to.throw Error, 'attributes.pattern is required'
it 'allows an item to provide required attributes', -> it 'allows an item to provide required attributes', ->
recipe = new Recipe item:{name:'Gold Gear', slug:'gold_gear'}, input:input, pattern:pattern item = new Item name:'Gold Gear'
recipe = new Recipe item:item, input:input, pattern:pattern
recipe.name.should.equal 'Gold Gear' recipe.name.should.equal 'Gold Gear'
(o.slug for o in recipe.output).should.eql ['gold_gear'] (o.slug for o in recipe.output).should.eql ['gold_gear']