Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
240a14da58 |
@@ -1,7 +1,5 @@
|
|||||||
*.log
|
*.log
|
||||||
*.swp
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
|
||||||
.sass-cache
|
.sass-cache
|
||||||
.tm_properties
|
.tm_properties
|
||||||
aws.config
|
aws.config
|
||||||
|
|||||||
+42
-11
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - Gruntfile.coffee
|
# Crafting Guide - Gruntfile.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -17,6 +17,29 @@ EXTERNAL_LIBS = [
|
|||||||
'./vendor/email.js:emailjs'
|
'./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)->
|
module.exports = (grunt)->
|
||||||
@@ -38,15 +61,20 @@ module.exports = (grunt)->
|
|||||||
{expand: true, cwd:'./build/static', src:'**/*.txt', dest:'./dist/'}
|
{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:
|
copy:
|
||||||
assets_build:
|
assets_build:
|
||||||
files: [
|
files: [
|
||||||
{expand:true, cwd:'./assets/', src:'**/*', dest:'./build/static'}
|
{expand:true, cwd:'./assets/', src:'**/*', dest:'./build/static'}
|
||||||
]
|
]
|
||||||
env_build:
|
|
||||||
files: [
|
|
||||||
{expand:true, cwd:'.', src:'.env', dest:'./build'}
|
|
||||||
]
|
|
||||||
build_to_dist:
|
build_to_dist:
|
||||||
files: [
|
files: [
|
||||||
expand: true
|
expand: true
|
||||||
@@ -98,9 +126,9 @@ module.exports = (grunt)->
|
|||||||
|
|
||||||
mochaTest:
|
mochaTest:
|
||||||
options:
|
options:
|
||||||
bail: false
|
bail: true
|
||||||
color: true
|
color: true
|
||||||
reporter: 'list'
|
reporter: 'dot'
|
||||||
require: [
|
require: [
|
||||||
'coffee-script/register'
|
'coffee-script/register'
|
||||||
'./src/test_helper.coffee'
|
'./src/test_helper.coffee'
|
||||||
@@ -164,7 +192,7 @@ module.exports = (grunt)->
|
|||||||
['jade', 'build:copy', 'build:css', 'browserify:external', 'browserify:internal']
|
['jade', 'build:copy', 'build:css', 'browserify:external', 'browserify:internal']
|
||||||
|
|
||||||
grunt.registerTask 'build:copy', 'helper task for build',
|
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',
|
grunt.registerTask 'build:css', 'helper task for build',
|
||||||
['sass_globbing', 'sass']
|
['sass_globbing', 'sass']
|
||||||
@@ -184,11 +212,14 @@ module.exports = (grunt)->
|
|||||||
grunt.registerTask 'dist', 'build the project to be run from Amazon S3',
|
grunt.registerTask 'dist', 'build the project to be run from Amazon S3',
|
||||||
['build', 'copy:build_to_dist', 'uglify', 'compress']
|
['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',
|
grunt.registerTask 'publish', 'publish the project to NPM',
|
||||||
['prepublish', 'script:publish', 'clean', 'build']
|
['prepublish', 'script:publish', 'clean', 'build']
|
||||||
|
|
||||||
grunt.registerTask 'start', 'build the project and start a local HTTP server',
|
grunt.registerTask 'start', 'build the project and start a local HTTP server',
|
||||||
['script:start']
|
['build', 'script:start']
|
||||||
|
|
||||||
grunt.registerTask 'test', 'run unit tests',
|
grunt.registerTask 'test', 'run unit tests',
|
||||||
['mochaTest']
|
['mochaTest']
|
||||||
@@ -199,7 +230,7 @@ module.exports = (grunt)->
|
|||||||
grunt.registerTask 'upload:staging', 'upload the project the staging Amazon S3 environment',
|
grunt.registerTask 'upload:staging', 'upload the project the staging Amazon S3 environment',
|
||||||
['dist', 'script:s3_upload:staging']
|
['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']
|
['script:use_local_deps']
|
||||||
|
|
||||||
# Code Tasks #######################################################################################################
|
# Code Tasks #######################################################################################################
|
||||||
@@ -242,7 +273,7 @@ module.exports = (grunt)->
|
|||||||
|
|
||||||
grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
|
grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
|
||||||
done = this.async()
|
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", ->
|
grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", ->
|
||||||
done = this.async()
|
done = this.async()
|
||||||
|
|||||||
@@ -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",
|
"name": "crafting-guide",
|
||||||
"version": "3.2.6",
|
"version": "3.2.4",
|
||||||
"description": "A guide for building anything in Minecraft or its many mods",
|
"description": "A guide for building anything in Minecraft or its many mods",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"repository": "http://github.com/andrewminer/crafting-guide",
|
"repository": "http://github.com/andrewminer/crafting-guide",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "grunt build",
|
"build": "grunt build",
|
||||||
|
"prepublish": "grunt prepublish",
|
||||||
"start": "grunt start",
|
"start": "grunt start",
|
||||||
"test": "grunt test"
|
"test": "grunt test"
|
||||||
},
|
},
|
||||||
"author": "Andrew Miner",
|
"author": "Andrew Miner",
|
||||||
"license": "UNLICENSED",
|
"license": "none",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "3.5.0",
|
"chai": "^1.10.0",
|
||||||
"coffee-script": "1.12.5",
|
"coffee-script": "^1.9.3",
|
||||||
"coffeeify": "2.1.0",
|
"coffeeify": "^1.1.0",
|
||||||
"grunt": "1.0.1",
|
"grunt": "^0.4.5",
|
||||||
"grunt-contrib-clean": "1.1.0",
|
"grunt-contrib-clean": "^0.6.0",
|
||||||
"grunt-contrib-coffee": "1.0.0",
|
"grunt-contrib-coffee": "^1.0.0",
|
||||||
"grunt-contrib-compress": "1.4.1",
|
"grunt-contrib-compress": "^0.14.0",
|
||||||
"grunt-contrib-copy": "1.0.0",
|
"grunt-contrib-copy": "^0.8.2",
|
||||||
"grunt-contrib-jade": "1.0.0",
|
"grunt-contrib-jade": "^0.15.0",
|
||||||
"grunt-contrib-sass": "1.0.0",
|
"grunt-contrib-sass": "^0.9.2",
|
||||||
"grunt-contrib-uglify": "2.3.0",
|
"grunt-contrib-uglify": "^0.11.0",
|
||||||
"grunt-contrib-watch": "1.0.0",
|
"grunt-contrib-watch": "^0.6.1",
|
||||||
"grunt-mocha-test": "0.13.2",
|
"grunt-mocha-test": "^0.12.7",
|
||||||
"grunt-sass-globbing": "1.5.1",
|
"grunt-sass-globbing": "^1.4.0",
|
||||||
"mocha": "3.3.0",
|
"mocha": "^2.4.5",
|
||||||
"sass": "0.5.0",
|
"sass": "^0.5.0",
|
||||||
"sinon": "2.2.0",
|
"sinon": "^1.14.1",
|
||||||
"sinon-chai": "2.10.0"
|
"sinon-chai": "^2.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"backbone": "1.3.3",
|
"backbone": "^1.2.3",
|
||||||
"body-parser": "1.17.1",
|
"body-parser": "^1.13.3",
|
||||||
"client-sessions": "0.8.0",
|
"client-sessions": "^0.7.0",
|
||||||
"cookie-parser": "1.4.3",
|
"cookie-parser": "^1.3.5",
|
||||||
"crafting-guide-common": ">=5.0.0",
|
"crafting-guide-common": "^1.5.1",
|
||||||
"dotenv": "4.0.0",
|
"express": "^4.13.3",
|
||||||
"express": "4.15.2",
|
"express-session": "^1.11.3",
|
||||||
"express-session": "1.15.2",
|
"jade": "^1.11.0",
|
||||||
"jade": "1.11.0",
|
"jquery": "^2.1.4",
|
||||||
"jquery": "3.2.1",
|
"marked": "^0.3.5",
|
||||||
"marked": "0.3.6",
|
"underscore": "^1.8.3",
|
||||||
"underscore": "1.8.3",
|
"underscore.inflections": "^0.2.1",
|
||||||
"underscore.inflections": "0.2.1",
|
"when": "^3.7.3"
|
||||||
"when": "3.7.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
clear && printf '\e[3J'
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
grunt watch &
|
|
||||||
|
|
||||||
pushd './build/' &>/dev/null
|
pushd './build/' &>/dev/null
|
||||||
clear
|
clear
|
||||||
nodemon ./server.coffee $*
|
nodemon ./server.coffee $*
|
||||||
|
|||||||
Binary file not shown.
+36
-51
@@ -1,93 +1,78 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - client.coffee
|
# Crafting Guide - client.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
############################################################################################################
|
############################################################################################################
|
||||||
|
|
||||||
# Allow Node.js-style `global` in addition to `window`
|
# Allow Node.js-style `global` in addition to `window`
|
||||||
if typeof(global) is "undefined"
|
if typeof(global) is 'undefined'
|
||||||
window.global = window
|
window.global = window
|
||||||
|
|
||||||
global._ = require "../common/underscore"
|
global._ = require '../common/underscore'
|
||||||
global.$ = require "jquery"
|
global.$ = require 'jquery'
|
||||||
global.c = require "../common/constants"
|
global.c = require '../common/constants'
|
||||||
global.π = Math.PI
|
global.π = Math.PI
|
||||||
global.ε = 0.0001
|
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
|
global.logger = new Logger
|
||||||
|
|
||||||
Tracker = require "./tracker"
|
marked = require 'marked'
|
||||||
global.tracker = new Tracker
|
|
||||||
|
|
||||||
marked = require "marked"
|
|
||||||
marked.setOptions sanitize:true
|
marked.setOptions sanitize:true
|
||||||
|
|
||||||
global.Backbone = Backbone = require "backbone"
|
|
||||||
Backbone.$ = $
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
global.hostName = window.location.hostname
|
global.hostName = window.location.hostname
|
||||||
switch global.hostName
|
switch global.hostName
|
||||||
when "prerender.crafting-guide.com"
|
when 'prerender.crafting-guide.com'
|
||||||
global.env = "prerender"
|
global.env = 'prerender'
|
||||||
logger.level = Logger.FATAL
|
logger.level = Logger.FATAL
|
||||||
apiBaseUrl = "http://prerender.crafting-guide.com:4347"
|
apiBaseUrl = 'http://local.crafting-guide.com:8001'
|
||||||
when "local.crafting-guide.com", "localhost"
|
when 'local.crafting-guide.com', 'localhost'
|
||||||
global.env = "local"
|
global.env = 'local'
|
||||||
logger.level = Logger.DEBUG
|
logger.level = Logger.DEBUG
|
||||||
apiBaseUrl = "http://#{global.hostName}:4347"
|
apiBaseUrl = 'http://local.crafting-guide.com:8001'
|
||||||
when "staging.crafting-guide.com"
|
when 'staging.crafting-guide.com'
|
||||||
global.env = "staging"
|
global.env = 'staging'
|
||||||
logger.level = Logger.VERBOSE
|
logger.level = Logger.VERBOSE
|
||||||
apiBaseUrl = "http://api-staging.crafting-guide.com"
|
apiBaseUrl = 'http://api-staging.crafting-guide.com'
|
||||||
when "crafting-guide.com"
|
when 'crafting-guide.com'
|
||||||
global.env = "production"
|
global.env = 'production'
|
||||||
logger.level = Logger.INFO
|
logger.level = Logger.INFO
|
||||||
tracker.enabled = true
|
apiBaseUrl = 'http://api.crafting-guide.com'
|
||||||
apiBaseUrl = "http://api.crafting-guide.com"
|
|
||||||
else
|
else
|
||||||
throw new Error "cannot determine the environment of: #{window.location.hostname}"
|
throw new Error "cannot determine the environment of: #{window.location.hostname}"
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
Storage = require "./storage"
|
Storage = require './storage'
|
||||||
storage = new Storage storage:global.localStorage
|
storage = new Storage storage:global.localStorage
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
tracker.trackPageView()
|
{CraftingGuideClient} = require 'crafting-guide-common'
|
||||||
|
|
||||||
{CraftingGuideClient} = require("crafting-guide-common").api
|
|
||||||
client = _(new CraftingGuideClient(baseUrl:apiBaseUrl)).extend Backbone.Events
|
client = _(new CraftingGuideClient(baseUrl:apiBaseUrl)).extend Backbone.Events
|
||||||
client.onStatusChanged = (client, oldStatus, newStatus)->
|
client.onStatusChanged = (client, oldStatus, newStatus)->
|
||||||
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
|
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
|
||||||
client.trigger "change:status", client, oldStatus, newStatus
|
client.trigger 'change:status', client, oldStatus, newStatus
|
||||||
client.trigger "change", client
|
client.trigger 'change', client
|
||||||
|
|
||||||
client.startMonitoringStatus()
|
client.checkStatus()
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
{http, ModPackStore, ItemDetailStore} = require("crafting-guide-common").api
|
SiteController = require './site/site_controller'
|
||||||
storeOptions = http:http, baseUrl:"#{location.protocol}//#{location.hostname}:#{location.port}"
|
global.site = site = new SiteController client:client, storage:storage
|
||||||
global.stores =
|
site.render()
|
||||||
modPack: new ModPackStore storeOptions
|
site.loadDefaultModPack()
|
||||||
itemDetail: new ItemDetailStore storeOptions
|
site.loadCurrentUser()
|
||||||
|
|
||||||
########################################################################################################################
|
Backbone.history.start pushState:true
|
||||||
|
logger.info -> "CraftingGuide is ready"
|
||||||
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"
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//-
|
//-
|
||||||
//- Crafting Guide - _google_analytics.jade
|
//- Crafting Guide - _google_analytics.jade
|
||||||
//-
|
//-
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
//- Copyright © 2014-2016 by Redwood Labs
|
||||||
//- All rights reserved.
|
//- All rights reserved.
|
||||||
//-
|
//-
|
||||||
|
|
||||||
@@ -15,6 +15,4 @@ script.
|
|||||||
ga('create', 'UA-54970416-2', 'crafting-guide.com');
|
ga('create', 'UA-54970416-2', 'crafting-guide.com');
|
||||||
ga('require', 'displayfeatures');
|
ga('require', 'displayfeatures');
|
||||||
ga('require', 'linkid', 'linkid.js');
|
ga('require', 'linkid', 'linkid.js');
|
||||||
} else {
|
|
||||||
window.ga = function() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//-
|
//-
|
||||||
//- Crafting Guide - _google_sitelinks.jade
|
//- Crafting Guide - _google_sitelinks.jade
|
||||||
//-
|
//-
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
//- Copyright © 2014-2016 by Redwood Labs
|
||||||
//- All rights reserved.
|
//- 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
|
# Crafting Guide - craft_page.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
{Inventory} = require("crafting-guide-common").models
|
BaseModel = require '../base_model'
|
||||||
{Observable} = require("crafting-guide-common").util
|
Craftsman = require '../crafting/craftsman'
|
||||||
{PlanBuilder} = require("crafting-guide-common").crafting
|
Inventory = require '../game/inventory'
|
||||||
{ResourcesEvaluator} = require("crafting-guide-common").crafting
|
ModPack = require '../game/mod_pack'
|
||||||
{StepsEvaluator} = require("crafting-guide-common").crafting
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
module.exports = class CraftPage extends Observable
|
module.exports = class CraftPage extends BaseModel
|
||||||
|
|
||||||
constructor: (attributes={})->
|
constructor: (attributes={}, options={})->
|
||||||
super
|
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
|
@modPack.on c.event.change, => @_consumeParams()
|
||||||
|
@on c.event.change + ':params', => @_consumeParams()
|
||||||
@_currentPlan = null
|
@craftsman.on c.event.change + ':stage', => @trigger c.event.change, this
|
||||||
@_have = new Inventory
|
@craftsman.on c.event.change + ':complete', => @trigger c.event.change, this
|
||||||
@_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
|
|
||||||
|
|
||||||
# Private Methods ##############################################################################
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
_consumeParams: (params)->
|
_consumeParams: ->
|
||||||
return unless params?.inventoryText?
|
return unless @params?
|
||||||
|
|
||||||
newWant = Inventory.fromUrlString params.inventoryText, @modPack
|
@craftsman.want.clear()
|
||||||
@_want.clear()
|
if not @params.inventoryText?
|
||||||
|
@params = null
|
||||||
|
else
|
||||||
|
inventory = new Inventory
|
||||||
|
inventory.parse @params.inventoryText
|
||||||
|
|
||||||
for itemId, stack of newWant.stacks
|
inventory.each (stack)=>
|
||||||
continue unless stack.item.isCraftable
|
item = @modPack.findItem stack.itemSlug, enableAsNeeded:true
|
||||||
@_want.add stack.item, stack.quantity
|
return unless item? and item.isCraftable
|
||||||
|
|
||||||
_onHaveChanged: ->
|
@craftsman.want.add stack.itemSlug, stack.quantity
|
||||||
@triggerPropertyChange "isDirty", @_isDirty, true
|
inventory.remove stack.itemSlug
|
||||||
|
|
||||||
_onWantChanged: ->
|
if inventory.isEmpty then @params = null
|
||||||
@triggerPropertyChange "isDirty", @_isDirty, true, ->
|
|
||||||
@_isDirty = true
|
|
||||||
if @want.isEmpty then @triggerPropertyChange "currentPlan", @_currentPlan, null
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - editable_file.coffee
|
# Crafting Guide - editable_file.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
{BaseModel} = require("crafting-guide-common").deprecated
|
BaseModel = require '../base_model'
|
||||||
w = require "when"
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - email_client.coffee
|
# Crafting Guide - email_client.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - file_cache.coffee
|
# Crafting Guide - file_cache.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
w = require "when"
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
module.exports = class FileCache extends Backbone.Events
|
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'
|
FileCache = require './file_cache'
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - github_user.coffee
|
# Crafting Guide - github_user.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# 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
|
# Crafting Guide - item_page.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
{Item} = require("crafting-guide-common").models
|
BaseModel = require '../base_model'
|
||||||
ItemDisplay = require "./item_display"
|
|
||||||
{Observable} = require("crafting-guide-common").util
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
module.exports = class ItemPage extends Observable
|
module.exports = class ItemPage extends BaseModel
|
||||||
|
|
||||||
constructor: (item)->
|
constructor: (attributes={}, options={})->
|
||||||
if item.constructor isnt Item then throw new Error "item must be an Item instance"
|
if not attributes.modPack? then throw new Error 'attributes.modPack is required'
|
||||||
@item = item
|
attributes.item ?= null
|
||||||
super
|
super attributes, options
|
||||||
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Property Methods #############################################################################
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
findComponentInItems: ->
|
findComponentInItems: ->
|
||||||
return @_findItemsWithMatchingRecipes (recipe)=>
|
return @_findRecipesMatching (recipe)=> recipe.requires @item.slug
|
||||||
recipe.computeQuantityRequired @item
|
|
||||||
|
|
||||||
findRecipes: ->
|
|
||||||
return (recipe for recipeId, recipe of @item.recipes)
|
|
||||||
|
|
||||||
findSimilarItems: ->
|
findSimilarItems: ->
|
||||||
group = @mod.itemGroups[@item.groupName]
|
return null unless @item?.modVersion?
|
||||||
return unless group?
|
|
||||||
|
|
||||||
return null unless group.length > 0
|
result = []
|
||||||
return group
|
@item.modVersion.eachItemInGroup @item.group, (item)=>
|
||||||
|
result.push item
|
||||||
|
|
||||||
findToolForItems: ->
|
return null unless result.length > 0
|
||||||
return @_findItemsWithMatchingRecipes (recipe)=>
|
return result
|
||||||
return recipe.tools[@item.id]?
|
|
||||||
|
|
||||||
loadDetails: ->
|
findRecipes: ->
|
||||||
stores.itemDetail.loadDetailFor @item
|
return @modPack.findRecipes @item?.slug, [], alwaysFromOwningMod:true
|
||||||
.catch (error)=>
|
|
||||||
if error.message.indexOf("404") is -1
|
findToolForRecipes: ->
|
||||||
logger.error "Could not load details for item #{@item.id}: #{error.message}"
|
return @_findRecipesMatching (recipe)=> recipe.requiresTool @item.slug
|
||||||
else
|
|
||||||
logger.info "Item #{@item.id} has no details file."
|
|
||||||
|
|
||||||
# Private Methods ##############################################################################
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
_findItemsWithMatchingRecipes: (isMatching)->
|
_findRecipesMatching: (isAcceptable)->
|
||||||
|
return null unless @item?
|
||||||
|
|
||||||
result = {}
|
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
|
result = _.values result
|
||||||
for itemId, item of mod.items
|
return null unless result.length > 0
|
||||||
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
|
|
||||||
|
|
||||||
|
return result.sort (a, b)-> a.compareTo b
|
||||||
|
|||||||
@@ -1,104 +1,89 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - item_selector.coffee
|
# Crafting Guide - item_selector.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# 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={})->
|
constructor: (attributes={}, options={})->
|
||||||
super
|
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
|
@_maxResults = 100
|
||||||
@_minHintLength = 3
|
@_minHintLength = 3
|
||||||
|
@_modPack = options.modPack
|
||||||
@_results = []
|
@_results = []
|
||||||
|
|
||||||
@modPack = modPack
|
|
||||||
|
|
||||||
# Property Methods #############################################################################
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
Object.defineProperties @prototype,
|
Object.defineProperties @prototype,
|
||||||
|
|
||||||
hint:
|
hint:
|
||||||
get: -> return @_hint
|
get: ->
|
||||||
set: (hint)->
|
return @_hint
|
||||||
@triggerPropertyChange "hint", @_hint, hint, ->
|
|
||||||
@_hint = hint?.toLowerCase()
|
|
||||||
@_refreshResults()
|
|
||||||
|
|
||||||
modPack:
|
set: (newHint)->
|
||||||
get: -> return @_modPack
|
oldHint = @_hint
|
||||||
set: (modPack)->
|
return if newHint is oldHint
|
||||||
if not modPack? then throw new Error "modPack is required"
|
|
||||||
if @_modPack? then throw new Error "modPack cannot be reassigned"
|
@_hint = newHint.toLowerCase()
|
||||||
@_modPack = modPack
|
|
||||||
|
@trigger c.event.change + ':hint', this, oldHint, newHint
|
||||||
|
@_refreshResults()
|
||||||
|
@trigger c.event.change, this
|
||||||
|
|
||||||
results:
|
results:
|
||||||
get: -> return @_results
|
get: ->
|
||||||
|
return @_results
|
||||||
|
|
||||||
# Private Methods ##############################################################################
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
_computeScore: (item)->
|
_isMatch: (name, itemSlug)->
|
||||||
hintIndex = 0
|
hintIndex = 0
|
||||||
hintLetter = @_hint[hintIndex]
|
hintLetter = @_hint[hintIndex]
|
||||||
name = item.displayName.toLowerCase()
|
name = name.toLowerCase()
|
||||||
nextScore = 1
|
|
||||||
totalScore = 0
|
|
||||||
|
|
||||||
for nameIndex in [0...name.length] by 1
|
for nameIndex in [0...name.length] by 1
|
||||||
if name.charAt(nameIndex) is hintLetter
|
if name.charAt(nameIndex) is hintLetter
|
||||||
totalScore += nextScore
|
|
||||||
nextScore += 1
|
|
||||||
hintIndex += 1
|
hintIndex += 1
|
||||||
if hintIndex is @_hint.length
|
if hintIndex is @_hint.length
|
||||||
return 0 unless @_isAcceptable item
|
item = @_modPack.findItem itemSlug
|
||||||
break
|
return false unless item?
|
||||||
|
return @_isAcceptable item
|
||||||
hintLetter = @_hint[hintIndex]
|
hintLetter = @_hint[hintIndex]
|
||||||
else
|
|
||||||
nextScore = 1
|
|
||||||
|
|
||||||
return 0 if hintIndex < @_hint.length
|
return false
|
||||||
return totalScore
|
|
||||||
|
|
||||||
_refreshResults: ->
|
_refreshResults: ->
|
||||||
scoredItems = []
|
oldResults = @_results
|
||||||
|
newResults = {}
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
|
logger.verbose => "Looking for items which match: #{@_hint}"
|
||||||
if @_hint.length >= @_minHintLength
|
if @_hint.length >= @_minHintLength
|
||||||
for modId, mod of @_modPack.mods
|
@_modPack.eachMod (mod)=>
|
||||||
return if count >= @_maxResults
|
return if count >= @_maxResults
|
||||||
return unless mod.isEnabled
|
return unless mod.enabled
|
||||||
|
|
||||||
for itemId, item of mod.items
|
mod.eachName (name, itemSlug)=>
|
||||||
score = @_computeScore item
|
return if count >= @_maxResults
|
||||||
if score > 0
|
|
||||||
scoredItems.push score:score, item:item
|
|
||||||
|
|
||||||
scoredItems.sort (a, b)->
|
if @_isMatch name, itemSlug
|
||||||
if a.score isnt b.score
|
if itemSlug.isQualified
|
||||||
return if a.score > b.score then -1 else +1
|
newResults[itemSlug] = itemSlug
|
||||||
|
count += 1
|
||||||
|
|
||||||
nameA = a.item.displayName
|
newResults = _.values(newResults)
|
||||||
nameB = b.item.displayName
|
newResults.sort ItemSlug.compare
|
||||||
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
|
|
||||||
|
|
||||||
|
@_results = newResults
|
||||||
|
@trigger c.event.change + ':results', this, oldResults, newResults
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - markdown_image.coffee
|
# Crafting Guide - markdown_image.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - markdown_image_list.coffee
|
# Crafting Guide - markdown_image_list.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
{Observable} = require("crafting-guide-common").util
|
BaseModel = require '../base_model'
|
||||||
MarkdownImage = require './markdown_image'
|
MarkdownImage = require './markdown_image'
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
module.exports = class MarkdownImageList extends Observable
|
module.exports = class MarkdownImageList extends BaseModel
|
||||||
|
|
||||||
constructor: (options={})->
|
constructor: (attributes={}, options={})->
|
||||||
super
|
attributes.imageBase ?= ''
|
||||||
|
attributes.markdownText ?= null
|
||||||
|
super attributes, options
|
||||||
|
|
||||||
@_images = []
|
@client = options.client
|
||||||
@muted =>
|
@_images = {}
|
||||||
@client = options.client
|
|
||||||
@imageBase = options.imageBase
|
|
||||||
@markdownText = options.markdownText
|
|
||||||
|
|
||||||
|
@on c.event.change + ':imageBase', =>
|
||||||
|
for fileName, image of @_images
|
||||||
|
image.path = @imageBase
|
||||||
|
|
||||||
|
@on c.event.change + ':markdownText', => @_analyzeMarkdownText()
|
||||||
@_analyzeMarkdownText()
|
@_analyzeMarkdownText()
|
||||||
|
|
||||||
# Public Methods ###############################################################################
|
# Public Methods ###############################################################################
|
||||||
@@ -42,43 +46,22 @@ module.exports = class MarkdownImageList extends Observable
|
|||||||
|
|
||||||
# Property Methods #############################################################################
|
# 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,
|
Object.defineProperties @prototype,
|
||||||
|
all: {get:@prototype.getAll}
|
||||||
all:
|
valid: {get:@prototype.isValid}
|
||||||
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
|
|
||||||
|
|
||||||
# Private Methods ##############################################################################
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
@@ -96,8 +79,8 @@ module.exports = class MarkdownImageList extends Observable
|
|||||||
image = @_images[fileName]
|
image = @_images[fileName]
|
||||||
if not image?
|
if not image?
|
||||||
image = new MarkdownImage {fileName:fileName, path:@imageBase}, {client:@client}
|
image = new MarkdownImage {fileName:fileName, path:@imageBase}, {client:@client}
|
||||||
image.on Observable::CHANGE, this, "_onImageChanged"
|
@listenTo image, c.event.change, => @trigger c.event.change, this
|
||||||
@trigger Observable::ADD, image
|
@trigger c.event.add, this, image
|
||||||
changed = true
|
changed = true
|
||||||
|
|
||||||
newImages[fileName] = image
|
newImages[fileName] = image
|
||||||
@@ -109,7 +92,4 @@ module.exports = class MarkdownImageList extends Observable
|
|||||||
|
|
||||||
if changed
|
if changed
|
||||||
@_images = newImages
|
@_images = newImages
|
||||||
@trigger Observable::CHANGE
|
@trigger c.event.change, this
|
||||||
|
|
||||||
_onImageChanged: ->
|
|
||||||
@trigger Observable::CHANGE
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - markdown_image_list.test.coffee
|
# Crafting Guide - markdown_image_list.test.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ list = null
|
|||||||
describe 'markdown_image_list.coffee', ->
|
describe 'markdown_image_list.coffee', ->
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
list = new MarkdownImageList client:{fetchFile:sinon.stub().returns(w.promise(->))}
|
list = new MarkdownImageList {}, client:{fetchFile:sinon.stub().returns(w.promise(->))}
|
||||||
|
|
||||||
describe '_analyzeMarkdownText', ->
|
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
|
# Crafting Guide - base_controller.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# 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()
|
@off()
|
||||||
|
|
||||||
routeLinkClick: (event)->
|
routeLinkClick: (event)->
|
||||||
$link = $(event.target)
|
event.preventDefault()
|
||||||
if not $link.attr('href')?
|
href = $(event.target).attr 'href'
|
||||||
$link = $(event.currentTarget)
|
href ?= $(event.currentTarget).attr 'href'
|
||||||
|
logger.info "Re-routing link to internal navigation: #{href}"
|
||||||
href = $link.attr 'href'
|
|
||||||
target = $link.attr 'target'
|
|
||||||
|
|
||||||
logger.info -> "Re-routing link to internal navigation: #{href}"
|
|
||||||
|
|
||||||
if href? and href.match /^http/
|
if href? and href.match /^http/
|
||||||
tracker.trackEvent c.tracking.category.navigate, 'external-link', href
|
window.location.href = href
|
||||||
.then -> window.location.href = href
|
|
||||||
return false
|
|
||||||
else if target?
|
|
||||||
tracker.trackEvent c.tracking.category.navigate, 'external-link', href
|
|
||||||
return true
|
|
||||||
else
|
else
|
||||||
tracker.trackEvent c.tracking.category.navigate, 'internal-link', href
|
|
||||||
@router.navigate href, trigger:true
|
@router.navigate href, trigger:true
|
||||||
return false
|
|
||||||
|
|
||||||
show: ($el)->
|
show: ($el)->
|
||||||
$el ?= @$el
|
$el ?= @$el
|
||||||
@@ -85,6 +73,9 @@ module.exports = class BaseController extends Backbone.View
|
|||||||
onDidModelChange: ->
|
onDidModelChange: ->
|
||||||
@tryRefresh()
|
@tryRefresh()
|
||||||
|
|
||||||
|
onDidModelSync: ->
|
||||||
|
@tryRefresh()
|
||||||
|
|
||||||
onWillRender: -> # do nothing
|
onWillRender: -> # do nothing
|
||||||
|
|
||||||
onDidRender: -> # do nothing
|
onDidRender: -> # do nothing
|
||||||
@@ -94,12 +85,11 @@ module.exports = class BaseController extends Backbone.View
|
|||||||
onDidShow: -> # do nothing
|
onDidShow: -> # do nothing
|
||||||
|
|
||||||
onWillChangeModel: (oldModel, newModel)->
|
onWillChangeModel: (oldModel, newModel)->
|
||||||
if oldModel?.isObservable
|
if oldModel?.on?
|
||||||
oldModel.off target:this
|
@stopListening oldModel
|
||||||
|
if newModel?.on?
|
||||||
if newModel?.isObservable
|
@listenTo newModel, 'sync', (e)=> @onDidModelSync e
|
||||||
newModel.on Observable::ANY, this, "onDidModelChange"
|
@listenTo newModel, 'change', (e)=> @onDidModelChange e
|
||||||
|
|
||||||
return newModel
|
return newModel
|
||||||
|
|
||||||
# Property Methods #############################################################################
|
# Property Methods #############################################################################
|
||||||
@@ -112,7 +102,7 @@ module.exports = class BaseController extends Backbone.View
|
|||||||
|
|
||||||
newModel = @onWillChangeModel @_model, newModel
|
newModel = @onWillChangeModel @_model, newModel
|
||||||
@_model = newModel
|
@_model = newModel
|
||||||
@onDidModelChange()
|
@tryRefresh()
|
||||||
|
|
||||||
rendered:
|
rendered:
|
||||||
get: -> @_rendered
|
get: -> @_rendered
|
||||||
@@ -146,13 +136,9 @@ module.exports = class BaseController extends Backbone.View
|
|||||||
|
|
||||||
@$el.append $renderedEl.children()
|
@$el.append $renderedEl.children()
|
||||||
@$el.addClass $renderedEl.attr 'class'
|
@$el.addClass $renderedEl.attr 'class'
|
||||||
|
@$el.data $renderedEl.data()
|
||||||
elementData = $renderedEl.data()
|
|
||||||
elementData.controller = this
|
|
||||||
elementData.model = data
|
|
||||||
@$el.data elementData
|
|
||||||
|
|
||||||
@delegateEvents()
|
@delegateEvents()
|
||||||
|
|
||||||
@_rendered = true
|
@_rendered = true
|
||||||
@onDidRender()
|
@onDidRender()
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
//-
|
//-
|
||||||
//- Crafting Guide - browse_page.jade
|
//- Crafting Guide - browse_page.jade
|
||||||
//-
|
//-
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
//- Copyright © 2014-2016 by Redwood Labs
|
||||||
//- All rights reserved.
|
//- All rights reserved.
|
||||||
//-
|
//-
|
||||||
|
|
||||||
.content.view__browse_page
|
.content.view__browse_page
|
||||||
.left
|
.left
|
||||||
include ../common/youtube/youtube.jade
|
|
||||||
|
|
||||||
.right
|
.right
|
||||||
.tile_container
|
.tile_container
|
||||||
section.view__mod_ballot
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// Crafting Guide - browse_page.scss
|
// Crafting Guide - browse_page.scss
|
||||||
//
|
//
|
||||||
// Copyright © 2014-2017 by Redwood Labs
|
// Copyright © 2014-2016 by Redwood Labs
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
@@ -10,11 +10,8 @@
|
|||||||
|
|
||||||
.right {
|
.right {
|
||||||
.tile_container {
|
.tile_container {
|
||||||
margin-bottom : $size-margin-large;
|
display : flex;
|
||||||
|
flex-wrap : wrap;
|
||||||
display : flex;
|
|
||||||
flex-wrap : wrap;
|
|
||||||
margin-right : -12px;
|
|
||||||
|
|
||||||
.view__mod_tile {
|
.view__mod_tile {
|
||||||
flex : 1 1 calc(50% - 12px); // $size-margin-large / 2
|
flex : 1 1 calc(50% - 12px); // $size-margin-large / 2
|
||||||
@@ -29,16 +26,14 @@
|
|||||||
.view__browse_page {
|
.view__browse_page {
|
||||||
.right {
|
.right {
|
||||||
.tile_container {
|
.tile_container {
|
||||||
margin-right : 0;
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
flex-direction : column;
|
align-items: stretch;
|
||||||
flex-wrap : nowrap;
|
|
||||||
align-items : stretch;
|
|
||||||
|
|
||||||
.view__mod_tile {
|
.view__mod_tile {
|
||||||
flex : 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-right : 0;
|
margin-right: 0;
|
||||||
width : 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,25 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - browse_page_controller.coffee
|
# Crafting Guide - browse_page_controller.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
{CraftingGuideClient} = require('crafting-guide-common').api
|
ModTileController = require './mod_tile/mod_tile_controller'
|
||||||
ModBallotController = require './mod_ballot/mod_ballot_controller'
|
PageController = require '../page_controller'
|
||||||
ModTileController = require './mod_tile/mod_tile_controller'
|
|
||||||
PageController = require '../page_controller'
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
module.exports = class BrowsePageController extends PageController
|
module.exports = class BrowsePageController extends PageController
|
||||||
|
|
||||||
constructor: (options={})->
|
constructor: (options={})->
|
||||||
if not options.client then throw new Error 'options.client is required'
|
if not options.modPack then throw new Error 'options.modPack 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'
|
|
||||||
|
|
||||||
options.templateName = 'browse_page'
|
options.templateName = 'browse_page'
|
||||||
super options
|
super options
|
||||||
|
|
||||||
@_client = options.client
|
|
||||||
@_imageLoader = options.imageLoader
|
|
||||||
@_modPack = options.modPack
|
@_modPack = options.modPack
|
||||||
@_tileControllers = []
|
@_tileControllers = []
|
||||||
|
|
||||||
@_client.on c.event.change, => @tryRefresh()
|
|
||||||
|
|
||||||
# Property Methods #############################################################################
|
|
||||||
|
|
||||||
Object.defineProperties @prototype,
|
|
||||||
|
|
||||||
user:
|
|
||||||
get: -> @_user
|
|
||||||
|
|
||||||
set: (newUser)->
|
|
||||||
@_user = newUser
|
|
||||||
@tryRefresh()
|
|
||||||
|
|
||||||
# PageController Overrides #####################################################################
|
# PageController Overrides #####################################################################
|
||||||
|
|
||||||
getMetaDescription: ->
|
getMetaDescription: ->
|
||||||
@@ -51,17 +31,10 @@ module.exports = class BrowsePageController extends PageController
|
|||||||
# BaseController Overrides #####################################################################
|
# BaseController Overrides #####################################################################
|
||||||
|
|
||||||
onDidRender: ->
|
onDidRender: ->
|
||||||
@_modBallotController = @addChild ModBallotController, '.view__mod_ballot',
|
|
||||||
client: @_client
|
|
||||||
imageLoader: @_imageLoader
|
|
||||||
user: @_user
|
|
||||||
|
|
||||||
@$tileContainer = @$('.tile_container')
|
@$tileContainer = @$('.tile_container')
|
||||||
|
|
||||||
refresh: ->
|
refresh: ->
|
||||||
@_modBallotController.user = @user
|
|
||||||
@_refreshModTiles()
|
@_refreshModTiles()
|
||||||
@_refreshBallot()
|
|
||||||
super
|
super
|
||||||
|
|
||||||
# Backbone.View Overrides ######################################################################
|
# Backbone.View Overrides ######################################################################
|
||||||
@@ -72,15 +45,9 @@ module.exports = class BrowsePageController extends PageController
|
|||||||
|
|
||||||
# Private Methods ##############################################################################
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
_refreshBallot: ->
|
|
||||||
if @_client.status is CraftingGuideClient.Status.Down
|
|
||||||
@_modBallotController.hide()
|
|
||||||
else
|
|
||||||
@_modBallotController.show()
|
|
||||||
|
|
||||||
_refreshModTiles: ->
|
_refreshModTiles: ->
|
||||||
index = 0
|
index = 0
|
||||||
mods = (mod for modId, mod of @_modPack.mods)
|
mods = @_modPack.getAllMods()
|
||||||
for mod in mods
|
for mod in mods
|
||||||
controller = @_tileControllers[index]
|
controller = @_tileControllers[index]
|
||||||
if not controller
|
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
|
//- Crafting Guide - mod_tile.jade
|
||||||
//-
|
//-
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
//- Copyright © 2014-2016 by Redwood Labs
|
||||||
//- All rights reserved.
|
//- All rights reserved.
|
||||||
//-
|
//-
|
||||||
|
|
||||||
.view__mod_tile
|
.view__mod_tile
|
||||||
.tile
|
a
|
||||||
a
|
.left
|
||||||
.left
|
img
|
||||||
img
|
.right
|
||||||
.right
|
p.title
|
||||||
p.title
|
p.description
|
||||||
p.description
|
|
||||||
|
|
||||||
.view__mod_version_selector
|
|
||||||
|
|||||||
@@ -1,60 +1,53 @@
|
|||||||
//
|
//
|
||||||
// Crafting Guide - mod_tile.scss
|
// Crafting Guide - mod_tile.scss
|
||||||
//
|
//
|
||||||
// Copyright © 2014-2017 by Redwood Labs
|
// Copyright © 2014-2016 by Redwood Labs
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
.view__mod_tile {
|
.view__mod_tile {
|
||||||
|
min-width : $size-minecraft-block * 9;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.tile {
|
a {
|
||||||
|
@include floating-panel;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
||||||
@include floating-panel;
|
border : $size-border-medium solid $color-background-panel;
|
||||||
min-width : $size-minecraft-block * 9;
|
color : $color-text;
|
||||||
|
text-decoration : none;
|
||||||
|
|
||||||
display: flex;
|
display : flex;
|
||||||
flex-direction: column;
|
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 {
|
img {
|
||||||
flex: 1 1 auto;
|
width: $size-minecraft-block * 4;
|
||||||
color : $color-text;
|
max-height: $size-minecraft-block * 4;
|
||||||
text-decoration : none;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
display : flex;
|
& > .right {
|
||||||
padding : $size-margin-medium;
|
flex : 1 1 100%;
|
||||||
|
|
||||||
& > .left {
|
overflow : hidden;
|
||||||
flex: 0 0 $size-minecraft-block * 4;
|
|
||||||
margin-right: $size-margin-medium;
|
|
||||||
|
|
||||||
img {
|
.title {
|
||||||
width: $size-minecraft-block * 4;
|
max-width: 0;
|
||||||
max-height: $size-minecraft-block * 4;
|
margin-bottom : $size-margin-medium;
|
||||||
}
|
|
||||||
|
font-family : $font-family-header;
|
||||||
|
font-size : $font-size-xlarge;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .right {
|
.description {
|
||||||
flex : 1 1 100%;
|
font-family : $font-family-text;
|
||||||
|
font-size : $font-size-medium;
|
||||||
overflow : hidden;
|
line-height : $font-size-medium * 1.5;
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,18 +55,13 @@
|
|||||||
background: $color-blue-faint;
|
background: $color-blue-faint;
|
||||||
border-color: $color-link-normal;
|
border-color: $color-link-normal;
|
||||||
|
|
||||||
& a > .right {
|
& > .right {
|
||||||
.title {
|
.title {
|
||||||
color: $color-link-normal;
|
color: $color-link-normal;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.view__mod_version_selector {
|
|
||||||
margin: 0;
|
|
||||||
padding: $size-margin-small $size-margin-medium;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - mod_tile_controller.coffee
|
# Crafting Guide - mod_tile_controller.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
BaseController = require '../../base_controller'
|
BaseController = require '../../base_controller'
|
||||||
ModVersionSelectorController = require '../../common/mod_version_selector/mod_version_selector_controller'
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
@@ -20,8 +19,6 @@ module.exports = class ModTileController extends BaseController
|
|||||||
# BaseController Overrides #####################################################################
|
# BaseController Overrides #####################################################################
|
||||||
|
|
||||||
onDidRender: ->
|
onDidRender: ->
|
||||||
@modVersionSelector = @addChild ModVersionSelectorController, '.view__mod_version_selector', model:@model
|
|
||||||
|
|
||||||
@$link = @$('a')
|
@$link = @$('a')
|
||||||
@$logoImage = @$('img')
|
@$logoImage = @$('img')
|
||||||
@$title = @$('p.title')
|
@$title = @$('p.title')
|
||||||
@@ -29,12 +26,12 @@ module.exports = class ModTileController extends BaseController
|
|||||||
super
|
super
|
||||||
|
|
||||||
refresh: ->
|
refresh: ->
|
||||||
@$link.attr 'href', c.url.mod modId:@model.id
|
@$link.attr 'href', c.url.mod modSlug:@model.slug
|
||||||
@$logoImage.attr 'src', c.url.modIcon modId:@model.id
|
@$logoImage.attr 'src', c.url.modIcon modSlug:@model.slug
|
||||||
@$title.text @model.displayName
|
@$title.text @model.name
|
||||||
@$description.text @model.description
|
@$description.text @model.description
|
||||||
|
|
||||||
if @model.isEnabled
|
if @model.enabled
|
||||||
@$el.removeClass 'disabled'
|
@$el.removeClass 'disabled'
|
||||||
else
|
else
|
||||||
@$el.addClass 'disabled'
|
@$el.addClass 'disabled'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//-
|
//-
|
||||||
//- Crafting Guide - adsense.jade
|
//- Crafting Guide - adsense.jade
|
||||||
//-
|
//-
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
//- Copyright © 2014-2016 by Redwood Labs
|
||||||
//- All rights reserved.
|
//- All rights reserved.
|
||||||
//-
|
//-
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// Crafting Guide - adsense.scss
|
// Crafting Guide - adsense.scss
|
||||||
//
|
//
|
||||||
// Copyright © 2014-2017 by Redwood Labs
|
// Copyright © 2014-2016 by Redwood Labs
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# Crafting Guide - adsense_controller.coffee
|
# Crafting Guide - adsense_controller.coffee
|
||||||
#
|
#
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ module.exports = class AdsenseController
|
|||||||
maxAds = c.adsense[adType].slotIds.length - @_adsPlaced
|
maxAds = c.adsense[adType].slotIds.length - @_adsPlaced
|
||||||
return if maxAds is 0
|
return if maxAds is 0
|
||||||
|
|
||||||
|
logger.verbose => "Looking to place #{maxAds} more #{adType} ads..."
|
||||||
|
|
||||||
if adType is 'skyscraper'
|
if adType is 'skyscraper'
|
||||||
placedNewAd = @_createSkyscraperPositions()
|
placedNewAd = @_createSkyscraperPositions()
|
||||||
else
|
else
|
||||||
@@ -140,7 +142,7 @@ module.exports = class AdsenseController
|
|||||||
adCount = @_computeAdCount @_computeAdType()
|
adCount = @_computeAdCount @_computeAdType()
|
||||||
|
|
||||||
if adCount is 0
|
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
|
@_waiting = true
|
||||||
_.delay (=> @_waiting = false; @_waitForPageReadiness()), c.adsense.readinessCheckInterval
|
_.delay (=> @_waiting = false; @_waitForPageReadiness()), c.adsense.readinessCheckInterval
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
//-
|
|
||||||
//- Crafting Guide - amazon.jade
|
|
||||||
//-
|
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
|
||||||
//- All rights reserved.
|
|
||||||
//-
|
|
||||||
|
|
||||||
#amzn_assoc_ad_div_adunit0_0
|
|
||||||
script(src="//z-na.amazon-adsystem.com/widgets/onejs?MarketPlace=US&adInstanceId=357573bb-98d8-4294-8cdf-4361dbf11cee")
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Crafting Guide - amazon.scss
|
|
||||||
//
|
|
||||||
// Copyright © 2014-2017 by Redwood Labs
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
.view__amazon {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//
|
//
|
||||||
// Crafting Guide - crafting_grid.scss
|
// Crafting Guide - crafting_grid.scss
|
||||||
//
|
//
|
||||||
// Copyright © 2014-2017 by Redwood Labs
|
// Copyright © 2014-2016 by Redwood Labs
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user