Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
240a14da58 |
@@ -1,7 +1,5 @@
|
||||
*.log
|
||||
*.swp
|
||||
.DS_Store
|
||||
.env
|
||||
.sass-cache
|
||||
.tm_properties
|
||||
aws.config
|
||||
|
||||
+42
-11
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - Gruntfile.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
@@ -17,6 +17,29 @@ EXTERNAL_LIBS = [
|
||||
'./vendor/email.js:emailjs'
|
||||
]
|
||||
|
||||
PUBLISHED_FILES = [
|
||||
'./client/models/base_model.coffee'
|
||||
'./client/models/game/inventory.coffee'
|
||||
'./client/models/game/item.coffee'
|
||||
'./client/models/game/item_slug.coffee'
|
||||
'./client/models/game/mod.coffee'
|
||||
'./client/models/game/mod_version.coffee'
|
||||
'./client/models/game/multiblock.coffee'
|
||||
'./client/models/game/recipe.coffee'
|
||||
'./client/models/game/stack.coffee'
|
||||
'./client/models/game/simple_stack.coffee'
|
||||
'./client/models/parsing/command_parser_version_base.coffee'
|
||||
'./client/models/parsing/mod_parser.coffee'
|
||||
'./client/models/parsing/mod_parser_v1.coffee'
|
||||
'./client/models/parsing/mod_version_parser.coffee'
|
||||
'./client/models/parsing/mod_version_parser_v1.coffee'
|
||||
'./client/models/parsing/versioned_parser_base.coffee'
|
||||
'./client/models/site/tutorial.coffee'
|
||||
'./common/constants.coffee'
|
||||
'./common/underscore.coffee'
|
||||
'./index.coffee'
|
||||
]
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = (grunt)->
|
||||
@@ -38,15 +61,20 @@ module.exports = (grunt)->
|
||||
{expand: true, cwd:'./build/static', src:'**/*.txt', dest:'./dist/'}
|
||||
]
|
||||
|
||||
coffee:
|
||||
npm_package:
|
||||
options: {
|
||||
bare: true
|
||||
}
|
||||
files: [
|
||||
{expand:true, cwd:'./src', src:PUBLISHED_FILES, dest:'./dist', ext:'.js'}
|
||||
]
|
||||
|
||||
copy:
|
||||
assets_build:
|
||||
files: [
|
||||
{expand:true, cwd:'./assets/', src:'**/*', dest:'./build/static'}
|
||||
]
|
||||
env_build:
|
||||
files: [
|
||||
{expand:true, cwd:'.', src:'.env', dest:'./build'}
|
||||
]
|
||||
build_to_dist:
|
||||
files: [
|
||||
expand: true
|
||||
@@ -98,9 +126,9 @@ module.exports = (grunt)->
|
||||
|
||||
mochaTest:
|
||||
options:
|
||||
bail: false
|
||||
bail: true
|
||||
color: true
|
||||
reporter: 'list'
|
||||
reporter: 'dot'
|
||||
require: [
|
||||
'coffee-script/register'
|
||||
'./src/test_helper.coffee'
|
||||
@@ -164,7 +192,7 @@ module.exports = (grunt)->
|
||||
['jade', 'build:copy', 'build:css', 'browserify:external', 'browserify:internal']
|
||||
|
||||
grunt.registerTask 'build:copy', 'helper task for build',
|
||||
['copy:assets_build', 'copy:env_build', 'copy:common_source', 'copy:server_source']
|
||||
['copy:assets_build', 'copy:common_source', 'copy:server_source']
|
||||
|
||||
grunt.registerTask 'build:css', 'helper task for build',
|
||||
['sass_globbing', 'sass']
|
||||
@@ -184,11 +212,14 @@ module.exports = (grunt)->
|
||||
grunt.registerTask 'dist', 'build the project to be run from Amazon S3',
|
||||
['build', 'copy:build_to_dist', 'uglify', 'compress']
|
||||
|
||||
grunt.registerTask 'prepublish', 'build the project to be published to NPM as shared code',
|
||||
['clean', 'coffee']
|
||||
|
||||
grunt.registerTask 'publish', 'publish the project to NPM',
|
||||
['prepublish', 'script:publish', 'clean', 'build']
|
||||
|
||||
grunt.registerTask 'start', 'build the project and start a local HTTP server',
|
||||
['script:start']
|
||||
['build', 'script:start']
|
||||
|
||||
grunt.registerTask 'test', 'run unit tests',
|
||||
['mochaTest']
|
||||
@@ -199,7 +230,7 @@ module.exports = (grunt)->
|
||||
grunt.registerTask 'upload:staging', 'upload the project the staging Amazon S3 environment',
|
||||
['dist', 'script:s3_upload:staging']
|
||||
|
||||
grunt.registerTask 'use-local-deps', 'use local dependencies instead of NPM dependencies',
|
||||
grunt.registerTask 'useLocalDeps', 'use local dependencies instead of NPM dependencies',
|
||||
['script:use_local_deps']
|
||||
|
||||
# Code Tasks #######################################################################################################
|
||||
@@ -242,7 +273,7 @@ module.exports = (grunt)->
|
||||
|
||||
grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
|
||||
done = this.async()
|
||||
grunt.util.spawn cmd:'./scripts/clear_buffer', opts:{stdio:'inherit'}, (error)-> done(error)
|
||||
grunt.util.spawn cmd:'clear', opts:{stdio:'inherit'}, (error)-> done(error)
|
||||
|
||||
grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", ->
|
||||
done = this.async()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Crafting Guide
|
||||
|
||||
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
|
||||
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From the your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
|
||||
|
||||
**Check out the [Trello Board](https://trello.com/b/s9N0aTYd/crafting-guide) to see what's coming up!**
|
||||
|
||||
## Adding new features
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
google-site-verification: google077275160ac09f2f.html
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 723 B |
Generated
-2917
File diff suppressed because it is too large
Load Diff
+34
-34
@@ -1,50 +1,50 @@
|
||||
{
|
||||
"name": "crafting-guide",
|
||||
"version": "3.2.6",
|
||||
"version": "3.2.4",
|
||||
"description": "A guide for building anything in Minecraft or its many mods",
|
||||
"main": "./dist/index.js",
|
||||
"repository": "http://github.com/andrewminer/crafting-guide",
|
||||
"scripts": {
|
||||
"build": "grunt build",
|
||||
"prepublish": "grunt prepublish",
|
||||
"start": "grunt start",
|
||||
"test": "grunt test"
|
||||
},
|
||||
"author": "Andrew Miner",
|
||||
"license": "UNLICENSED",
|
||||
"license": "none",
|
||||
"devDependencies": {
|
||||
"chai": "3.5.0",
|
||||
"coffee-script": "1.12.5",
|
||||
"coffeeify": "2.1.0",
|
||||
"grunt": "1.0.1",
|
||||
"grunt-contrib-clean": "1.1.0",
|
||||
"grunt-contrib-coffee": "1.0.0",
|
||||
"grunt-contrib-compress": "1.4.1",
|
||||
"grunt-contrib-copy": "1.0.0",
|
||||
"grunt-contrib-jade": "1.0.0",
|
||||
"grunt-contrib-sass": "1.0.0",
|
||||
"grunt-contrib-uglify": "2.3.0",
|
||||
"grunt-contrib-watch": "1.0.0",
|
||||
"grunt-mocha-test": "0.13.2",
|
||||
"grunt-sass-globbing": "1.5.1",
|
||||
"mocha": "3.3.0",
|
||||
"sass": "0.5.0",
|
||||
"sinon": "2.2.0",
|
||||
"sinon-chai": "2.10.0"
|
||||
"chai": "^1.10.0",
|
||||
"coffee-script": "^1.9.3",
|
||||
"coffeeify": "^1.1.0",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-coffee": "^1.0.0",
|
||||
"grunt-contrib-compress": "^0.14.0",
|
||||
"grunt-contrib-copy": "^0.8.2",
|
||||
"grunt-contrib-jade": "^0.15.0",
|
||||
"grunt-contrib-sass": "^0.9.2",
|
||||
"grunt-contrib-uglify": "^0.11.0",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"grunt-sass-globbing": "^1.4.0",
|
||||
"mocha": "^2.4.5",
|
||||
"sass": "^0.5.0",
|
||||
"sinon": "^1.14.1",
|
||||
"sinon-chai": "^2.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"backbone": "1.3.3",
|
||||
"body-parser": "1.17.1",
|
||||
"client-sessions": "0.8.0",
|
||||
"cookie-parser": "1.4.3",
|
||||
"crafting-guide-common": ">=5.0.0",
|
||||
"dotenv": "4.0.0",
|
||||
"express": "4.15.2",
|
||||
"express-session": "1.15.2",
|
||||
"jade": "1.11.0",
|
||||
"jquery": "3.2.1",
|
||||
"marked": "0.3.6",
|
||||
"underscore": "1.8.3",
|
||||
"underscore.inflections": "0.2.1",
|
||||
"when": "3.7.8"
|
||||
"backbone": "^1.2.3",
|
||||
"body-parser": "^1.13.3",
|
||||
"client-sessions": "^0.7.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"crafting-guide-common": "^1.5.1",
|
||||
"express": "^4.13.3",
|
||||
"express-session": "^1.11.3",
|
||||
"jade": "^1.11.0",
|
||||
"jquery": "^2.1.4",
|
||||
"marked": "^0.3.5",
|
||||
"underscore": "^1.8.3",
|
||||
"underscore.inflections": "^0.2.1",
|
||||
"when": "^3.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
clear && printf '\e[3J'
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
grunt watch &
|
||||
|
||||
pushd './build/' &>/dev/null
|
||||
clear
|
||||
nodemon ./server.coffee $*
|
||||
|
||||
Binary file not shown.
+36
-51
@@ -1,93 +1,78 @@
|
||||
#
|
||||
# Crafting Guide - client.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
############################################################################################################
|
||||
|
||||
# Allow Node.js-style `global` in addition to `window`
|
||||
if typeof(global) is "undefined"
|
||||
if typeof(global) is 'undefined'
|
||||
window.global = window
|
||||
|
||||
global._ = require "../common/underscore"
|
||||
global.$ = require "jquery"
|
||||
global.c = require "../common/constants"
|
||||
global._ = require '../common/underscore'
|
||||
global.$ = require 'jquery'
|
||||
global.c = require '../common/constants'
|
||||
global.π = Math.PI
|
||||
global.ε = 0.0001
|
||||
global.w = require "when"
|
||||
global.w = require 'when'
|
||||
|
||||
{Logger} = require("crafting-guide-common").util
|
||||
global.Backbone = Backbone = require 'backbone'
|
||||
Backbone.$ = $
|
||||
|
||||
{Logger} = require 'crafting-guide-common'
|
||||
global.logger = new Logger
|
||||
|
||||
Tracker = require "./tracker"
|
||||
global.tracker = new Tracker
|
||||
|
||||
marked = require "marked"
|
||||
marked = require 'marked'
|
||||
marked.setOptions sanitize:true
|
||||
|
||||
global.Backbone = Backbone = require "backbone"
|
||||
Backbone.$ = $
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
global.hostName = window.location.hostname
|
||||
switch global.hostName
|
||||
when "prerender.crafting-guide.com"
|
||||
global.env = "prerender"
|
||||
when 'prerender.crafting-guide.com'
|
||||
global.env = 'prerender'
|
||||
logger.level = Logger.FATAL
|
||||
apiBaseUrl = "http://prerender.crafting-guide.com:4347"
|
||||
when "local.crafting-guide.com", "localhost"
|
||||
global.env = "local"
|
||||
apiBaseUrl = 'http://local.crafting-guide.com:8001'
|
||||
when 'local.crafting-guide.com', 'localhost'
|
||||
global.env = 'local'
|
||||
logger.level = Logger.DEBUG
|
||||
apiBaseUrl = "http://#{global.hostName}:4347"
|
||||
when "staging.crafting-guide.com"
|
||||
global.env = "staging"
|
||||
apiBaseUrl = 'http://local.crafting-guide.com:8001'
|
||||
when 'staging.crafting-guide.com'
|
||||
global.env = 'staging'
|
||||
logger.level = Logger.VERBOSE
|
||||
apiBaseUrl = "http://api-staging.crafting-guide.com"
|
||||
when "crafting-guide.com"
|
||||
global.env = "production"
|
||||
logger.level = Logger.INFO
|
||||
tracker.enabled = true
|
||||
apiBaseUrl = "http://api.crafting-guide.com"
|
||||
apiBaseUrl = 'http://api-staging.crafting-guide.com'
|
||||
when 'crafting-guide.com'
|
||||
global.env = 'production'
|
||||
logger.level = Logger.INFO
|
||||
apiBaseUrl = 'http://api.crafting-guide.com'
|
||||
else
|
||||
throw new Error "cannot determine the environment of: #{window.location.hostname}"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
Storage = require "./storage"
|
||||
Storage = require './storage'
|
||||
storage = new Storage storage:global.localStorage
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
tracker.trackPageView()
|
||||
|
||||
{CraftingGuideClient} = require("crafting-guide-common").api
|
||||
{CraftingGuideClient} = require 'crafting-guide-common'
|
||||
client = _(new CraftingGuideClient(baseUrl:apiBaseUrl)).extend Backbone.Events
|
||||
client.onStatusChanged = (client, oldStatus, newStatus)->
|
||||
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
|
||||
client.trigger "change:status", client, oldStatus, newStatus
|
||||
client.trigger "change", client
|
||||
client.trigger 'change:status', client, oldStatus, newStatus
|
||||
client.trigger 'change', client
|
||||
|
||||
client.startMonitoringStatus()
|
||||
client.checkStatus()
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
{http, ModPackStore, ItemDetailStore} = require("crafting-guide-common").api
|
||||
storeOptions = http:http, baseUrl:"#{location.protocol}//#{location.hostname}:#{location.port}"
|
||||
global.stores =
|
||||
modPack: new ModPackStore storeOptions
|
||||
itemDetail: new ItemDetailStore storeOptions
|
||||
SiteController = require './site/site_controller'
|
||||
global.site = site = new SiteController client:client, storage:storage
|
||||
site.render()
|
||||
site.loadDefaultModPack()
|
||||
site.loadCurrentUser()
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
global.stores.modPack.load c.modPacks.default
|
||||
.then (modPack)->
|
||||
SiteController = require "./site/site_controller"
|
||||
global.site = site = new SiteController client:client, storage:storage, modPack:modPack
|
||||
site.render()
|
||||
site.loadCurrentUser()
|
||||
|
||||
Backbone.history.start pushState:true
|
||||
logger.info -> "CraftingGuide is ready"
|
||||
Backbone.history.start pushState:true
|
||||
logger.info -> "CraftingGuide is ready"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//-
|
||||
//- Crafting Guide - _google_analytics.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- Copyright © 2014-2016 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
@@ -15,6 +15,4 @@ script.
|
||||
ga('create', 'UA-54970416-2', 'crafting-guide.com');
|
||||
ga('require', 'displayfeatures');
|
||||
ga('require', 'linkid', 'linkid.js');
|
||||
} else {
|
||||
window.ga = function() {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//-
|
||||
//- Crafting Guide - _google_sitelinks.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- Copyright © 2014-2016 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
script.
|
||||
! function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0],
|
||||
p = /^http:/.test(d.location) ? 'http' : 'https';
|
||||
if (!d.getElementById(id)) {
|
||||
js = d.createElement(s);
|
||||
js.id = id;
|
||||
js.src = p + '://platform.twitter.com/widgets.js';
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}
|
||||
}(document, 'script', 'twitter-wjs');
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#
|
||||
# Crafting Guide - crafting_node.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftingNode
|
||||
|
||||
@::TYPES =
|
||||
INVENTORY: 0
|
||||
ITEM: 1
|
||||
RECIPE: 2
|
||||
|
||||
@::TYPE = null
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.modPack? then throw new Error 'options.modPack is required'
|
||||
|
||||
@modPack = options.modPack
|
||||
|
||||
@_children = []
|
||||
@_complete = null
|
||||
@_id = _.uniqueId 'node_'
|
||||
@_rotations = 0
|
||||
@_valid = null
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
expand: (queue=[])->
|
||||
return unless @valid
|
||||
return queue if @children.length > 0
|
||||
|
||||
for child in @_createChildren()
|
||||
child.parent = this
|
||||
@_children.push child
|
||||
queue.push child
|
||||
return queue
|
||||
|
||||
acceptVisitor: (visitor)->
|
||||
enter = visitor[@ENTER_METHOD]
|
||||
if enter?
|
||||
enter.call visitor, this
|
||||
else
|
||||
enter = visitor['onEnterOtherNode']
|
||||
if enter? then enter.call visitor, this
|
||||
|
||||
for child in @_children
|
||||
child.acceptVisitor visitor
|
||||
|
||||
leave = visitor[@LEAVE_METHOD]
|
||||
if leave?
|
||||
leave.call visitor, this
|
||||
else
|
||||
leave = visitor['onLeaveOtherNode']
|
||||
if leave? then leave.call visitor, this
|
||||
|
||||
rotateChildren: ->
|
||||
@_children.push @_children.shift()
|
||||
@_rotations += 1
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
children:
|
||||
get: -> return @_children
|
||||
|
||||
complete:
|
||||
get: ->
|
||||
if not @_complete?
|
||||
@_complete = @_checkCompleteness()
|
||||
return @_complete
|
||||
|
||||
completeText:
|
||||
get: -> if @complete then "◼︎" else "◻︎"
|
||||
|
||||
depth:
|
||||
get: ->
|
||||
maxDepth = 1
|
||||
for child in @children
|
||||
maxDepth = Math.max maxDepth, child.depth + 1
|
||||
return maxDepth
|
||||
|
||||
id:
|
||||
get: -> return @_id
|
||||
|
||||
rotations:
|
||||
get: -> return @_rotations
|
||||
|
||||
size:
|
||||
get: ->
|
||||
size = 1
|
||||
for child in @children
|
||||
size += child.size
|
||||
return size
|
||||
|
||||
valid:
|
||||
get: -> @_checkValidity()
|
||||
|
||||
validText:
|
||||
get: -> if @valid then "✓" else "✗"
|
||||
|
||||
|
||||
# Virtual Methods ##############################################################################
|
||||
|
||||
# Subclasses must override this method to create such children as are appropriate for that kind of node. The new
|
||||
# nodes should be appended to the given array.
|
||||
_createChildren: (result)->
|
||||
throw new Error "#{@constructor.name} must override the _createChildren method"
|
||||
|
||||
# Subclasses must override this method to indicate whether the node has at least one "complete" subtree rooted with
|
||||
# itself. Completeness may be defined differently by different subclasses.
|
||||
_checkCompleteness: ->
|
||||
throw new Error "#{@constructor.name} must override the _checkCompletness method"
|
||||
|
||||
# Subclasses must override this method to determine whether the subtree represented by a node should still be
|
||||
# explored for having a useful crafting plan.
|
||||
_checkValidity: ->
|
||||
throw new Error "#{@constructor.name} must override the _checkValidity method"
|
||||
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Crafting Guide - crafting_node.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
fixtures = require './fixtures.test'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
root = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'crafting_node.coffee', ->
|
||||
|
||||
describe 'acceptVisitor', ->
|
||||
|
||||
it 'can provide a prefix, depth-first traversal', ->
|
||||
root = fixtures.makeTree 'test__iron_ingot'
|
||||
|
||||
visitor =
|
||||
nodes: []
|
||||
onEnterOtherNode: (node)-> visitor.nodes.push node
|
||||
|
||||
root.acceptVisitor visitor
|
||||
|
||||
(v.constructor.name.replace('Node', '') for v in visitor.nodes).should.eql [
|
||||
"Inventory", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Item"
|
||||
]
|
||||
|
||||
it 'can provide a postfix, depth-first traversal', ->
|
||||
root = fixtures.makeTree 'test__iron_ingot'
|
||||
|
||||
visitor =
|
||||
nodes: []
|
||||
onLeaveOtherNode: (node)-> visitor.nodes.push node
|
||||
|
||||
root.acceptVisitor visitor
|
||||
|
||||
(n.constructor.name.replace('Node', '') for n in visitor.nodes).should.eql [
|
||||
"Item", "Item", "Item", "Recipe", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Inventory"
|
||||
]
|
||||
@@ -0,0 +1,178 @@
|
||||
#
|
||||
# Crafting Guide - crafting_plan.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
SimpleInventory = require './simple_inventory'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftingPlan
|
||||
|
||||
constructor: (modPack, want, have, steps)->
|
||||
if not modPack? then throw new Error 'modPack is required'
|
||||
if not want? then throw new Error 'want is required'
|
||||
if not have? then throw new Error 'have is required'
|
||||
if not steps? then throw new Error 'steps is required'
|
||||
|
||||
@_have = have
|
||||
@_made = null
|
||||
@_modPack = modPack
|
||||
@_need = null
|
||||
@_rawScores = {}
|
||||
@_scores = {}
|
||||
@_steps = steps
|
||||
@_tools = null
|
||||
@_want = want
|
||||
|
||||
@_numberSteps()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
computeRequired: ->
|
||||
@_need = new SimpleInventory modPack:@_modPack
|
||||
@_need.addInventory @_want
|
||||
|
||||
@_made = new SimpleInventory modPack:@_modPack
|
||||
@_made.addInventory @_have
|
||||
|
||||
@_tools = new SimpleInventory modPack:@_modPack
|
||||
|
||||
for i in [@_steps.length-1..0] by -1
|
||||
step = @_steps[i]
|
||||
step.multiplier = 0
|
||||
recipe = step.recipe
|
||||
|
||||
for stack in step.recipe.output
|
||||
continue if recipe.isPassThroughFor stack.itemSlug
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
|
||||
while @_need.quantityOf(qualifiedSlug) > 0
|
||||
@_executeStep step
|
||||
|
||||
if step.multiplier > 0
|
||||
for stack in step.recipe.output
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
continue unless recipe.isPassThroughFor stack.itemSlug
|
||||
continue if @_made.hasAtLeast qualifiedSlug, 1
|
||||
continue if @_need.hasAtLeast qualifiedSlug, 1
|
||||
continue if @_tools.hasAtLeast qualifiedSlug, 1
|
||||
@_need.add qualifiedSlug, 1
|
||||
@_tools.add qualifiedSlug, 1
|
||||
|
||||
@_made.addInventory @_want
|
||||
@_made.addInventory @_tools
|
||||
@_pruneEmptySteps()
|
||||
@_numberSteps()
|
||||
|
||||
hasRawScore: (name)->
|
||||
return @_rawScores[name]?
|
||||
|
||||
getRawScore: (name)->
|
||||
return @_rawScores[name]
|
||||
|
||||
setRawScore: (name, rawScore)->
|
||||
@_rawScores[name] = rawScore
|
||||
|
||||
hasScore: (name)->
|
||||
return @_scores[name]?
|
||||
|
||||
getScore: (name)->
|
||||
return @_scores[name]
|
||||
|
||||
setScore: (name, score)->
|
||||
@_scores[name] = score
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
have:
|
||||
get: -> @_have
|
||||
length:
|
||||
get: -> @steps.length
|
||||
made:
|
||||
get: -> @_made
|
||||
need:
|
||||
get: -> @_need
|
||||
steps:
|
||||
get: -> @_steps
|
||||
want:
|
||||
get: -> @_want
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
result = ["To Make:"]
|
||||
@_want.each (stack)->
|
||||
result.push " #{stack}"
|
||||
|
||||
result.push "When you already have:"
|
||||
@_have.each (stack)->
|
||||
result.push " #{stack}"
|
||||
|
||||
result.push "Start with:"
|
||||
if @_need?
|
||||
@_need.each (stack)->
|
||||
result.push " #{stack}"
|
||||
|
||||
result.push "Use these recipes:"
|
||||
for step in @_steps
|
||||
result.push " #{step}"
|
||||
|
||||
result.push "To produce:"
|
||||
if @_made?
|
||||
@_made.each (stack)->
|
||||
result.push " #{stack}"
|
||||
|
||||
if _.keys(@_rawScores).length > 0
|
||||
result.push "Scores:"
|
||||
for criteria, score of @_rawScores
|
||||
if @_scores[criteria]?
|
||||
result.push " #{criteria}: #{score} (#{@_scores[criteria]})"
|
||||
else
|
||||
result.push " #{criteria}: #{score}"
|
||||
|
||||
return result.join '\n'
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_executeStep: (step)->
|
||||
step.multiplier += 1
|
||||
recipe = step.recipe
|
||||
|
||||
for stack in recipe.input
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
|
||||
available = @_made.quantityOf qualifiedSlug
|
||||
required = recipe.getQuantityRequired stack.itemSlug
|
||||
consumed = Math.min required, available
|
||||
deficit = required - consumed
|
||||
|
||||
@_made.remove qualifiedSlug, consumed
|
||||
@_need.add qualifiedSlug, deficit
|
||||
|
||||
for stack in recipe.output
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
|
||||
deficit = @_need.quantityOf qualifiedSlug
|
||||
created = recipe.getQuantityProduced stack.itemSlug
|
||||
replenished = Math.min deficit, created
|
||||
surplus = created - replenished
|
||||
|
||||
@_made.add qualifiedSlug, surplus
|
||||
@_need.remove qualifiedSlug, replenished
|
||||
|
||||
_numberSteps: ->
|
||||
for step, i in @_steps
|
||||
step.number = i + 1
|
||||
|
||||
_pruneEmptySteps: ->
|
||||
index = 0
|
||||
while index < @_steps.length
|
||||
step = @_steps[index]
|
||||
if step.multiplier is 0
|
||||
@_steps.splice index, 1
|
||||
else
|
||||
index++
|
||||
@@ -0,0 +1,123 @@
|
||||
#
|
||||
# Crafting Guide - crafting_plan.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingPlan = require './crafting_plan'
|
||||
fixtures = require './fixtures.test'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'crafting_plan.coffee', ->
|
||||
|
||||
it 'requires wanted item if gatherable', ->
|
||||
plans = fixtures.makePlans [1, 'test__coal']
|
||||
plans.length.should.equal 1
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal'
|
||||
plan.made.unparse().should.equal 'coal'
|
||||
|
||||
it 'can compute a single item with one single step plan', ->
|
||||
plans = fixtures.makePlans [1, 'test__charcoal']
|
||||
plans.length.should.equal 1
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal:8.oak_wood'
|
||||
plan.made.unparse().should.equal '8.charcoal'
|
||||
(s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
|
||||
|
||||
it 'can compute a large quantity of a single item with one single step plan', ->
|
||||
plans = fixtures.makePlans [15, 'test__charcoal']
|
||||
plans.length.should.equal 1
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal '2.coal:16.oak_wood'
|
||||
plan.made.unparse().should.equal '16.charcoal'
|
||||
(s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
|
||||
|
||||
it 'can compute a single item with multiple plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__iron_ingot']
|
||||
plans.length.should.equal 2
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal:8.iron_ore:8.oak_wood'
|
||||
plan.made.unparse().should.equal '7.charcoal:8.iron_ingot'
|
||||
(s.toString() for s in plan.steps).should.eql [
|
||||
'1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'
|
||||
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
]
|
||||
|
||||
plan = plans[1]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal:8.iron_ore'
|
||||
plan.made.unparse().should.equal '8.iron_ingot'
|
||||
(s.toString() for s in plan.steps).should.eql [
|
||||
'1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
]
|
||||
|
||||
it 'uses the correct amount of passthrough items', ->
|
||||
plans = fixtures.makePlans [10, 'test__split_oak_wood'], [10, 'test__split_spruce_wood']
|
||||
plan = plans[2]
|
||||
plan.computeRequired()
|
||||
|
||||
plan.need.unparse().should.equal 'coal:8.iron_ore:6.oak_wood:5.spruce_wood'
|
||||
plan.made.unparse().should.equal '4.iron_ingot:maul:2.oak_planks:10.split_oak_wood:10.split_spruce_wood:stick'
|
||||
(s.toString() for s in plan.steps).should.eql [
|
||||
'1x test__oak_wood>... .0. ...>>4 test__oak_planks'
|
||||
'1x test__oak_planks>.0. .0. ...>>4 test__stick'
|
||||
'1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
'1x test__iron_ingot,test__stick>001 001 ..1>test__crafting_table>test__maul'
|
||||
'5x test__spruce_wood,test__maul>.1. .0. ...>>2 test__split_spruce_wood,test__maul'
|
||||
'5x test__oak_wood,test__maul>.1. .0. ...>>2 test__split_oak_wood,test__maul'
|
||||
]
|
||||
|
||||
it 'can compute multiple items with multiple plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword']
|
||||
plans.length.should.equal 4
|
||||
|
||||
for plan in plans
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/
|
||||
plan.made.unparse().should.match /copper_block.*:iron_sword/
|
||||
|
||||
plan = plans[0]
|
||||
plan.need.unparse().should.match /3.coal.*9.oak_wood/
|
||||
plan.made.unparse().should.match /7.charcoal/
|
||||
"#{plan.steps[3]}".should.equal \
|
||||
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[4]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 7
|
||||
|
||||
plan = plans[1]
|
||||
plan.need.unparse().should.match /3.coal.*:oak_wood/
|
||||
plan.made.unparse().should.not.match /charcoal/
|
||||
"#{plan.steps[2]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[3]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 6
|
||||
|
||||
plan = plans[2]
|
||||
plan.need.unparse().should.match /^coal.*9.oak_wood/
|
||||
plan.made.unparse().should.match /5.charcoal/
|
||||
"#{plan.steps[3]}".should.equal \
|
||||
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[4]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 7
|
||||
|
||||
plan = plans[3]
|
||||
plan.need.unparse().should.match /2.coal.*9.oak_wood/
|
||||
plan.made.unparse().should.match /6.charcoal/
|
||||
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[4]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 7
|
||||
@@ -0,0 +1,73 @@
|
||||
#
|
||||
# Crafting Guide - crafting_step.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
SimpleInventory = require './simple_inventory'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftingStep
|
||||
|
||||
constructor: (recipe, modPack, multiplier=0)->
|
||||
if not recipe? then throw new Error 'recipe is required'
|
||||
if not modPack? then throw new Error 'modPack is required'
|
||||
if multiplier < 0 then throw new Error 'multiplier must be at least 1'
|
||||
|
||||
@number = null
|
||||
|
||||
@_inventory = null
|
||||
@_modPack = modPack
|
||||
@_multiplier = multiplier
|
||||
@_recipe = recipe
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
addToolsTo: (targetInventory)->
|
||||
for stack in @_recipe.tools
|
||||
targetInventory.add stack.itemSlug, stack.quantity, insert:true
|
||||
|
||||
completeInto: (targetInventory)->
|
||||
for stack in @_recipe.output
|
||||
continue if @_recipe.isPassThroughFor stack.itemSlug
|
||||
quantity = @_recipe.getQuantityProduced stack.itemSlug
|
||||
targetInventory.add stack.itemSlug, quantity * @_multiplier
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
inventory:
|
||||
get: ->
|
||||
if not @_inventory?
|
||||
@_refreshInventory()
|
||||
return @_inventory
|
||||
|
||||
recipe:
|
||||
get: -> @_recipe
|
||||
|
||||
multiplier:
|
||||
get: -> @_multiplier
|
||||
set: (newMultiplier)->
|
||||
@_multiplier = newMultiplier
|
||||
@_refreshInventory() if @_inventory?
|
||||
|
||||
slug:
|
||||
get: -> "#{@multiplier}x #{@_recipe.slug}"
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
return @slug
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_refreshInventory: ->
|
||||
@_inventory = new SimpleInventory
|
||||
for stack in @_recipe.input
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
required = @_recipe.getQuantityRequired(stack.itemSlug) - @_recipe.getQuantityProduced(stack.itemSlug)
|
||||
required = if required > 0 then required * @multiplier else 1
|
||||
@_inventory.add qualifiedSlug, required
|
||||
@@ -0,0 +1,148 @@
|
||||
#
|
||||
# Crafting Guide - craftsman.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
GraphBuilder = require './graph_builder'
|
||||
Inventory = require '../game/inventory'
|
||||
PlanBuilder = require './plan_builder'
|
||||
PlanEvaluator = require './plan_evaluator'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class Craftsman extends BaseModel
|
||||
|
||||
@::ANALYZE_STEP_INCREMENT = 29
|
||||
|
||||
@::GRAPH_STEP_INCREMENT = 59
|
||||
|
||||
@::PLAN_STEP_INCREMENT = 39
|
||||
|
||||
@::STAGE =
|
||||
EMPTY: 'empty'
|
||||
READY: 'ready'
|
||||
GRAPHING: 'examining recipes'
|
||||
PLANNING: 'computing plans'
|
||||
ANALYZING: 'analyzing plans'
|
||||
COMPLETE: 'complete'
|
||||
INVALID: 'invalid'
|
||||
OUTDATED: 'outdated'
|
||||
|
||||
constructor: (modPack)->
|
||||
if not modPack? then throw new Error 'modPack is required'
|
||||
attributes =
|
||||
stage: @STAGE.READY
|
||||
stageCount: 0
|
||||
super attributes, {}
|
||||
|
||||
@_modPack = modPack
|
||||
|
||||
@_have = new Inventory modPack:@_modPack
|
||||
@_have.on c.event.change, => @_resetStage()
|
||||
|
||||
@_want = new Inventory modPack:@_modPack
|
||||
@_want.on c.event.change, => @_resetStage()
|
||||
|
||||
@on c.event.change + ':stage', => logger.info "Craftsman has started #{@stage}..."
|
||||
@on 'scheduleNextWork', => @_scheduleNextWork()
|
||||
@reset()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
reset: ->
|
||||
@_graphBuilder = null
|
||||
@_planBuilder = null
|
||||
@_planEvaluator = null
|
||||
@_plans = null
|
||||
|
||||
@_resetStage()
|
||||
|
||||
work: ->
|
||||
return if @_want.isEmpty
|
||||
|
||||
if not @_graphBuilder?
|
||||
@_want.localize()
|
||||
@_have.localize()
|
||||
logger.info -> "Craftsman starting to build #{@_want} from #{@_have}"
|
||||
|
||||
@_graphBuilder = new GraphBuilder modPack:@_modPack, want:@_want, have:@_have
|
||||
@stage = @STAGE.GRAPHING
|
||||
@stageCount = 0
|
||||
else if not @_graphBuilder.complete
|
||||
@_graphBuilder.expandGraph @GRAPH_STEP_INCREMENT
|
||||
@stageCount = @_graphBuilder.stepCount
|
||||
logger.verbose => "Craftsman is expanding graph: #{@stageCount} nodes"
|
||||
else if not @_graphBuilder.rootNode.valid
|
||||
@stage = @STAGE.INVALID
|
||||
@stageCount = 0
|
||||
logger.warning => "Craftsman could not complete a crafting plan."
|
||||
else if not @_planBuilder?
|
||||
logger.debug => "Craftsman finished computing graph:\n#{@_graphBuilder.rootNode}"
|
||||
@_planBuilder = new PlanBuilder @_graphBuilder.rootNode, @_modPack, have:@_have, want:@_want
|
||||
@stage = @STAGE.PLANNING
|
||||
@stageCount = 0
|
||||
else if not @_planBuilder.complete
|
||||
@_planBuilder.producePlans @PLAN_STEP_INCREMENT
|
||||
@stageCount = @_planBuilder.plans.length
|
||||
logger.verbose => "Craftsman is producing plans: #{@_planBuilder.plans.length} plans"
|
||||
else if not @_planEvaluator?
|
||||
logger.verbose => "Craftsman finished producing plans: #{@_planBuilder.plans.length} plans"
|
||||
@_planEvaluator = new PlanEvaluator @_planBuilder.plans
|
||||
@stage = @STAGE.ANALYZING
|
||||
@stageCount = 0
|
||||
else if not @_planEvaluator.complete
|
||||
@_planEvaluator.scorePlans @ANALYZE_STEP_INCREMENT
|
||||
@stageCount = @_planEvaluator.lastScored
|
||||
logger.verbose => "Craftsman is rating plans: #{@stageCount} plans"
|
||||
else
|
||||
@_plans = [
|
||||
@_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.FEWEST_STEPS
|
||||
@_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.LEAST_MATERIALS
|
||||
]
|
||||
@stage = @STAGE.COMPLETE
|
||||
@stageCount = 0
|
||||
|
||||
logger.info => "Craftsman has finished with plans: #{(p.toString() for p in @_plans).join('\n\n')}"
|
||||
|
||||
@trigger c.event.change, this
|
||||
@trigger 'scheduleNextWork'
|
||||
return @complete
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
complete:
|
||||
get: -> @stage in [@STAGE.COMPLETE, @STAGE.INVALID, @STAGE.OUTDATED]
|
||||
|
||||
have:
|
||||
get: -> @_have
|
||||
|
||||
plan:
|
||||
get: -> @_plans?[0]
|
||||
|
||||
want:
|
||||
get: -> @_want
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_resetStage: ->
|
||||
@stageCount = 0
|
||||
|
||||
if @want.isEmpty
|
||||
if @stage isnt @STAGE.EMPTY
|
||||
@stage = @STAGE.EMPTY
|
||||
@reset()
|
||||
else if not @_plans?
|
||||
@stage = @STAGE.READY
|
||||
else
|
||||
@stage = @STAGE.OUTDATED
|
||||
|
||||
_scheduleNextWork: ->
|
||||
return if @want.isEmpty
|
||||
return if @complete
|
||||
|
||||
_.defer => @work()
|
||||
@@ -0,0 +1,189 @@
|
||||
#
|
||||
# Crafting Guide - fixtures.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
GraphBuilder = require './graph_builder'
|
||||
Inventory = require '../game/inventory'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
Mod = require '../game/mod'
|
||||
ModPack = require '../game/mod_pack'
|
||||
ModVersion = require '../game/mod_version'
|
||||
PlanBuilder = require './plan_builder'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
MOD_VERSION_FILE =
|
||||
"""
|
||||
schema: 1
|
||||
|
||||
item: Bed
|
||||
recipe:
|
||||
input: Oak Planks, Wool
|
||||
pattern: ... 000 111
|
||||
|
||||
item: Charcoal
|
||||
recipe:
|
||||
input: 8 Oak Wood, Coal
|
||||
pattern: .0. ... .1.
|
||||
quantity: 8
|
||||
|
||||
item: Crafting Table
|
||||
recipe:
|
||||
input: Oak Planks
|
||||
pattern: .00 .00 ...
|
||||
|
||||
item: Coal
|
||||
gatherable: yes
|
||||
|
||||
item: Cobblestone
|
||||
gatherable: yes
|
||||
|
||||
item: Copper Block
|
||||
recipe:
|
||||
input: Copper Ingot
|
||||
pattern: 000 000 000
|
||||
tools: Crafting Table
|
||||
|
||||
item: Copper Ingot
|
||||
recipe:
|
||||
input: 8 Copper Ore, Coal
|
||||
pattern: .0. ... .1.
|
||||
quantity: 8
|
||||
tools: Furnace
|
||||
recipe:
|
||||
input: 8 Copper Ore, Charcoal
|
||||
pattern: .0. ... .1.
|
||||
quantity: 8
|
||||
tools: Furnace
|
||||
recipe:
|
||||
input: Copper Block
|
||||
pattern: ... .0. ...
|
||||
quantity: 9
|
||||
|
||||
item: Copper Ore
|
||||
gatherable: yes
|
||||
|
||||
item: Furnace
|
||||
recipe:
|
||||
input: Cobblestone
|
||||
pattern: 000 0.0 000
|
||||
tools: Crafting Table
|
||||
|
||||
item: Iron Ore
|
||||
gatherable: yes
|
||||
|
||||
item: Iron Ingot
|
||||
recipe:
|
||||
input: 8 Iron Ore, Charcoal
|
||||
pattern: .0. ... .1.
|
||||
quantity: 8
|
||||
tools: Furnace
|
||||
recipe:
|
||||
input: 8 Iron Ore, Coal
|
||||
pattern: .0. ... .1.
|
||||
quantity: 8
|
||||
tools: Furnace
|
||||
|
||||
item: Iron Sword
|
||||
recipe:
|
||||
input: Iron Ingot, Stick
|
||||
pattern: .0. .0. .1.
|
||||
tools: Crafting Table
|
||||
|
||||
item: Lever
|
||||
recipe:
|
||||
input: Stick, Cobblestone
|
||||
pattern: .0. .1. ...
|
||||
|
||||
item: Maul
|
||||
recipe:
|
||||
input: Iron Ingot, Stick
|
||||
pattern: 001 001 ..1
|
||||
tools: Crafting Table
|
||||
|
||||
item: Oak Planks
|
||||
recipe:
|
||||
input: Oak Wood
|
||||
pattern: ... .0. ...
|
||||
quantity: 4
|
||||
|
||||
item: Oak Wood
|
||||
gatherable: yes
|
||||
|
||||
item: Spruce Wood
|
||||
gatherable: yes
|
||||
|
||||
item: Stick
|
||||
recipe:
|
||||
input: Oak Planks
|
||||
pattern: .0. .0. ...
|
||||
quantity: 4
|
||||
|
||||
item: Split Oak Wood
|
||||
recipe:
|
||||
extras: Maul
|
||||
input: Oak Wood, Maul
|
||||
pattern: .1. .0. ...
|
||||
quantity: 2
|
||||
|
||||
item: Split Spruce Wood
|
||||
recipe:
|
||||
extras: Maul
|
||||
input: Spruce Wood, Maul
|
||||
pattern: .1. .0. ...
|
||||
quantity: 2
|
||||
|
||||
item: String
|
||||
gatherable: yes
|
||||
|
||||
item: Wool
|
||||
gatherable: yes
|
||||
recipe:
|
||||
input: String
|
||||
pattern: 00. 00. ...
|
||||
"""
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = fixtures =
|
||||
|
||||
makeGraphBuilder: ->
|
||||
return new GraphBuilder modPack:fixtures.makeModPack(), want:new Inventory
|
||||
|
||||
makeModPack: ->
|
||||
modPack = new ModPack
|
||||
|
||||
mod = new Mod name:'Test', slug:'test'
|
||||
modPack.addMod mod
|
||||
|
||||
modVersion = new ModVersion modSlug:'test', version:'0.0'
|
||||
modVersion.parse MOD_VERSION_FILE
|
||||
mod.addModVersion modVersion
|
||||
|
||||
return modPack
|
||||
|
||||
makePlans: (stacks...)->
|
||||
modPack = fixtures.makeModPack()
|
||||
|
||||
have = new Inventory
|
||||
want = new Inventory
|
||||
for stack in stacks
|
||||
want.add ItemSlug.slugify(stack[1]), stack[0]
|
||||
|
||||
graphBuilder = new GraphBuilder modPack:modPack, want:want
|
||||
graphBuilder.expandGraph()
|
||||
|
||||
planBuilder = new PlanBuilder graphBuilder.rootNode, modPack, want:want, have:have
|
||||
return planBuilder.producePlans()
|
||||
|
||||
makeTree: (itemSlug, quantity=1)->
|
||||
want = new Inventory
|
||||
want.add ItemSlug.slugify itemSlug
|
||||
|
||||
builder = new GraphBuilder modPack:fixtures.makeModPack(), want:want
|
||||
builder.expandGraph()
|
||||
|
||||
return builder.rootNode
|
||||
@@ -0,0 +1,69 @@
|
||||
#
|
||||
# Crafting Guide - graph_builder.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
Inventory = require '../game/inventory'
|
||||
InventoryNode = require './inventory_node'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class GraphBuilder
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.modPack? then throw new Error 'options.modPack is required'
|
||||
if not options.want? then throw new Error 'options.want is required'
|
||||
|
||||
@_maximumGraphSize = c.limits.maximumGraphSize
|
||||
@_modPack = options.modPack
|
||||
@_stepCount = 0
|
||||
@_want = options.want
|
||||
|
||||
@_rootNode = new InventoryNode modPack:@_modPack, inventory:@_want
|
||||
@_queue = [@_rootNode]
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
expandGraph: (steps=null)->
|
||||
maxSteps = if steps? then @_stepCount + steps else Number.MAX_VALUE
|
||||
@_queue ?= []
|
||||
|
||||
while true
|
||||
break if @_queue.length is 0
|
||||
break if @_stepCount >= maxSteps
|
||||
|
||||
node = @_queue.shift()
|
||||
break unless node?
|
||||
|
||||
node.expand @_queue
|
||||
@_stepCount += 1
|
||||
|
||||
reset: ->
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
complete:
|
||||
get: ->
|
||||
return true if @_stepCount > @_maximumGraphSize
|
||||
return false unless @_rootNode?
|
||||
return false unless @_queue?
|
||||
return false unless @_queue.length is 0
|
||||
return true
|
||||
|
||||
rootNode:
|
||||
get: -> @_rootNode
|
||||
|
||||
stepCount:
|
||||
get: -> @_stepCount
|
||||
|
||||
want:
|
||||
get: -> @_want
|
||||
|
||||
# Object Overrides ############################################################################
|
||||
|
||||
toString: (indent='')->
|
||||
"Build Tree\n#{@_rootNode.toString(indent + ' ')}"
|
||||
@@ -0,0 +1,77 @@
|
||||
#
|
||||
# Crafting Guide - graph_builder.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
fixtures = require './fixtures.test'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
builder = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'GraphBuilder.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
builder = fixtures.makeGraphBuilder()
|
||||
|
||||
describe 'expand', ->
|
||||
|
||||
it 'can work a few steps at a time', ->
|
||||
builder.want.add ItemSlug.slugify 'test__iron_sword'
|
||||
builder.expandGraph 9
|
||||
|
||||
builder.rootNode.depth.should.equal 6
|
||||
builder.rootNode.size.should.equal 13
|
||||
builder.complete.should.be.false
|
||||
|
||||
builder.expandGraph 9
|
||||
|
||||
builder.rootNode.depth.should.equal 8
|
||||
builder.rootNode.size.should.equal 18
|
||||
builder.complete.should.be.true
|
||||
|
||||
it 'works properly with an empty inventory', ->
|
||||
builder.expandGraph 100
|
||||
|
||||
builder.rootNode.depth.should.equal 1
|
||||
builder.rootNode.size.should.equal 1
|
||||
builder.complete.should.be.true
|
||||
|
||||
describe 'can build a tree for', ->
|
||||
|
||||
runSingleItemTreeBuildingTest = (slug, depth, size)->
|
||||
builder.want.add ItemSlug.slugify slug
|
||||
builder.expandGraph 100
|
||||
|
||||
builder.rootNode.depth.should.equal depth
|
||||
builder.rootNode.size.should.equal size
|
||||
builder.complete.should.be.true
|
||||
|
||||
it 'a gatherable item', ->
|
||||
runSingleItemTreeBuildingTest 'test__oak_wood', 2, 2
|
||||
|
||||
it 'a single-step item', ->
|
||||
runSingleItemTreeBuildingTest 'test__crafting_table', 6, 6
|
||||
|
||||
it 'an item with multiple inputs', ->
|
||||
runSingleItemTreeBuildingTest 'test__lever', 8, 9
|
||||
|
||||
it 'an item with multiple recipes', ->
|
||||
runSingleItemTreeBuildingTest 'test__iron_ingot', 6, 11
|
||||
|
||||
it 'an item with multiple inputs and multiple recipes', ->
|
||||
runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18
|
||||
|
||||
it 'an item with one recursive recipe', ->
|
||||
runSingleItemTreeBuildingTest 'test__copper_ingot', 6, 15
|
||||
|
||||
it 'an item which gatherable and craftable', ->
|
||||
runSingleItemTreeBuildingTest 'test__wool', 4, 4
|
||||
|
||||
it 'an item which requires a gatherable and craftable item', ->
|
||||
runSingleItemTreeBuildingTest 'test__bed', 6, 7
|
||||
@@ -0,0 +1,55 @@
|
||||
#
|
||||
# Crafting Guide - inventory_node.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingNode = require './crafting_node'
|
||||
ItemNode = require './item_node'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class InventoryNode extends CraftingNode
|
||||
|
||||
@::ENTER_METHOD = 'onEnterInventoryNode'
|
||||
@::LEAVE_METHOD = 'onLeaveInventoryNode'
|
||||
@::TYPE = CraftingNode::TYPES.INVENTORY
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.inventory? then throw new Error 'options.inventory is required'
|
||||
super options
|
||||
|
||||
@inventory = options.inventory
|
||||
|
||||
# CraftingNode Overrides #######################################################################
|
||||
|
||||
_createChildren: (result=[])->
|
||||
@inventory.each (stack)=>
|
||||
item = @modPack.findItem stack.itemSlug
|
||||
if not item? then throw new Error "Could not find an item for slug: #{stack.itemSlug}"
|
||||
result.push new ItemNode modPack:@modPack, item:item, ignoreGatherable:true
|
||||
return result
|
||||
|
||||
_checkCompleteness: ->
|
||||
for child in @children
|
||||
return false unless child.complete
|
||||
return true
|
||||
|
||||
_checkValidity: ->
|
||||
for child in @children
|
||||
return false unless child.valid
|
||||
return true
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: (options={})->
|
||||
options.indent ?= ''
|
||||
options.recursive ?= true
|
||||
|
||||
parts = ["#{options.indent}#{@completeText} #{@validText} InventoryNode for #{@inventory}"]
|
||||
nextIndent = options.indent + ' '
|
||||
if options.recursive
|
||||
for child in @children
|
||||
parts.push child.toString indent:nextIndent
|
||||
return parts.join '\n'
|
||||
@@ -0,0 +1,98 @@
|
||||
#
|
||||
# Crafting Guide - item_node.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingNode = require './crafting_node'
|
||||
RecipeNode = require './recipe_node'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemNode extends CraftingNode
|
||||
|
||||
@::ENTER_METHOD = 'onEnterItemNode'
|
||||
@::LEAVE_METHOD = 'onLeaveItemNode'
|
||||
@::TYPE = CraftingNode::TYPES.ITEM
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.item? then throw new Error 'options.item is required'
|
||||
super options
|
||||
|
||||
@item = options.item
|
||||
@_ignoreGatherable = options.ignoreGatherable ?= false
|
||||
@_recipes = null
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
getRecipes: ->
|
||||
if not @_recipes?
|
||||
@_recipes = @modPack.findRecipes @item.slug, forCrafting:true, onlyPrimary:true
|
||||
if not @_recipes
|
||||
@_recipes = @modPack.findRecipes @item.slug, forCrafting:true
|
||||
|
||||
return @_recipes or []
|
||||
|
||||
isGatherable: ->
|
||||
if not @_ignoreGatherable
|
||||
return true if @item.isGatherable
|
||||
return true if @getRecipes().length is 0
|
||||
return false
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
gatherable: { get:@prototype.isGatherable }
|
||||
recipes: { get:@prototype.getRecipes }
|
||||
|
||||
# CraftingNode Overrides #######################################################################
|
||||
|
||||
_createChildren: (result=[])->
|
||||
recipes = @getRecipes()
|
||||
return [] if @gatherable
|
||||
|
||||
for recipe in recipes
|
||||
child = new RecipeNode modPack:@modPack, recipe:recipe
|
||||
child.parent = this
|
||||
if child.valid
|
||||
result.push child
|
||||
|
||||
return result
|
||||
|
||||
_checkCompleteness: ->
|
||||
return true if @gatherable
|
||||
return false unless @children?
|
||||
|
||||
for child in @children
|
||||
return true if child.complete
|
||||
return false
|
||||
|
||||
_checkValidity: ->
|
||||
return false if @_isRepeatedItem()
|
||||
return true unless @children.length > 0
|
||||
|
||||
for child in @children
|
||||
return true if child.valid
|
||||
return false
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: (options={})->
|
||||
options.indent ?= ''
|
||||
options.recursive ?= true
|
||||
|
||||
parts = ["#{options.indent}#{@completeText} #{@validText} ItemNode for #{@item.name}"]
|
||||
nextIndent = options.indent + ' '
|
||||
if options.recursive
|
||||
for child in @children
|
||||
parts.push child.toString indent:nextIndent
|
||||
return parts.join '\n'
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_isRepeatedItem: ->
|
||||
nextParent = @parent
|
||||
while nextParent?
|
||||
return true if nextParent.item is @item
|
||||
nextParent = nextParent.parent
|
||||
|
||||
return false
|
||||
@@ -0,0 +1,127 @@
|
||||
#
|
||||
# Crafting Guide - plan_builder.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingNode = require './crafting_node'
|
||||
CraftingPlan = require './crafting_plan'
|
||||
CraftingStep = require './crafting_step'
|
||||
Inventory = require '../game/inventory'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class PlanBuilder
|
||||
|
||||
constructor: (rootNode, modPack, options={})->
|
||||
if not rootNode? then throw new Error 'rootNode is required'
|
||||
if not modPack? then throw new Error 'modPack is required'
|
||||
|
||||
@want = options.want
|
||||
@have = options.have
|
||||
|
||||
@_choiceNodes = []
|
||||
@_complete = false
|
||||
@_maxPlanCount = c.limits.maximumPlanCount
|
||||
@_modPack = modPack
|
||||
@_plans = []
|
||||
@_rootNode = rootNode
|
||||
|
||||
@_isolateChoiceNodes()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
producePlans: (maxPlans=null)->
|
||||
maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE
|
||||
|
||||
if @plans.length >= @_maxPlanCount
|
||||
@_complete = true
|
||||
|
||||
while not @complete and (@plans.length < maxPlans)
|
||||
plan = @_captureCurrentPlan()
|
||||
if plan?
|
||||
@plans.push plan
|
||||
|
||||
@_incrementChoiceNodes()
|
||||
|
||||
return @_plans
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
complete:
|
||||
get: -> @_complete
|
||||
|
||||
have:
|
||||
get: -> @_have
|
||||
set: (have)-> @_have = have or new Inventory
|
||||
|
||||
maxPlanCount:
|
||||
get: -> @_maxPlanCount
|
||||
set: (value)-> @_maxPlanCount = value
|
||||
|
||||
plans:
|
||||
get: -> @_plans
|
||||
|
||||
want:
|
||||
get: -> @_want
|
||||
set: (want)-> @_want = want or new Inventory
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_captureCurrentPlan: ->
|
||||
toVisit = [@_rootNode]
|
||||
stepNodes = []
|
||||
|
||||
while toVisit.length > 0
|
||||
node = toVisit.shift()
|
||||
|
||||
if node.TYPE is CraftingNode::TYPES.INVENTORY
|
||||
toVisit.push(c) for c in node.children
|
||||
else if node.TYPE is CraftingNode::TYPES.ITEM
|
||||
toVisit.push node.children[0] if node.children.length > 0
|
||||
else if node.TYPE is CraftingNode::TYPES.RECIPE
|
||||
stepNodes.push node
|
||||
toVisit.push(c) for c in node.children
|
||||
|
||||
steps = []
|
||||
seenRecipes = {}
|
||||
index = stepNodes.length - 1
|
||||
while index >= 0
|
||||
node = stepNodes[index]
|
||||
index -= 1
|
||||
return null unless node.valid and node.complete
|
||||
|
||||
recipeSlug = node.recipe.slug
|
||||
continue if seenRecipes[recipeSlug]?
|
||||
|
||||
seenRecipes[recipeSlug] = true
|
||||
steps.push new CraftingStep node.recipe, @_modPack
|
||||
|
||||
plan = new CraftingPlan @_modPack, @_want, @_have, steps
|
||||
return plan
|
||||
|
||||
_incrementChoiceNodes: ->
|
||||
if @_choiceNodes.length is 0
|
||||
@_complete = true
|
||||
return
|
||||
|
||||
index = @_choiceNodes.length - 1
|
||||
while true
|
||||
if index is -1
|
||||
@_complete = true
|
||||
return
|
||||
|
||||
node = @_choiceNodes[index]
|
||||
node.rotateChildren()
|
||||
|
||||
return unless node.rotations % node.children.length is 0
|
||||
index -= 1
|
||||
|
||||
_isolateChoiceNodes: ->
|
||||
@_rootNode.acceptVisitor
|
||||
onEnterItemNode: (node)=>
|
||||
if node.children.length > 1
|
||||
@_choiceNodes.push node
|
||||
@@ -0,0 +1,43 @@
|
||||
#
|
||||
# Crafting Guide - plan_builder.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
fixtures = require './fixtures.test'
|
||||
PlanBuilder = require './plan_builder'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'plan_builder.coffee', ->
|
||||
|
||||
printPlan = (plan)->
|
||||
return '' unless plan?
|
||||
return ((s.recipe.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > '
|
||||
|
||||
it 'generates an empty plan for a gatherable item', ->
|
||||
plans = fixtures.makePlans [1, 'test__oak_wood']
|
||||
|
||||
plans.length.should.equal 1
|
||||
plans[0].length.should.equal 0
|
||||
|
||||
it 'can find a multi-step plan', ->
|
||||
plans = fixtures.makePlans [1, 'test__lever']
|
||||
|
||||
printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever'
|
||||
plans.length.should.equal 1
|
||||
|
||||
it 'can find multiple plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__iron_ingot']
|
||||
|
||||
printPlan(plans[0]).should.equal '8 test__charcoal > 8 test__iron_ingot'
|
||||
printPlan(plans[1]).should.equal '8 test__iron_ingot'
|
||||
plans.length.should.equal 2
|
||||
|
||||
it 'ignores invalid plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__copper_block']
|
||||
|
||||
printPlan(plans[0]).should.equal '8 test__copper_ingot > test__copper_block'
|
||||
printPlan(plans[1]).should.equal '8 test__charcoal > 8 test__copper_ingot > test__copper_block'
|
||||
plans.length.should.equal 2
|
||||
@@ -0,0 +1,98 @@
|
||||
#
|
||||
# Crafting Guide - plan_evaluator.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class PlanEvaluator
|
||||
|
||||
@::CRITERIA =
|
||||
FEWEST_STEPS: 'fewest steps'
|
||||
LEAST_MATERIALS: 'least materials'
|
||||
|
||||
constructor: (plans)->
|
||||
if not plans? then throw new Error 'plans is required'
|
||||
|
||||
@_plans = plans
|
||||
@_lastScored = -1
|
||||
@_normalized = false
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
findBestPlan: (mainCriteria)->
|
||||
criteriaList = [mainCriteria].concat (criteria for key, criteria of @CRITERIA when criteria isnt mainCriteria)
|
||||
@_plans.sort (a, b)->
|
||||
for criteria in criteriaList
|
||||
scoreA = a.getScore criteria
|
||||
scoreB = b.getScore criteria
|
||||
|
||||
if scoreA isnt scoreB
|
||||
return if scoreA > scoreB then -1 else +1
|
||||
|
||||
return 0
|
||||
|
||||
return @_plans[0]
|
||||
|
||||
scorePlans: (count)->
|
||||
count ?= @_plans.length
|
||||
return if @complete
|
||||
|
||||
maxPlanIndex = Math.min @_lastScored + count, @_plans.length - 1
|
||||
for i in [(@_lastScored + 1)..maxPlanIndex]
|
||||
@_plans[i].computeRequired()
|
||||
@_scorePlan @_plans[i]
|
||||
@_lastScored = i
|
||||
|
||||
if @complete then @_normalizeScores()
|
||||
|
||||
return @complete
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
complete:
|
||||
get: -> @_lastScored is @_plans.length - 1
|
||||
|
||||
lastScored:
|
||||
get: -> @_lastScored
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_computeFewestStepsScore: (plan)->
|
||||
total = 0
|
||||
for step in plan.steps
|
||||
total += step.multiplier
|
||||
plan.setRawScore @CRITERIA.FEWEST_STEPS, total
|
||||
|
||||
_computeLeastMaterialsScore: (plan)->
|
||||
total = 0
|
||||
plan.need.each (stack)->
|
||||
total += stack.quantity
|
||||
plan.setRawScore @CRITERIA.LEAST_MATERIALS, total
|
||||
|
||||
_normalizeScores: ->
|
||||
for key, criteria of @CRITERIA
|
||||
maxScore = 0
|
||||
minScore = Number.MAX_VALUE
|
||||
|
||||
for plan in @_plans
|
||||
continue unless plan.hasRawScore criteria
|
||||
maxScore = Math.max maxScore, plan.getRawScore(criteria)
|
||||
minScore = Math.min minScore, plan.getRawScore(criteria)
|
||||
|
||||
adjustedMaxScore = maxScore - minScore
|
||||
for plan in @_plans
|
||||
continue unless plan.hasRawScore criteria
|
||||
if adjustedMaxScore > 0
|
||||
adjustedRawScore = plan.getRawScore(criteria) - minScore
|
||||
plan.setScore criteria, 1.0 - (1.0 * adjustedRawScore / adjustedMaxScore)
|
||||
else
|
||||
plan.setScore criteria, 1.0
|
||||
|
||||
_scorePlan: (plan)->
|
||||
@_computeFewestStepsScore plan
|
||||
@_computeLeastMaterialsScore plan
|
||||
@@ -0,0 +1,80 @@
|
||||
#
|
||||
# Crafting Guide - plan_evaluator.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingPlan = require './crafting_plan'
|
||||
fixtures = require './fixtures.test'
|
||||
Inventory = require '../game/inventory'
|
||||
PlanEvaluator = require './plan_evaluator'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
criteria = evaluator = planA = planB = planC = plans = wanted = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'plan_evaluator.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS
|
||||
|
||||
describe 'findBestPlan', ->
|
||||
|
||||
it 'can find the shortest of two options', ->
|
||||
evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block']
|
||||
evaluator.scorePlans()
|
||||
|
||||
bestPlan = evaluator.findBestPlan criteria
|
||||
bestPlan.getScore(criteria).should.equal 1.0
|
||||
|
||||
it 'can find the shortest of many options', ->
|
||||
evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword']
|
||||
evaluator.scorePlans()
|
||||
|
||||
bestPlan = evaluator.findBestPlan criteria
|
||||
bestPlan.getScore(criteria).should.equal 1.0
|
||||
|
||||
it 'can break ties using the non-primary criteria', ->
|
||||
evaluator = new PlanEvaluator fixtures.makePlans [2, 'test__iron_ingot'], [1, 'test__charcoal']
|
||||
evaluator.scorePlans()
|
||||
|
||||
bestPlan = evaluator.findBestPlan criteria
|
||||
bestPlan.getScore(criteria).should.equal 1.0
|
||||
|
||||
(p.getRawScore('fewest steps') for p in evaluator._plans).should.eql [2, 2]
|
||||
(p.getRawScore('least materials') for p in evaluator._plans).should.eql [17, 18]
|
||||
|
||||
describe '_normalizeScores', ->
|
||||
|
||||
beforeEach ->
|
||||
modPack = fixtures.makeModPack()
|
||||
wanted = new Inventory modPack:modPack
|
||||
have = new Inventory modPack:modPack
|
||||
planA = new CraftingPlan modPack, wanted, have, []
|
||||
planB = new CraftingPlan modPack, wanted, have, []
|
||||
planC = new CraftingPlan modPack, wanted, have, []
|
||||
plans = [planA, planB, planC]
|
||||
evaluator = new PlanEvaluator plans
|
||||
criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS
|
||||
|
||||
it 'does not set scores when none have been evaluated', ->
|
||||
evaluator._normalizeScores()
|
||||
(plan.hasRawScore(criteria) for plan in plans).should.eql [false, false, false]
|
||||
|
||||
it 'assigns a 1.0 when all plans equal', ->
|
||||
for plan in plans
|
||||
plan.setRawScore criteria, 5
|
||||
|
||||
evaluator._normalizeScores()
|
||||
(plan.getScore(criteria) for plan in plans).should.eql [1.0, 1.0, 1.0]
|
||||
|
||||
it 'computes correct scores from raw scores', ->
|
||||
planA.setRawScore criteria, 7
|
||||
planB.setRawScore criteria, 8
|
||||
planC.setRawScore criteria, 9
|
||||
|
||||
evaluator._normalizeScores()
|
||||
(plan.getScore(criteria) for plan in plans).should.eql [1.0, 0.5, 0.0]
|
||||
@@ -0,0 +1,87 @@
|
||||
#
|
||||
# Crafting Guide - recipe_node.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingNode = require './crafting_node'
|
||||
Item = require '../game/item'
|
||||
# ItemNode = require './item_node' # don't include here, causes a cycle
|
||||
ItemSlug = require '../game/item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class RecipeNode extends CraftingNode
|
||||
|
||||
@::ENTER_METHOD = 'onEnterRecipeNode'
|
||||
@::LEAVE_METHOD = 'onLeaveRecipeNode'
|
||||
@::TYPE = CraftingNode::TYPES.RECIPE
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.recipe? then throw new Error 'options.recipe is required'
|
||||
super options
|
||||
|
||||
@recipe = options.recipe
|
||||
|
||||
# CraftingNode Overrides #######################################################################
|
||||
|
||||
_createChildren: (result=[])->
|
||||
ItemNode = require './item_node' # include here to avoid a cycle
|
||||
|
||||
for stack in @recipe.input
|
||||
item = @modPack.findItem stack.itemSlug
|
||||
if not item?
|
||||
name = @modPack.findName stack.itemSlug
|
||||
item = new Item name:name, slug:stack.itemSlug, gatherable:true
|
||||
result.push new ItemNode modPack:@modPack, item:item
|
||||
return result
|
||||
|
||||
_checkCompleteness: ->
|
||||
for child in @children
|
||||
return false unless child.complete
|
||||
return true
|
||||
|
||||
_checkValidity: ->
|
||||
return false if @_isRepeatedRecipe()
|
||||
return false if @_requiresToolBeingMade()
|
||||
|
||||
for child in @children
|
||||
return false unless child.valid
|
||||
|
||||
return true
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_isRepeatedRecipe: ->
|
||||
nextParent = @parent
|
||||
while nextParent?
|
||||
return true if nextParent.recipe is @recipe
|
||||
nextParent = nextParent.parent
|
||||
|
||||
return false
|
||||
|
||||
_requiresToolBeingMade: ->
|
||||
for toolStack in @recipe.tools
|
||||
toolSlug = toolStack.itemSlug
|
||||
|
||||
nextParent = @parent
|
||||
while nextParent?
|
||||
if ItemSlug.equal toolSlug, nextParent.item?.slug
|
||||
return true
|
||||
nextParent = nextParent.parent
|
||||
|
||||
return false
|
||||
|
||||
# Object Overrides ############################################################################
|
||||
|
||||
toString: (options={})->
|
||||
options.indent ?= ''
|
||||
options.recursive ?= true
|
||||
|
||||
parts = ["#{options.indent}#{@completeText} #{@validText} RecipeNode for #{@recipe.slug}"]
|
||||
nextIndent = options.indent + ' '
|
||||
if options.recursive
|
||||
for child in @children
|
||||
parts.push child.toString indent:nextIndent
|
||||
return parts.join '\n'
|
||||
@@ -0,0 +1,212 @@
|
||||
#
|
||||
# Crafting Guide - simple_inventory.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
ItemSlug = require '../game/item_slug'
|
||||
SimpleStack = require './simple_stack'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class SimpleInventory
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
@clear()
|
||||
|
||||
if options.modPack?
|
||||
@modPack = options.modPack
|
||||
|
||||
if options.clone?
|
||||
@addInventory options.clone
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@Delimiters =
|
||||
Item: '.'
|
||||
Stack: ':'
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
add: (itemSlug, quantity=1)->
|
||||
return this unless quantity > 0
|
||||
|
||||
@_add itemSlug, quantity
|
||||
return this
|
||||
|
||||
addInventory: (inventory)->
|
||||
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
|
||||
return this
|
||||
|
||||
clear: (options={})->
|
||||
@_stacks = {}
|
||||
@_itemSlugs = []
|
||||
|
||||
clone: ->
|
||||
inventory = new SimpleInventory
|
||||
inventory.addInventory this
|
||||
return inventory
|
||||
|
||||
each: (callback)->
|
||||
for itemSlug in @_itemSlugs
|
||||
callback @_stacks[itemSlug]
|
||||
|
||||
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 SimpleStack itemSlug:qualifiedSlug, quantity:stack.quantity
|
||||
else
|
||||
newSlugs.push itemSlug
|
||||
newStacks.push stack
|
||||
|
||||
if changed
|
||||
@_itemSlugs = newSlugs
|
||||
@_stacks = {}
|
||||
for stack in newStacks
|
||||
@_stacks[stack.itemSlug] = stack
|
||||
|
||||
@_sort()
|
||||
|
||||
pop: ->
|
||||
itemSlug = @_itemSlugs.pop()
|
||||
return null unless itemSlug?
|
||||
|
||||
stack = @_stacks[itemSlug]
|
||||
delete @_stacks[itemSlug]
|
||||
|
||||
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
|
||||
for currentItemSlug, index in @_itemSlugs
|
||||
if ItemSlug.equal itemSlug, currentItemSlug
|
||||
@_itemSlugs.splice index, 1
|
||||
break
|
||||
|
||||
return this
|
||||
|
||||
# Parsing Methods ##############################################################################
|
||||
|
||||
parse: (data)->
|
||||
return this if not data? or data.length is 0
|
||||
|
||||
stacks = data.split SimpleInventory.Delimiters.Stack
|
||||
for stackText in stacks
|
||||
stackParts = stackText.split SimpleInventory.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}#{SimpleInventory.Delimiters.Item}#{slugText}"
|
||||
|
||||
return parts.join SimpleInventory.Delimiters.Stack
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
getIsEmpty: ->
|
||||
return @_itemSlugs.length is 0
|
||||
|
||||
getTotalQuantity: ->
|
||||
total = 0
|
||||
@each (stack)->
|
||||
total += stack.quantity
|
||||
return total
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
isEmpty: { get:@prototype.getIsEmpty }
|
||||
totalQuantity: { get:@prototype.getTotalQuantity }
|
||||
|
||||
# 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)->
|
||||
return unless itemSlug?
|
||||
return unless quantity > 0
|
||||
|
||||
stack = @_stacks[itemSlug]
|
||||
if not stack?
|
||||
stack = new SimpleStack itemSlug:itemSlug, quantity:quantity
|
||||
@_stacks[itemSlug] = stack
|
||||
@_itemSlugs.push itemSlug
|
||||
@_sort()
|
||||
else
|
||||
stack.quantity += quantity
|
||||
|
||||
_sort: ->
|
||||
@_itemSlugs.sort (a, b)-> ItemSlug.compare a, b
|
||||
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# Crafting Guide - simple_stack.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
ItemSlug = require '../game/item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class Stack
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
|
||||
attributes.quantity ?= 1
|
||||
|
||||
@itemSlug = attributes.itemSlug
|
||||
@quantity = attributes.quantity
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@compare: (a, b)->
|
||||
if a? and not b? then return -1
|
||||
if not a? and b? then return +1
|
||||
if a.quantity isnt b.quantity
|
||||
return if a.quantity > b.quantity then -1 else +1
|
||||
return ItemSlug.compare a.itemSlug, b.itemSlug
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
return "#{@quantity} #{@itemSlug}"
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -0,0 +1,213 @@
|
||||
#
|
||||
# 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]
|
||||
@@ -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()
|
||||
@@ -0,0 +1,96 @@
|
||||
#
|
||||
# Crafting Guide - item_slug.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemSlug
|
||||
|
||||
constructor: ->
|
||||
@_item = @_mod = null
|
||||
|
||||
if arguments.length is 1
|
||||
parts = _.decomposeSlug arguments[0]
|
||||
@_mod = _.slugify parts[0]
|
||||
@item = _.slugify parts[1]
|
||||
else if arguments.length is 2
|
||||
@_mod = arguments[0]
|
||||
@item = arguments[1]
|
||||
else
|
||||
throw new Error 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@compare: (a, b)->
|
||||
if a.item isnt b.item
|
||||
return if a.item < b.item then -1 else +1
|
||||
if a.mod isnt b.mod
|
||||
return if a.mod < b.mod then -1 else +1
|
||||
return 0
|
||||
|
||||
@equal: (a, b)->
|
||||
return true if not a? and not b?
|
||||
return false unless a? and b?
|
||||
return false unless a.mod is b.mod
|
||||
return false unless a.item is b.item
|
||||
return true
|
||||
|
||||
@slugify: (arg)->
|
||||
return arg if arg?.constructor?.name is 'ItemSlug'
|
||||
|
||||
[modSlug, itemSlug] = _.decomposeSlug arg
|
||||
itemSlug = _.slugify itemSlug
|
||||
|
||||
if modSlug?
|
||||
return new ItemSlug modSlug, itemSlug
|
||||
else
|
||||
return new ItemSlug itemSlug
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
compareTo: (that)->
|
||||
return ItemSlug.compare this, that
|
||||
|
||||
matches: (slug, options={exact:false})->
|
||||
return false unless slug?
|
||||
return false unless typeof(slug.matches) is 'function'
|
||||
|
||||
if slug.isQualified and this.isQualified
|
||||
return slug.qualified is this.qualified
|
||||
else
|
||||
return slug.item is this.item
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
isQualified:
|
||||
get: -> @_mod?
|
||||
|
||||
item:
|
||||
get: -> @_item
|
||||
|
||||
set: (newItem)->
|
||||
if not newItem? then throw new Error 'item is required'
|
||||
@_item = newItem
|
||||
@mod = @mod # reset @_qualified
|
||||
|
||||
mod:
|
||||
get: -> @_mod
|
||||
|
||||
set: (newMod)->
|
||||
@_mod = newMod
|
||||
@_qualified = if @_mod? then _.composeSlugs(@_mod, @_item) else @_item
|
||||
|
||||
qualified:
|
||||
get: -> @_qualified
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
return @_qualified
|
||||
|
||||
valueOf: ->
|
||||
return @_qualified.valueOf()
|
||||
@@ -0,0 +1,122 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -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
|
||||
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,189 @@
|
||||
#
|
||||
# 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[..]
|
||||
|
||||
# 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
|
||||
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
|
||||
@@ -0,0 +1,103 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -0,0 +1,200 @@
|
||||
#
|
||||
# Crafting Guide - mod_version.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
Item = require './item'
|
||||
ItemSlug = require './item_slug'
|
||||
Recipe = require './recipe'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModVersion extends BaseModel
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.modSlug? then throw new Error 'attributes.modSlug is required'
|
||||
if not attributes.version? then throw new Error 'attributes.version is required'
|
||||
attributes.mod ?= null
|
||||
super attributes, options
|
||||
|
||||
@_groups = {}
|
||||
@_items = {}
|
||||
@_names = {}
|
||||
@_recipes = {}
|
||||
@_slugs = []
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
compareTo: (that)->
|
||||
if this.mod? and that.mod?
|
||||
return this.mod.compareTo that.mod
|
||||
|
||||
if this.modSlug isnt that.modSlug
|
||||
return if this.modSlug < that.modSlug then -1 else +1
|
||||
|
||||
return 0
|
||||
|
||||
sort: ->
|
||||
@_slugs.sort (a, b)-> ItemSlug.compare a, b
|
||||
|
||||
# Item Methods #################################################################################
|
||||
|
||||
addItem: (item)->
|
||||
if @findItem(item.slug)? then throw new Error "duplicate item for #{item.name}"
|
||||
|
||||
item.modVersion = this
|
||||
@_items[item.slug.item] = item
|
||||
@_groups[item.group] ?= {}
|
||||
@_groups[item.group][item.slug.item] = item
|
||||
|
||||
@registerName item.slug, item.name
|
||||
return this
|
||||
|
||||
allItemsInGroup: (group)->
|
||||
result = []
|
||||
@eachItemInGroup group, (item)-> result.push item
|
||||
return null if result.length is 0
|
||||
return result
|
||||
|
||||
chooseRandomItem: ->
|
||||
itemIndex = Math.floor Math.random() * @_slugs.length
|
||||
return @_slugs[itemIndex]
|
||||
|
||||
eachItem: (callback)->
|
||||
for slug in @_slugs
|
||||
item = @findItem slug
|
||||
continue unless item?
|
||||
callback item
|
||||
return this
|
||||
|
||||
eachItemInGroup: (group, callback)->
|
||||
itemMap = @_groups[group]
|
||||
return unless itemMap?
|
||||
|
||||
items = _.values(itemMap).sort (a, b)-> a.compareTo b
|
||||
for item in items
|
||||
callback item
|
||||
|
||||
findItem: (itemSlug)->
|
||||
return @_items[itemSlug.item]
|
||||
|
||||
findItemByName: (name)->
|
||||
for itemSlug, item of @_items
|
||||
return item if item.name is name
|
||||
return null
|
||||
|
||||
# Group Methods ################################################################################
|
||||
|
||||
getAllGroups: ->
|
||||
result = []
|
||||
@eachGroup (group)-> result.push group
|
||||
return result
|
||||
|
||||
eachGroup: (callback)->
|
||||
groupNames = _.keys @_groups
|
||||
groupNames.sort (a, b)->
|
||||
if a is b then return 0
|
||||
if a is Item.Group.Other then return -1
|
||||
if b is Item.Group.Other then return +1
|
||||
return if a < b then -1 else +1
|
||||
|
||||
for groupName in groupNames
|
||||
callback groupName
|
||||
|
||||
# Name Methods #################################################################################
|
||||
|
||||
eachName: (callback)->
|
||||
for slug in @_slugs
|
||||
callback @_names[slug.item], slug
|
||||
return this
|
||||
|
||||
findName: (itemSlug)->
|
||||
return @_names[itemSlug.item]
|
||||
|
||||
registerName: (itemSlug, name)->
|
||||
return if @_names[itemSlug.item]
|
||||
@_names[itemSlug.item] = name
|
||||
@_slugs.push itemSlug
|
||||
return this
|
||||
|
||||
# Recipe Methods ###############################################################################
|
||||
|
||||
addRecipe: (recipe)->
|
||||
return unless recipe?
|
||||
|
||||
recipe.modVersion = this
|
||||
if @_recipes[recipe.slug]? then throw new Error "duplicate recipe: #{recipe.slug}"
|
||||
|
||||
@_recipes[recipe.slug] = recipe
|
||||
return this
|
||||
|
||||
eachRecipe: (callback)->
|
||||
recipes = _.values(@_recipes).sort (a, b)-> Recipe.compareFor a, b
|
||||
for recipe in recipes
|
||||
callback recipe
|
||||
return this
|
||||
|
||||
findRecipes: (itemSlug, result=[], options={})->
|
||||
options.onlyPrimary ?= false
|
||||
options.forCrafting ?= false
|
||||
|
||||
primaryRecipes = []
|
||||
otherRecipes = []
|
||||
|
||||
for recipe in _.values @_recipes
|
||||
continue unless recipe.isConditionSatisfied()
|
||||
continue unless recipe.hasAllTools()
|
||||
continue if options.forCrafting and recipe.ignoreDuringCrafting
|
||||
|
||||
if recipe.itemSlug.matches itemSlug
|
||||
primaryRecipes.push recipe
|
||||
else if recipe.produces itemSlug
|
||||
otherRecipes.push recipe
|
||||
|
||||
for recipe in primaryRecipes
|
||||
result.push recipe
|
||||
|
||||
if not options.onlyPrimary
|
||||
for recipe in otherRecipes
|
||||
result.push recipe
|
||||
|
||||
return result
|
||||
|
||||
findExternalRecipes: ->
|
||||
result = {}
|
||||
for k, recipe of @_recipes
|
||||
continue if recipe.itemSlug.isQualified
|
||||
|
||||
recipeList = result[recipe.itemSlug]
|
||||
if not recipeList then recipeList = result[recipe.itemSlug] = []
|
||||
|
||||
recipeList.push recipe
|
||||
|
||||
return result
|
||||
|
||||
hasRecipes: (itemSlug)->
|
||||
for k, recipe of @_recipes
|
||||
return true if recipe.produces itemSlug
|
||||
return false
|
||||
|
||||
# Backbone.Model Overrides #####################################################################
|
||||
|
||||
parse: (text)->
|
||||
ModVersionParser = require '../parsing/mod_version_parser' # to avoid require cycles
|
||||
@_parser ?= new ModVersionParser model:this
|
||||
@_parser.parse text
|
||||
|
||||
return null # prevent calling `set`
|
||||
|
||||
url: ->
|
||||
return c.url.modVersionData modSlug:@modSlug, modVersion:@version
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
return "ModVersion (#{@cid}) {
|
||||
modSlug:#{@modSlug}, version:#{@version}, items:«#{@_slugs.length} items»
|
||||
}"
|
||||
@@ -0,0 +1,101 @@
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
|
||||
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', 'cake', 'cake']
|
||||
|
||||
it 'skip recipes whose conditions are not met', ->
|
||||
recipes = modVersion.findRecipes ItemSlug.slugify('test__cake')
|
||||
recipes.length.should.equal 2
|
||||
@@ -0,0 +1,85 @@
|
||||
#
|
||||
# Crafting Guide - multiblock.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
Inventory = require './inventory'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class Multiblock extends BaseModel
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.input? then throw new Error 'attributes.input is required'
|
||||
if not attributes.layers? then throw new Error 'attributes.layers is required'
|
||||
if attributes.layers.length < 1 then throw new Error 'attributes.layers.length must be >= 1'
|
||||
super attributes, options
|
||||
|
||||
@_analyzePattern()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
getStackAt: (x, y, z)->
|
||||
value = @_stackCache[y]?[z]?[x]
|
||||
return null if value is undefined
|
||||
return value
|
||||
|
||||
getLayerInventory: (y)->
|
||||
@_layerInventories ?= []
|
||||
result = @_layerInventories[y]
|
||||
if not result? then result = @_layerInventories[y] = new Inventory
|
||||
return result
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
depth:
|
||||
get: -> @_depth
|
||||
|
||||
height:
|
||||
get: -> @_height
|
||||
|
||||
inventory:
|
||||
get: -> @_inventory
|
||||
|
||||
width:
|
||||
get: -> @_width
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_analyzeLayer: (layer, y, stackCacheLayer)->
|
||||
rows = layer.split ' '
|
||||
@_depth = Math.max @_depth, rows.length
|
||||
|
||||
for row in rows
|
||||
@_width = Math.max @_width, row.length
|
||||
stackCacheRow = []
|
||||
stackCacheLayer.push stackCacheRow
|
||||
@_analyzeRow row, stackCacheRow, @getLayerInventory(y)
|
||||
|
||||
_analyzePattern: ->
|
||||
@_depth = @_height = @_width = 0
|
||||
@_inventory = new Inventory
|
||||
@_stackCache = []
|
||||
|
||||
for layer, y in @layers
|
||||
stackCacheLayer = []
|
||||
@_stackCache.push stackCacheLayer
|
||||
@_analyzeLayer layer, y, stackCacheLayer
|
||||
|
||||
@_height = @layers.length
|
||||
|
||||
_analyzeRow: (row, stackCacheRow, layerInventory)->
|
||||
for cell, x in row.split ''
|
||||
index = parseInt cell
|
||||
stack = null
|
||||
|
||||
if not _.isNaN index
|
||||
stack = @input[index]
|
||||
@_inventory.add stack.itemSlug, stack.quantity
|
||||
layerInventory.add stack.itemSlug, stack.quantity
|
||||
|
||||
stackCacheRow.push stack
|
||||
@@ -0,0 +1,85 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -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
|
||||
@@ -0,0 +1,90 @@
|
||||
#
|
||||
# Crafting Guide - recipe.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
Item = require './item'
|
||||
ItemSlug = require './item_slug'
|
||||
Recipe = require './recipe'
|
||||
Stack = require './stack'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
input = output = pattern = recipe = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'recipe.coffee', ->
|
||||
|
||||
describe 'constructor', ->
|
||||
|
||||
beforeEach ->
|
||||
input = [
|
||||
new Stack(itemSlug:new ItemSlug('iron_gear')),
|
||||
new Stack(itemSlug:new ItemSlug('gold_ingot'), quantity:4)
|
||||
]
|
||||
pattern = '.1. 101 .1.'
|
||||
|
||||
it 'requires input', ->
|
||||
expect(-> new Recipe slug:'gold_gear', pattern:pattern).to.throw Error, 'attributes.input is required'
|
||||
|
||||
it 'requires a pattern', ->
|
||||
expect(-> new Recipe slug:'gold_gear', input:input).to.throw Error, 'attributes.pattern is required'
|
||||
|
||||
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 '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
|
||||
|
||||
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'
|
||||
|
||||
describe 'getStackAtSlot', ->
|
||||
|
||||
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 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'
|
||||
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# Crafting Guide - simple_stack.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
ItemSlug = require './item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class Stack
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
|
||||
attributes.quantity ?= 1
|
||||
|
||||
@itemSlug = attributes.itemSlug
|
||||
@quantity = attributes.quantity
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@compare: (a, b)->
|
||||
if a? and not b? then return -1
|
||||
if not a? and b? then return +1
|
||||
if a.quantity isnt b.quantity
|
||||
return if a.quantity > b.quantity then -1 else +1
|
||||
return ItemSlug.compare a.itemSlug, b.itemSlug
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
return "#{@quantity} #{@itemSlug}"
|
||||
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# Crafting Guide - stack.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class Stack extends BaseModel
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
|
||||
attributes.quantity ?= 1
|
||||
options.logEvents ?= false
|
||||
super attributes, options
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
return "#{@quantity} #{@itemSlug}"
|
||||
@@ -0,0 +1,138 @@
|
||||
#
|
||||
# Crafting Guide - command_parser_version_base.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{StringBuilder} = require 'crafting-guide-common'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CommandParserVersionBase
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.model? then throw new Error 'options.model is required'
|
||||
options.showAllErrors ?= false
|
||||
|
||||
@_model = options.model
|
||||
@_showAllErrors = options.showAllErrors
|
||||
@errors = []
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@COMMAND = /\ *([^:]*):?(.*)/
|
||||
|
||||
@COMMENT = /([^\\]?)#.*/
|
||||
|
||||
@simplify: (text)->
|
||||
text = text.trim()
|
||||
text = text.replace /\ */g , ' '
|
||||
text = text.replace /\n/g, ';'
|
||||
text = text.replace /; */g, ';'
|
||||
text = text.replace /;;*/g, ';'
|
||||
text = text.replace /: /g, ':'
|
||||
return text
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
parse: (text)->
|
||||
@_rawData = {}
|
||||
@_lineNumber = 1
|
||||
@errors = []
|
||||
|
||||
@_lines = text.split '\n'
|
||||
@_lineNumber = 0
|
||||
while @_lineNumber < @_lines.length
|
||||
@_lineNumber += 1
|
||||
commands = @_parseLine @_lines[@_lineNumber - 1]
|
||||
for command in commands
|
||||
@_handleErrors @_execute, command
|
||||
|
||||
@_handleErrors @_buildModel, @_rawData, @_model
|
||||
return @_model
|
||||
|
||||
unparse: ->
|
||||
builder = new StringBuilder context:@_model
|
||||
@_unparseModel builder, @_model
|
||||
return builder.toString()
|
||||
|
||||
# Subclass Methods #############################################################################
|
||||
|
||||
_buildModel: (rawData, model)->
|
||||
throw new Error 'Subclasses must override this method'
|
||||
|
||||
_unparseModel: (builder, model)->
|
||||
throw new Error 'Subclasses must override this method'
|
||||
|
||||
_command_schema: -> # do nothing
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_execute: (command)->
|
||||
method = this["_command_#{command.name}"]
|
||||
if not method? then throw new Error "Unknown command: #{command.name}"
|
||||
|
||||
@_handleErrors method, command.args
|
||||
|
||||
_parseLine: (line)->
|
||||
line = line.replace CommandParserVersionBase.COMMENT, '$1'
|
||||
line = line.trim()
|
||||
return [] if line.length is 0
|
||||
|
||||
[line, hereDoc] = @_parseHereDoc line
|
||||
|
||||
lineParts = (part.trim() for part in line.split(';'))
|
||||
commands = []
|
||||
for linePart in lineParts
|
||||
continue if linePart.length is 0
|
||||
|
||||
match = CommandParserVersionBase.COMMAND.exec linePart
|
||||
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
|
||||
|
||||
args = []
|
||||
args = (s for s in match[2].split(',') when s.length > 0) if match[2]?
|
||||
args = (s.trim() for s in args)
|
||||
args = (s for s in args when s.length > 0)
|
||||
args.push hereDoc if hereDoc?
|
||||
commands.push name:match[1], args:args
|
||||
|
||||
return commands
|
||||
|
||||
_handleErrors: (callback, args...)->
|
||||
if args.length is 1 and _.isArray(args[0]) then args = args[0]
|
||||
|
||||
try
|
||||
callback.apply this, args
|
||||
catch e
|
||||
e.message = "line #{@_lineNumber}: #{e.message}"
|
||||
if not @_showAllErrors then throw e
|
||||
@errors.push e
|
||||
logger.error -> e.message
|
||||
|
||||
_parseHereDoc: (line)->
|
||||
hereDocIndex = line.indexOf '<<-'
|
||||
return [line, null] unless hereDocIndex isnt -1
|
||||
|
||||
hereDocStopText = line[hereDocIndex+3...line.length]
|
||||
line = line[0...hereDocIndex]
|
||||
|
||||
hereDocLines = []
|
||||
while true
|
||||
@_lineNumber += 1
|
||||
break if @_lineNumber >= @_lines.length
|
||||
|
||||
nextLine = @_lines[@_lineNumber-1]
|
||||
break if nextLine.trim() is hereDocStopText
|
||||
hereDocLines.push nextLine
|
||||
|
||||
shortestIndent = Number.MAX_VALUE
|
||||
for hereDocLine in hereDocLines
|
||||
continue if hereDocLine.trim().length is 0
|
||||
shortestIndent = Math.min hereDocLine.match(/( *).*/)[1].length, shortestIndent
|
||||
|
||||
for i in [0...hereDocLines.length]
|
||||
hereDocLines[i] = hereDocLines[i][shortestIndent..]
|
||||
|
||||
return [line, null] unless hereDocLines.length > 0
|
||||
return [line, hereDocLines.join('\n')]
|
||||
@@ -0,0 +1,48 @@
|
||||
#
|
||||
# Crafting Guide - command_parser_version_base.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CommandParserVersionBase = require './command_parser_version_base'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
parser = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'command_parser_version_base.coffee', ->
|
||||
|
||||
beforeEach -> parser = new CommandParserVersionBase model:{}
|
||||
|
||||
describe '_parseHereDoc', ->
|
||||
|
||||
it 'returns null for non-heredoc lines', ->
|
||||
result = parser._parseHereDoc 'foobar: baz'
|
||||
expect(result[1]).to.be.null
|
||||
|
||||
it 'identifies the right text for a real heredoc', ->
|
||||
parser._lines = ['command: <<-END', 'alpha', 'bravo', 'charlie', 'END', 'command1: arg2']
|
||||
parser._lineNumber = 1
|
||||
|
||||
result = parser._parseHereDoc parser._lines[0]
|
||||
result[0].should.equal 'command: '
|
||||
result[1].should.equal 'alpha\nbravo\ncharlie'
|
||||
|
||||
it 'identifies an empty heredoc', ->
|
||||
parser._lines = ['command: <<-END', 'END']
|
||||
parser._lineNumber = 1
|
||||
|
||||
result = parser._parseHereDoc parser._lines[0]
|
||||
result[0].should.equal 'command: '
|
||||
expect(result[1]).to.be.null
|
||||
|
||||
it 'trims smallest leading whitespace', ->
|
||||
parser._lines = ['command: <<-END', ' alpha', ' bravo', '', ' charlie', 'END', 'command1: arg2']
|
||||
parser._lineNumber = 1
|
||||
|
||||
result = parser._parseHereDoc parser._lines[0]
|
||||
result[0].should.equal 'command: '
|
||||
result[1].should.equal 'alpha\n bravo\n\ncharlie'
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Crafting Guide - item_parser.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
VersionedParserBase = require './versioned_parser_base'
|
||||
ItemParserV1 = require './item_parser_v1'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemParser extends VersionedParserBase
|
||||
|
||||
# VersionedParserBase Overrides ################################################################
|
||||
|
||||
_createParsers: (options)->
|
||||
return result =
|
||||
'1': new ItemParserV1 options
|
||||
@@ -0,0 +1,72 @@
|
||||
#
|
||||
# Crafting Guide - item_parser_v1.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CommandParserVersionBase = require './command_parser_version_base'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemParserV1 extends CommandParserVersionBase
|
||||
|
||||
# CommandParserVersionBase Overrides ###########################################################
|
||||
|
||||
_buildModel: (rawData, model)->
|
||||
@_buildItem rawData, model
|
||||
|
||||
_unparseModel: (builder, model)->
|
||||
builder.line 'schema: ', 1
|
||||
builder.line()
|
||||
|
||||
@_unparseItem builder, model
|
||||
|
||||
# Command Methods ##############################################################################
|
||||
|
||||
_command_description: (textParts...)->
|
||||
if not @_rawData.description?
|
||||
@_rawData.description = ''
|
||||
else
|
||||
@_rawData.description += '\n'
|
||||
@_rawData.description += textParts.join ', '
|
||||
|
||||
_command_officialUrl: (officialUrl)->
|
||||
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
|
||||
if not officialUrl? or (officialUrl.length is 0) then throw new Error 'officialUrl cannot be empty'
|
||||
@_rawData.officialUrl = officialUrl
|
||||
|
||||
_command_video: (youTubeId, nameParts...)->
|
||||
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
|
||||
name = nameParts.join ', '
|
||||
if not name?.length then throw new Error 'video declaration requires a name'
|
||||
|
||||
@_rawData.videos ?= []
|
||||
@_rawData.videos.push youTubeId:youTubeId, name:name
|
||||
|
||||
# Object Building Methods ######################################################################
|
||||
|
||||
_buildItem: (rawData, model)->
|
||||
model.description = rawData.description if rawData.description?
|
||||
model.officialUrl = rawData.officialUrl if rawData.officialUrl?
|
||||
model.videos = rawData.videos if rawData.videos?
|
||||
|
||||
# Un-parsing Methods ###########################################################################
|
||||
|
||||
_unparseItem: (builder, model)->
|
||||
if model.officialUrl?
|
||||
builder.line 'officialUrl: ', model.officialUrl
|
||||
builder.line()
|
||||
|
||||
if model.description?
|
||||
if model.description.indexOf('\n') isnt -1
|
||||
builder.line 'description: <<-END'
|
||||
builder.line model.description
|
||||
builder.line 'END'
|
||||
else
|
||||
builder.line 'description: ', model.description
|
||||
builder.line()
|
||||
|
||||
for video in model.videos
|
||||
builder.line 'video: ', video.youTubeId, ', ', video.name
|
||||
builder.line()
|
||||
@@ -0,0 +1,99 @@
|
||||
#
|
||||
# Crafting Guide - item_parser_v1.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
Item = require '../game/item'
|
||||
ItemParserV1 = require './item_parser_v1'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
baseText = item = parser = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'item_parser_v1.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
item = new Item name:'alpha'
|
||||
parser = new ItemParserV1 model:item
|
||||
|
||||
describe 'officialUrl', ->
|
||||
|
||||
it 'may be omitted', ->
|
||||
parser.parse 'schema: 1\nvideo: youtubeid, Video Alpha\ndescription: Bravo, Charlie'
|
||||
expect(item.officialUrl).to.be.null
|
||||
|
||||
it 'is assigned properly when given', ->
|
||||
parser.parse 'schema: 1\nofficialUrl: http://testurl.com'
|
||||
item.officialUrl.should.equal 'http://testurl.com'
|
||||
|
||||
it 'does not allow duplicate declarations', ->
|
||||
func = -> parser.parse 'schema: 1\nofficialUrl: http://testurl.com\nofficialUrl: http://testurl2.com'
|
||||
expect(func).to.throw Error, 'duplicate'
|
||||
|
||||
it 'does not allow an empty value if given', ->
|
||||
func = -> parser.parse 'schema: 1\nofficialUrl:'
|
||||
expect(func).to.throw Error, 'empty'
|
||||
|
||||
describe 'description', ->
|
||||
|
||||
it 'may be omitted', ->
|
||||
parser.parse 'schema: 1\nvideo: youTubeId, Video Alpha\nofficialUrl: http://testurl.com'
|
||||
expect(item.description).to.be.null
|
||||
|
||||
it 'is assigned properly when given', ->
|
||||
parser.parse 'schema: 1\ndescription: Alpha Bravo Charlie'
|
||||
item.description.should.equal 'Alpha Bravo Charlie'
|
||||
|
||||
it 'can be a heredoc', ->
|
||||
parser.parse 'schema: 1\ndescription: <<-END\nAlpha\nBravo\nCharlie\nEND'
|
||||
item.description.should.equal 'Alpha\nBravo\nCharlie'
|
||||
|
||||
it 'concatenates multiple declarations', ->
|
||||
parser.parse 'schema: 1\ndescription: Alpha\ndescription: Bravo'
|
||||
item.description.should.equal 'Alpha\nBravo'
|
||||
|
||||
describe 'video', ->
|
||||
|
||||
it 'may be omitted', ->
|
||||
parser.parse 'schema: 1\nofficialUrl: http://testurl.com\ndescription: Alpha Bravo Charlie'
|
||||
item.videos.should.eql []
|
||||
|
||||
it 'is assigned properly when given', ->
|
||||
parser.parse 'schema: 1\nvideo: youtubeid, Alpha Bravo'
|
||||
item.videos[0].should.eql youTubeId:'youtubeid', name:'Alpha Bravo'
|
||||
item.videos.length.should.equal 1
|
||||
|
||||
it 'may be included multiple times', ->
|
||||
parser.parse 'schema: 1\nvideo: youtubeid1, Alpha\nvideo: youtubeid2, Bravo'
|
||||
item.videos[0].should.eql youTubeId:'youtubeid1', name:'Alpha'
|
||||
item.videos[1].should.eql youTubeId:'youtubeid2', name:'Bravo'
|
||||
item.videos.length.should.equal 2
|
||||
|
||||
it 'requires a YouTubeId and name', ->
|
||||
func = -> parser.parse 'schema: 1\nvideo: alpha'
|
||||
expect(func).to.throw Error, 'requires a name'
|
||||
|
||||
describe 'unparsing', ->
|
||||
|
||||
it 'can round-trip a fully described item', ->
|
||||
text = """
|
||||
schema: 1
|
||||
|
||||
officialUrl: http://testurl.com
|
||||
|
||||
description: <<-END
|
||||
Alpha
|
||||
Bravo
|
||||
END
|
||||
|
||||
video: youtubeid1, Alpha Bravo
|
||||
video: youtubeid2, Charlie Delta
|
||||
|
||||
|
||||
"""
|
||||
parser.parse text
|
||||
parser.unparse().should.equal text
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Crafting Guide - mod_parser.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
VersionedParserBase = require './versioned_parser_base'
|
||||
ModParserV1 = require './mod_parser_v1'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModParser extends VersionedParserBase
|
||||
|
||||
# VersionedParserBase Overrides ################################################################
|
||||
|
||||
_createParsers: (options)->
|
||||
return result =
|
||||
'1': new ModParserV1 options
|
||||
@@ -0,0 +1,94 @@
|
||||
#
|
||||
# Crafting Guide - mod_parser_v1.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CommandParserVersionBase = require './command_parser_version_base'
|
||||
Mod = require '../game/mod'
|
||||
ModVersion = require '../game/mod_version'
|
||||
Tutorial = require '../site/tutorial'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModParserV1 extends CommandParserVersionBase
|
||||
|
||||
# CommandParserVersionBase Overrides ###########################################################
|
||||
|
||||
_buildModel: (rawData, model)->
|
||||
@_buildMod rawData, model
|
||||
|
||||
_unparseModel: (builder, model)->
|
||||
@_unparseMod builder, model
|
||||
|
||||
# Command Methods ##############################################################################
|
||||
|
||||
_command_author: (authorParts...)->
|
||||
if @_rawData.author? then throw new Error 'duplicate declaration of "author"'
|
||||
author = authorParts.join ', '
|
||||
if author.length is 0 then throw new Error '"author" cannot be empty, but may be omitted'
|
||||
|
||||
@_rawData.author = author
|
||||
|
||||
_command_description: (descriptionParts...)->
|
||||
if @_rawData.description? then throw new Error 'duplicate declaration of "description"'
|
||||
description = descriptionParts.join ', '
|
||||
if description.length is 0 then throw new Error '"description" cannot be empty, but may be omitted'
|
||||
|
||||
@_rawData.description = description
|
||||
|
||||
_command_documentationUrl: (documentationUrl)->
|
||||
documentationUrl ?= ''
|
||||
if @_rawData.documentationUrl? then throw new Error 'duplicate declaration of "documentationUrl"'
|
||||
if documentationUrl.length is 0 then throw new Error 'documentationUrl cannot be empty (omit it instead)'
|
||||
@_rawData.documentationUrl = documentationUrl
|
||||
|
||||
_command_downloadUrl: (downloadUrl)->
|
||||
if @_rawData.downloadUrl? then throw new Error 'duplicate declaration of "downloadUrl"'
|
||||
if downloadUrl.length is 0 then throw new Error 'downloadUrl cannot be empty (omit it instead)'
|
||||
@_rawData.downloadUrl = downloadUrl
|
||||
|
||||
_command_homePageUrl: (homePageUrl='')->
|
||||
if @_rawData.homePageUrl? then throw new Error 'duplicate declaration of "homePageUrl"'
|
||||
if homePageUrl.length is 0 then throw new Error 'homePageUrl cannot be empty'
|
||||
|
||||
@_rawData.homePageUrl = homePageUrl
|
||||
|
||||
_command_name: (name)->
|
||||
if @_rawData.name? then throw new Error 'duplicate declaration of "name"'
|
||||
if name.length is 0 then throw new Error '"name" cannot be empty'
|
||||
@_rawData.name = name
|
||||
|
||||
_command_tutorial: (nameParts...)->
|
||||
name = nameParts.join(', ').trim()
|
||||
if name.length is 0 then throw new Error '"name" cannot be empty'
|
||||
@_rawData.tutorialNames ?= []
|
||||
@_rawData.tutorialNames.push name
|
||||
|
||||
_command_version: (version='')->
|
||||
if version.length is 0 then throw new Error 'version cannot be empty'
|
||||
|
||||
@_rawData.versions ?= []
|
||||
@_rawData.versions.push version
|
||||
|
||||
# Object Building Methods ######################################################################
|
||||
|
||||
_buildMod: (rawData, model)->
|
||||
if not rawData.name? then throw new Error 'the "name" declaration is required'
|
||||
if not rawData.homePageUrl? then throw new Error 'the "homePageUrl" declaration is required'
|
||||
if not rawData.versions? then throw new Error 'at least one "version" declaration is required'
|
||||
|
||||
model.author = rawData.author if rawData.author?
|
||||
model.description = rawData.description if rawData.description?
|
||||
model.documentationUrl = rawData.documentationUrl if rawData.documentationUrl?
|
||||
model.downloadUrl = rawData.downloadUrl if rawData.downloadUrl?
|
||||
model.name = rawData.name
|
||||
model.homePageUrl = rawData.homePageUrl
|
||||
|
||||
if rawData.tutorialNames?
|
||||
for tutorialName in rawData.tutorialNames
|
||||
model.addTutorial new Tutorial name:tutorialName
|
||||
|
||||
for version in rawData.versions
|
||||
model.addModVersion new ModVersion modSlug:model.slug, version:version
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Crafting Guide - mod_version_parser.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
VersionedParserBase = require './versioned_parser_base'
|
||||
ModVersionParserV1 = require './mod_version_parser_v1'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModVersionParser extends VersionedParserBase
|
||||
|
||||
# VersionedParserBase Overrides ################################################################
|
||||
|
||||
_createParsers: (options)->
|
||||
return result =
|
||||
'1': new ModVersionParserV1 options
|
||||
@@ -0,0 +1,380 @@
|
||||
#
|
||||
# Crafting Guide - mod_version_parser_v1.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CommandParserVersionBase = require './command_parser_version_base'
|
||||
Item = require '../game/item'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
ModVersion = require '../game/mod_version'
|
||||
Multiblock = require '../game/multiblock'
|
||||
Recipe = require '../game/recipe'
|
||||
Stack = require '../game/simple_stack'
|
||||
{StringBuilder} = require 'crafting-guide-common'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModVersionParserV1 extends CommandParserVersionBase
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@INTEGER = /[0-9]+/
|
||||
|
||||
@PATTERN = /^[0-9.]{3} ?[0-9.]{3} ?[0-9.]{3}$/
|
||||
|
||||
@STACK = /^([0-9]+) +(.*)$/
|
||||
|
||||
# CommandParserVersionBase Overrides ###########################################################
|
||||
|
||||
_buildModel: (rawData, model)->
|
||||
@_buildModVersion rawData, model
|
||||
|
||||
_unparseModel: (builder, model)->
|
||||
@_unparseModVersion builder, model
|
||||
|
||||
# Command Methods ##############################################################################
|
||||
|
||||
_command_extras: (extraTerms...)->
|
||||
if not @_recipeData? then throw new Error 'cannot declare "extras" before "recipe"'
|
||||
if @_recipeData.output.length isnt 1 then throw new Error 'duplicate declaration of "extras"'
|
||||
|
||||
for term in extraTerms
|
||||
@_recipeData.output.push @_parseStack term
|
||||
|
||||
_command_gatherable: (gatherable)->
|
||||
if not @_itemData? then throw new Error 'cannot declare "gatherable" before "item"'
|
||||
if @_itemData.gatherable? then throw new Error 'duplicate declaration of "gatherable"'
|
||||
if not (gatherable in ['yes', 'no']) then throw new Error 'gatherable must be either "yes" or "no"'
|
||||
|
||||
@_itemData.gatherable = (gatherable is 'yes')
|
||||
|
||||
_command_group: (group)->
|
||||
if group.length is 0 then throw new Error 'a group name cannot be empty'
|
||||
@_rawData.group = group
|
||||
|
||||
_command_item: (name='')->
|
||||
if not name.length > 0 then throw new Error 'the item name cannot be empty'
|
||||
|
||||
@_itemData = name:name, line:@_lineNumber, group:@_rawData.group, type:'new'
|
||||
@_rawData.items ?= {}
|
||||
@_rawData.items[name] = @_itemData
|
||||
|
||||
@_recipeData = null
|
||||
|
||||
_command_ignoreDuringCrafting: (value)->
|
||||
if not @_recipeData? then throw new Error 'cannot declare "ignoreDuringCrafting" before "recipe"'
|
||||
if @_recipeData.ignoreDuringCrafting? then throw new Error 'duplicate declaration of "ignoreDuringCrafting"'
|
||||
if not (value in ['yes', 'no']) then throw new Error 'ignoreDuringCrafting must be either "yes" or "no"'
|
||||
|
||||
@_recipeData.ignoreDuringCrafting = (value is 'yes')
|
||||
|
||||
_command_input: (stackDescriptions...)->
|
||||
activeData = if @_recipeData? then @_recipeData else @_multiblockData
|
||||
|
||||
if not activeData? then throw new Error 'cannot declare "input" before "recipe" or "multiblock"'
|
||||
if activeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"'
|
||||
|
||||
for stackDescription in stackDescriptions
|
||||
activeData.input.push @_parseStack stackDescription
|
||||
|
||||
_command_layer: (layerText)->
|
||||
if not @_multiblockData? then throw new Error 'cannot declare "layer" before "multiblock"'
|
||||
if not layerText? then throw new Error 'cannot have an empty layer'
|
||||
if layerText.length is 0 then throw new Error 'cannot have an empty layer'
|
||||
|
||||
@_multiblockData.layers.push layerText
|
||||
|
||||
_command_multiblock: ->
|
||||
if not @_itemData? then throw new Error 'cannot declare "multiblock" before "item"'
|
||||
if @_itemData.multiblockData? then throw new Error 'duplicate declaration of "multiblock"'
|
||||
|
||||
@_recipeData = null
|
||||
@_multiblockData = input:[], layers:[], line:@_lineNumber
|
||||
@_itemData.multiblockData = @_multiblockData
|
||||
|
||||
_command_onlyIf: (condition)->
|
||||
words = condition.split ' '
|
||||
if words.length < 2 then throw new Error 'condition must include a verb followed by a noun'
|
||||
|
||||
inverted = false
|
||||
if words[0] is 'not'
|
||||
inverted = true
|
||||
words.shift()
|
||||
|
||||
verb = words[0]
|
||||
noun = words[1..].join ' '
|
||||
|
||||
if not (verb in ['item', 'mod']) then throw new Error "unknown verb: #{verb}"
|
||||
@_recipeData.condition = verb:verb, noun:noun, inverted:inverted
|
||||
|
||||
_command_pattern: (pattern='')->
|
||||
if not @_recipeData? then throw new Error 'cannot declare "pattern" before "recipe"'
|
||||
if @_recipeData.pattern? then throw new Error 'duplicate declaration of "pattern"'
|
||||
if not ModVersionParserV1.PATTERN.test pattern
|
||||
throw new Error 'a pattern must have 9 digits using 0-9 for items and "." for an empty spot;
|
||||
spaces are optional'
|
||||
|
||||
@_recipeData.pattern = pattern
|
||||
|
||||
_command_quantity: (quantity)->
|
||||
if not @_recipeData? then throw new Error 'cannot declare "quantity" before "recipe"'
|
||||
if @_recipeData.quantity? then throw new Error 'duplicate declaration of "quantity"'
|
||||
if not ModVersionParserV1.INTEGER.test(quantity) then throw new Error 'quantity must be an integer'
|
||||
|
||||
@_recipeData.quantity = quantity
|
||||
@_recipeData.output[0].quantity = parseInt quantity
|
||||
|
||||
_command_recipe: ->
|
||||
if not @_itemData? then throw new Error 'cannot delcare "recipe" before "item"'
|
||||
|
||||
@_multiblockData = null
|
||||
@_recipeData = line:@_lineNumber, input:[], output:[{quantity:1, name:@_itemData.name}], tools:[]
|
||||
@_itemData.recipes ?= []
|
||||
@_itemData.recipes.push @_recipeData
|
||||
|
||||
_command_tools: (toolNames...)->
|
||||
if not @_recipeData? then throw new Error 'cannot declare "tools" before "recipe"'
|
||||
if @_recipeData.tools.length isnt 0 then throw new Error 'duplicate declaration of "tools"'
|
||||
|
||||
for name in toolNames
|
||||
if name.length is 0 then throw new Error 'tool names cannot be empty'
|
||||
@_recipeData.tools.push name:name, quantity:1
|
||||
|
||||
_command_update: (name='')->
|
||||
if not name.length > 0 then throw new 'the item name cannot be empty'
|
||||
|
||||
@_itemData = name:name, line:@_lineNumber, type:'update'
|
||||
@_recipeData = null
|
||||
|
||||
@_rawData.items ?= {}
|
||||
@_rawData.items[name] = @_itemData
|
||||
|
||||
# Parsing Helpers ##############################################################################
|
||||
|
||||
_parseStack: (stackText)->
|
||||
match = ModVersionParserV1.STACK.exec stackText
|
||||
if match?
|
||||
return quantity:parseInt(match[1]), name:match[2]
|
||||
else
|
||||
return quantity:1, name:stackText
|
||||
|
||||
# Object Creation Methods ######################################################################
|
||||
|
||||
_buildModVersion: (modVersionData, modVersion)->
|
||||
modVersionData.items ?= []
|
||||
|
||||
for itemName, itemData of modVersionData.items
|
||||
@_handleErrors @_buildItem, modVersion, itemData
|
||||
|
||||
modVersion.sort()
|
||||
return modVersion
|
||||
|
||||
_buildItem: (modVersion, itemData)->
|
||||
@_lineNumber = itemData.line
|
||||
itemData.gatherable ?= false
|
||||
itemData.ignoreDuringCrafting ?= false
|
||||
itemData.recipes ?= []
|
||||
|
||||
if itemData.type is 'new'
|
||||
item = new Item
|
||||
name: itemData.name,
|
||||
ignoreDuringCrafting: itemData.ignoreDuringCrafting,
|
||||
isGatherable: itemData.gatherable,
|
||||
group: itemData.group
|
||||
modVersion.addItem item
|
||||
itemData.slug = item.slug
|
||||
else
|
||||
itemData.slug = ItemSlug.slugify itemData.name
|
||||
modVersion.registerName itemData.slug, itemData.name
|
||||
|
||||
if itemData.multiblockData
|
||||
item.multiblock = @_handleErrors @_buildMultiblock, modVersion, item, itemData.multiblockData
|
||||
|
||||
for recipeData in itemData.recipes
|
||||
@_handleErrors @_buildRecipe, modVersion, itemData, recipeData
|
||||
|
||||
return item
|
||||
|
||||
_buildMultiblock: (modVersion, item, multiblockData)->
|
||||
@_lineNumber = multiblockData.line
|
||||
if multiblockData.layers.length is 0 then throw new Error '"multiblock" requires at least one "layer"'
|
||||
if multiblockData.input.length is 0 then throw new Error '"multiblock" requires at least one "input"'
|
||||
|
||||
input = @_buildStackList modVersion, multiblockData.input
|
||||
multiblock = new Multiblock input:input, layers:multiblockData.layers
|
||||
return multiblock
|
||||
|
||||
_buildRecipe: (modVersion, itemData, recipeData)->
|
||||
@_lineNumber = recipeData.line
|
||||
if recipeData.input.length is 0 then throw new Error 'the "input" declaration is required'
|
||||
if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required'
|
||||
|
||||
recipe = new Recipe
|
||||
condition: recipeData.condition
|
||||
ignoreDuringCrafting: recipeData.ignoreDuringCrafting
|
||||
input: @_buildStackList modVersion, recipeData.input, recipeData.pattern
|
||||
output: @_buildStackList modVersion, recipeData.output
|
||||
pattern: recipeData.pattern
|
||||
tools: @_buildStackList modVersion, recipeData.tools
|
||||
|
||||
modVersion.addRecipe recipe
|
||||
return recipe
|
||||
|
||||
_buildStackList: (modVersion, data, pattern=null)->
|
||||
createSlug = (name)=>
|
||||
item = @_rawData.items[name]
|
||||
if item? and item.type isnt 'update'
|
||||
slug = new ItemSlug modVersion.modSlug, _.slugify name
|
||||
else
|
||||
slug = new ItemSlug name
|
||||
modVersion.registerName slug, name
|
||||
return slug
|
||||
|
||||
stacks = []
|
||||
for stackData in data
|
||||
itemSlug = createSlug stackData.name
|
||||
stacks.push new Stack itemSlug:itemSlug, quantity:stackData.quantity
|
||||
|
||||
if pattern?
|
||||
expectedIndexes = _.reduce [0...stacks.length], ((obj, i)-> obj[i] = true; return obj), {}
|
||||
for c in pattern
|
||||
continue if c is '.'
|
||||
continue if c is ' '
|
||||
delete expectedIndexes[c]
|
||||
if not stacks[parseInt(c)]? then throw new Error "there is no item #{c} in this recipe"
|
||||
|
||||
unusedNames = _.map(_.keys(expectedIndexes), ((i)-> data[parseInt(i)].name))
|
||||
if unusedNames.length > 1
|
||||
throw new Error "#{unusedNames.join(', ')} are listed for this recipe, but do not appear in the pattern"
|
||||
else if unusedNames.length is 1
|
||||
throw new Error "#{unusedNames[0]} is listed for this recipe, but does not appear in the pattern"
|
||||
|
||||
return stacks
|
||||
|
||||
# Un-parsing Methods ###########################################################################
|
||||
|
||||
_unparseModVersion: (builder, modVersion)->
|
||||
builder
|
||||
.line 'schema: ', 1
|
||||
.line()
|
||||
|
||||
modVersion.eachGroup (group)=>
|
||||
@_unparseGroup builder, modVersion, group
|
||||
|
||||
externalRecipes = modVersion.findExternalRecipes()
|
||||
keys = _.keys(externalRecipes).sort()
|
||||
for itemSlugText in keys
|
||||
recipeList = externalRecipes[itemSlugText]
|
||||
|
||||
builder
|
||||
.line 'update: ', modVersion.findName ItemSlug.slugify(itemSlugText)
|
||||
.indent()
|
||||
.loop(recipeList, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r))
|
||||
.outdent()
|
||||
.line()
|
||||
|
||||
_unparseGroup: (builder, modVersion, group)->
|
||||
if group isnt Item.Group.Other
|
||||
builder
|
||||
.line 'group: ', group
|
||||
.line()
|
||||
.indent()
|
||||
|
||||
modVersion.eachItemInGroup group, (item)=>
|
||||
@_unparseItem builder, modVersion, item
|
||||
builder.line()
|
||||
|
||||
if group isnt Item.Group.Other
|
||||
builder.outdent()
|
||||
|
||||
_unparseItem: (builder, modVersion, item)->
|
||||
recipes = modVersion.findRecipes item.slug, [], onlyPrimary:true
|
||||
|
||||
builder
|
||||
.line 'item: ', item.name
|
||||
.indent()
|
||||
.onlyIf item.isGatherable, => builder.line 'gatherable: yes'
|
||||
.onlyIf item.multiblock?, =>
|
||||
builder
|
||||
.indent()
|
||||
.call => @_unparseMultiblock builder, item.multiblock
|
||||
.outdent()
|
||||
.onlyIf recipes.length > 0, =>
|
||||
builder.loop recipes, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r)
|
||||
.outdent()
|
||||
|
||||
_unparseMultiblock: (builder, multiblock)->
|
||||
builder
|
||||
.line 'multiblock:'
|
||||
.indent()
|
||||
.push "input: "
|
||||
.call => @_unparseStackList builder, multiblock.input
|
||||
.push ";\n"
|
||||
.loop(multiblock.layers, delimiter:'', onEach:(builder, layer)=> builder.line "layer: #{layer}")
|
||||
.outdent()
|
||||
|
||||
_unparseRecipe: (builder, recipe)->
|
||||
inputStacks = recipe.input[..]
|
||||
inputStacks.sort (a, b)-> Stack.compare a, b
|
||||
inputNames = []
|
||||
for stack in inputStacks
|
||||
name = builder.context.findName stack.itemSlug
|
||||
if stack.quantity > 1
|
||||
inputNames.push "#{stack.quantity} #{name}"
|
||||
else
|
||||
inputNames.push name
|
||||
|
||||
patternMap = {'.', '.'}
|
||||
for i in [0...recipe.input.length]
|
||||
stack = recipe.input[i]
|
||||
name = builder.context.findName(stack.itemSlug)
|
||||
name = "#{stack.quantity} #{name}" if stack.quantity > 1
|
||||
patternMap["#{i}"] = "#{inputNames.indexOf name}"
|
||||
|
||||
pattern = recipe.pattern or recipe.defaultPattern
|
||||
newPattern = []
|
||||
for c in pattern.split('')
|
||||
newPattern.push patternMap[c]
|
||||
newPattern = newPattern.join ''
|
||||
newPattern = newPattern.replace /(...)(...)(...)/, '$1 $2 $3'
|
||||
|
||||
quantity = recipe.output[0].quantity
|
||||
|
||||
extraOutputs = recipe.output[0...recipe.output.length]
|
||||
extraOutputs.shift()
|
||||
|
||||
builder
|
||||
.line 'recipe:'
|
||||
.indent()
|
||||
.onlyIf recipe.condition?, =>
|
||||
builder.push 'onlyIf: '
|
||||
.onlyIf recipe.condition.inverted, -> builder.push 'not '
|
||||
.line recipe.condition.verb, ' ', recipe.condition.noun
|
||||
.onlyIf extraOutputs.length > 0, =>
|
||||
builder
|
||||
.push 'extras: '
|
||||
.call => @_unparseStackList builder, extraOutputs
|
||||
.line()
|
||||
.onlyIf recipe.ignoreDuringCrafting, => builder.line 'ignoreDuringCrafting: yes'
|
||||
.push 'input: '
|
||||
.loop inputNames
|
||||
.line()
|
||||
.line 'pattern: ', newPattern
|
||||
.onlyIf quantity > 1, => builder.line 'quantity: ', quantity
|
||||
.onlyIf recipe.tools.length > 0, =>
|
||||
builder
|
||||
.push 'tools: '
|
||||
.call => @_unparseStackList builder, recipe.tools
|
||||
.line()
|
||||
.outdent()
|
||||
|
||||
_unparseStackList: (builder, stackList)->
|
||||
if stackList.length is 1 and stackList[0].quantity is 1
|
||||
builder.push builder.context.findName(stackList[0].itemSlug)
|
||||
else
|
||||
builder.loop stackList, onEach:(b, stack)=>
|
||||
builder
|
||||
.onlyIf stack.quantity > 1, => builder.push stack.quantity, ' '
|
||||
.push builder.context.findName stack.itemSlug
|
||||
@@ -0,0 +1,344 @@
|
||||
#
|
||||
# Crafting Guide - mod_version_parser_v1.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CommandParserVersionBase = require './command_parser_version_base'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
ModVersion = require '../game/mod_version'
|
||||
ModVersionParserV1 = require './mod_version_parser_v1'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
baseText = modVersion = parser = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'mod_version_parser_v1.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
modVersion = new ModVersion modSlug:'test', version:'0.0'
|
||||
parser = new ModVersionParserV1 model:modVersion
|
||||
|
||||
describe 'Item', ->
|
||||
|
||||
it 'allows multiple recipes', ->
|
||||
recipes = "item: Charlie;
|
||||
recipe:; input:Alpha; pattern:... .0. ...;
|
||||
recipe:; input:Bravo; pattern:... 0.0 ...;"
|
||||
modVersion = parser.parse recipes
|
||||
recipes = modVersion.findRecipes ItemSlug.slugify 'test__charlie'
|
||||
recipes[0].input[0].itemSlug.qualified.should.equal 'alpha'
|
||||
recipes[1].input[0].itemSlug.qualified.should.equal 'bravo'
|
||||
|
||||
describe 'name', ->
|
||||
|
||||
it 'adds the name when present', ->
|
||||
modVersion = parser.parse 'item: Charlie'
|
||||
modVersion._items.charlie.name.should.equal 'Charlie'
|
||||
|
||||
it 'requires a non-empty name', ->
|
||||
func = -> parser.parse 'item: \n'
|
||||
expect(func).to.throw Error, 'cannot be empty'
|
||||
|
||||
describe 'gatherable', ->
|
||||
|
||||
it 'adds "gatherable" when present', ->
|
||||
modVersion = parser.parse 'item: Alpha Bravo; gatherable: yes'
|
||||
modVersion._items.alpha_bravo.isGatherable.should.be.true
|
||||
|
||||
it 'does not allow a duplicate "gatherable" declaration', ->
|
||||
func = -> parser.parse 'item: Alpha Bravo; gatherable: yes; gatherable: yes'
|
||||
expect(func).to.throw Error, 'duplicate'
|
||||
|
||||
it 'requires "gatherable" to be "yes" or "no"', ->
|
||||
func = -> parser.parse 'item: Alpha Bravo; gatherable: true'
|
||||
expect(func).to.throw Error, 'gatherable must be'
|
||||
|
||||
it 'does not allow "gatherable" before "item"', ->
|
||||
func = -> parser.parse 'gatherable: yes; item: Alpha Bravo; gatherable: yes'
|
||||
expect(func).to.throw Error, '"gatherable" before "item"'
|
||||
|
||||
describe 'Multiblock', ->
|
||||
|
||||
it 'adds a "multiblock" when present', ->
|
||||
modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo; layer: 0'
|
||||
item = modVersion.findItemByName 'Alpha'
|
||||
item.multiblock.height.should.equal 1
|
||||
|
||||
it 'requires "item" be declared before "multiblock"', ->
|
||||
func = -> parser.parse "multiblock:"
|
||||
expect(func).to.throw Error, '"multiblock" before "item"'
|
||||
|
||||
it 'prohibits multiple "multiblock" commands per item"', ->
|
||||
func = -> parser.parse "item: Alpha; multiblock:; multiblock:"
|
||||
expect(func).to.throw Error, 'duplicate'
|
||||
|
||||
it 'prohibits multiblocks with no inputs', ->
|
||||
func = -> parser.parse "item: Alpha; multiblock:; layer: 000"
|
||||
expect(func).to.throw Error, 'at least one "input"'
|
||||
|
||||
describe 'layer', ->
|
||||
|
||||
it 'allows multiple "layer" commands', ->
|
||||
modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo, Charlie; layer: 01 10; layer: 10 01'
|
||||
item = modVersion.findItemByName 'Alpha'
|
||||
item.multiblock.depth.should.equal 2
|
||||
item.multiblock.height.should.equal 2
|
||||
item.multiblock.width.should.equal 2
|
||||
|
||||
it 'prohibits empty layers', ->
|
||||
func = -> parser.parse 'item: Alpha; multiblock:; input: Bravo; layer:; layer: 00 00'
|
||||
expect(func).to.throw Error, 'empty layer'
|
||||
|
||||
it 'prohibits multiblocks with no layers', ->
|
||||
func = -> parser.parse "item: Alpha; multiblock:; input: Bravo"
|
||||
expect(func).to.throw Error, 'at least one "layer"'
|
||||
|
||||
describe 'Recipe', ->
|
||||
|
||||
beforeEach -> baseText = 'item: Charlie; '
|
||||
|
||||
describe 'input', ->
|
||||
|
||||
it 'adds "input" when present', ->
|
||||
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: ... 010 ...'
|
||||
charlieSlug = ItemSlug.slugify('test__charlie')
|
||||
slugs = (s.itemSlug.item for s in modVersion.findRecipes(charlieSlug)[0].input)
|
||||
slugs.should.eql ['alpha', 'bravo']
|
||||
|
||||
it 'requires an "input" declaration', ->
|
||||
func = -> parser.parse baseText + 'recipe:; pattern: ... .0. ...'
|
||||
expect(func).to.throw Error, 'the "input" declaration is required'
|
||||
|
||||
it 'does not allow a duplicate "input" declaration', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:....0....; input:Bravo'
|
||||
expect(func).to.throw Error, 'duplicate declaration of "input"'
|
||||
|
||||
it 'does not allow "input" before "recipe"', ->
|
||||
func = -> parser.parse baseText + 'input:Alpha, Bravo; recipe:; pattern:....0....'
|
||||
expect(func).to.throw Error, 'cannot declare "input" before "recipe"'
|
||||
|
||||
it 'registers slugs for each input name', ->
|
||||
modVersion = parser.parse baseText + 'recipe:; input:Delta, Echo, Foxtrot; pattern:...012...'
|
||||
(s.item for s in modVersion._slugs).should.eql ['charlie', 'delta', 'echo', 'foxtrot']
|
||||
|
||||
it 'correctly handles recipes which use the same input multiple times', ->
|
||||
modVersion = parser.parse baseText + 'recipe:; input:Alpha; pattern:.0..0....'
|
||||
recipe = _.values(modVersion._recipes)[0]
|
||||
recipe.getQuantityRequired(ItemSlug.slugify('alpha')).should.equal 2
|
||||
|
||||
it 'allows a quantity for each input', ->
|
||||
modVersion = parser.parse baseText + 'recipe:; input: 12 Delta, 3 Echo; pattern:... 0.1 ...'
|
||||
recipe = _.values(modVersion._recipes)[0]
|
||||
recipe.input[0].quantity.should.equal 12
|
||||
recipe.input[0].itemSlug.qualified.should.equal 'delta'
|
||||
recipe.input[1].quantity.should.equal 3
|
||||
recipe.input[1].itemSlug.qualified.should.equal 'echo'
|
||||
|
||||
describe 'pattern', ->
|
||||
|
||||
it 'adds "pattern" when present', ->
|
||||
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:... .0. .1.'
|
||||
modVersion.findRecipes(ItemSlug.slugify('test__charlie'))[0].pattern.should.equal '... .0. .1.'
|
||||
|
||||
it 'requires a "pattern" declaration', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo'
|
||||
expect(func).to.throw Error, 'the "pattern" declaration is required'
|
||||
|
||||
it 'does not allow a duplicate "pattern" declaration', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:....0..1.; pattern:01.......'
|
||||
expect(func).to.throw Error, 'duplicate declaration of "pattern"'
|
||||
|
||||
it 'requires pattern to be the right length', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:000'
|
||||
expect(func).to.throw Error, 'a pattern must have'
|
||||
|
||||
it 'requires pattern to only use proper characters', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:abc def ghi'
|
||||
expect(func).to.throw Error, 'a pattern must have'
|
||||
|
||||
it 'requires pattern to only refer to existing items', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:... 010 ...'
|
||||
expect(func).to.throw Error, 'there is no item 1 in this recipe'
|
||||
|
||||
it 'requires all items to appear in the pattern', ->
|
||||
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: 000 0.0 000'
|
||||
expect(func).to.throw Error, 'Bravo is listed'
|
||||
|
||||
it 'does not allow "pattern" before "recipe"', ->
|
||||
func = -> parser.parse baseText + 'pattern:... .0. ...; recipe:; inputs:Alpha'
|
||||
expect(func).to.throw Error, 'cannot declare "pattern" before "recipe"'
|
||||
|
||||
describe 'quantity', ->
|
||||
|
||||
beforeEach ->
|
||||
baseText = 'item: Charlie; recipe:; input:Alpha; pattern:...0.0...; '
|
||||
|
||||
it 'adds "quantity" when present', ->
|
||||
modVersion = parser.parse baseText + 'quantity: 2'
|
||||
modVersion.findRecipes(ItemSlug.slugify('test__charlie'))[0].output[0].quantity.should.equal 2
|
||||
|
||||
it 'does not allow a duplicate "quantity" declaration', ->
|
||||
func = -> parser.parse baseText + 'quantity:1; quantity:2'
|
||||
expect(func).to.throw Error, 'duplicate declaration of "quantity"'
|
||||
|
||||
it 'requires quantity to be an integer', ->
|
||||
func = -> parser.parse baseText + 'quantity:ten'
|
||||
expect(func).to.throw Error, 'quantity must be an integer'
|
||||
|
||||
it 'assumes a quantity of 1 by default', ->
|
||||
modVersion = parser.parse baseText
|
||||
modVersion.findRecipes(ItemSlug.slugify('test__charlie'))[0].output[0].quantity.should.equal 1
|
||||
|
||||
it 'does not allow "quantity" before recipe', ->
|
||||
func = -> parser.parse 'item:Bravo; quantity:12; recipe:;'
|
||||
expect(func).to.throw Error, 'cannot declare "quantity" before "recipe"'
|
||||
|
||||
describe 'onlyIf', ->
|
||||
|
||||
beforeEach ->
|
||||
baseText = 'item: Charlie; recipe:; input:Alpha; pattern:...0.0...; '
|
||||
|
||||
it 'understands the "item" verb', ->
|
||||
modVersion = parser.parse baseText + 'onlyIf: item Iron Ingot'
|
||||
recipes = []
|
||||
modVersion.eachRecipe (recipe)-> recipes.push recipe
|
||||
recipe = recipes[0]
|
||||
recipe.condition.should.eql verb:'item', noun:'Iron Ingot', inverted:false
|
||||
|
||||
it 'understands the "mod" verb', ->
|
||||
modVersion = parser.parse baseText + 'onlyIf: mod BuildCraft'
|
||||
recipes = []
|
||||
modVersion.eachRecipe (recipe)-> recipes.push recipe
|
||||
recipe = recipes[0]
|
||||
recipe.condition.should.eql verb:'mod', noun:'BuildCraft', inverted:false
|
||||
|
||||
it 'understands inverting verbs', ->
|
||||
modVersion = parser.parse baseText + 'onlyIf: not item Iron Ingot'
|
||||
recipes = []
|
||||
modVersion.eachRecipe (recipe)-> recipes.push recipe
|
||||
recipe = recipes[0]
|
||||
recipe.condition.should.eql verb:'item', noun:'Iron Ingot', inverted:true
|
||||
|
||||
it 'requires at least two words', ->
|
||||
func = -> parser.parse baseText + 'onlyIf: item'
|
||||
expect(func).to.throw Error, 'verb followed by a noun'
|
||||
|
||||
it 'only allows known verbs', ->
|
||||
func = -> parser.parse baseText + 'onlyIf: foo Iron Ingot'
|
||||
expect(func).to.throw Error, 'unknown verb'
|
||||
|
||||
describe 'output', ->
|
||||
|
||||
beforeEach ->
|
||||
baseText = 'item: Delta; item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
|
||||
|
||||
it 'adds a single item as the default output', ->
|
||||
modVersion = parser.parse baseText
|
||||
stack = modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].output[0]
|
||||
stack.itemSlug.qualified.should.equal 'test__bravo'
|
||||
stack.quantity.should.equal 1
|
||||
|
||||
it 'can add multiple extras with quantities', ->
|
||||
modVersion = parser.parse baseText + 'extras:2 Delta, 4 Echo'
|
||||
output = modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].output
|
||||
output[0].itemSlug.qualified.should.equal 'test__bravo'
|
||||
output[0].quantity.should.equal 1
|
||||
output[1].itemSlug.qualified.should.equal 'test__delta'
|
||||
output[1].quantity.should.equal 2
|
||||
output[2].itemSlug.qualified.should.equal 'echo'
|
||||
output[2].quantity.should.equal 4
|
||||
|
||||
it 'does not allow "extras" before "recipe"', ->
|
||||
func = -> parser.parse 'item:Bravo; extras:Charlie'
|
||||
expect(func).to.throw Error, 'cannot declare "extras" before "recipe"'
|
||||
|
||||
it 'registers slugs for each output name', ->
|
||||
modVersion = parser.parse baseText + 'extras:Delta, Echo'
|
||||
(s.qualified for s in modVersion._slugs).should.eql [
|
||||
'test__bravo', 'charlie', 'test__delta', 'echo'
|
||||
]
|
||||
|
||||
it 'does not allow a duplicate "extras" declaration', ->
|
||||
func = -> parser.parse baseText + 'extras:Echo; extras:Delta'
|
||||
expect(func).to.throw Error, 'duplicate declaration of "extras"'
|
||||
|
||||
describe 'tools', ->
|
||||
|
||||
beforeEach ->
|
||||
baseText = 'item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
|
||||
|
||||
it 'can add a single tool', ->
|
||||
modVersion = parser.parse baseText + 'tools: Furnace'
|
||||
modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].tools[0].itemSlug.item.should.equal 'furnace'
|
||||
|
||||
it 'can add multiple tools', ->
|
||||
modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace'
|
||||
tools = modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].tools
|
||||
tools[0].itemSlug.item.should.equal 'crafting_table'
|
||||
tools[1].itemSlug.item.should.equal 'furnace'
|
||||
|
||||
it 'registers slugs for each tool name', ->
|
||||
modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace'
|
||||
(s.item for s in modVersion._slugs).should.eql ['bravo', 'charlie', 'crafting_table', 'furnace']
|
||||
|
||||
it 'does not allow a duplicate "tools" declaration', ->
|
||||
func = -> parser.parse baseText + 'tools:Crafting Table; tools:Furnace'
|
||||
expect(func).to.throw Error, 'duplicate declaration of "tools"'
|
||||
|
||||
describe "unparsing", ->
|
||||
|
||||
beforeEach ->
|
||||
baseText = """
|
||||
schema: 1
|
||||
|
||||
group: Agriculture
|
||||
|
||||
item: Apple
|
||||
|
||||
item: Baked Potato
|
||||
recipe:
|
||||
input: furnace fuel, Potato
|
||||
pattern: .1. ... .0.
|
||||
tools: Furnace
|
||||
|
||||
item: (filled) Canned Food
|
||||
recipe:
|
||||
input: 4 (Empty) Tin Can, Apple
|
||||
pattern: .1. .0. ...
|
||||
|
||||
item: Pyramid
|
||||
multiblock:
|
||||
input: Cobblestone
|
||||
layer: 000 000 000
|
||||
layer: ... .0. ...
|
||||
|
||||
group: Functional Blocks
|
||||
|
||||
item: Furnace
|
||||
recipe:
|
||||
input: Cobblestone
|
||||
pattern: 000 0.0 000
|
||||
tools: Crafting Table
|
||||
|
||||
update: Iron Ingot
|
||||
recipe:
|
||||
input: furnace fuel, Iron Dust
|
||||
pattern: .1. ... .0.
|
||||
tools: Furnace
|
||||
recipe:
|
||||
onlyIf: item Redstone Furnace
|
||||
input: Iron Ore
|
||||
pattern: ... .0. ...
|
||||
tools: Redstone Furnace
|
||||
"""
|
||||
|
||||
it 'can round-trip a data file', ->
|
||||
text = parser.unparse parser.parse baseText
|
||||
actual = CommandParserVersionBase.simplify text
|
||||
expected = CommandParserVersionBase.simplify baseText
|
||||
|
||||
actual.should.equal expected
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Crafting Guide - tutorial_parser.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
VersionedParserBase = require './versioned_parser_base'
|
||||
TutorialParserV1 = require './tutorial_parser_v1'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class TutorialParser extends VersionedParserBase
|
||||
|
||||
# VersionedParserBase Overrides ################################################################
|
||||
|
||||
_createParsers: (options)->
|
||||
return result =
|
||||
'1': new TutorialParserV1 options
|
||||
@@ -0,0 +1,65 @@
|
||||
#
|
||||
# Crafting Guide - tutorial_parser_v1.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CommandParserVersionBase = require './command_parser_version_base'
|
||||
Tutorial = require '../site/tutorial'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class TutorialParserV1 extends CommandParserVersionBase
|
||||
|
||||
# CommandParserVersionBase Overrides ###########################################################
|
||||
|
||||
_buildModel: (rawData, model)->
|
||||
@_buildTutorial rawData, model
|
||||
|
||||
_unparseModel: (builder, model)->
|
||||
@_unparseTutorial builder, model
|
||||
|
||||
# Command Methods ##############################################################################
|
||||
|
||||
_command_content: (contentParts...)->
|
||||
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
|
||||
if @_rawData.currentSection.content? then throw new Error 'duplicate declaration of content'
|
||||
content = contentParts.join(', ').trim()
|
||||
if not content.length > 0 then throw new Error 'content cannot be empty'
|
||||
|
||||
@_rawData.currentSection.content = content
|
||||
|
||||
_command_officialUrl: (officialUrl)->
|
||||
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
|
||||
if officialUrl.length is 0 then throw new Error 'officialUrl cannot be empty'
|
||||
@_rawData.officialUrl = officialUrl
|
||||
|
||||
_command_section: (textParts...)->
|
||||
@_rawData.sections ?= []
|
||||
@_rawData.sections.push @_rawData.currentSection = {}
|
||||
|
||||
_command_title: (titleParts...)->
|
||||
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
|
||||
if @_rawData.currentSection.title? then throw new Error 'duplicate declaration of title'
|
||||
title = titleParts.join(', ').trim()
|
||||
if not title.length > 0 then throw new Error 'title cannot be empty'
|
||||
|
||||
@_rawData.currentSection.title = title
|
||||
|
||||
_command_video: (youTubeId, nameParts...)->
|
||||
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
|
||||
name = nameParts.join ', '
|
||||
if not name?.length then throw new Error 'video declaration requires a name'
|
||||
|
||||
@_rawData.videos ?= []
|
||||
@_rawData.videos.push youTubeId:youTubeId, name:name
|
||||
|
||||
# Object Building Methods ######################################################################
|
||||
|
||||
_buildTutorial: (rawData, model)->
|
||||
if not rawData.sections? then throw new Error 'the "section" declaration is required'
|
||||
|
||||
model.officialUrl = rawData.officialUrl
|
||||
model.videos = rawData.videos
|
||||
model.sections = rawData.sections
|
||||
@@ -0,0 +1,51 @@
|
||||
#
|
||||
# Crafting Guide - versioned_parser_base.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class VersionedParserBase
|
||||
|
||||
constructor: (options={})->
|
||||
@_parsers = @_createParsers options
|
||||
@_currentSchema = _.chain(@_parsers).keys().last().value()
|
||||
@errors = []
|
||||
|
||||
# Class Members ################################################################################
|
||||
|
||||
@SCHEMA = /schema: *([0-9]+)/
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
parse: (text)->
|
||||
return unless text?
|
||||
|
||||
schema = @_identifySchema text
|
||||
parser = @_parsers[schema]
|
||||
if not parser? then throw new Error "schema version #{schema} is not supported"
|
||||
|
||||
parser.parse text
|
||||
@errors = parser.errors
|
||||
|
||||
return @_model
|
||||
|
||||
unparse: (schema=null)->
|
||||
schema ?= @_currentSchema
|
||||
|
||||
parser = @_parsers["#{schema}"]
|
||||
if not parser? then throw new Error "version #{schema} is not supported"
|
||||
|
||||
return parser.unparse()
|
||||
|
||||
# Overridable Methods ##########################################################################
|
||||
|
||||
_createParsers: (options)->
|
||||
throw new Error 'subclasses must override this method'
|
||||
|
||||
_identifySchema: (text)->
|
||||
match = VersionedParserBase.SCHEMA.exec text
|
||||
if not match? then throw new Error 'missing "schema" declaration'
|
||||
return match[1]
|
||||
@@ -1,94 +1,47 @@
|
||||
#
|
||||
# Crafting Guide - craft_page.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{Inventory} = require("crafting-guide-common").models
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
{PlanBuilder} = require("crafting-guide-common").crafting
|
||||
{ResourcesEvaluator} = require("crafting-guide-common").crafting
|
||||
{StepsEvaluator} = require("crafting-guide-common").crafting
|
||||
BaseModel = require '../base_model'
|
||||
Craftsman = require '../crafting/craftsman'
|
||||
Inventory = require '../game/inventory'
|
||||
ModPack = require '../game/mod_pack'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftPage extends Observable
|
||||
module.exports = class CraftPage extends BaseModel
|
||||
|
||||
constructor: (attributes={})->
|
||||
super
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.modPack then throw new Error 'attributes.modPack is required'
|
||||
attributes.params ?= null
|
||||
attributes.craftsman ?= new Craftsman attributes.modPack
|
||||
super attributes, options
|
||||
|
||||
@muted -> @modPack = attributes.modPack
|
||||
|
||||
@_currentPlan = null
|
||||
@_have = new Inventory
|
||||
@_planBuilder = new PlanBuilder new StepsEvaluator
|
||||
@_want = new Inventory
|
||||
|
||||
@_consumeParams attributes.params
|
||||
|
||||
@_have.on Observable::ANY, this, "_onHaveChanged"
|
||||
@_want.on Observable::ANY, this, "_onWantChanged"
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
currentPlan:
|
||||
get: -> return @_currentPlan
|
||||
set: -> throw new Error "currentPlan cannot be assigned"
|
||||
|
||||
have:
|
||||
get: -> return @_have
|
||||
set: -> throw new "have cannot be assigned"
|
||||
|
||||
isDirty:
|
||||
get: -> return @_isDirty
|
||||
set: -> throw new Error "isDirty cannot be assigned"
|
||||
|
||||
isOutdated:
|
||||
get: -> return @currentPlan? and @isDirty
|
||||
set: -> throw new Error "isOutdated cannot be assigned"
|
||||
|
||||
modPack:
|
||||
get: -> return @_modPack
|
||||
set: (modPack)->
|
||||
if @_modPack? then throw new Error "modPack cannot be reassigned"
|
||||
if not modPack? then throw new Error "modPack is required"
|
||||
@_modPack = modPack
|
||||
|
||||
planBuilder:
|
||||
get: -> return @_planBuilder
|
||||
set: -> throw new Error "planBuilder cannot be assigned"
|
||||
|
||||
want:
|
||||
get: -> return @_want
|
||||
set: -> throw new Error "want cannot be assigned"
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
createPlan: ->
|
||||
newPlan = @_planBuilder.createPlan @_want, @_have
|
||||
@triggerPropertyChange "currentPlan", @_currentPlan, newPlan, ->
|
||||
@_currentPlan = newPlan
|
||||
@triggerPropertyChange "isDirty", @_isDirty, false
|
||||
@modPack.on c.event.change, => @_consumeParams()
|
||||
@on c.event.change + ':params', => @_consumeParams()
|
||||
@craftsman.on c.event.change + ':stage', => @trigger c.event.change, this
|
||||
@craftsman.on c.event.change + ':complete', => @trigger c.event.change, this
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_consumeParams: (params)->
|
||||
return unless params?.inventoryText?
|
||||
_consumeParams: ->
|
||||
return unless @params?
|
||||
|
||||
newWant = Inventory.fromUrlString params.inventoryText, @modPack
|
||||
@_want.clear()
|
||||
@craftsman.want.clear()
|
||||
if not @params.inventoryText?
|
||||
@params = null
|
||||
else
|
||||
inventory = new Inventory
|
||||
inventory.parse @params.inventoryText
|
||||
|
||||
for itemId, stack of newWant.stacks
|
||||
continue unless stack.item.isCraftable
|
||||
@_want.add stack.item, stack.quantity
|
||||
inventory.each (stack)=>
|
||||
item = @modPack.findItem stack.itemSlug, enableAsNeeded:true
|
||||
return unless item? and item.isCraftable
|
||||
|
||||
_onHaveChanged: ->
|
||||
@triggerPropertyChange "isDirty", @_isDirty, true
|
||||
@craftsman.want.add stack.itemSlug, stack.quantity
|
||||
inventory.remove stack.itemSlug
|
||||
|
||||
_onWantChanged: ->
|
||||
@triggerPropertyChange "isDirty", @_isDirty, true, ->
|
||||
@_isDirty = true
|
||||
if @want.isEmpty then @triggerPropertyChange "currentPlan", @_currentPlan, null
|
||||
if inventory.isEmpty then @params = null
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#
|
||||
# Crafting Guide - editable_file.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{BaseModel} = require("crafting-guide-common").deprecated
|
||||
w = require "when"
|
||||
BaseModel = require '../base_model'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - email_client.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#
|
||||
# Crafting Guide - file_cache.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
w = require "when"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class FileCache extends Backbone.Events
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
#
|
||||
# Crafting Guide - file_cache.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
FileCache = require './file_cache'
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#
|
||||
# Crafting Guide - github_user.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{BaseModel} = require("crafting-guide-common").deprecated
|
||||
BaseModel = require '../base_model'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - item_display.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
_ = require "../../../common/underscore"
|
||||
c = require "../../../common/constants"
|
||||
{Inventory} = require("crafting-guide-common").models
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemDisplay
|
||||
|
||||
constructor: (item)->
|
||||
@item = item
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
craftingUrl:
|
||||
get: ->
|
||||
inventory = new Inventory
|
||||
inventory.add @item
|
||||
c.url.crafting inventoryText:inventory.toUrlString()
|
||||
|
||||
iconUrl:
|
||||
get: -> return c.url.itemIcon this
|
||||
set: -> throw new Error "iconUrl cannot be assigned"
|
||||
|
||||
item:
|
||||
get: -> return @_item
|
||||
set: (item)->
|
||||
if @_item? then throw new Error "item cannot be re-assigned"
|
||||
if not item? then throw new Error "item is required"
|
||||
@_item = item
|
||||
|
||||
itemSlug:
|
||||
get: -> return _.slugify @name
|
||||
set: -> throw new Error "slug cannot be assigned"
|
||||
|
||||
mod:
|
||||
get: -> return @_item.mod
|
||||
set: -> throw new Error "mod cannot be assigned"
|
||||
|
||||
modId:
|
||||
get: -> return @_item.mod.id
|
||||
set: -> throw new Error "modId cannot be assigned"
|
||||
|
||||
modUrl:
|
||||
get: -> return c.url.mod modId:@item.mod.id
|
||||
set: -> throw new Error "modUrl cannot be assigned"
|
||||
|
||||
name:
|
||||
get: -> return @item.displayName
|
||||
set: -> throw new Error "name cannot be assigned"
|
||||
|
||||
url:
|
||||
get: -> return c.url.item this
|
||||
set: -> throw new Error "url cannot be assigned"
|
||||
@@ -1,95 +1,57 @@
|
||||
#
|
||||
# Crafting Guide - item_page.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{Item} = require("crafting-guide-common").models
|
||||
ItemDisplay = require "./item_display"
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
BaseModel = require '../base_model'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemPage extends Observable
|
||||
module.exports = class ItemPage extends BaseModel
|
||||
|
||||
constructor: (item)->
|
||||
if item.constructor isnt Item then throw new Error "item must be an Item instance"
|
||||
@item = item
|
||||
super
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
item:
|
||||
get: -> return @_item
|
||||
set: (item)->
|
||||
if @_item? then throw new Error "item cannot be reassigned"
|
||||
if not item? then throw new Error "item is required"
|
||||
if @_item? then @_item.off Observable::ANY, this
|
||||
@_item = item
|
||||
@_item.on Observable::ANY, this, "_onItemChanged"
|
||||
|
||||
itemDisplay:
|
||||
get: -> return @_itemDisplay ?= new ItemDisplay @item
|
||||
|
||||
mod:
|
||||
get: -> return @_item.mod
|
||||
set: -> throw new Error "mod cannot be assigned"
|
||||
|
||||
modPack:
|
||||
get: -> return @mod.modPack
|
||||
set: -> throw new Error "modPack cannot be assigned"
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.modPack? then throw new Error 'attributes.modPack is required'
|
||||
attributes.item ?= null
|
||||
super attributes, options
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
findComponentInItems: ->
|
||||
return @_findItemsWithMatchingRecipes (recipe)=>
|
||||
recipe.computeQuantityRequired @item
|
||||
|
||||
findRecipes: ->
|
||||
return (recipe for recipeId, recipe of @item.recipes)
|
||||
return @_findRecipesMatching (recipe)=> recipe.requires @item.slug
|
||||
|
||||
findSimilarItems: ->
|
||||
group = @mod.itemGroups[@item.groupName]
|
||||
return unless group?
|
||||
return null unless @item?.modVersion?
|
||||
|
||||
return null unless group.length > 0
|
||||
return group
|
||||
result = []
|
||||
@item.modVersion.eachItemInGroup @item.group, (item)=>
|
||||
result.push item
|
||||
|
||||
findToolForItems: ->
|
||||
return @_findItemsWithMatchingRecipes (recipe)=>
|
||||
return recipe.tools[@item.id]?
|
||||
return null unless result.length > 0
|
||||
return result
|
||||
|
||||
loadDetails: ->
|
||||
stores.itemDetail.loadDetailFor @item
|
||||
.catch (error)=>
|
||||
if error.message.indexOf("404") is -1
|
||||
logger.error "Could not load details for item #{@item.id}: #{error.message}"
|
||||
else
|
||||
logger.info "Item #{@item.id} has no details file."
|
||||
findRecipes: ->
|
||||
return @modPack.findRecipes @item?.slug, [], alwaysFromOwningMod:true
|
||||
|
||||
findToolForRecipes: ->
|
||||
return @_findRecipesMatching (recipe)=> recipe.requiresTool @item.slug
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_findItemsWithMatchingRecipes: (isMatching)->
|
||||
_findRecipesMatching: (isAcceptable)->
|
||||
return null unless @item?
|
||||
|
||||
result = {}
|
||||
@modPack.eachMod (mod)=>
|
||||
mod.eachRecipe (recipe)=>
|
||||
if isAcceptable recipe
|
||||
for outputStack in recipe.output
|
||||
continue if recipe.isPassThroughFor outputStack.itemSlug
|
||||
outputItem = @modPack.findItem outputStack.itemSlug, includeDisabled:true
|
||||
result[outputItem.slug] = outputItem
|
||||
|
||||
for modId, mod of @modPack.mods
|
||||
for itemId, item of mod.items
|
||||
for recipeId, recipe of item.recipes
|
||||
if isMatching(recipe)
|
||||
result[item.id] = item
|
||||
|
||||
result = (item for itemId, item of result)
|
||||
result.sort (a, b)->
|
||||
if not a?.displayName? and not b?.displayName? then return 0
|
||||
if not b?.displayName? then return +1
|
||||
if not a?.displayName? then return +1
|
||||
return a.displayName.localeCompare b.displayName
|
||||
|
||||
return result
|
||||
|
||||
_onItemChanged: ->
|
||||
@trigger Observable::CHANGE
|
||||
result = _.values result
|
||||
return null unless result.length > 0
|
||||
|
||||
return result.sort (a, b)-> a.compareTo b
|
||||
|
||||
@@ -1,104 +1,89 @@
|
||||
#
|
||||
# Crafting Guide - item_selector.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
BaseModel = require '../base_model'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemSelector extends Observable
|
||||
module.exports = class ItemSelector extends BaseModel
|
||||
|
||||
constructor: (modPack, options={})->
|
||||
super
|
||||
constructor: (attributes={}, options={})->
|
||||
if not options.modPack? then throw new Error 'options.modPack is required'
|
||||
options.isAcceptable ?= (item)-> return true # accept everything by default
|
||||
super attributes, options
|
||||
|
||||
@_isAcceptable = options.isAcceptable or (item)-> return true # accept everything by default
|
||||
@_isAcceptable = options.isAcceptable
|
||||
@_maxResults = 100
|
||||
@_minHintLength = 3
|
||||
@_modPack = options.modPack
|
||||
@_results = []
|
||||
|
||||
@modPack = modPack
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
hint:
|
||||
get: -> return @_hint
|
||||
set: (hint)->
|
||||
@triggerPropertyChange "hint", @_hint, hint, ->
|
||||
@_hint = hint?.toLowerCase()
|
||||
@_refreshResults()
|
||||
get: ->
|
||||
return @_hint
|
||||
|
||||
modPack:
|
||||
get: -> return @_modPack
|
||||
set: (modPack)->
|
||||
if not modPack? then throw new Error "modPack is required"
|
||||
if @_modPack? then throw new Error "modPack cannot be reassigned"
|
||||
@_modPack = modPack
|
||||
set: (newHint)->
|
||||
oldHint = @_hint
|
||||
return if newHint is oldHint
|
||||
|
||||
@_hint = newHint.toLowerCase()
|
||||
|
||||
@trigger c.event.change + ':hint', this, oldHint, newHint
|
||||
@_refreshResults()
|
||||
@trigger c.event.change, this
|
||||
|
||||
results:
|
||||
get: -> return @_results
|
||||
get: ->
|
||||
return @_results
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_computeScore: (item)->
|
||||
_isMatch: (name, itemSlug)->
|
||||
hintIndex = 0
|
||||
hintLetter = @_hint[hintIndex]
|
||||
name = item.displayName.toLowerCase()
|
||||
nextScore = 1
|
||||
totalScore = 0
|
||||
name = name.toLowerCase()
|
||||
|
||||
for nameIndex in [0...name.length] by 1
|
||||
if name.charAt(nameIndex) is hintLetter
|
||||
totalScore += nextScore
|
||||
nextScore += 1
|
||||
hintIndex += 1
|
||||
if hintIndex is @_hint.length
|
||||
return 0 unless @_isAcceptable item
|
||||
break
|
||||
|
||||
item = @_modPack.findItem itemSlug
|
||||
return false unless item?
|
||||
return @_isAcceptable item
|
||||
hintLetter = @_hint[hintIndex]
|
||||
else
|
||||
nextScore = 1
|
||||
|
||||
return 0 if hintIndex < @_hint.length
|
||||
return totalScore
|
||||
return false
|
||||
|
||||
_refreshResults: ->
|
||||
scoredItems = []
|
||||
oldResults = @_results
|
||||
newResults = {}
|
||||
count = 0
|
||||
|
||||
logger.verbose => "Looking for items which match: #{@_hint}"
|
||||
if @_hint.length >= @_minHintLength
|
||||
for modId, mod of @_modPack.mods
|
||||
@_modPack.eachMod (mod)=>
|
||||
return if count >= @_maxResults
|
||||
return unless mod.isEnabled
|
||||
return unless mod.enabled
|
||||
|
||||
for itemId, item of mod.items
|
||||
score = @_computeScore item
|
||||
if score > 0
|
||||
scoredItems.push score:score, item:item
|
||||
mod.eachName (name, itemSlug)=>
|
||||
return if count >= @_maxResults
|
||||
|
||||
scoredItems.sort (a, b)->
|
||||
if a.score isnt b.score
|
||||
return if a.score > b.score then -1 else +1
|
||||
if @_isMatch name, itemSlug
|
||||
if itemSlug.isQualified
|
||||
newResults[itemSlug] = itemSlug
|
||||
count += 1
|
||||
|
||||
nameA = a.item.displayName
|
||||
nameB = b.item.displayName
|
||||
if nameA.length isnt nameB.length
|
||||
return if nameA.length < nameB.length then -1 else +1
|
||||
|
||||
if nameA isnt nameB
|
||||
return if nameA < nameB then -1 else +1
|
||||
|
||||
return 0
|
||||
|
||||
@_results = []
|
||||
for element in scoredItems
|
||||
@_results.push element.item
|
||||
break if @_results.length >= @_maxResults
|
||||
|
||||
@trigger c.event.change + ':results', this
|
||||
newResults = _.values(newResults)
|
||||
newResults.sort ItemSlug.compare
|
||||
|
||||
@_results = newResults
|
||||
@trigger c.event.change + ':results', this, oldResults, newResults
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - markdown_image.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
#
|
||||
# Crafting Guide - markdown_image_list.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
BaseModel = require '../base_model'
|
||||
MarkdownImage = require './markdown_image'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class MarkdownImageList extends Observable
|
||||
module.exports = class MarkdownImageList extends BaseModel
|
||||
|
||||
constructor: (options={})->
|
||||
super
|
||||
constructor: (attributes={}, options={})->
|
||||
attributes.imageBase ?= ''
|
||||
attributes.markdownText ?= null
|
||||
super attributes, options
|
||||
|
||||
@_images = []
|
||||
@muted =>
|
||||
@client = options.client
|
||||
@imageBase = options.imageBase
|
||||
@markdownText = options.markdownText
|
||||
@client = options.client
|
||||
@_images = {}
|
||||
|
||||
@on c.event.change + ':imageBase', =>
|
||||
for fileName, image of @_images
|
||||
image.path = @imageBase
|
||||
|
||||
@on c.event.change + ':markdownText', => @_analyzeMarkdownText()
|
||||
@_analyzeMarkdownText()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
@@ -42,43 +46,22 @@ module.exports = class MarkdownImageList extends Observable
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
getAll: ->
|
||||
fileNames = (fileName for fileName, image of @_images).sort()
|
||||
result = []
|
||||
for fileName in fileNames
|
||||
result.push @_images[fileName]
|
||||
|
||||
return result
|
||||
|
||||
isValid: ->
|
||||
for fileName, image of @_images
|
||||
return false unless image.valid
|
||||
return true
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
all:
|
||||
get: ->
|
||||
fileNames = (fileName for fileName, image of @_images).sort()
|
||||
result = []
|
||||
for fileName in fileNames
|
||||
result.push @_images[fileName]
|
||||
|
||||
return result
|
||||
|
||||
client:
|
||||
get: -> return @_client
|
||||
set: (client)->
|
||||
if not client? then throw new Error "client is required"
|
||||
@_client = client
|
||||
|
||||
imageBase: ->
|
||||
get: -> return @_imageBase
|
||||
set: (imageBase)->
|
||||
@triggerPropertyChange "imageBase", @_imageBase, imageBase, ->
|
||||
@_imageBase = imageBase
|
||||
for fileName, image of @_images
|
||||
image.path = @imageBase
|
||||
|
||||
markdownText:
|
||||
get: -> return @_markdownText
|
||||
set: (markdownText)->
|
||||
@triggerPropertyChange "markdownText", @_markdownText, markdownText, ->
|
||||
@_markdownText = markdownText
|
||||
@_analyzeMarkdownText()
|
||||
|
||||
valid:
|
||||
get: ->
|
||||
for fileName, image of @_images
|
||||
return false unless image.valid
|
||||
return true
|
||||
all: {get:@prototype.getAll}
|
||||
valid: {get:@prototype.isValid}
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
@@ -96,8 +79,8 @@ module.exports = class MarkdownImageList extends Observable
|
||||
image = @_images[fileName]
|
||||
if not image?
|
||||
image = new MarkdownImage {fileName:fileName, path:@imageBase}, {client:@client}
|
||||
image.on Observable::CHANGE, this, "_onImageChanged"
|
||||
@trigger Observable::ADD, image
|
||||
@listenTo image, c.event.change, => @trigger c.event.change, this
|
||||
@trigger c.event.add, this, image
|
||||
changed = true
|
||||
|
||||
newImages[fileName] = image
|
||||
@@ -109,7 +92,4 @@ module.exports = class MarkdownImageList extends Observable
|
||||
|
||||
if changed
|
||||
@_images = newImages
|
||||
@trigger Observable::CHANGE
|
||||
|
||||
_onImageChanged: ->
|
||||
@trigger Observable::CHANGE
|
||||
@trigger c.event.change, this
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - markdown_image_list.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
@@ -16,7 +16,7 @@ list = null
|
||||
describe 'markdown_image_list.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
list = new MarkdownImageList client:{fetchFile:sinon.stub().returns(w.promise(->))}
|
||||
list = new MarkdownImageList {}, client:{fetchFile:sinon.stub().returns(w.promise(->))}
|
||||
|
||||
describe '_analyzeMarkdownText', ->
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - multiblock_display.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{Inventory} = require("crafting-guide-common").models
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class MultiblockDisplay extends Observable
|
||||
|
||||
constructor: (recipe)->
|
||||
@_layerInventories = []
|
||||
@recipe = recipe
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
recipe:
|
||||
get: -> return @_recipe
|
||||
set: (recipe)->
|
||||
if not recipe? then throw new Error "recipe is required"
|
||||
if @_recipe? then throw new Error "recipe cannot be reassigned"
|
||||
@_recipe = recipe
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
getInventory: (z)->
|
||||
if not z?
|
||||
return @_fullInventory ?= @_createFullInventory()
|
||||
else
|
||||
return @_layerInventories[z] ?= @_addLayerInventory z
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_addLayerInventory: (z, inventory=null)->
|
||||
inventory ?= new Inventory
|
||||
for x in [0..@recipe.width]
|
||||
for y in [0..@recipe.height]
|
||||
stack = @recipe.getInputAt x, y, z
|
||||
continue unless stack?
|
||||
inventory.add stack.item, stack.quantity
|
||||
return inventory
|
||||
|
||||
_createFullInventory: (inventory=null)->
|
||||
inventory ?= new Inventory()
|
||||
for z in [0..@recipe.depth]
|
||||
@_addLayerInventory z, inventory
|
||||
return inventory
|
||||
@@ -1,134 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - recipe_display.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
_ = require "../../../common/underscore"
|
||||
c = require "../../../common/constants"
|
||||
{Inventory} = require("crafting-guide-common").models
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class RecipeDisplay extends Observable
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.recipe? then throw new Error "options.recipe is required"
|
||||
super
|
||||
|
||||
@_layerInventories = []
|
||||
|
||||
@multiplier = options.multiplier or 1
|
||||
@recipe = options.recipe
|
||||
@showInputInventory = if options.showInputInventory then options.showInputInventory or false
|
||||
@showOutputInventory = if options.showOutputInventory then options.showOutputInventory or false
|
||||
@showOutputSlot = if options.showOutputSlot then options.showOutputSlot or false
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@::RECIPE_TYPE =
|
||||
CRAFTING_TABLE: "crafting_table"
|
||||
MULTIBLOCK: "multiblock"
|
||||
|
||||
@::ALL_RECIPE_TYPES = (value for key, value of @::RECIPE_TYPE)
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
depth:
|
||||
get: -> return @recipe.depth
|
||||
set: -> throw new Error "depth cannot be assigned"
|
||||
|
||||
height:
|
||||
get: -> return @recipe.height
|
||||
set: -> throw new Error "height cannot be assigned"
|
||||
|
||||
multiplier:
|
||||
get: -> return @_multiplier
|
||||
set: (multiplier)->
|
||||
if not _.isNumber(multiplier) then multiplier = 1
|
||||
@triggerPropertyChange "multiplier", @_multiplier, multiplier
|
||||
|
||||
recipe:
|
||||
get: -> return @_recipe
|
||||
set: (recipe)->
|
||||
return if recipe is @_recipe
|
||||
if not recipe? then throw new Error "recipe is required"
|
||||
if @_recipe? then throw new Error "recipe cannot be reassigned"
|
||||
@_recipe = recipe
|
||||
@_computeType()
|
||||
|
||||
showInputInventory:
|
||||
get: -> return @_showInputInventory
|
||||
set: (showInputInventory)->
|
||||
@triggerPropertyChange "showInputInventory", @_showInputInventory, !!showInputInventory
|
||||
|
||||
showOutputInventory:
|
||||
get: -> return @_showOutputInventory
|
||||
set: (showOutputInventory)->
|
||||
@triggerPropertyChange "showOutputInventory", @_showOutputInventory, !!showOutputInventory
|
||||
|
||||
showOutputSlot:
|
||||
get: -> return @_showOutputSlot
|
||||
set: (showOutputSlot)->
|
||||
@triggerPropertyChange "showOutputSlot", @_showOutputSlot, !!showOutputSlot
|
||||
|
||||
tools:
|
||||
get: -> return @recipe.tools
|
||||
set: -> throw new Error "tools cannot be assigned"
|
||||
|
||||
type:
|
||||
get: -> return @_type
|
||||
set: -> throw new Error "type cannot be assigned"
|
||||
|
||||
width:
|
||||
get: -> return @recipe.width
|
||||
set: -> throw new Error "width cannot be assigned"
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
getInputAt: (x, y, z)->
|
||||
return @recipe.getInputAt x, y, z
|
||||
|
||||
getInventory: (z)->
|
||||
if not z?
|
||||
return @_fullInventory ?= @_createFullInventory()
|
||||
else
|
||||
return @_layerInventories[z] ?= @_addLayerInventory z
|
||||
|
||||
getOutputInventory: ->
|
||||
if not @_outputInventory?
|
||||
@_outputInventory = new Inventory
|
||||
for stack in @recipe.allProducts
|
||||
@_outputInventory.add stack.item, stack.quantity
|
||||
|
||||
return @_outputInventory
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_addLayerInventory: (z, inventory=null)->
|
||||
inventory ?= new Inventory
|
||||
for x in [0..@recipe.width]
|
||||
for y in [0..@recipe.height]
|
||||
stack = @recipe.getInputAt x, y, z
|
||||
continue unless stack?
|
||||
inventory.add stack.item, stack.quantity * @multiplier
|
||||
return inventory
|
||||
|
||||
_computeType: ->
|
||||
if not @recipe?
|
||||
type = null
|
||||
else if @recipe.depth > 1
|
||||
type = @RECIPE_TYPE.MULTIBLOCK
|
||||
else
|
||||
type = @RECIPE_TYPE.CRAFTING_TABLE
|
||||
@triggerPropertyChange "type", @_type, type
|
||||
|
||||
_createFullInventory: (inventory=null)->
|
||||
inventory ?= new Inventory()
|
||||
for z in [0..@recipe.depth]
|
||||
@_addLayerInventory z, inventory
|
||||
return inventory
|
||||
@@ -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
|
||||
@@ -1,12 +1,11 @@
|
||||
#
|
||||
# Crafting Guide - base_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
templates = require './templates'
|
||||
templates = require './templates'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
@@ -50,26 +49,15 @@ module.exports = class BaseController extends Backbone.View
|
||||
@off()
|
||||
|
||||
routeLinkClick: (event)->
|
||||
$link = $(event.target)
|
||||
if not $link.attr('href')?
|
||||
$link = $(event.currentTarget)
|
||||
|
||||
href = $link.attr 'href'
|
||||
target = $link.attr 'target'
|
||||
|
||||
logger.info -> "Re-routing link to internal navigation: #{href}"
|
||||
event.preventDefault()
|
||||
href = $(event.target).attr 'href'
|
||||
href ?= $(event.currentTarget).attr 'href'
|
||||
logger.info "Re-routing link to internal navigation: #{href}"
|
||||
|
||||
if href? and href.match /^http/
|
||||
tracker.trackEvent c.tracking.category.navigate, 'external-link', href
|
||||
.then -> window.location.href = href
|
||||
return false
|
||||
else if target?
|
||||
tracker.trackEvent c.tracking.category.navigate, 'external-link', href
|
||||
return true
|
||||
window.location.href = href
|
||||
else
|
||||
tracker.trackEvent c.tracking.category.navigate, 'internal-link', href
|
||||
@router.navigate href, trigger:true
|
||||
return false
|
||||
|
||||
show: ($el)->
|
||||
$el ?= @$el
|
||||
@@ -85,6 +73,9 @@ module.exports = class BaseController extends Backbone.View
|
||||
onDidModelChange: ->
|
||||
@tryRefresh()
|
||||
|
||||
onDidModelSync: ->
|
||||
@tryRefresh()
|
||||
|
||||
onWillRender: -> # do nothing
|
||||
|
||||
onDidRender: -> # do nothing
|
||||
@@ -94,12 +85,11 @@ module.exports = class BaseController extends Backbone.View
|
||||
onDidShow: -> # do nothing
|
||||
|
||||
onWillChangeModel: (oldModel, newModel)->
|
||||
if oldModel?.isObservable
|
||||
oldModel.off target:this
|
||||
|
||||
if newModel?.isObservable
|
||||
newModel.on Observable::ANY, this, "onDidModelChange"
|
||||
|
||||
if oldModel?.on?
|
||||
@stopListening oldModel
|
||||
if newModel?.on?
|
||||
@listenTo newModel, 'sync', (e)=> @onDidModelSync e
|
||||
@listenTo newModel, 'change', (e)=> @onDidModelChange e
|
||||
return newModel
|
||||
|
||||
# Property Methods #############################################################################
|
||||
@@ -112,7 +102,7 @@ module.exports = class BaseController extends Backbone.View
|
||||
|
||||
newModel = @onWillChangeModel @_model, newModel
|
||||
@_model = newModel
|
||||
@onDidModelChange()
|
||||
@tryRefresh()
|
||||
|
||||
rendered:
|
||||
get: -> @_rendered
|
||||
@@ -146,13 +136,9 @@ module.exports = class BaseController extends Backbone.View
|
||||
|
||||
@$el.append $renderedEl.children()
|
||||
@$el.addClass $renderedEl.attr 'class'
|
||||
|
||||
elementData = $renderedEl.data()
|
||||
elementData.controller = this
|
||||
elementData.model = data
|
||||
@$el.data elementData
|
||||
|
||||
@$el.data $renderedEl.data()
|
||||
@delegateEvents()
|
||||
|
||||
@_rendered = true
|
||||
@onDidRender()
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
//-
|
||||
//- Crafting Guide - browse_page.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- Copyright © 2014-2016 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.content.view__browse_page
|
||||
.left
|
||||
include ../common/youtube/youtube.jade
|
||||
|
||||
.right
|
||||
.tile_container
|
||||
section.view__mod_ballot
|
||||
.tile_container
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// Crafting Guide - browse_page.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// Copyright © 2014-2016 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
|
||||
.right {
|
||||
.tile_container {
|
||||
margin-bottom : $size-margin-large;
|
||||
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
margin-right : -12px;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
|
||||
.view__mod_tile {
|
||||
flex : 1 1 calc(50% - 12px); // $size-margin-large / 2
|
||||
@@ -29,16 +26,14 @@
|
||||
.view__browse_page {
|
||||
.right {
|
||||
.tile_container {
|
||||
margin-right : 0;
|
||||
|
||||
flex-direction : column;
|
||||
flex-wrap : nowrap;
|
||||
align-items : stretch;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
|
||||
.view__mod_tile {
|
||||
flex : 0 0 auto;
|
||||
margin-right : 0;
|
||||
width : 100%;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,25 @@
|
||||
#
|
||||
# Crafting Guide - browse_page_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
{CraftingGuideClient} = require('crafting-guide-common').api
|
||||
ModBallotController = require './mod_ballot/mod_ballot_controller'
|
||||
ModTileController = require './mod_tile/mod_tile_controller'
|
||||
PageController = require '../page_controller'
|
||||
ModTileController = require './mod_tile/mod_tile_controller'
|
||||
PageController = require '../page_controller'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class BrowsePageController extends PageController
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.client then throw new Error 'options.client is required'
|
||||
if not options.imageLoader then throw new Error 'options.imageLoader is required'
|
||||
if not options.modPack then throw new Error 'options.modPack is required'
|
||||
|
||||
if not options.modPack then throw new Error 'options.modPack is required'
|
||||
options.templateName = 'browse_page'
|
||||
super options
|
||||
|
||||
@_client = options.client
|
||||
@_imageLoader = options.imageLoader
|
||||
@_modPack = options.modPack
|
||||
@_tileControllers = []
|
||||
|
||||
@_client.on c.event.change, => @tryRefresh()
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
user:
|
||||
get: -> @_user
|
||||
|
||||
set: (newUser)->
|
||||
@_user = newUser
|
||||
@tryRefresh()
|
||||
|
||||
# PageController Overrides #####################################################################
|
||||
|
||||
getMetaDescription: ->
|
||||
@@ -51,17 +31,10 @@ module.exports = class BrowsePageController extends PageController
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@_modBallotController = @addChild ModBallotController, '.view__mod_ballot',
|
||||
client: @_client
|
||||
imageLoader: @_imageLoader
|
||||
user: @_user
|
||||
|
||||
@$tileContainer = @$('.tile_container')
|
||||
|
||||
refresh: ->
|
||||
@_modBallotController.user = @user
|
||||
@_refreshModTiles()
|
||||
@_refreshBallot()
|
||||
super
|
||||
|
||||
# Backbone.View Overrides ######################################################################
|
||||
@@ -72,15 +45,9 @@ module.exports = class BrowsePageController extends PageController
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_refreshBallot: ->
|
||||
if @_client.status is CraftingGuideClient.Status.Down
|
||||
@_modBallotController.hide()
|
||||
else
|
||||
@_modBallotController.show()
|
||||
|
||||
_refreshModTiles: ->
|
||||
index = 0
|
||||
mods = (mod for modId, mod of @_modPack.mods)
|
||||
mods = @_modPack.getAllMods()
|
||||
for mod in mods
|
||||
controller = @_tileControllers[index]
|
||||
if not controller
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//-
|
||||
//- Crafting Guide - mod_ballot.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.view__mod_ballot
|
||||
.status.initial
|
||||
h2 Are we missing a mod you like?<br>Vote for your favorites!
|
||||
.button.vote-now: .bezel: p Vote Now!
|
||||
|
||||
.status.loading
|
||||
img(src="/images/wait.gif")
|
||||
.status-message
|
||||
|
||||
.status.loaded
|
||||
h2 Are we missing a mod you like?<br>Vote for your favorites!
|
||||
.instructions.
|
||||
You can cast up to 3 votes. Change them any time. You get a vote back when a mod is added.
|
||||
.instructions.remaining.
|
||||
You have <span class="remaining-count">0</span> votes remaining.
|
||||
|
||||
.ballot-list
|
||||
|
||||
.view__suggest_mod_panel
|
||||
|
||||
.status.error
|
||||
.status-message
|
||||
.button.reload: .bezel: p Try Again
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// Crafting Guide - mod_ballot.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
$ballot-move-duration: 0.2s;
|
||||
|
||||
.view__mod_ballot {
|
||||
@include floating-panel;
|
||||
|
||||
display : flex;
|
||||
padding : $size-margin-medium;
|
||||
|
||||
.status {
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $size-margin-large;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
margin-bottom: $size-margin-large;
|
||||
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-medium;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.initial {
|
||||
padding: 0 15%;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin-top: $size-margin-large;
|
||||
margin-bottom: $size-margin-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&.loaded {
|
||||
padding: 0 15%;
|
||||
|
||||
& > .instructions {
|
||||
margin-bottom: $size-margin-small;
|
||||
|
||||
font-family: $font-family-text;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
|
||||
.remaining {
|
||||
margin-top: $size-margin-medium;
|
||||
font-size: $font-size-large;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ballot-list {
|
||||
position: relative;
|
||||
margin-top: $size-margin-large;
|
||||
|
||||
.view__mod_ballot_line {
|
||||
position: absolute;
|
||||
margin-bottom: $size-margin-small;
|
||||
transition: top $ballot-move-duration ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.view__suggest_mod_panel {
|
||||
margin-top: $size-margin-xlarge;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
width: $size-minecraft-block * 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include screen-size(mobile) {
|
||||
.view__mod_ballot {
|
||||
.status {
|
||||
&.loaded {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - mod_ballot_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require '../../base_controller'
|
||||
ModBallotLineController = require './mod_ballot_line/mod_ballot_line_controller'
|
||||
SuggestModPanelController = require './suggest_mod_panel/suggest_mod_panel_controller'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModBallotController extends BaseController
|
||||
|
||||
@::LOAD_ERROR = 'Sorry! Something went wrong loading the mod list. Please try again.'
|
||||
|
||||
@::MAX_VOTES = 3
|
||||
|
||||
@::STATUS =
|
||||
ERROR: 'error'
|
||||
INITIAL: 'initial'
|
||||
LOADED: 'loaded'
|
||||
LOADING: 'loading'
|
||||
LOGGED_OUT: 'logged-out'
|
||||
|
||||
@::VOTE_ERROR = 'Sorry! Something went wrong casting your vote. Please try again.'
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.client then throw new Error 'options.client is required'
|
||||
if not options.imageLoader then throw new Error 'options.imageLoader is required'
|
||||
|
||||
options.model = ballot:null, votes:null
|
||||
options.templateName = 'browse_page/mod_ballot'
|
||||
super options
|
||||
|
||||
@_client = options.client
|
||||
@_imageLoader = options.imageLoader
|
||||
@_lineControllers = {}
|
||||
@_status = @STATUS.INITIAL
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
isVotingAllowed:
|
||||
get: ->
|
||||
voteCount = @model.votes?.length or 0
|
||||
return voteCount < @MAX_VOTES
|
||||
|
||||
user:
|
||||
get: -> @_user
|
||||
|
||||
set: (newUser)->
|
||||
@_user = newUser
|
||||
|
||||
# Event Methods ################################################################################
|
||||
|
||||
onReloadClicked: ->
|
||||
tracker.trackEvent c.tracking.category.modVote, 'reload'
|
||||
@_reloadData()
|
||||
return false
|
||||
|
||||
onVoteButtonClicked: (controller)->
|
||||
modId = controller.model.ballotLine.modId
|
||||
|
||||
if controller.model.vote?
|
||||
tracker.trackEvent c.tracking.category.modVote, 'cancel-vote', @_findModName modId
|
||||
@_cancelVote modId
|
||||
else
|
||||
tracker.trackEvent c.tracking.category.modVote, 'cast-vote', @_findModName modId
|
||||
@_castVote modId
|
||||
|
||||
onVoteNowClicked: ->
|
||||
tracker.trackEvent c.tracking.category.modVote, 'vote-now'
|
||||
|
||||
if not @user?
|
||||
global.site.login()
|
||||
else
|
||||
@_reloadData()
|
||||
|
||||
return false
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@_suggestPanelController = @addChild SuggestModPanelController, '.view__suggest_mod_panel'
|
||||
|
||||
@$ballotList = @$('.ballot-list')
|
||||
@$remainingCount = @$('.remaining-count')
|
||||
@$remainingMessage = @$('.remaining')
|
||||
@$statusMessage = @$('.status-message')
|
||||
@$statusSections = @$('.status')
|
||||
super
|
||||
|
||||
refresh: ->
|
||||
@_refreshLines()
|
||||
@_refreshRemaining()
|
||||
@_refreshStatus()
|
||||
super
|
||||
|
||||
# Backbone.View Methods ########################################################################
|
||||
|
||||
events: ->
|
||||
_.extend super,
|
||||
'click .button.reload': 'onReloadClicked'
|
||||
'click .button.vote-now': 'onVoteNowClicked'
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_cancelVote: (modId)->
|
||||
controller = @_lineControllers[modId]
|
||||
vote = @_findVote modId
|
||||
|
||||
controller.isWaiting = true
|
||||
controller.tryRefresh()
|
||||
|
||||
@_client.cancelVote modVoteId:vote.id
|
||||
.then =>
|
||||
@model.votes = _(@model.votes).without vote
|
||||
controller.model.vote = null
|
||||
controller.model.ballotLine.voteCount -= 1
|
||||
.catch =>
|
||||
@_changeStatus @STATUS.ERROR, @VOTE_ERROR
|
||||
.finally =>
|
||||
controller.isWaiting = false
|
||||
@tryRefresh()
|
||||
|
||||
_castVote: (modId)->
|
||||
controller = @_lineControllers[modId]
|
||||
vote = @_findVote modId
|
||||
|
||||
controller.isWaiting = true
|
||||
controller.tryRefresh()
|
||||
|
||||
@_client.castVote modId:modId, userId:@user.id
|
||||
.then (response)=>
|
||||
vote = response.json
|
||||
@model.votes.push vote
|
||||
controller.model.vote = vote
|
||||
controller.model.ballotLine.voteCount += 1
|
||||
.catch (error)=>
|
||||
logger.error -> "vote could not be cast: #{error}"
|
||||
@_changeStatus @STATUS.ERROR, @VOTE_ERROR
|
||||
.finally =>
|
||||
controller.isWaiting = false
|
||||
@tryRefresh()
|
||||
|
||||
_computeLines: ->
|
||||
return [] unless @model.ballot?.lines?
|
||||
|
||||
@model.ballot.lines.sort (a, b)->
|
||||
if a.voteCount != b.voteCount
|
||||
return if a.voteCount > b.voteCount then -1 else +1
|
||||
if a.name != b.name
|
||||
return if a.name < b.name then -1 else +1
|
||||
return 0
|
||||
|
||||
return @model.ballot.lines
|
||||
|
||||
_changeStatus: (status, message='')->
|
||||
@_status = status
|
||||
@_message = message
|
||||
|
||||
_findVote: (modId)->
|
||||
return null unless @model.votes?
|
||||
|
||||
for vote in @model.votes
|
||||
return vote if vote.modId is modId
|
||||
return null
|
||||
|
||||
_findModName: (modId)->
|
||||
return '' unless @model.ballot?
|
||||
|
||||
for line in @model.ballot.lines
|
||||
return line.name if line.modId is modId
|
||||
return ''
|
||||
|
||||
_refreshLines: ->
|
||||
extraControllers = _.clone @_lineControllers
|
||||
lines = @_computeLines()
|
||||
|
||||
for line, index in lines
|
||||
controller = @_lineControllers[line.modId]
|
||||
delete extraControllers[line.modId]
|
||||
|
||||
model = {ballotLine:line, vote:@_findVote(line.modId)}
|
||||
if not controller?
|
||||
controller = new ModBallotLineController
|
||||
imageLoader: @_imageLoader
|
||||
model: model
|
||||
onVoteButtonClicked: (controller)=> @onVoteButtonClicked controller
|
||||
controller.render()
|
||||
@$ballotList.append controller.$el
|
||||
@_lineControllers[line.modId] = controller
|
||||
else
|
||||
controller.model = model
|
||||
|
||||
controller.isVotingAllowed = @isVotingAllowed
|
||||
|
||||
for modId, controller of extraControllers
|
||||
delete @_lineControllers[modId]
|
||||
controller.remove()
|
||||
|
||||
_.delay (=> @_refreshLineLayout()), 100
|
||||
|
||||
_refreshLineLayout: ->
|
||||
lines = @_computeLines()
|
||||
offset = @$ballotList.offset()
|
||||
|
||||
for line in lines
|
||||
controller = @_lineControllers[line.modId]
|
||||
continue unless controller?
|
||||
|
||||
if controller.$el.height() is 0
|
||||
# keep trying until the elements have been created and laid out by the browser
|
||||
_.delay (=> @_refreshLineLayout()), 100
|
||||
return
|
||||
|
||||
controller.$el.offset offset
|
||||
offset.top += controller.$el.outerHeight(true)
|
||||
|
||||
@$ballotList.height offset.top - @$ballotList.offset().top
|
||||
|
||||
_refreshRemaining: ->
|
||||
if @model.votes?
|
||||
@$remainingCount.html @MAX_VOTES - @model.votes.length
|
||||
@show @$remainingMessage
|
||||
else
|
||||
@hide @$remainingMessage
|
||||
|
||||
_refreshStatus: ->
|
||||
@hide @$('.status')
|
||||
@show @$(".status.#{@_status}")
|
||||
@$statusMessage.html @_message
|
||||
|
||||
_reloadBallot: ->
|
||||
return @_loadingBallot if @_loadingBallot?
|
||||
|
||||
@model.ballot = null
|
||||
@_changeStatus @STATUS.LOADING, "Loading ballot data..."
|
||||
@tryRefresh()
|
||||
|
||||
@_loadingBallot = @_client.getModBallot()
|
||||
.then (response)=>
|
||||
@model.ballot = response.json
|
||||
if not @user? or @model.votes? then @_changeStatus @STATUS.LOADED
|
||||
@tryRefresh()
|
||||
.catch (error)=>
|
||||
@_changeStatus @STATUS.ERROR, @LOAD_ERROR
|
||||
.finally =>
|
||||
@_loadingBallot = null
|
||||
|
||||
_reloadData: ->
|
||||
@_reloadVotes()
|
||||
@_reloadBallot()
|
||||
|
||||
_reloadVotes: ->
|
||||
return @_loadingVotes if @_loadingVotes?
|
||||
return unless @user?
|
||||
|
||||
@model.votes = null
|
||||
@_changeStatus @STATUS.LOADING, "Loading ballot data..."
|
||||
@tryRefresh()
|
||||
|
||||
@_loadingVotes = @_client.getModVotes()
|
||||
.then (response)=>
|
||||
@model.votes = response.json
|
||||
if @model.ballot? then @_changeStatus @STATUS.LOADED
|
||||
@tryRefresh()
|
||||
.catch (error)=>
|
||||
@_changeStatus @STATUS.ERROR, @LOAD_ERROR
|
||||
.finally =>
|
||||
@_loadingVotes = null
|
||||
@@ -1,14 +0,0 @@
|
||||
//-
|
||||
//- Crafting Guide - mod_ballot_line.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.view__mod_ballot_line
|
||||
.vote-count
|
||||
img.icon(src='/images/unknown.png')
|
||||
.name
|
||||
.button.vote: .bezel: p ✓
|
||||
.wait
|
||||
img(src='/images/wait-small.gif')
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Crafting Guide - mod_ballot_line.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
.view__mod_ballot_line {
|
||||
width: 100%;
|
||||
|
||||
display : flex;
|
||||
align-items : center;
|
||||
|
||||
.vote-count {
|
||||
flex : 0 0 $size-minecraft-block;
|
||||
margin-right : $size-margin-medium;
|
||||
|
||||
font-family : $font-family-text;
|
||||
font-size : $font-size-large;
|
||||
text-align : right;
|
||||
}
|
||||
|
||||
& > img {
|
||||
margin-right : $size-margin-medium;
|
||||
|
||||
flex : 0 0 auto;
|
||||
height : $size-minecraft-block;
|
||||
width : $size-minecraft-block;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-right : $size-margin-medium;
|
||||
|
||||
flex : 1 1 100%;
|
||||
font-family : $font-family-text;
|
||||
font-size : $font-size-large;
|
||||
}
|
||||
|
||||
.button {
|
||||
flex : 0 0 auto;
|
||||
width : $size-minecraft-block * 1.5;
|
||||
}
|
||||
|
||||
.wait {
|
||||
flex : 0 0 auto;
|
||||
height : $size-button-height;
|
||||
width : $size-minecraft-block * 1.5;
|
||||
|
||||
display : flex;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
}
|
||||
}
|
||||
|
||||
@include screen-size(mobile) {
|
||||
.view__mod_ballot_line {
|
||||
.name {
|
||||
overflow: hidden;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - mod_ballot_line_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require '../../../base_controller'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ModBallotLineController extends BaseController
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.imageLoader then throw new Error 'options.imageLoader is required'
|
||||
options.templateName = 'browse_page/mod_ballot/mod_ballot_line'
|
||||
super options
|
||||
|
||||
@_imageLoader = options.imageLoader
|
||||
@_onVoteButtonClicked = options.onVoteButtonClicked or (controller)-> # do nothing
|
||||
|
||||
# Event Methods ################################################################################
|
||||
|
||||
onButtonClicked: ->
|
||||
return if @$button.hasClass 'disabled'
|
||||
|
||||
@_onVoteButtonClicked this
|
||||
return false
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
isVotingAllowed:
|
||||
get: -> @_isVotingAllowed
|
||||
|
||||
set: (isVotingAllowed)->
|
||||
@_isVotingAllowed = !! isVotingAllowed
|
||||
@tryRefresh()
|
||||
|
||||
isWaiting:
|
||||
get: -> @_isWaiting
|
||||
|
||||
set: (isWaiting)->
|
||||
@_isWaiting = !! isWaiting
|
||||
@tryRefresh()
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@$button = @$('.button')
|
||||
@$image = @$('img.icon')
|
||||
@$name = @$('.name')
|
||||
@$voteCount = @$('.vote-count')
|
||||
@$wait = @$('.wait')
|
||||
super
|
||||
|
||||
refresh: ->
|
||||
@$name.html @model.ballotLine?.name or ''
|
||||
@$voteCount.html @model.ballotLine?.voteCount or ''
|
||||
@_refreshIcon()
|
||||
@_refreshVoteButton()
|
||||
super
|
||||
|
||||
# Backbone.View Overrides ######################################################################
|
||||
|
||||
events: ->
|
||||
return _.extend super,
|
||||
'click .button': 'onButtonClicked'
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_refreshIcon: ->
|
||||
if @model.ballotLine?
|
||||
modSlug = _.slugify(@model.ballotLine.name)
|
||||
url = c.url.modIcon modSlug:modSlug
|
||||
@_imageLoader.load url, @$image
|
||||
else
|
||||
@$image.attr 'src', '/images/unknown.png'
|
||||
|
||||
_refreshVoteButton: ->
|
||||
if @isWaiting
|
||||
@$wait.removeClass 'hidden'
|
||||
@$button.addClass 'hidden'
|
||||
else
|
||||
@$wait.addClass 'hidden'
|
||||
@$button.removeClass 'hidden'
|
||||
|
||||
if @model.vote?
|
||||
@$button.addClass 'active'
|
||||
else
|
||||
@$button.removeClass 'active'
|
||||
|
||||
if @isVotingAllowed
|
||||
@$button.removeClass 'disabled'
|
||||
else
|
||||
@$button.addClass 'disabled'
|
||||
@@ -1,25 +0,0 @@
|
||||
//-
|
||||
//- Crafting Guide - suggest_mod_panel.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.view__suggest_mod_panel
|
||||
.initial
|
||||
.instructions Still don't see the mod you want?<br>Tell us about it!
|
||||
|
||||
.entry-form
|
||||
input.name(placeholder="enter the mod name")
|
||||
input.url(placeholder="enter the website")
|
||||
.buttons
|
||||
.button: .bezel: p send
|
||||
|
||||
.sending
|
||||
.instructions Sending...
|
||||
|
||||
.error
|
||||
.instructions Something went wrong! Please try again.
|
||||
|
||||
.success
|
||||
.instructions Thanks! We'll add that one to the list.
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// Crafting Guide - suggest_mod_panel.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
.view__suggest_mod_panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.instructions {
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-xlarge;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.initial {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.instructions {
|
||||
margin-bottom: $size-margin-large;
|
||||
}
|
||||
|
||||
.entry-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
input {
|
||||
margin-bottom: $size-margin-medium;
|
||||
|
||||
font-family: $font-family-input;
|
||||
font-size: $font-size-large;
|
||||
|
||||
padding: $size-margin-xsmall $size-margin-small;
|
||||
|
||||
&.invalid {
|
||||
border-color: $color-error;
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
&.valid {
|
||||
border-color: $color-valid;
|
||||
color: $color-valid;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $size-margin-large;
|
||||
|
||||
.button {
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error, .sending, .success {
|
||||
margin: $size-margin-xlarge 0;
|
||||
}
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - suggest_mod_panel_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require '../../../base_controller'
|
||||
EmailClient = require '../../../../models/site/email_client'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class SuggestModPanelController extends BaseController
|
||||
|
||||
constructor: (options={})->
|
||||
options.templateName = 'browse_page/mod_ballot/suggest_mod_panel'
|
||||
super options
|
||||
|
||||
@_emailClient = new EmailClient
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
isFormComplete:
|
||||
get: ->
|
||||
return false unless @name.length > 0
|
||||
return false unless @url.length > 0
|
||||
return true
|
||||
|
||||
name:
|
||||
get: ->
|
||||
return '' unless @rendered
|
||||
return @$nameField.val()
|
||||
|
||||
url:
|
||||
get: ->
|
||||
return '' unless @rendered
|
||||
return @$urlField.val()
|
||||
|
||||
user:
|
||||
get: -> @_user
|
||||
set: (user)-> @_user = user
|
||||
|
||||
# Event Methods ################################################################################
|
||||
|
||||
onButtonClicked: (event)->
|
||||
return false unless @isFormComplete
|
||||
|
||||
tracker.trackEvent c.tracking.category.feedback, 'suggest-mod', @name
|
||||
@_sendRequest()
|
||||
return false
|
||||
|
||||
onNameChanged: (event)->
|
||||
@refresh()
|
||||
return false
|
||||
|
||||
onUrlChanged: (event)->
|
||||
@refresh()
|
||||
return false
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@$button = @$('.button')
|
||||
@$errorPanel = @$('.error')
|
||||
@$initialPanel = @$('.initial')
|
||||
@$nameField = @$('input.name')
|
||||
@$sendingPanel = @$('.sending')
|
||||
@$successPanel = @$('.success')
|
||||
@$urlField = @$('input.url')
|
||||
|
||||
@hide @$errorPanel
|
||||
@hide @$successPanel
|
||||
@hide @$sendingPanel
|
||||
super
|
||||
|
||||
refresh: ->
|
||||
@_refreshButtonState()
|
||||
super
|
||||
|
||||
# Backbone.View Overrides ######################################################################
|
||||
|
||||
events: ->
|
||||
return _.extend super,
|
||||
'input .name': 'onNameChanged'
|
||||
'input .url': 'onUrlChanged'
|
||||
'click .button': 'onButtonClicked'
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_refreshButtonState: ->
|
||||
if @isFormComplete
|
||||
@$button.removeClass 'disabled'
|
||||
else
|
||||
@$button.addClass 'disabled'
|
||||
|
||||
_sendRequest: ->
|
||||
showPanel = ($selectedPanel)=>
|
||||
@hide $panel for $panel in [@$initialPanel, @$sendingPanel, @$successPanel, @$errorPanel]
|
||||
@show $selectedPanel
|
||||
|
||||
data =
|
||||
name: @name
|
||||
url: @url
|
||||
user: @user?.name
|
||||
email: @user?.email
|
||||
|
||||
showPanel @$sendingPanel
|
||||
w(@_emailClient.send 'mod-suggestion', name:@name, url:@url)
|
||||
.then =>
|
||||
showPanel @$successPanel
|
||||
@$urlField.val ""
|
||||
@$nameField.val ""
|
||||
@refresh()
|
||||
.catch => showPanel @$errorPanel
|
||||
.delay c.duration.slow * 2
|
||||
.then => showPanel @$initialPanel
|
||||
@@ -1,17 +1,14 @@
|
||||
//-
|
||||
//- Crafting Guide - mod_tile.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- Copyright © 2014-2016 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.view__mod_tile
|
||||
.tile
|
||||
a
|
||||
.left
|
||||
img
|
||||
.right
|
||||
p.title
|
||||
p.description
|
||||
|
||||
.view__mod_version_selector
|
||||
a
|
||||
.left
|
||||
img
|
||||
.right
|
||||
p.title
|
||||
p.description
|
||||
|
||||
@@ -1,60 +1,53 @@
|
||||
//
|
||||
// Crafting Guide - mod_tile.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// Copyright © 2014-2016 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
.view__mod_tile {
|
||||
min-width : $size-minecraft-block * 9;
|
||||
|
||||
display: flex;
|
||||
|
||||
.tile {
|
||||
a {
|
||||
@include floating-panel;
|
||||
flex: 1 1 auto;
|
||||
|
||||
@include floating-panel;
|
||||
min-width : $size-minecraft-block * 9;
|
||||
border : $size-border-medium solid $color-background-panel;
|
||||
color : $color-text;
|
||||
text-decoration : none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display : flex;
|
||||
padding : $size-margin-medium;
|
||||
|
||||
border : $size-border-medium solid $color-background-panel;
|
||||
& > .left {
|
||||
flex: 0 0 $size-minecraft-block * 4;
|
||||
margin-right: $size-margin-medium;
|
||||
|
||||
a {
|
||||
flex: 1 1 auto;
|
||||
color : $color-text;
|
||||
text-decoration : none;
|
||||
img {
|
||||
width: $size-minecraft-block * 4;
|
||||
max-height: $size-minecraft-block * 4;
|
||||
}
|
||||
}
|
||||
|
||||
display : flex;
|
||||
padding : $size-margin-medium;
|
||||
& > .right {
|
||||
flex : 1 1 100%;
|
||||
|
||||
& > .left {
|
||||
flex: 0 0 $size-minecraft-block * 4;
|
||||
margin-right: $size-margin-medium;
|
||||
overflow : hidden;
|
||||
|
||||
img {
|
||||
width: $size-minecraft-block * 4;
|
||||
max-height: $size-minecraft-block * 4;
|
||||
}
|
||||
.title {
|
||||
max-width: 0;
|
||||
margin-bottom : $size-margin-medium;
|
||||
|
||||
font-family : $font-family-header;
|
||||
font-size : $font-size-xlarge;
|
||||
}
|
||||
|
||||
& > .right {
|
||||
flex : 1 1 100%;
|
||||
|
||||
overflow : hidden;
|
||||
|
||||
.title {
|
||||
max-width: 0;
|
||||
margin-bottom : $size-margin-medium;
|
||||
|
||||
font-family : $font-family-header;
|
||||
font-size : $font-size-xlarge;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-family : $font-family-text;
|
||||
font-size : $font-size-medium;
|
||||
line-height : $font-size-medium * 1.5;
|
||||
}
|
||||
.description {
|
||||
font-family : $font-family-text;
|
||||
font-size : $font-size-medium;
|
||||
line-height : $font-size-medium * 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,18 +55,13 @@
|
||||
background: $color-blue-faint;
|
||||
border-color: $color-link-normal;
|
||||
|
||||
& a > .right {
|
||||
& > .right {
|
||||
.title {
|
||||
color: $color-link-normal;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view__mod_version_selector {
|
||||
margin: 0;
|
||||
padding: $size-margin-small $size-margin-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#
|
||||
# Crafting Guide - mod_tile_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require '../../base_controller'
|
||||
ModVersionSelectorController = require '../../common/mod_version_selector/mod_version_selector_controller'
|
||||
BaseController = require '../../base_controller'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
@@ -20,8 +19,6 @@ module.exports = class ModTileController extends BaseController
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@modVersionSelector = @addChild ModVersionSelectorController, '.view__mod_version_selector', model:@model
|
||||
|
||||
@$link = @$('a')
|
||||
@$logoImage = @$('img')
|
||||
@$title = @$('p.title')
|
||||
@@ -29,12 +26,12 @@ module.exports = class ModTileController extends BaseController
|
||||
super
|
||||
|
||||
refresh: ->
|
||||
@$link.attr 'href', c.url.mod modId:@model.id
|
||||
@$logoImage.attr 'src', c.url.modIcon modId:@model.id
|
||||
@$title.text @model.displayName
|
||||
@$link.attr 'href', c.url.mod modSlug:@model.slug
|
||||
@$logoImage.attr 'src', c.url.modIcon modSlug:@model.slug
|
||||
@$title.text @model.name
|
||||
@$description.text @model.description
|
||||
|
||||
if @model.isEnabled
|
||||
if @model.enabled
|
||||
@$el.removeClass 'disabled'
|
||||
else
|
||||
@$el.addClass 'disabled'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//-
|
||||
//- Crafting Guide - adsense.jade
|
||||
//-
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- Copyright © 2014-2016 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// Crafting Guide - adsense.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// Copyright © 2014-2016 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - adsense_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
@@ -21,6 +21,8 @@ module.exports = class AdsenseController
|
||||
maxAds = c.adsense[adType].slotIds.length - @_adsPlaced
|
||||
return if maxAds is 0
|
||||
|
||||
logger.verbose => "Looking to place #{maxAds} more #{adType} ads..."
|
||||
|
||||
if adType is 'skyscraper'
|
||||
placedNewAd = @_createSkyscraperPositions()
|
||||
else
|
||||
@@ -140,7 +142,7 @@ module.exports = class AdsenseController
|
||||
adCount = @_computeAdCount @_computeAdType()
|
||||
|
||||
if adCount is 0
|
||||
logger.trace "Adsense is waiting for room to insert ads"
|
||||
logger.verbose "Adsense is waiting for room to insert ads"
|
||||
@_waiting = true
|
||||
_.delay (=> @_waiting = false; @_waitForPageReadiness()), c.adsense.readinessCheckInterval
|
||||
else
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// Crafting Guide - crafting_grid.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// Copyright © 2014-2016 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
#
|
||||
# Crafting Guide - crafting_grid_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require "../../base_controller"
|
||||
{Recipe} = require("crafting-guide-common").models
|
||||
SlotController = require "../slot/slot_controller"
|
||||
BaseController = require '../../base_controller'
|
||||
SlotController = require '../slot/slot_controller'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftingGridController extends BaseController
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.imageLoader? then throw new Error "options.imageLoader is required"
|
||||
if not options.modPack? then throw new Error "options.modPack is required"
|
||||
if not options.router? then throw new Error "options.router is required"
|
||||
options.templateName = "common/crafting_grid"
|
||||
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
|
||||
if not options.modPack? then throw new Error 'options.modPack is required'
|
||||
if not options.router? then throw new Error 'options.router is required'
|
||||
# options.model should be a recipe
|
||||
options.templateName = 'common/crafting_grid'
|
||||
super options
|
||||
|
||||
@_imageLoader = options.imageLoader
|
||||
@_modPack = options.modPack
|
||||
@_router = options.router
|
||||
|
||||
# BaseController Methods #######################################################################
|
||||
@_modPack.on c.event.change, => @tryRefresh()
|
||||
|
||||
onWillChangeModel: (oldModel, newModel)->
|
||||
if newModel? and (newModel.constructor isnt Recipe) then throw new Error "options.model must be a Recipe"
|
||||
super
|
||||
# BaseController Methods #######################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@_slotControllers = []
|
||||
for el in @$(".view__slot")
|
||||
for el in @$('.view__slot')
|
||||
controller = new SlotController el:el, imageLoader:@_imageLoader, modPack:@_modPack, router:@_router
|
||||
controller.render()
|
||||
@_slotControllers.push controller
|
||||
super
|
||||
|
||||
refresh: ->
|
||||
index = 0
|
||||
for y in [0..2]
|
||||
for x in [0..2]
|
||||
controller = @_slotControllers[index++]
|
||||
if @model?
|
||||
controller.model = @model.getInputAt x, y
|
||||
else
|
||||
controller.model = null
|
||||
for controller, index in @_slotControllers
|
||||
controller.model = @model?.getStackAtSlot(index)
|
||||
|
||||
super
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user