diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 42cc2b10b..ce937dacb 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -130,7 +130,7 @@ module.exports = (grunt)-> mochaTest: options: - bail: true + bail: false color: true reporter: 'list' require: [ diff --git a/src/client/models-old/base_model.coffee b/src/client/models-old/base_model.coffee new file mode 100644 index 000000000..8da42e281 --- /dev/null +++ b/src/client/models-old/base_model.coffee @@ -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 + diff --git a/src/client/models-old/converter.coffee b/src/client/models-old/converter.coffee new file mode 100644 index 000000000..d765a13b6 --- /dev/null +++ b/src/client/models-old/converter.coffee @@ -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 \ No newline at end of file diff --git a/src/client/models-old/event_recorder.coffee b/src/client/models-old/event_recorder.coffee new file mode 100644 index 000000000..507ed32cd --- /dev/null +++ b/src/client/models-old/event_recorder.coffee @@ -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} diff --git a/src/client/models-old/game/inventory.coffee b/src/client/models-old/game/inventory.coffee new file mode 100644 index 000000000..a024cb2d7 --- /dev/null +++ b/src/client/models-old/game/inventory.coffee @@ -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 diff --git a/src/client/models-old/game/item.coffee b/src/client/models-old/game/item.coffee new file mode 100644 index 000000000..9ab2545aa --- /dev/null +++ b/src/client/models-old/game/item.coffee @@ -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() diff --git a/src/client/models/game/item_slug.coffee b/src/client/models-old/game/item_slug.coffee similarity index 100% rename from src/client/models/game/item_slug.coffee rename to src/client/models-old/game/item_slug.coffee diff --git a/src/client/models-old/game/mod.coffee b/src/client/models-old/game/mod.coffee new file mode 100644 index 000000000..e5e058564 --- /dev/null +++ b/src/client/models-old/game/mod.coffee @@ -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 diff --git a/src/client/models-old/game/mod_pack.coffee b/src/client/models-old/game/mod_pack.coffee new file mode 100644 index 000000000..50dd10493 --- /dev/null +++ b/src/client/models-old/game/mod_pack.coffee @@ -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 diff --git a/src/client/models/game/mod_version.coffee b/src/client/models-old/game/mod_version.coffee similarity index 100% rename from src/client/models/game/mod_version.coffee rename to src/client/models-old/game/mod_version.coffee diff --git a/src/client/models/game/multiblock.coffee b/src/client/models-old/game/multiblock.coffee similarity index 100% rename from src/client/models/game/multiblock.coffee rename to src/client/models-old/game/multiblock.coffee diff --git a/src/client/models-old/game/recipe.coffee b/src/client/models-old/game/recipe.coffee new file mode 100644 index 000000000..a3416a42f --- /dev/null +++ b/src/client/models-old/game/recipe.coffee @@ -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 diff --git a/src/client/models/game/simple_stack.coffee b/src/client/models-old/game/simple_stack.coffee similarity index 100% rename from src/client/models/game/simple_stack.coffee rename to src/client/models-old/game/simple_stack.coffee diff --git a/src/client/models-old/game/stack.coffee b/src/client/models-old/game/stack.coffee new file mode 100644 index 000000000..6797330dd --- /dev/null +++ b/src/client/models-old/game/stack.coffee @@ -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}" diff --git a/src/client/models/parsing/command_parser_version_base.coffee b/src/client/models-old/parsing/command_parser_version_base.coffee similarity index 100% rename from src/client/models/parsing/command_parser_version_base.coffee rename to src/client/models-old/parsing/command_parser_version_base.coffee diff --git a/src/client/models/parsing/command_parser_version_base.test.coffee b/src/client/models-old/parsing/command_parser_version_base.test.coffee similarity index 100% rename from src/client/models/parsing/command_parser_version_base.test.coffee rename to src/client/models-old/parsing/command_parser_version_base.test.coffee diff --git a/src/client/models/parsing/item_parser.coffee b/src/client/models-old/parsing/item_parser.coffee similarity index 100% rename from src/client/models/parsing/item_parser.coffee rename to src/client/models-old/parsing/item_parser.coffee diff --git a/src/client/models/parsing/item_parser_v1.coffee b/src/client/models-old/parsing/item_parser_v1.coffee similarity index 100% rename from src/client/models/parsing/item_parser_v1.coffee rename to src/client/models-old/parsing/item_parser_v1.coffee diff --git a/src/client/models/parsing/item_parser_v1.test.coffee b/src/client/models-old/parsing/item_parser_v1.test.coffee similarity index 100% rename from src/client/models/parsing/item_parser_v1.test.coffee rename to src/client/models-old/parsing/item_parser_v1.test.coffee diff --git a/src/client/models/parsing/mod_parser.coffee b/src/client/models-old/parsing/mod_parser.coffee similarity index 100% rename from src/client/models/parsing/mod_parser.coffee rename to src/client/models-old/parsing/mod_parser.coffee diff --git a/src/client/models/parsing/mod_parser_v1.coffee b/src/client/models-old/parsing/mod_parser_v1.coffee similarity index 100% rename from src/client/models/parsing/mod_parser_v1.coffee rename to src/client/models-old/parsing/mod_parser_v1.coffee diff --git a/src/client/models/parsing/mod_version_parser.coffee b/src/client/models-old/parsing/mod_version_parser.coffee similarity index 100% rename from src/client/models/parsing/mod_version_parser.coffee rename to src/client/models-old/parsing/mod_version_parser.coffee diff --git a/src/client/models/parsing/mod_version_parser_v1.coffee b/src/client/models-old/parsing/mod_version_parser_v1.coffee similarity index 100% rename from src/client/models/parsing/mod_version_parser_v1.coffee rename to src/client/models-old/parsing/mod_version_parser_v1.coffee diff --git a/src/client/models/parsing/mod_version_parser_v1.test.coffee b/src/client/models-old/parsing/mod_version_parser_v1.test.coffee similarity index 100% rename from src/client/models/parsing/mod_version_parser_v1.test.coffee rename to src/client/models-old/parsing/mod_version_parser_v1.test.coffee diff --git a/src/client/models/parsing/tutorial_parser.coffee b/src/client/models-old/parsing/tutorial_parser.coffee similarity index 100% rename from src/client/models/parsing/tutorial_parser.coffee rename to src/client/models-old/parsing/tutorial_parser.coffee diff --git a/src/client/models/parsing/tutorial_parser_v1.coffee b/src/client/models-old/parsing/tutorial_parser_v1.coffee similarity index 100% rename from src/client/models/parsing/tutorial_parser_v1.coffee rename to src/client/models-old/parsing/tutorial_parser_v1.coffee diff --git a/src/client/models/parsing/versioned_parser_base.coffee b/src/client/models-old/parsing/versioned_parser_base.coffee similarity index 100% rename from src/client/models/parsing/versioned_parser_base.coffee rename to src/client/models-old/parsing/versioned_parser_base.coffee diff --git a/src/client/models-old/site/tutorial.coffee b/src/client/models-old/site/tutorial.coffee new file mode 100644 index 000000000..702f0d718 --- /dev/null +++ b/src/client/models-old/site/tutorial.coffee @@ -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 diff --git a/src/client/models/crafting/crafting_plan.coffee b/src/client/models/crafting/crafting_plan.coffee index 98f791f49..283bb073a 100644 --- a/src/client/models/crafting/crafting_plan.coffee +++ b/src/client/models/crafting/crafting_plan.coffee @@ -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 ################################################################################### diff --git a/src/client/models/crafting/crafting_plan.test.coffee b/src/client/models/crafting/crafting_plan.test.coffee index 83249514e..4011acd40 100644 --- a/src/client/models/crafting/crafting_plan.test.coffee +++ b/src/client/models/crafting/crafting_plan.test.coffee @@ -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' ######################################################################################################################## diff --git a/src/client/models/crafting/evaluation.coffee b/src/client/models/crafting/evaluation.coffee index ea4cde295..c5ff49a52 100644 --- a/src/client/models/crafting/evaluation.coffee +++ b/src/client/models/crafting/evaluation.coffee @@ -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 ############################################################################## diff --git a/src/client/models/crafting/evaluator.coffee b/src/client/models/crafting/evaluator.coffee index 079e56384..386550090 100644 --- a/src/client/models/crafting/evaluator.coffee +++ b/src/client/models/crafting/evaluator.coffee @@ -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 diff --git a/src/client/models/crafting/inventory.coffee b/src/client/models/crafting/inventory.coffee deleted file mode 100644 index 4eadaa7ba..000000000 --- a/src/client/models/crafting/inventory.coffee +++ /dev/null @@ -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}" diff --git a/src/client/models/crafting/item.coffee b/src/client/models/crafting/item.coffee deleted file mode 100644 index beb259cfe..000000000 --- a/src/client/models/crafting/item.coffee +++ /dev/null @@ -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}>" diff --git a/src/client/models/crafting/mod.coffee b/src/client/models/crafting/mod.coffee deleted file mode 100644 index b4e013b86..000000000 --- a/src/client/models/crafting/mod.coffee +++ /dev/null @@ -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}>" diff --git a/src/client/models/crafting/mod_pack.coffee b/src/client/models/crafting/mod_pack.coffee deleted file mode 100644 index 2272fef3c..000000000 --- a/src/client/models/crafting/mod_pack.coffee +++ /dev/null @@ -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}>" diff --git a/src/client/models/crafting/plan_builder.test.coffee b/src/client/models/crafting/plan_builder.test.coffee index bb2224457..e374fc102 100644 --- a/src/client/models/crafting/plan_builder.test.coffee +++ b/src/client/models/crafting/plan_builder.test.coffee @@ -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 diff --git a/src/client/models/crafting/recipe.coffee b/src/client/models/crafting/recipe.coffee deleted file mode 100644 index 5c1cbe6fa..000000000 --- a/src/client/models/crafting/recipe.coffee +++ /dev/null @@ -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}>" diff --git a/src/client/models/crafting/resources_evaluator.coffee b/src/client/models/crafting/resources_evaluator.coffee index 51c5b6655..55756cc0d 100644 --- a/src/client/models/crafting/resources_evaluator.coffee +++ b/src/client/models/crafting/resources_evaluator.coffee @@ -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)-> diff --git a/src/client/models/crafting/resources_evaluator.test.coffee b/src/client/models/crafting/resources_evaluator.test.coffee index 29b716fee..324a3087c 100644 --- a/src/client/models/crafting/resources_evaluator.test.coffee +++ b/src/client/models/crafting/resources_evaluator.test.coffee @@ -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' diff --git a/src/client/models/crafting/stack.coffee b/src/client/models/crafting/stack.coffee deleted file mode 100644 index aa8541cfc..000000000 --- a/src/client/models/crafting/stack.coffee +++ /dev/null @@ -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}" \ No newline at end of file diff --git a/src/client/models/crafting/steps_evaluator.coffee b/src/client/models/crafting/steps_evaluator.coffee index 3e938cb69..41472ce73 100644 --- a/src/client/models/crafting/steps_evaluator.coffee +++ b/src/client/models/crafting/steps_evaluator.coffee @@ -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 diff --git a/src/client/models/crafting/fixtures.coffee b/src/client/models/fixtures.coffee similarity index 77% rename from src/client/models/crafting/fixtures.coffee rename to src/client/models/fixtures.coffee index 7b2751c35..ebc346a09 100644 --- a/src/client/models/crafting/fixtures.coffee +++ b/src/client/models/fixtures.coffee @@ -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 diff --git a/src/client/models/game/inventory.coffee b/src/client/models/game/inventory.coffee index a024cb2d7..4eadaa7ba 100644 --- a/src/client/models/game/inventory.coffee +++ b/src/client/models/game/inventory.coffee @@ -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}" diff --git a/src/client/models/game/inventory.test.coffee b/src/client/models/game/inventory.test.coffee deleted file mode 100644 index 78b704a0f..000000000 --- a/src/client/models/game/inventory.test.coffee +++ /dev/null @@ -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] diff --git a/src/client/models/game/item.coffee b/src/client/models/game/item.coffee index 9ab2545aa..2e0a4796e 100644 --- a/src/client/models/game/item.coffee +++ b/src/client/models/game/item.coffee @@ -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}>" diff --git a/src/client/models/game/item_slug.test.coffee b/src/client/models/game/item_slug.test.coffee deleted file mode 100644 index 106b8ce5a..000000000 --- a/src/client/models/game/item_slug.test.coffee +++ /dev/null @@ -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' diff --git a/src/client/models/game/mod.coffee b/src/client/models/game/mod.coffee index e5e058564..b4e013b86 100644 --- a/src/client/models/game/mod.coffee +++ b/src/client/models/game/mod.coffee @@ -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}>" diff --git a/src/client/models/game/mod.test.coffee b/src/client/models/game/mod.test.coffee deleted file mode 100644 index bbfd66776..000000000 --- a/src/client/models/game/mod.test.coffee +++ /dev/null @@ -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 diff --git a/src/client/models/game/mod_pack.coffee b/src/client/models/game/mod_pack.coffee index 50dd10493..f7e3842a9 100644 --- a/src/client/models/game/mod_pack.coffee +++ b/src/client/models/game/mod_pack.coffee @@ -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}>" diff --git a/src/client/models/game/mod_pack.test.coffee b/src/client/models/game/mod_pack.test.coffee deleted file mode 100644 index ade40cfc5..000000000 --- a/src/client/models/game/mod_pack.test.coffee +++ /dev/null @@ -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' diff --git a/src/client/models/game/mod_version.test.coffee b/src/client/models/game/mod_version.test.coffee deleted file mode 100644 index 93a1d7305..000000000 --- a/src/client/models/game/mod_version.test.coffee +++ /dev/null @@ -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 diff --git a/src/client/models/game/multiblock.test.coffee b/src/client/models/game/multiblock.test.coffee deleted file mode 100644 index 777db1baa..000000000 --- a/src/client/models/game/multiblock.test.coffee +++ /dev/null @@ -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' diff --git a/src/client/models/game/recipe.coffee b/src/client/models/game/recipe.coffee index a3416a42f..8cb220542 100644 --- a/src/client/models/game/recipe.coffee +++ b/src/client/models/game/recipe.coffee @@ -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}>" diff --git a/src/client/models/game/recipe.test.coffee b/src/client/models/game/recipe.test.coffee index 38daf33de..ef9bffda0 100644 --- a/src/client/models/game/recipe.test.coffee +++ b/src/client/models/game/recipe.test.coffee @@ -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 \ No newline at end of file diff --git a/src/client/models/game/stack.coffee b/src/client/models/game/stack.coffee index 6797330dd..aa8541cfc 100644 --- a/src/client/models/game/stack.coffee +++ b/src/client/models/game/stack.coffee @@ -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}" \ No newline at end of file diff --git a/src/client/models/parsing/mod_pack_json.test.coffee b/src/client/models/parsing/mod_pack_json.test.coffee new file mode 100644 index 000000000..6053ed7ad --- /dev/null +++ b/src/client/models/parsing/mod_pack_json.test.coffee @@ -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 diff --git a/src/client/models/parsing/mod_pack_json_formatter.coffee b/src/client/models/parsing/mod_pack_json_formatter.coffee new file mode 100644 index 000000000..f0032a1de --- /dev/null +++ b/src/client/models/parsing/mod_pack_json_formatter.coffee @@ -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 = {} diff --git a/src/client/models/parsing/mod_pack_json_parser.coffee b/src/client/models/parsing/mod_pack_json_parser.coffee new file mode 100644 index 000000000..e0aa25f74 --- /dev/null +++ b/src/client/models/parsing/mod_pack_json_parser.coffee @@ -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 diff --git a/src/client/models/stores/mod_pack_store.coffee b/src/client/models/stores/mod_pack_store.coffee new file mode 100644 index 000000000..4ddcfe86e --- /dev/null +++ b/src/client/models/stores/mod_pack_store.coffee @@ -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] diff --git a/src/client/site/site_controller.coffee b/src/client/site/site_controller.coffee index 0c95e6353..5c51d3a7a 100644 --- a/src/client/site/site_controller.coffee +++ b/src/client/site/site_controller.coffee @@ -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)=> diff --git a/src/common/constants.coffee b/src/common/constants.coffee index e14156acf..78d35d37a 100644 --- a/src/common/constants.coffee +++ b/src/common/constants.coffee @@ -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 %>/"