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 = {}
Url.crafting = _.template "/crafting/<%= inventoryText %>"
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.modData = _.template "/data/<%= modSlug %>/mod.cg"
Url.modVersion = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg"
@@ -68,8 +68,8 @@ module.exports = class FullRecipeController extends BaseController
result = []
if @model?
for stack in @model.tools
name = @modPack.findName stack.slug
result.push name if name?
item = @modPack.findItem stack.slug
result.push item.name if item?
return result
_refreshInputs: ->
@@ -28,7 +28,7 @@ module.exports = class ItemController extends BaseController
super
refresh: ->
display = @_modPack.findItemDisplay @model.slug
display = @_modPack.findItemDisplay @model.qualifiedSlug
@$icon.attr 'src', display.iconUrl
@$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
@_resolveItemSlug()
display = @_modPack.findItemDisplay @model.item?.slug
display = @_modPack.findItemDisplay @model.item?.qualifiedSlug
if display?
@$craftingPlanLink.attr href:display.craftingUrl
@$craftingPlanLink.fadeIn duration:Duration.normal
@@ -143,6 +143,4 @@ module.exports = class ItemPageController extends BaseController
@$similarContainer.fadeOut duration:Duration.normal
_resolveItemSlug: ->
oldItem = @model.item
@model.item = @_modPack.findItem @_itemSlug, includeDisabled:true
newItem = @model.item
@@ -72,6 +72,6 @@ module.exports = class MinimalRecipeController extends BaseController
result = []
if @model?
for stack in @model.tools
name = @_modPack.findName stack.slug
result.push name if name?
item = @_modPack.findItem stack.slug
result.push item.name if item?
return result
+13 -7
View File
@@ -57,10 +57,11 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
@_recordPageView()
routes:
'': 'root'
'item/:itemSlug': 'item'
'crafting/(:text)': 'crafting'
'mod/:modSlug': 'mod'
'': 'root'
'item/:itemSlug': 'item'
'crafting/(:text)': 'crafting'
'mod/:modSlug': 'mod'
'mod/:modSlug/:itemSlug': 'modItem'
# Route Methods ################################################################################
@@ -69,7 +70,12 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
controller.model.params = inventoryText:text
@_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
@_setPage 'item', controller
@@ -84,9 +90,9 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
text = ''
if params.recipeName?
if params.count?
text = "#{params.count}.#{params.recipeName}"
text = "#{params.count}.#{_.slugify(params.recipeName)}"
else
text = "#{params.recipeName}"
text = _.slugify params.recipeName
@navigate Url.crafting(inventoryText:text), trigger:true
+1 -1
View File
@@ -24,7 +24,7 @@ global.logger = new Logger
switch window.location.hostname
when 'localhost'
global.env = 'development'
logger.level = Logger.INFO
logger.level = Logger.DEBUG
when 'new.crafting-guide.com'
global.env = 'staging'
logger.level = Logger.VERBOSE
+4 -4
View File
@@ -25,10 +25,6 @@ module.exports = class BaseModel extends Backbone.Model
@logEvents = options.logEvents or false
@state = ModelState.unloaded
@on 'request', => @state = ModelState.loading
@on 'sync', => @state = ModelState.loaded
@on 'error', => @state = ModelState.error
@loading = null
Object.defineProperties this,
@@ -42,6 +38,8 @@ module.exports = class BaseModel extends Backbone.Model
onLoadSucceeded: (text, status, xhr)->
try
@set @parse text
@state = ModelState.loaded
@trigger Event.change, this
@trigger Event.sync, this
logger.info => "#{@constructor.name}.#{@cid} loaded successfully"
@@ -50,6 +48,7 @@ module.exports = class BaseModel extends Backbone.Model
@onLoadFailed e.message, 'parsing failed', xhr
onLoadFailed: (error, status, xhr)->
@state = ModelState.error
logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}"
@trigger Event.error, this, error
@@ -62,6 +61,7 @@ module.exports = class BaseModel extends Backbone.Model
url = @url()
logger.info => "#{@constructor.name}.#{@cid} reading from url: #{url}"
@state = ModelState.loading
@trigger Event.request, this
@loading = w.promise (resolve, reject)=>
$.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={})->
attributes.modPack ?= new ModPack
@@ -40,7 +40,7 @@ module.exports = class extends BaseModel
inventory = @_parser.parse @params.inventoryText
inventory.each (stack)=>
item = @modPack.findItemByName stack.slug, enableAsNeeded:true
item = @modPack.findItem stack.slug, enableAsNeeded:true
return unless item? and item.isCraftable
@plan.want.add stack.slug, stack.quantity
inventory.remove stack.slug
+35 -23
View File
@@ -23,14 +23,14 @@ module.exports = class CraftingPlan extends BaseModel
@need = new Inventory
@result = new Inventory
recraft = _.debounce (=> @craft()), 100
for inventory in [@have, @want]
inventory.on 'change', recraft
@on Event.change + ':includingTools', recraft
@clear()
@have.on Event.change, => @craft()
@want.on Event.change, => @craft()
@modPack.on Event.change, => @craft()
@on Event.change + ':includingTools', => @craft()
# Public Methods ###############################################################################
clear: (options={})->
@@ -42,7 +42,12 @@ module.exports = class CraftingPlan extends BaseModel
return this
craft: ->
toolsMessage = if @includingTools then ' (including tools)' else ''
logger.info => "crafting #{@want}#{toolsMessage} starting with #{@have}"
@clear()
@have.localizeTo @modPack
@want.localizeTo @modPack
@result.addInventory @have
@@ -50,12 +55,14 @@ module.exports = class CraftingPlan extends BaseModel
@_reservedSteps = {}
@want.each (stack)=>
@_findSteps stack.slug
@need.add stack.slug, stack.quantity
item = @modPack.findItem stack.slug
@need.add item.qualifiedSlug, stack.quantity
@_reservedSteps = null
@steps = _.values @steps
@_resolveNeeds()
@_removeExtraSteps()
@result.addInventory @want
@need.trigger 'change', @need
@@ -91,14 +98,13 @@ module.exports = class CraftingPlan extends BaseModel
# Private Methods ##############################################################################
_addStep: (recipe)->
logger.verbose -> "adding step: #{recipe.slug}"
@steps[recipe.slug] = recipe:recipe
logger.verbose -> "adding step: #{recipe.item.qualifiedSlug}"
@steps[recipe.item.qualifiedSlug] = recipe:recipe
_chooseRecipe: (item)->
return item.getPrimaryRecipe()
_findSteps: (slug)->
logger.debug -> "finding steps for #{slug}"
item = @modPack.findItem slug
return unless item?
return unless item.isCraftable
@@ -111,9 +117,8 @@ module.exports = class CraftingPlan extends BaseModel
if not @_hasStep toolStack.slug
@_findSteps toolStack.slug
return if @_hasStep item.slug
logger.debug -> "reserving: #{item.slug}"
@_reservedSteps[item.slug] = recipe
return if @_hasStep item.qualifiedSlug
@_reservedSteps[item.qualifiedSlug] = recipe
for inputStack in recipe.input
@_findSteps inputStack.slug
@@ -125,6 +130,11 @@ module.exports = class CraftingPlan extends BaseModel
return true if @_reservedSteps[slug]?
return false
_qualifyItemSlug: (slug)->
item = @modPack.findItem slug
return item.qualifiedSlug if item?
return slug
_removeExtraSteps: ->
result = (step for step in @steps when step.multiplier > 0)
@steps = result
@@ -134,29 +144,31 @@ module.exports = class CraftingPlan extends BaseModel
step = @steps[i]
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
for stack in recipe.tools
slug = stack.slug
slug = @_qualifyItemSlug stack.slug
available = @result.quantityOf(slug) + @need.quantityOf(slug)
needed = Math.max 0, stack.quantity - available
@need.add stack.slug, needed
@result.add stack.slug, needed
@need.add slug, needed
@result.add slug, needed
for stack in recipe.input
slug = @_qualifyItemSlug stack.slug
needed = step.multiplier * stack.quantity
consumed = Math.min needed, @result.quantityOf(stack.slug)
consumed = Math.min needed, @result.quantityOf slug
remaining = needed - consumed
@result.remove stack.slug, consumed
@need.add stack.slug, remaining
@result.remove slug, consumed
@need.add slug, remaining
for stack in recipe.output
slug = @_qualifyItemSlug stack.slug
created = stack.quantity * step.multiplier
consumed = Math.min created, @need.quantityOf stack.slug
consumed = Math.min created, @need.quantityOf slug
remaining = created - consumed
@result.add stack.slug, remaining
@need.remove stack.slug, consumed
@result.add slug, remaining
@need.remove slug, consumed
+37 -4
View File
@@ -5,9 +5,10 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Stack = require './stack'
BaseModel = require './base_model'
{Event} = require '../constants'
{RequiredMods} = require '../constants'
Stack = require './stack'
########################################################################################################################
@@ -59,6 +60,22 @@ module.exports = class Inventory extends BaseModel
return false unless stack?
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: ->
slug = @_slugs.pop()
return null unless slug?
@@ -129,6 +146,22 @@ module.exports = class Inventory extends BaseModel
stack = new Stack slug:slug, quantity:quantity
@_stacks[slug] = stack
@_slugs.push slug
@_slugs.sort()
@_sort()
else
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
if stackParts.length is 2
quantity = parseInt stackParts[0]
slug = _.slugify stackParts[1]
slug = stackParts[1]
else if stackParts.length is 1
quantity = 1
slug = _.slugify stackParts[0]
slug = stackParts[0]
else
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.isGatherable ?= false
attributes.modVersion ?= null
attributes.slug ?= _.slugify attributes.name
options.logEvents ?= false
super attributes, options
@_recipes = []
Object.defineProperties this,
isCraftable: { get:-> @_recipes.length > 0 }
qualifiedSlug: { get:@getQualifiedSlug }
primaryRecipe: { get:@getPrimaryRecipe }
@on Event.change + ':modVersion', => @_qualifiedSlug = null
# Public Methods ###############################################################################
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
eachRecipe: (callback)->
for recipe in @_recipes
callback recipe
getPrimaryRecipe: ->
return @_recipes[0]
compareTo: (that)->
if this.slug isnt that.slug
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 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 #############################################################################
toString: ->
+4 -4
View File
@@ -31,14 +31,14 @@ module.exports = class ItemPage extends BaseModel
findComponentInItems: ->
return null unless @item?
itemSlug = @item.slug
itemSlug = @item.qualifiedSlug
result = {}
@modPack.eachMod (mod)->
mod.eachItem (item)->
item.eachRecipe (recipe)->
for stack in recipe.input
if stack.slug is itemSlug
result[item.slug] = item
result[item.qualifiedSlug] = item
result = _.values(result).sort (a, b)-> a.compareTo b
return null unless result.length > 0
@@ -56,7 +56,7 @@ module.exports = class ItemPage extends BaseModel
return result
findRecipes: ->
result = @modPack.findRecipes @item?.slug
result = @modPack.findRecipes @item?.qualifiedSlug
return null unless result.length > 0
return result
@@ -66,7 +66,7 @@ module.exports = class ItemPage extends BaseModel
@_plan.clear()
if @item?
@_plan.want.add @item.slug
@_plan.want.add @item.qualifiedSlug
@_plan.craft()
if @_plan.steps.length > 0
+1
View File
@@ -21,6 +21,7 @@ module.exports = class Mod extends BaseModel
attributes.documentationUrl ?= null
attributes.downloadUrl ?= null
attributes.homePageUrl ?= null
attributes.modPack ?= null
attributes.name ?= ''
super attributes, options
+20 -8
View File
@@ -25,9 +25,16 @@ module.exports = class ModPack extends BaseModel
findItem: (slug, options={})->
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
continue unless mod.enabled or options.includeDisabled
item = mod.findItem slug, options
item = mod.findItem itemSlug, options
return item if item?
return null
@@ -36,10 +43,9 @@ module.exports = class ModPack extends BaseModel
options.enableAsNeeded ?= false
options.includeDisabled = true if options.enableAsNeeded
slug = _.slugify name
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
item = mod.findItem slug, options
item = mod.findItemByName name, options
return item if item?
return null
@@ -74,6 +80,14 @@ module.exports = class ModPack extends BaseModel
return null
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
continue unless mod.enabled
mod.findRecipes slug, result
@@ -88,12 +102,9 @@ module.exports = class ModPack extends BaseModel
isValidName: (name)->
slug = _.slugify name
for mod in @_mods
continue unless mod.enabled
name = mod.findName slug
return true if name
existingName = @findName slug
return false
return name is existingName
# Property Methods #############################################################################
@@ -101,6 +112,7 @@ module.exports = class ModPack extends BaseModel
if not mod? then throw new Error 'mod is required'
return if @_mods.indexOf(mod) isnt -1
mod.modPack = this
@_mods.push mod
@listenTo mod, Event.change, => @trigger Event.change, this
@trigger Event.add + ':mod', mod, this
+1
View File
@@ -19,6 +19,7 @@ module.exports = class ModVersion extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.modSlug? then throw new Error 'attributes.modSlug is required'
if not attributes.version? then throw new Error 'attributes.version is required'
attributes.mod ?= null
super attributes, options
@_groups = {}
+1 -1
View File
@@ -67,7 +67,7 @@ module.exports = class NameFinder
item = mod.findItem slug
if not @includeGatherable
return unless item? and (not item.isGatherable)
return unless item? and item.isCraftable
scanName = "#{mod.name} : #{name}"
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'
@_itemData = name:name, line:@_lineNumber, group:@_rawData.group
@_rawData.items ?= []
@_rawData.items.push @_itemData
@_rawData.items ?= {}
@_rawData.items[name] = @_itemData
@_recipeData = null
@@ -112,7 +112,7 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
_buildModVersion: (modVersionData, modVersion)->
modVersionData.items ?= []
for itemData in modVersionData.items
for itemName, itemData of modVersionData.items
@_handleErrors @_buildItem, modVersion, itemData
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.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.extras ?= []
recipeData.tools ?= []
@@ -143,6 +149,7 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
for name in recipeData.input
slug = _.slugify name
modVersion.registerSlug slug, name
slug = localizeSlug name, slug
inputStacks.push new Stack slug:slug, quantity:0
for c in recipeData.pattern
@@ -158,16 +165,18 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
name = modVersion.findName stack.slug
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
slug = _.slugify extraData.name
modVersion.registerSlug slug, extraData.name
slug = localizeSlug extraData.name, slug
outputStacks.push new Stack slug:slug, quantity:extraData.quantity
toolStacks = []
for name in recipeData.tools
slug = _.slugify name
modVersion.registerSlug slug, name
slug = localizeSlug name, slug
toolStacks.push new Stack slug:slug, quantity:1
attributes =
+10 -3
View File
@@ -15,8 +15,7 @@ module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})->
if attributes.item?
attributes.name = attributes.item.name
attributes.slug = attributes.item.slug
attributes.output ?= [new Stack slug:attributes.item.slug, quantity:1]
attributes.output ?= [new Stack slug:attributes.item.qualifiedSlug, quantity:1]
if not attributes.name? then throw new Error 'attributes.name 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.output ?= [new Stack slug:_.slugify(attributes.name), quantity:1]
attributes.pattern = @_parsePattern attributes.pattern
attributes.slug ?= attributes.output[0].slug
attributes.tools ?= []
options.logEvents ?= false
super attributes, options
Object.defineProperties this,
slug: {get:@getSlug}
# Public Methods ###############################################################################
getItemSlugAt: (patternSlot)->
@@ -48,6 +49,12 @@ module.exports = class Recipe extends BaseModel
return true if stack.slug is itemSlug
return false
# Property Methods #############################################################################
getSlug: ->
return @item.qualifiedSlug if @item?
return @output[0].slug
# Object Overrides #############################################################################
toString: ->
+12
View File
@@ -16,3 +16,15 @@ _.mixin
result = result.replace /^_/, ''
result = result.replace /_$/, ''
return result
composeSlugs: (part1, part2)->
return "#{part1}__#{part2}"
decomposeSlug: (slug)->
return [null, null] unless slug?
parts = slug.split '__'
if parts.length is 1
parts = [ null, parts[0] ]
return parts