Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ddbd5cda7 | ||
|
|
31f6e12844 | ||
|
|
14278c81df |
@@ -1,7 +1,5 @@
|
|||||||
*.log
|
*.log
|
||||||
*.swp
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
|
||||||
.sass-cache
|
.sass-cache
|
||||||
.tm_properties
|
.tm_properties
|
||||||
aws.config
|
aws.config
|
||||||
|
|||||||
+44
-18
@@ -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)->
|
||||||
@@ -31,28 +54,30 @@ module.exports = (grunt)->
|
|||||||
mode: 'gzip'
|
mode: 'gzip'
|
||||||
files: [
|
files: [
|
||||||
{expand: true, cwd:'./build/static', src:'**/*.css', dest:'./dist/'}
|
{expand: true, cwd:'./build/static', src:'**/*.css', dest:'./dist/'}
|
||||||
{expand: true, cwd:'./build/static', src:['**/*.html', '!**/index.html'], dest:'./dist/'}
|
{expand: true, cwd:'./build/static', src:'**/*.html', dest:'./dist/'}
|
||||||
{expand: true, cwd:'./build/static', src:'**/*.ugly.js', dest:'./dist/', ext: '.js'}
|
{expand: true, cwd:'./build/static', src:'**/*.ugly.js', dest:'./dist/', ext: '.js'}
|
||||||
{expand: true, cwd:'./build/static', src:'**/*.json', dest:'./dist/'}
|
{expand: true, cwd:'./build/static', src:'**/*.json', dest:'./dist/'}
|
||||||
{expand: true, cwd:'./build/static', src:'**/*.ttf', dest:'./dist/'}
|
{expand: true, cwd:'./build/static', src:'**/*.ttf', dest:'./dist/'}
|
||||||
{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, cwd:'./build/static/', src:['**/*', '!**/*.map', '!**/*.js'], dest:'./dist'}
|
||||||
cwd: './build/static/'
|
|
||||||
src: ['**/*', '!**/*.map', '!**/*.js', '!index.html']
|
|
||||||
dest: './dist'
|
|
||||||
]
|
]
|
||||||
common_source:
|
common_source:
|
||||||
files: [
|
files: [
|
||||||
@@ -98,9 +123,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'
|
||||||
@@ -110,8 +135,6 @@ module.exports = (grunt)->
|
|||||||
|
|
||||||
sass:
|
sass:
|
||||||
all:
|
all:
|
||||||
options:
|
|
||||||
sourcemap: 'none'
|
|
||||||
files:
|
files:
|
||||||
'./build/static/main.css': [ './build/imports.scss' ]
|
'./build/static/main.css': [ './build/imports.scss' ]
|
||||||
|
|
||||||
@@ -164,7 +187,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 +207,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 +225,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 +268,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,6 +1,8 @@
|
|||||||
# Crafting Guide
|
# Crafting Guide
|
||||||
|
|
||||||
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
|
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From the your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
|
||||||
|
|
||||||
|
**Check out the [Trello Board](https://trello.com/b/s9N0aTYd/crafting-guide) to see what's coming up!**
|
||||||
|
|
||||||
## Adding new features
|
## Adding new features
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
google-site-verification: google077275160ac09f2f.html
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 267 B |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 723 B |
Binary file not shown.
|
Before Width: | Height: | Size: 300 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.2",
|
||||||
"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 $*
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>ActiveLayerIndex</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>ApplicationVersion</key>
|
|
||||||
<array>
|
|
||||||
<string>com.omnigroup.OmniGraffle.MacAppStore</string>
|
|
||||||
<string>139.18</string>
|
|
||||||
</array>
|
|
||||||
<key>AutoAdjust</key>
|
|
||||||
<false/>
|
|
||||||
<key>BackgroundGraphic</key>
|
|
||||||
<dict>
|
|
||||||
<key>Bounds</key>
|
|
||||||
<string>{{0, 0}, {320, 320}}</string>
|
|
||||||
<key>Class</key>
|
|
||||||
<string>SolidGraphic</string>
|
|
||||||
<key>ID</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
<key>Style</key>
|
|
||||||
<dict>
|
|
||||||
<key>shadow</key>
|
|
||||||
<dict>
|
|
||||||
<key>Draws</key>
|
|
||||||
<string>NO</string>
|
|
||||||
</dict>
|
|
||||||
<key>stroke</key>
|
|
||||||
<dict>
|
|
||||||
<key>Draws</key>
|
|
||||||
<string>NO</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>BaseZoom</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>CanvasOrigin</key>
|
|
||||||
<string>{0, 0}</string>
|
|
||||||
<key>CanvasSize</key>
|
|
||||||
<string>{320, 320}</string>
|
|
||||||
<key>ColumnAlign</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>ColumnSpacing</key>
|
|
||||||
<real>36</real>
|
|
||||||
<key>CreationDate</key>
|
|
||||||
<string>2016-04-23 05:39:56 +0000</string>
|
|
||||||
<key>Creator</key>
|
|
||||||
<string>Andrew Miner</string>
|
|
||||||
<key>DisplayScale</key>
|
|
||||||
<string>1 pt = 1 px</string>
|
|
||||||
<key>GraphDocumentVersion</key>
|
|
||||||
<integer>8</integer>
|
|
||||||
<key>GraphicsList</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>AllowConnections</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>AllowLabelDrop</key>
|
|
||||||
<false/>
|
|
||||||
<key>AllowToConnect</key>
|
|
||||||
<false/>
|
|
||||||
<key>Class</key>
|
|
||||||
<string>LineGraphic</string>
|
|
||||||
<key>ID</key>
|
|
||||||
<integer>5</integer>
|
|
||||||
<key>Points</key>
|
|
||||||
<array>
|
|
||||||
<string>{160, 480}</string>
|
|
||||||
<string>{480, 160}</string>
|
|
||||||
</array>
|
|
||||||
<key>Style</key>
|
|
||||||
<dict>
|
|
||||||
<key>stroke</key>
|
|
||||||
<dict>
|
|
||||||
<key>Cap</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>Color</key>
|
|
||||||
<dict>
|
|
||||||
<key>a</key>
|
|
||||||
<string>0.2</string>
|
|
||||||
<key>b</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>g</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>r</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>
|
|
||||||
<key>HeadArrow</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>Legacy</key>
|
|
||||||
<true/>
|
|
||||||
<key>TailArrow</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>Width</key>
|
|
||||||
<real>120</real>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>AllowConnections</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>AllowLabelDrop</key>
|
|
||||||
<false/>
|
|
||||||
<key>AllowToConnect</key>
|
|
||||||
<false/>
|
|
||||||
<key>Class</key>
|
|
||||||
<string>LineGraphic</string>
|
|
||||||
<key>ID</key>
|
|
||||||
<integer>4</integer>
|
|
||||||
<key>Points</key>
|
|
||||||
<array>
|
|
||||||
<string>{-160, 160}</string>
|
|
||||||
<string>{160, -160}</string>
|
|
||||||
</array>
|
|
||||||
<key>Style</key>
|
|
||||||
<dict>
|
|
||||||
<key>stroke</key>
|
|
||||||
<dict>
|
|
||||||
<key>Cap</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>Color</key>
|
|
||||||
<dict>
|
|
||||||
<key>a</key>
|
|
||||||
<string>0.2</string>
|
|
||||||
<key>b</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>g</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>r</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>
|
|
||||||
<key>HeadArrow</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>Legacy</key>
|
|
||||||
<true/>
|
|
||||||
<key>TailArrow</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>Width</key>
|
|
||||||
<real>120</real>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>AllowConnections</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>AllowLabelDrop</key>
|
|
||||||
<false/>
|
|
||||||
<key>AllowToConnect</key>
|
|
||||||
<false/>
|
|
||||||
<key>Class</key>
|
|
||||||
<string>LineGraphic</string>
|
|
||||||
<key>ID</key>
|
|
||||||
<integer>3</integer>
|
|
||||||
<key>Points</key>
|
|
||||||
<array>
|
|
||||||
<string>{0, 320}</string>
|
|
||||||
<string>{320, 0}</string>
|
|
||||||
</array>
|
|
||||||
<key>Style</key>
|
|
||||||
<dict>
|
|
||||||
<key>stroke</key>
|
|
||||||
<dict>
|
|
||||||
<key>Cap</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>Color</key>
|
|
||||||
<dict>
|
|
||||||
<key>a</key>
|
|
||||||
<string>0.2</string>
|
|
||||||
<key>b</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>g</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>r</key>
|
|
||||||
<string>1</string>
|
|
||||||
</dict>
|
|
||||||
<key>HeadArrow</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>Legacy</key>
|
|
||||||
<true/>
|
|
||||||
<key>TailArrow</key>
|
|
||||||
<string>0</string>
|
|
||||||
<key>Width</key>
|
|
||||||
<real>120</real>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>GridInfo</key>
|
|
||||||
<dict>
|
|
||||||
<key>GridSpacing</key>
|
|
||||||
<real>10</real>
|
|
||||||
<key>MajorGridSpacing</key>
|
|
||||||
<integer>10</integer>
|
|
||||||
<key>ShowsGrid</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>SnapsToGrid</key>
|
|
||||||
<string>YES</string>
|
|
||||||
</dict>
|
|
||||||
<key>GuidesLocked</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>GuidesVisible</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>HPages</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>ImageCounter</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>KeepToScale</key>
|
|
||||||
<false/>
|
|
||||||
<key>Layers</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>Lock</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>Name</key>
|
|
||||||
<string>Layer 1</string>
|
|
||||||
<key>Print</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>View</key>
|
|
||||||
<string>YES</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>LayoutInfo</key>
|
|
||||||
<dict>
|
|
||||||
<key>Animate</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>circoMinDist</key>
|
|
||||||
<real>18</real>
|
|
||||||
<key>circoSeparation</key>
|
|
||||||
<real>0.0</real>
|
|
||||||
<key>layoutEngine</key>
|
|
||||||
<string>dot</string>
|
|
||||||
<key>neatoSeparation</key>
|
|
||||||
<real>0.0</real>
|
|
||||||
<key>twopiSeparation</key>
|
|
||||||
<real>0.0</real>
|
|
||||||
</dict>
|
|
||||||
<key>LinksVisible</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>MagnetsVisible</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>MasterSheets</key>
|
|
||||||
<array/>
|
|
||||||
<key>ModificationDate</key>
|
|
||||||
<string>2016-04-27 03:11:38 +0000</string>
|
|
||||||
<key>Modifier</key>
|
|
||||||
<string>Andrew Miner</string>
|
|
||||||
<key>NotesVisible</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>Orientation</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
<key>OriginVisible</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>PageBreaks</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>PrintInfo</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSBottomMargin</key>
|
|
||||||
<array>
|
|
||||||
<string>float</string>
|
|
||||||
<string>41</string>
|
|
||||||
</array>
|
|
||||||
<key>NSHorizonalPagination</key>
|
|
||||||
<array>
|
|
||||||
<string>coded</string>
|
|
||||||
<string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
|
|
||||||
</array>
|
|
||||||
<key>NSLeftMargin</key>
|
|
||||||
<array>
|
|
||||||
<string>float</string>
|
|
||||||
<string>18</string>
|
|
||||||
</array>
|
|
||||||
<key>NSPaperSize</key>
|
|
||||||
<array>
|
|
||||||
<string>size</string>
|
|
||||||
<string>{611.99998474121094, 792}</string>
|
|
||||||
</array>
|
|
||||||
<key>NSPrintReverseOrientation</key>
|
|
||||||
<array>
|
|
||||||
<string>int</string>
|
|
||||||
<string>0</string>
|
|
||||||
</array>
|
|
||||||
<key>NSRightMargin</key>
|
|
||||||
<array>
|
|
||||||
<string>float</string>
|
|
||||||
<string>18</string>
|
|
||||||
</array>
|
|
||||||
<key>NSTopMargin</key>
|
|
||||||
<array>
|
|
||||||
<string>float</string>
|
|
||||||
<string>18</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
<key>PrintOnePage</key>
|
|
||||||
<false/>
|
|
||||||
<key>ReadOnly</key>
|
|
||||||
<string>NO</string>
|
|
||||||
<key>RowAlign</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>RowSpacing</key>
|
|
||||||
<real>36</real>
|
|
||||||
<key>SheetTitle</key>
|
|
||||||
<string>Canvas 1</string>
|
|
||||||
<key>SmartAlignmentGuidesActive</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>SmartDistanceGuidesActive</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>UniqueID</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>UseEntirePage</key>
|
|
||||||
<false/>
|
|
||||||
<key>VPages</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>WindowInfo</key>
|
|
||||||
<dict>
|
|
||||||
<key>CurrentSheet</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>ExpandedCanvases</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>name</key>
|
|
||||||
<string>Canvas 1</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>Frame</key>
|
|
||||||
<string>{{215, 0}, {2134, 1417}}</string>
|
|
||||||
<key>ListView</key>
|
|
||||||
<true/>
|
|
||||||
<key>OutlineWidth</key>
|
|
||||||
<integer>142</integer>
|
|
||||||
<key>RightSidebar</key>
|
|
||||||
<false/>
|
|
||||||
<key>ShowRuler</key>
|
|
||||||
<true/>
|
|
||||||
<key>Sidebar</key>
|
|
||||||
<true/>
|
|
||||||
<key>SidebarWidth</key>
|
|
||||||
<integer>120</integer>
|
|
||||||
<key>VisibleRegion</key>
|
|
||||||
<string>{{-840, -477}, {1999, 1275}}</string>
|
|
||||||
<key>Zoom</key>
|
|
||||||
<real>1</real>
|
|
||||||
<key>ZoomValues</key>
|
|
||||||
<array>
|
|
||||||
<array>
|
|
||||||
<string>Canvas 1</string>
|
|
||||||
<real>1</real>
|
|
||||||
<real>1</real>
|
|
||||||
</array>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
+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,18 +0,0 @@
|
|||||||
//-
|
|
||||||
//- Crafting Guide - _google_sitelinks.jade
|
|
||||||
//-
|
|
||||||
//- Copyright © 2014-2017 by Redwood Labs
|
|
||||||
//- All rights reserved.
|
|
||||||
//-
|
|
||||||
|
|
||||||
script(type="application/ld+json").
|
|
||||||
{
|
|
||||||
"@context": "http://schema.org",
|
|
||||||
"@type": "WebSite",
|
|
||||||
"url": "http://crafting-guide.com/",
|
|
||||||
"potentialAction": {
|
|
||||||
"@type": "SearchAction",
|
|
||||||
"target": "http://crafting-guide.com?search={search_term}",
|
|
||||||
"query-input": "required name=search_term"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - base_model.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class BaseModel extends Backbone.Model
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@logEvents = options.logEvents or false
|
||||||
|
@state = c.modelState.unloaded
|
||||||
|
|
||||||
|
@loading = null
|
||||||
|
|
||||||
|
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
|
||||||
|
@loading = 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
|
||||||
|
|
||||||
|
@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}"
|
||||||
@@ -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,164 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
@_want = want
|
||||||
|
|
||||||
|
@_numberSteps()
|
||||||
|
|
||||||
|
# Public Methods ###############################################################################
|
||||||
|
|
||||||
|
computeRequired: ->
|
||||||
|
@_need = new SimpleInventory modPack:@_modPack
|
||||||
|
@_need.addInventory @_want
|
||||||
|
|
||||||
|
@_made = new SimpleInventory modPack:@_modPack
|
||||||
|
@_made.addInventory @_have
|
||||||
|
|
||||||
|
for i in [@_steps.length-1..0] by -1
|
||||||
|
step = @_steps[i]
|
||||||
|
step.multiplier = 0
|
||||||
|
|
||||||
|
for stack in step.recipe.output
|
||||||
|
if not stack?
|
||||||
|
throw new Error 'stack should not be null here'
|
||||||
|
|
||||||
|
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||||
|
while @_need.quantityOf(qualifiedSlug) > 0
|
||||||
|
@_executeStep step
|
||||||
|
|
||||||
|
@_made.addInventory @_want
|
||||||
|
@_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,107 @@
|
|||||||
|
#
|
||||||
|
# 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 '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,70 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
completeInto: (targetInventory)->
|
||||||
|
for stack in @_recipe.output
|
||||||
|
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
|
||||||
|
@_inventory.add qualifiedSlug, @multiplier * @_recipe.getQuantityRequired stack.itemSlug
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#
|
||||||
|
# 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 =
|
||||||
|
WAITING: 'waiting'
|
||||||
|
GRAPHING: 'examining recipes'
|
||||||
|
PLANNING: 'computing plans'
|
||||||
|
ANALYZING: 'analyzing plans'
|
||||||
|
COMPLETE: 'complete'
|
||||||
|
INVALID: 'invalid'
|
||||||
|
|
||||||
|
constructor: (modPack)->
|
||||||
|
if not modPack? then throw new Error 'modPack is required'
|
||||||
|
attributes =
|
||||||
|
paused: false
|
||||||
|
stage: @STAGE.WAITING
|
||||||
|
stageCount: 0
|
||||||
|
super attributes, {}
|
||||||
|
|
||||||
|
@_modPack = modPack
|
||||||
|
|
||||||
|
reset = _.debounce (=> @reset()), 100
|
||||||
|
reevaluatePlans = _.debounce (=> @reevaluatePlans()), 100
|
||||||
|
|
||||||
|
@_have = new Inventory modPack:@_modPack
|
||||||
|
@_have.on c.event.change, reevaluatePlans
|
||||||
|
|
||||||
|
@_want = new Inventory modPack:@_modPack
|
||||||
|
@_want.on c.event.change, reset
|
||||||
|
|
||||||
|
@on c.event.change + ':paused', reset
|
||||||
|
@on c.event.change + ':stage', => logger.info "Craftsman has started #{@stage}..."
|
||||||
|
@on 'scheduleNextWork', => @_scheduleNextWork()
|
||||||
|
@reset()
|
||||||
|
|
||||||
|
# Public Methods ###############################################################################
|
||||||
|
|
||||||
|
reevaluatePlans: ->
|
||||||
|
@_plans = null
|
||||||
|
@_planEvaluator = null
|
||||||
|
@stage = @STAGE.WAITING
|
||||||
|
@stageCount = 0
|
||||||
|
|
||||||
|
@_scheduleNextWork()
|
||||||
|
|
||||||
|
reset: ->
|
||||||
|
@_graphBuilder = null
|
||||||
|
@_planBuilder = null
|
||||||
|
@_planEvaluator = null
|
||||||
|
@_plans = null
|
||||||
|
|
||||||
|
@stage = @STAGE.WAITING
|
||||||
|
@stageCount = 0
|
||||||
|
|
||||||
|
@_scheduleNextWork()
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
have:
|
||||||
|
get: -> @_have
|
||||||
|
|
||||||
|
plan:
|
||||||
|
get: -> @_plans?[0]
|
||||||
|
|
||||||
|
want:
|
||||||
|
get: -> @_want
|
||||||
|
|
||||||
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
|
_scheduleNextWork: ->
|
||||||
|
return if @paused
|
||||||
|
return if @want.isEmpty
|
||||||
|
return if @complete
|
||||||
|
|
||||||
|
_.defer => @work()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
#
|
||||||
|
# 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: Oak Planks
|
||||||
|
recipe:
|
||||||
|
input: Oak Wood
|
||||||
|
pattern: ... .0. ...
|
||||||
|
quantity: 4
|
||||||
|
|
||||||
|
item: Oak Wood
|
||||||
|
gatherable: yes
|
||||||
|
|
||||||
|
item: Stick
|
||||||
|
recipe:
|
||||||
|
input: Oak Planks
|
||||||
|
pattern: .0. .0. ...
|
||||||
|
quantity: 4
|
||||||
|
|
||||||
|
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 Inventory
|
||||||
|
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 Stack 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 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}#{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 (c) 2014-2015 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,227 @@
|
|||||||
|
#
|
||||||
|
# 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)->
|
||||||
|
return this unless quantity > 0
|
||||||
|
|
||||||
|
@_add itemSlug, quantity
|
||||||
|
@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
|
||||||
|
|
||||||
|
# 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)->
|
||||||
|
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
|
||||||
|
@_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,224 @@
|
|||||||
|
#
|
||||||
|
# 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 = []
|
||||||
|
|
||||||
|
@once c.event.sync, => @_verifyActiveModVersion()
|
||||||
|
|
||||||
|
# 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 #################################################################################
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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,176 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - mod_pack.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
BaseModel = require '../base_model'
|
||||||
|
Inventory = require './inventory'
|
||||||
|
ModVersionParser = require '../parsing/mod_version_parser'
|
||||||
|
Recipe = require './recipe'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ModPack extends BaseModel
|
||||||
|
|
||||||
|
constructor: (attributes={}, options={})->
|
||||||
|
super attributes, options
|
||||||
|
|
||||||
|
@_mods = []
|
||||||
|
@_cache = {}
|
||||||
|
|
||||||
|
@on c.event.change, => @_cache = {}
|
||||||
|
|
||||||
|
# Item Methods #################################################################################
|
||||||
|
|
||||||
|
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 Inventory 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)->
|
||||||
|
working = true
|
||||||
|
@eachMod (mod)->
|
||||||
|
mod.eachModVersion (modVersion)->
|
||||||
|
working = working and (modVersion.isUnloaded or modVersion.isLoading)
|
||||||
|
|
||||||
|
return if working
|
||||||
|
|
||||||
|
@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,196 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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,252 @@
|
|||||||
|
#
|
||||||
|
# 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)->
|
||||||
|
amountCreated = 0
|
||||||
|
for stack in @output
|
||||||
|
if stack.itemSlug.matches itemSlug
|
||||||
|
amountCreated += stack.quantity
|
||||||
|
|
||||||
|
for stack in @input
|
||||||
|
if stack.itemSlug.matches itemSlug
|
||||||
|
amountCreated -= stack.quantity
|
||||||
|
|
||||||
|
return amountCreated <= 0
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
for c in pattern.split ''
|
||||||
|
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,143 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - lexical_analyzer.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserCommand = require './parser_command'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class LexicalAnalyzer
|
||||||
|
|
||||||
|
constructor: (fileName, rawText)->
|
||||||
|
if not fileName? then throw new Error 'fileName is required'
|
||||||
|
if not rawText? then throw new Error 'rawText is required'
|
||||||
|
|
||||||
|
@_commands = []
|
||||||
|
@_fileName = fileName
|
||||||
|
@_rawText = rawText
|
||||||
|
@_lines = rawText.split '\n'
|
||||||
|
@_lineIndex = -1
|
||||||
|
@_lineNumber = 0
|
||||||
|
|
||||||
|
# Class Methods ################################################################################
|
||||||
|
|
||||||
|
@::COMMAND = /\ *([^:]*):?(.*)/
|
||||||
|
|
||||||
|
@::COMMENT = /([^\\]?)#.*/
|
||||||
|
|
||||||
|
@::FILE = /^#FILE +(.*)/
|
||||||
|
|
||||||
|
# Public Methods ###############################################################################
|
||||||
|
|
||||||
|
next: ->
|
||||||
|
command = @peek()
|
||||||
|
@_commands.shift()
|
||||||
|
return command
|
||||||
|
|
||||||
|
peek: ->
|
||||||
|
while @_commands.length is 0
|
||||||
|
break if @_lineNumber > @_lines.length
|
||||||
|
@_parseLine()
|
||||||
|
|
||||||
|
return null unless @_commands.length > 0
|
||||||
|
return @_commands[0]
|
||||||
|
|
||||||
|
push: (command)->
|
||||||
|
@_commands.unshift command
|
||||||
|
|
||||||
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
|
getIsFinished: ->
|
||||||
|
return @peek() is null
|
||||||
|
|
||||||
|
Object.defineProperties @prototype,
|
||||||
|
isFinished: { get:@::getIsFinished }
|
||||||
|
|
||||||
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
|
_currentLine: ->
|
||||||
|
return null if @_lineIndex >= @_lines.length
|
||||||
|
return @_lines[@_lineIndex]
|
||||||
|
|
||||||
|
_nextLine: ->
|
||||||
|
if @_lineIndex < @_lines.length
|
||||||
|
while true
|
||||||
|
@_lineIndex += 1
|
||||||
|
@_lineNumber += 1
|
||||||
|
break if @_lineIndex >= @_lines.length
|
||||||
|
|
||||||
|
line = @_lines[@_lineIndex]
|
||||||
|
|
||||||
|
fileMatch = line.match @FILE, '$1'
|
||||||
|
if fileMatch?
|
||||||
|
@_fileName = fileMatch[1]
|
||||||
|
@_lineNumber = 0
|
||||||
|
else
|
||||||
|
line = line.replace @COMMENT, '$1'
|
||||||
|
@_lines[@_lineIndex] = line
|
||||||
|
|
||||||
|
break if line.trim().length > 0
|
||||||
|
|
||||||
|
return @_currentLine()
|
||||||
|
|
||||||
|
_parseHereDoc: ->
|
||||||
|
line = @_currentLine()
|
||||||
|
hereDocIndex = line.indexOf '<<-'
|
||||||
|
return [line, null] unless hereDocIndex isnt -1
|
||||||
|
|
||||||
|
hereDocStopText = line[hereDocIndex+3...line.length]
|
||||||
|
line = line[0...hereDocIndex]
|
||||||
|
|
||||||
|
hereDocLines = []
|
||||||
|
while true
|
||||||
|
nextLine = @_nextLine()
|
||||||
|
break unless nextLine?
|
||||||
|
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')]
|
||||||
|
|
||||||
|
_parseLine: ->
|
||||||
|
line = @_nextLine()
|
||||||
|
return unless line?
|
||||||
|
|
||||||
|
startingLineNumber = @_lineNumber
|
||||||
|
[line, lineHereDoc] = @_parseHereDoc()
|
||||||
|
|
||||||
|
lineParts = (part.trim() for part in line.split(';'))
|
||||||
|
for linePart, index in lineParts
|
||||||
|
continue if linePart.length is 0
|
||||||
|
|
||||||
|
hereDoc = lineHereDoc if index is lineParts.length - 1
|
||||||
|
|
||||||
|
match = @COMMAND.exec linePart
|
||||||
|
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
|
||||||
|
|
||||||
|
argText = if match[2] then match[2].trim() else ''
|
||||||
|
args = (arg.trim() for arg in argText.split(','))
|
||||||
|
args = (arg for arg in args when arg.length > 0)
|
||||||
|
|
||||||
|
if hereDoc?
|
||||||
|
if argText.length > 0 then argText += ', '
|
||||||
|
argText += hereDoc
|
||||||
|
args.push hereDoc
|
||||||
|
|
||||||
|
@_commands.push new ParserCommand
|
||||||
|
argText: argText
|
||||||
|
args: args
|
||||||
|
fileName: @_fileName
|
||||||
|
hereDoc: hereDoc
|
||||||
|
lineNumber: startingLineNumber
|
||||||
|
name: match[1]
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - lexical_analyzer.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
LexicalAnalyzer = require './lexical_analyzer'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
lexer = null
|
||||||
|
|
||||||
|
createLexer = (text)->
|
||||||
|
return new LexicalAnalyzer 'file.cg', text
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'lexical_analyzer.coffee', ->
|
||||||
|
|
||||||
|
describe 'a document with', ->
|
||||||
|
|
||||||
|
it 'no content returns null immediately', ->
|
||||||
|
lexer = createLexer ''
|
||||||
|
expect(lexer.next()).to.be.null
|
||||||
|
|
||||||
|
it 'only whitespace returns null immediately', ->
|
||||||
|
lexer = createLexer '\n\n \n \n\n'
|
||||||
|
expect(lexer.next()).to.be.null
|
||||||
|
|
||||||
|
it 'a single command returns only that command', ->
|
||||||
|
lexer = createLexer 'alpha: bravo'
|
||||||
|
lexer.next().name.should.equal 'alpha'
|
||||||
|
expect(lexer.next()).to.be.null
|
||||||
|
|
||||||
|
it 'multiple commands on a single line returns all commands', ->
|
||||||
|
lexer = createLexer 'alpha: bravo; charlie: delta; echo: foxtrot'
|
||||||
|
lexer.next().name.should.equal 'alpha'
|
||||||
|
lexer.next().name.should.equal 'charlie'
|
||||||
|
lexer.next().name.should.equal 'echo'
|
||||||
|
expect(lexer.next()).to.be.null
|
||||||
|
|
||||||
|
it 'multiple commands on separate lines returns all commands', ->
|
||||||
|
lexer = createLexer 'alpha: bravo \n charlie: delta \n echo: foxtrot'
|
||||||
|
lexer.next().name.should.equal 'alpha'
|
||||||
|
lexer.next().name.should.equal 'charlie'
|
||||||
|
lexer.next().name.should.equal 'echo'
|
||||||
|
expect(lexer.next()).to.be.null
|
||||||
|
|
||||||
|
describe 'a command with', ->
|
||||||
|
|
||||||
|
it 'only a name has no arguments', ->
|
||||||
|
command = createLexer('alpha:').next()
|
||||||
|
command.argText.should.equal ''
|
||||||
|
command.args.should.eql []
|
||||||
|
|
||||||
|
it 'one argument has only that argument', ->
|
||||||
|
command = createLexer('alpha: bravo').next()
|
||||||
|
command.argText.should.equal 'bravo'
|
||||||
|
command.args.should.eql ['bravo']
|
||||||
|
|
||||||
|
it 'multiple arguments has all of them', ->
|
||||||
|
command = createLexer('alpha: bravo, charlie, delta').next()
|
||||||
|
command.argText.should.equal 'bravo, charlie, delta'
|
||||||
|
command.args.should.eql ['bravo', 'charlie', 'delta']
|
||||||
|
|
||||||
|
it 'only a heredoc has it recorded in the right places', ->
|
||||||
|
command = createLexer('alpha: <<-END\nbravo charlie\nEND').next()
|
||||||
|
command.hereDoc.should.equal 'bravo charlie'
|
||||||
|
command.args.should.eql ['bravo charlie']
|
||||||
|
command.argText.should.equal 'bravo charlie'
|
||||||
|
|
||||||
|
it 'multiple args and a hereDoc is built correctly', ->
|
||||||
|
command = createLexer('alpha: bravo, charlie <<-END\ndelta echo\nfoxtrot\nEND').next()
|
||||||
|
command.hereDoc.should.equal 'delta echo\nfoxtrot'
|
||||||
|
command.argText.should.equal 'bravo, charlie, delta echo\nfoxtrot'
|
||||||
|
command.args.should.eql ['bravo', 'charlie', 'delta echo\nfoxtrot']
|
||||||
|
|
||||||
|
describe 'file name is assigned correctly when', ->
|
||||||
|
|
||||||
|
it 'no file markers are given', ->
|
||||||
|
lexer = createLexer 'alpha: bravo\ncharlie: delta'
|
||||||
|
lexer.next().fileName.should.equal 'file.cg'
|
||||||
|
lexer.next().fileName.should.equal 'file.cg'
|
||||||
|
|
||||||
|
it 'commands appear before the first file marker', ->
|
||||||
|
lexer = createLexer 'alpha: bravo\n#FILE file2.cg\ncharlie: delta'
|
||||||
|
lexer.next().fileName.should.equal 'file.cg'
|
||||||
|
lexer.next().fileName.should.equal 'file2.cg'
|
||||||
|
|
||||||
|
it 'multiple file markers are used', ->
|
||||||
|
lexer = createLexer '#FILE file1.cg\na: b\n#FILE file2.cg\nc: d\n#FILE file3.cg\ne: f'
|
||||||
|
lexer.next().fileName.should.equal 'file1.cg'
|
||||||
|
lexer.next().fileName.should.equal 'file2.cg'
|
||||||
|
lexer.next().fileName.should.equal 'file3.cg'
|
||||||
|
|
||||||
|
describe 'line numbers are assigned correctly when', ->
|
||||||
|
|
||||||
|
it 'commands are listed one after another', ->
|
||||||
|
lexer = createLexer 'alpha: bravo\ncharlie: delta\necho: foxtrot'
|
||||||
|
lexer.next().lineNumber.should.equal 1
|
||||||
|
lexer.next().lineNumber.should.equal 2
|
||||||
|
lexer.next().lineNumber.should.equal 3
|
||||||
|
|
||||||
|
it 'commands are separated by blank lines and comments', ->
|
||||||
|
lexer = createLexer '# foo\n\nalpha: bravo\n\ncharlie: delta\n\n# bar\n\necho: foxtrot'
|
||||||
|
lexer.next().lineNumber.should.equal 3
|
||||||
|
lexer.next().lineNumber.should.equal 5
|
||||||
|
lexer.next().lineNumber.should.equal 9
|
||||||
|
|
||||||
|
it 'commands include hereDocs', ->
|
||||||
|
lexer = createLexer 'alpha: <<-END\nbravo charlie\nEND\ndelta: \n\necho: '
|
||||||
|
lexer.next().lineNumber.should.equal 1
|
||||||
|
lexer.next().lineNumber.should.equal 4
|
||||||
|
lexer.next().lineNumber.should.equal 6
|
||||||
|
|
||||||
|
it 'file markers are used', ->
|
||||||
|
lexer = createLexer '#FILE file1.cg\nalpha: \n#FILE file2.cg\n\nbravo: \n\n#FILE file3.cg\n\n\ncharlie: '
|
||||||
|
lexer.next().lineNumber.should.equal 1
|
||||||
|
lexer.next().lineNumber.should.equal 2
|
||||||
|
lexer.next().lineNumber.should.equal 3
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - parser.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
LexicalAnalyzer = require './lexical_analyzer'
|
||||||
|
ModPack = require '../game/mod_pack'
|
||||||
|
ParserData = require './parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class Parser
|
||||||
|
|
||||||
|
constructor: (modPack, fileName, rawText)->
|
||||||
|
@_extensions = []
|
||||||
|
@_fileName = fileName
|
||||||
|
@_lexer = new LexicalAnalyzer fileName, rawText
|
||||||
|
@_modPack = new ModPack
|
||||||
|
@_data = new ParserData
|
||||||
|
|
||||||
|
modPackData = @_data.create {}, 'modPack'
|
||||||
|
modPackData.instance = @_modPack
|
||||||
|
|
||||||
|
@_loadSchema()
|
||||||
|
|
||||||
|
# Public Methods ###############################################################################
|
||||||
|
|
||||||
|
next: ->
|
||||||
|
if not @_lexer.isFinished
|
||||||
|
@_processNextCommand()
|
||||||
|
else if not @_data.isValidated
|
||||||
|
@_validateNextEntry()
|
||||||
|
else if not @_data.isBuilt
|
||||||
|
@_buildNextEntry()
|
||||||
|
else if not @_data.isAssembled
|
||||||
|
@_assembleNextEntry()
|
||||||
|
|
||||||
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
|
getIsFinished: ->
|
||||||
|
return @_lexer.isFinished and @_data.isValidated and @_data.isBuilt and @_data.isAssembled
|
||||||
|
|
||||||
|
getData: ->
|
||||||
|
return @_data
|
||||||
|
|
||||||
|
getModPack: ->
|
||||||
|
return @_modPack
|
||||||
|
|
||||||
|
Object.defineProperties @prototype,
|
||||||
|
data: { get:@::getData }
|
||||||
|
isFinished: { get:@::getIsFinished }
|
||||||
|
modPack: { get:@::getModPack }
|
||||||
|
|
||||||
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
|
_assembleNextEntry: ->
|
||||||
|
entry = @_data.popNextToAssemble()
|
||||||
|
return unless entry?
|
||||||
|
|
||||||
|
for extensions in @_extensions
|
||||||
|
continue unless extension.canAssemble entry
|
||||||
|
extension.assemble entry
|
||||||
|
return
|
||||||
|
|
||||||
|
_buildNextEntry: ->
|
||||||
|
entry = @_data.popNextToBuild()
|
||||||
|
return unless entry?
|
||||||
|
|
||||||
|
for extensions in @_extensions
|
||||||
|
continue unless extension.canBuild entry
|
||||||
|
extension.build entry
|
||||||
|
return
|
||||||
|
|
||||||
|
_loadSchema: ->
|
||||||
|
command = @_lexer.next()
|
||||||
|
if command.name isnt 'schema' then throw new Error "#{@_fileName} must start with the \"schema\" command"
|
||||||
|
|
||||||
|
schema = command.argText
|
||||||
|
if schema is '1'
|
||||||
|
@_use new require './parser_extensions/current_pe_v1'
|
||||||
|
@_use new require './parser_extensions/item_pe_v1'
|
||||||
|
@_use new require './parser_extensions/mod_pe_v1'
|
||||||
|
@_use new require './parser_extensions/mod_version_pe_v1'
|
||||||
|
@_use new require './parser_extensions/recipe_pe_v1'
|
||||||
|
else
|
||||||
|
throw new Error "unknown schema version: #{schema}"
|
||||||
|
|
||||||
|
_processNextCommand: ->
|
||||||
|
command = @_lexer.next()
|
||||||
|
return unless command?
|
||||||
|
|
||||||
|
for extension in @_extensions
|
||||||
|
continue unless extension.canExecute command
|
||||||
|
extension.execute command
|
||||||
|
return
|
||||||
|
|
||||||
|
@_data.addError command, "unknown command: #{command.name}"
|
||||||
|
|
||||||
|
_use: (parserExtension)->
|
||||||
|
parserExtension.state = @_state
|
||||||
|
@_extensions.push parserExtension
|
||||||
|
|
||||||
|
_validateNextEntry: ->
|
||||||
|
entry = @_data.popNextToValidate()
|
||||||
|
return unless entry?
|
||||||
|
|
||||||
|
for extensions in @_extensions
|
||||||
|
continue unless extension.canValidate entry
|
||||||
|
extension.validate entry
|
||||||
|
return
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - parser_command.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ParserCommand
|
||||||
|
|
||||||
|
constructor: (options={})->
|
||||||
|
if not options.name? then throw 'options.name is required'
|
||||||
|
if not options.fileName? then throw new 'options.fileName is required'
|
||||||
|
if not options.lineNumber? then throw 'options.lineNumber is required'
|
||||||
|
|
||||||
|
{@name, @fileName, @lineNumber, @argText, @args, @hereDoc} = options
|
||||||
|
|
||||||
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
|
getLocation: ->
|
||||||
|
return "#{@fileName}:#{@lineNumber}"
|
||||||
|
|
||||||
|
Object.defineProperties @prototype,
|
||||||
|
location: { get:@::getLocation }
|
||||||
|
|
||||||
|
# Object Overrides #############################################################################
|
||||||
|
|
||||||
|
toString: ->
|
||||||
|
result = "#{@fileName}:#{@lineNumber} #{@name}: #{@argText}"
|
||||||
|
if @hereDoc?
|
||||||
|
result += " <<- END\n#{@hereDoc}\nEND"
|
||||||
|
return result
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - parser_data.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ParserData
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@clear()
|
||||||
|
|
||||||
|
@_toAssemble = []
|
||||||
|
@_toBuild = []
|
||||||
|
@_toValidate = []
|
||||||
|
|
||||||
|
# Public Methods ###############################################################################
|
||||||
|
|
||||||
|
addError: (command, message)->
|
||||||
|
if not command? then throw new Error 'command is required'
|
||||||
|
message ?= 'is not valid'
|
||||||
|
@_errors.push command:command, message:message
|
||||||
|
|
||||||
|
create: (command, type, id=null)->
|
||||||
|
id ?= _.uniqueId "#{type}-"
|
||||||
|
|
||||||
|
@clearCurrent type
|
||||||
|
typeData = @_findOrCreateType type
|
||||||
|
itemData = @_current = typeData['current'] = typeData[id] = id:id, command:command, type:type
|
||||||
|
|
||||||
|
@_toAssemble.push itemData
|
||||||
|
@_toBuild.push itemData
|
||||||
|
@_toValidate.push itemData
|
||||||
|
|
||||||
|
return itemData
|
||||||
|
|
||||||
|
clear: ->
|
||||||
|
@_current = null
|
||||||
|
@_data =
|
||||||
|
mod: {}
|
||||||
|
modVersion: {}
|
||||||
|
tutorial: {}
|
||||||
|
itemGroup: {}
|
||||||
|
item: {}
|
||||||
|
multiblock: {}
|
||||||
|
recipe: {}
|
||||||
|
@_errors = []
|
||||||
|
|
||||||
|
clearCurrent: (type)->
|
||||||
|
switch type
|
||||||
|
when 'mod'
|
||||||
|
delete @_data.mod.current
|
||||||
|
delete @_data.modVersion.current
|
||||||
|
delete @_data.tutorial.current
|
||||||
|
delete @_data.itemGroup.current
|
||||||
|
delete @_data.item.current
|
||||||
|
delete @_data.multiblock.current
|
||||||
|
delete @_data.recipe.current
|
||||||
|
when 'modVersion'
|
||||||
|
delete @_data.modVersion.current
|
||||||
|
delete @_data.tutorial.current
|
||||||
|
delete @_data.itemGroup.current
|
||||||
|
delete @_data.item.current
|
||||||
|
delete @_data.multiblock.current
|
||||||
|
delete @_data.recipe.current
|
||||||
|
when 'tutorial'
|
||||||
|
delete @_data.tutorial.current
|
||||||
|
when 'itemGroup'
|
||||||
|
delete @_data.itemGroup.current
|
||||||
|
delete @_data.item.current
|
||||||
|
delete @_data.multiblock.current
|
||||||
|
delete @_data.recipe.current
|
||||||
|
when 'item'
|
||||||
|
delete @_data.item.current
|
||||||
|
delete @_data.multiblock.current
|
||||||
|
delete @_data.recipe.current
|
||||||
|
when 'multiblock'
|
||||||
|
delete @_data.multiblock.current
|
||||||
|
when 'recipe'
|
||||||
|
delete @_data.recipe.current
|
||||||
|
|
||||||
|
get: (type, id)->
|
||||||
|
typeData = @_findOrCreateType type
|
||||||
|
itemData = typeData[id] or null
|
||||||
|
return itemData
|
||||||
|
|
||||||
|
getCurrent: (type)->
|
||||||
|
return @_current unless type?
|
||||||
|
|
||||||
|
typeData = @_findOrCreateType type
|
||||||
|
current = typeData['current'] or null
|
||||||
|
return current
|
||||||
|
|
||||||
|
popNextToAssemble: ->
|
||||||
|
return @_toAssemble.pop() or null
|
||||||
|
|
||||||
|
popNextToBuild: ->
|
||||||
|
return @_toBuild.pop() or null
|
||||||
|
|
||||||
|
popNextToValidate: ->
|
||||||
|
return @_toValidate.pop() or null
|
||||||
|
|
||||||
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
|
getErrors: ->
|
||||||
|
return @_errors[..]
|
||||||
|
|
||||||
|
getIsAssembled: ->
|
||||||
|
return @_toAssemble.length > 0
|
||||||
|
|
||||||
|
getIsBuilt: ->
|
||||||
|
return @_toBuild.length > 0
|
||||||
|
|
||||||
|
getIsValidated: ->
|
||||||
|
return @_toValidate.length > 0
|
||||||
|
|
||||||
|
Object.defineProperties @prototype,
|
||||||
|
errors: { get:@::getErrors }
|
||||||
|
isAssembled: { get:@::getIsAssembled }
|
||||||
|
isBuilt: { get:@::getIsBuilt }
|
||||||
|
isValidated: { get:@::getIsValidated }
|
||||||
|
|
||||||
|
|
||||||
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
|
_findOrCreateType: (type)->
|
||||||
|
typeData = @_data[type]
|
||||||
|
if not typeData?
|
||||||
|
typeData = @_data[type] = {}
|
||||||
|
typeData.type = type
|
||||||
|
|
||||||
|
return typeData
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - parser_data.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserCommand = require './parser_command'
|
||||||
|
ParserData = require './parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
command = state = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'parser_data.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
state = new ParserData
|
||||||
|
command = new ParserCommand name:'alpha', fileName:'file.cg', lineNumber:1
|
||||||
|
|
||||||
|
describe 'create', ->
|
||||||
|
|
||||||
|
it 'returns an object with the given id', ->
|
||||||
|
alpha = state.create command, 'alpha', 0
|
||||||
|
alpha.id.should.equal 0
|
||||||
|
alpha.command.location.should.equal 'file.cg:1'
|
||||||
|
|
||||||
|
it 'returns an object with a newly created id when none is given', ->
|
||||||
|
alpha = state.create command, 'alpha'
|
||||||
|
alpha.id.should.not.be.null
|
||||||
|
alpha.id.should.match /^alpha-[0-9]+$/
|
||||||
|
alpha.command.location.should.equal 'file.cg:1'
|
||||||
|
|
||||||
|
describe 'get', ->
|
||||||
|
|
||||||
|
it 'returns a previously created item', ->
|
||||||
|
alpha1 = state.create command, 'alpha', 0
|
||||||
|
alpha2 = state.get 'alpha', 0
|
||||||
|
alpha1.should.equal alpha2
|
||||||
|
|
||||||
|
it 'returns null when no such item exists', ->
|
||||||
|
alpha = state.create command, 'alpha', 0
|
||||||
|
expect(state.get 'alpha', 1).to.be.null
|
||||||
|
expect(state.get 'bravo', 0).to.be.null
|
||||||
|
|
||||||
|
describe 'getCurrent', ->
|
||||||
|
|
||||||
|
it 'returns the new item each time one is created', ->
|
||||||
|
alpha1 = state.create command, 'alpha', 0
|
||||||
|
state.getCurrent('alpha').id.should.equal 0
|
||||||
|
|
||||||
|
alpha2 = state.create command, 'alpha', 1
|
||||||
|
state.getCurrent('alpha').id.should.equal 1
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - parser_extension.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ParserExtension
|
||||||
|
|
||||||
|
constructor: (data, type, commandFunctions)->
|
||||||
|
@data = data
|
||||||
|
|
||||||
|
@_commandFunctions = commandFunctions or {}
|
||||||
|
@_type = type or null
|
||||||
|
|
||||||
|
# Public Methods ###############################################################################
|
||||||
|
|
||||||
|
canAssemble: (entry)->
|
||||||
|
return entry.type is @_type
|
||||||
|
|
||||||
|
canBuild: (entry)->
|
||||||
|
return entry.type is @_type
|
||||||
|
|
||||||
|
canExecute: (command)->
|
||||||
|
_.isFunction @_commandFunctions[command.name]
|
||||||
|
|
||||||
|
canValidate: (entry)->
|
||||||
|
return entry.type is @_type
|
||||||
|
|
||||||
|
execute: (command)->
|
||||||
|
if not @_data? then throw new Error '@data must be defined before executing commands'
|
||||||
|
|
||||||
|
func = @_commandFunctions[command.name]
|
||||||
|
if not func then throw new Error "#{@constructor.name} cannot handle command: #{command.name}"
|
||||||
|
|
||||||
|
func.call this, command
|
||||||
|
return this
|
||||||
|
|
||||||
|
# Error Checking Helpers #######################################################################
|
||||||
|
|
||||||
|
alreadyExists: (command, type, id)->
|
||||||
|
obj = @data.get(type, id)
|
||||||
|
if obj?
|
||||||
|
@data.addError command, "a #{type} called #{id} has already been declared"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
duplicateField: (command, obj, field)->
|
||||||
|
if obj[field]?
|
||||||
|
@data.addError command, "duplicate #{field}"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
incompatibleField: (command, obj, field, otherCommandName)->
|
||||||
|
otherCommandName ?= field
|
||||||
|
if obj[field]?
|
||||||
|
@data.addError "#{command.name} cannot be used with #{otherCommandName}"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
missingCurrent: (command, type)->
|
||||||
|
obj = @data.getCurrent type
|
||||||
|
if not obj?
|
||||||
|
@data.addError command, "you must declare a #{type} before using #{command.name}"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
missingArgs: (command)->
|
||||||
|
if not command.args or command.args.length is 0
|
||||||
|
@data.addError command, "#{command.name} requires at least one item"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
missingArgText: (command)->
|
||||||
|
if command.argText.length is 0
|
||||||
|
@data.addError command, "#{command.name} cannot be empty"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
invalidVerb: (command, verb)->
|
||||||
|
if not verb in ['mod', 'item']
|
||||||
|
@data.addError command, "#{command.name} only applies to either `mod` or `item`"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
tooFewArguments: (command, minimum)->
|
||||||
|
if command.args.length < minimum
|
||||||
|
@data.addError command, "#{command.name} requires at least #{minimum} arguments"
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Parsing Helpers ##############################################################################
|
||||||
|
|
||||||
|
parseBoolean: (command, text)->
|
||||||
|
return null unless text?
|
||||||
|
|
||||||
|
switch text.toLowerCase()
|
||||||
|
when "yes" then return true
|
||||||
|
when "no" then return false
|
||||||
|
|
||||||
|
@data.addError command, "#{command.name} requires \"yes\" or \"no\""
|
||||||
|
return null
|
||||||
|
|
||||||
|
parseInt: (command, text)->
|
||||||
|
return null unless text?
|
||||||
|
|
||||||
|
value = Number.parseInt text
|
||||||
|
if isNaN value
|
||||||
|
@addError command, "#{command.name} requires a number"
|
||||||
|
return null
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Property Methods #############################################################################
|
||||||
|
|
||||||
|
getData: ->
|
||||||
|
return @_data
|
||||||
|
|
||||||
|
setData: (data)->
|
||||||
|
if not data? then throw new Error 'data cannot be undefined'
|
||||||
|
@_data = data
|
||||||
|
|
||||||
|
Object.defineProperties @prototype,
|
||||||
|
data: { get:@::getData, set:@::setData }
|
||||||
|
|
||||||
|
# Overridable Methods ##########################################################################
|
||||||
|
|
||||||
|
assmeble: (entry)->
|
||||||
|
throw new Error "#{@constructor.name} must override the `assemble` method"
|
||||||
|
|
||||||
|
build: (entry)->
|
||||||
|
throw new Error "#{@constructor.name} must override the `build` method"
|
||||||
|
|
||||||
|
validate: (entry)->
|
||||||
|
throw new Error "#{@constructor.name} must override the `validate` method"
|
||||||
|
|
||||||
|
# Private Methods ##############################################################################
|
||||||
|
|
||||||
|
_findBuildMethod: (entry)->
|
||||||
|
methodName = "_build_#{entry.type}"
|
||||||
|
method = this[methodName]
|
||||||
|
return null unless _.isFunction method
|
||||||
|
return method
|
||||||
|
|
||||||
|
_findCommandMethod: (command)->
|
||||||
|
methodName = "_command_#{command.name}"
|
||||||
|
method = this[methodName]
|
||||||
|
return null unless _.isFunction method
|
||||||
|
return method
|
||||||
|
|
||||||
|
_findValidateMethod: (command)->
|
||||||
|
methodName = "_validate_#{command.name}"
|
||||||
|
method = this[methodName]
|
||||||
|
return null unless _.isFunction method
|
||||||
|
return method
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - current_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class CurrentParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, null,
|
||||||
|
description: @_command_description
|
||||||
|
name: @_command_name
|
||||||
|
officialUrl: @_command_officialUrl
|
||||||
|
video: @_command_video
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_description: (command)->
|
||||||
|
current = @data.getCurrent()
|
||||||
|
return if @duplicateField command, current, 'description'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
current.description = command.argText
|
||||||
|
|
||||||
|
_command_name: (command)->
|
||||||
|
current = @data.getCurrent()
|
||||||
|
return if @duplicateField command, current, 'name'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
current.name = command.argText
|
||||||
|
|
||||||
|
_command_officialUrl: (command)->
|
||||||
|
current = @data.getCurrent()
|
||||||
|
return if @duplicateField command, current, 'officialUrl'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
current.officialUrl = command.argText
|
||||||
|
|
||||||
|
_command_video: (command)->
|
||||||
|
current = @data.getCurrent()
|
||||||
|
return if @missingArgs command
|
||||||
|
return if @tooFewArguments command, 2
|
||||||
|
|
||||||
|
current.videos ?= []
|
||||||
|
current.videos.push youTubeId:command.args[0], caption:command.args[1..].join(', ')
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - current_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
CurrentParserExtensionV1 = require './current_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = state = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'current_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
state = new ParserData
|
||||||
|
parser = new CurrentParserExtensionV1 state
|
||||||
|
|
||||||
|
state.create {}, 'alpha'
|
||||||
|
|
||||||
|
describe 'description', ->
|
||||||
|
|
||||||
|
it 'assigns to the current object', ->
|
||||||
|
parser.execute name:'description', argText:'bravo'
|
||||||
|
state.getCurrent().description.should.equal 'bravo'
|
||||||
|
state.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'name', ->
|
||||||
|
|
||||||
|
it 'assigns to the current object', ->
|
||||||
|
parser.execute name:'name', argText:'bravo'
|
||||||
|
state.getCurrent().name.should.equal 'bravo'
|
||||||
|
state.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'officialUrl', ->
|
||||||
|
|
||||||
|
it 'assigns to the current object', ->
|
||||||
|
parser.execute name:'officialUrl', argText:'bravo'
|
||||||
|
state.getCurrent().officialUrl.should.equal 'bravo'
|
||||||
|
state.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'video', ->
|
||||||
|
|
||||||
|
it 'assigns to the current object', ->
|
||||||
|
parser.execute name:'video', args:['alpha', 'bravo']
|
||||||
|
videos = state.getCurrent().videos
|
||||||
|
videos[0].youTubeId.should.equal 'alpha'
|
||||||
|
videos[0].caption.should.equal 'bravo'
|
||||||
|
state.errors.should.eql []
|
||||||
|
|
||||||
|
it 'can assign multiple videos', ->
|
||||||
|
parser.execute name:'video', args:['alpha', 'bravo']
|
||||||
|
parser.execute name:'video', args:['charlie', 'delta']
|
||||||
|
state.getCurrent().videos.length.should.equal 2
|
||||||
|
state.errors.should.eql []
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - item_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ItemParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, 'item',
|
||||||
|
gatherable: @_command_gatherable
|
||||||
|
item: @_command_item
|
||||||
|
update: @_command_update
|
||||||
|
|
||||||
|
# ParserExtensions Overrides ###################################################################
|
||||||
|
|
||||||
|
assmeble: (entry)->
|
||||||
|
throw new Error "#{@constructor.name} must override the `assemble` method"
|
||||||
|
|
||||||
|
build: (entry)->
|
||||||
|
entry.instance = new
|
||||||
|
throw new Error "#{@constructor.name} must override the `build` method"
|
||||||
|
|
||||||
|
validate: (entry)-> # do nothing
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_gatherable: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @missingCurrent command, 'item'
|
||||||
|
|
||||||
|
item = @data.getCurrent 'item'
|
||||||
|
return if @duplicateField command, item, 'gatherable'
|
||||||
|
|
||||||
|
gatherable = @parseBoolean command, command.argText
|
||||||
|
return unless gatherable?
|
||||||
|
|
||||||
|
item.gatherable = gatherable
|
||||||
|
|
||||||
|
_command_item: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @missingCurrent command, 'modVersion'
|
||||||
|
return if @alreadyExists command, 'item', command.argText
|
||||||
|
|
||||||
|
item = @data.create command, 'item', command.argText
|
||||||
|
item.name = command.argText
|
||||||
|
item.isUpdate = false
|
||||||
|
|
||||||
|
group = @data.getCurrent 'itemGroup'
|
||||||
|
if group? then item.group = group
|
||||||
|
|
||||||
|
item.modVersion = @data.getCurrent 'modVersion'
|
||||||
|
|
||||||
|
_command_update: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @missingCurrent command, 'modVersion'
|
||||||
|
return if @alreadyExists command, 'item', command.argText
|
||||||
|
|
||||||
|
item = @data.create command, 'item', command.argText
|
||||||
|
item.name = command.argText
|
||||||
|
item.isUpdate = true
|
||||||
|
item.modVersion = @data.getCurrent 'modVersion'
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - item_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ItemParserExtensionV1 = require './item_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = data = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'item_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
data = new ParserData
|
||||||
|
parser = new ItemParserExtensionV1 data
|
||||||
|
|
||||||
|
data.create {}, 'modVersion', 0
|
||||||
|
|
||||||
|
describe 'gatherable', ->
|
||||||
|
|
||||||
|
it 'assigns to the current item', ->
|
||||||
|
data.create {}, 'item', 1
|
||||||
|
|
||||||
|
parser.execute name:'gatherable', argText:'yes'
|
||||||
|
data.getCurrent('item').gatherable.should.be.true
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'item', ->
|
||||||
|
|
||||||
|
it 'creates a new item', ->
|
||||||
|
data.create {}, 'itemGroup', 2
|
||||||
|
|
||||||
|
parser.execute name:'item', argText:'alpha'
|
||||||
|
item = data.getCurrent 'item'
|
||||||
|
item.id.should.equal 'alpha'
|
||||||
|
item.isUpdate.should.equal false
|
||||||
|
item.name.should.equal 'alpha'
|
||||||
|
item.modVersion.id.should.equal 0
|
||||||
|
item.group.id.should.equal 2
|
||||||
|
data.getErrors().should.eql []
|
||||||
|
|
||||||
|
describe 'update', ->
|
||||||
|
|
||||||
|
it 'creates an item update', ->
|
||||||
|
parser.execute name:'update', argText:'alpha'
|
||||||
|
item = data.getCurrent 'item'
|
||||||
|
item.isUpdate.should.equal true
|
||||||
|
data.getErrors().should.eql []
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - mod_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ModParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, 'mod',
|
||||||
|
author: @_command_author
|
||||||
|
documentationUrl: @_command_documentationUrl
|
||||||
|
downloadUrl: @_command_downloadUrl
|
||||||
|
homePageUrl: @_command_homePageUrl
|
||||||
|
mod: @_command_mod
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_author: (command)->
|
||||||
|
return if @missingCurrent command, 'mod'
|
||||||
|
|
||||||
|
mod = @data.getCurrent 'mod'
|
||||||
|
return if @duplicateField command, mod, 'author'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
mod.author = command.argText
|
||||||
|
|
||||||
|
_command_documentationUrl: (command)->
|
||||||
|
current = @data.getCurrent()
|
||||||
|
return if @duplicateField command, current, 'documentationUrl'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
current.documentationUrl = command.argText
|
||||||
|
|
||||||
|
_command_downloadUrl: (command)->
|
||||||
|
return if @missingCurrent command, 'mod'
|
||||||
|
|
||||||
|
mod = @data.getCurrent 'mod'
|
||||||
|
return if @duplicateField command, mod, 'downloadUrl'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
mod.downloadUrl = command.argText
|
||||||
|
|
||||||
|
_command_homePageUrl: (command)->
|
||||||
|
return if @missingCurrent command, 'mod'
|
||||||
|
|
||||||
|
mod = @data.getCurrent 'mod'
|
||||||
|
return if @duplicateField command, mod, 'homePageUrl'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
mod.homePageUrl = command.argText
|
||||||
|
|
||||||
|
_command_mod: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @alreadyExists command, 'mod', command.argText
|
||||||
|
|
||||||
|
@data.create command, 'mod', command.argText
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - mod_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ModParserExtensionV1 = require './mod_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = data = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'mod_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
data = new ParserData
|
||||||
|
parser = new ModParserExtensionV1 data
|
||||||
|
|
||||||
|
data.create {}, 'mod', 0
|
||||||
|
|
||||||
|
describe 'author', ->
|
||||||
|
|
||||||
|
it 'assigns to the current mod', ->
|
||||||
|
parser.execute name:'author', argText:'alpha'
|
||||||
|
data.getCurrent('mod').author.should.equal 'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'documentationUrl', ->
|
||||||
|
|
||||||
|
it 'assigns to the current mod', ->
|
||||||
|
parser.execute name:'documentationUrl', argText:'alpha'
|
||||||
|
data.getCurrent('mod').documentationUrl.should.equal 'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'downloadUrl', ->
|
||||||
|
|
||||||
|
it 'assigns to the current mod', ->
|
||||||
|
parser.execute name:'downloadUrl', argText:'alpha'
|
||||||
|
data.getCurrent('mod').downloadUrl.should.equal 'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'homePageUrl', ->
|
||||||
|
|
||||||
|
it 'assigns to the current mod', ->
|
||||||
|
parser.execute name:'homePageUrl', argText:'alpha'
|
||||||
|
data.getCurrent('mod').homePageUrl.should.equal 'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'mod', ->
|
||||||
|
|
||||||
|
it 'creates a new mod', ->
|
||||||
|
parser.execute name:'mod', argText:'alpha'
|
||||||
|
mod = data.getCurrent 'mod'
|
||||||
|
mod.id.should.equal 'alpha'
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - mod_version_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class ModVersionParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, 'modVersion',
|
||||||
|
group: @_command_group
|
||||||
|
version: @_command_version
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_group: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @missingCurrent command, 'modVersion'
|
||||||
|
|
||||||
|
group = @data.create command, 'itemGroup', command.argText
|
||||||
|
group.modVersion = @data.getCurrent 'modVersion'
|
||||||
|
|
||||||
|
_command_version: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @missingCurrent command, 'mod'
|
||||||
|
|
||||||
|
modVersion = @data.create command, 'modVersion', command.argText
|
||||||
|
modVersion.mod = @data.getCurrent 'mod'
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - mod_version_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ModVersionParserExtensionV1 = require './mod_version_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = data = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'mod_version_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
data = new ParserData
|
||||||
|
parser = new ModVersionParserExtensionV1 data
|
||||||
|
|
||||||
|
data.create {}, 'mod', 0
|
||||||
|
|
||||||
|
describe 'group', ->
|
||||||
|
|
||||||
|
it 'creates a new item group', ->
|
||||||
|
data.create {}, 'modVersion', 1
|
||||||
|
|
||||||
|
parser.execute name:'group', argText:'alpha'
|
||||||
|
itemGroup = data.getCurrent('itemGroup')
|
||||||
|
itemGroup.id.should.equal 'alpha'
|
||||||
|
itemGroup.modVersion.id.should.equal 1
|
||||||
|
|
||||||
|
describe 'version', ->
|
||||||
|
|
||||||
|
it 'creates a new mod version', ->
|
||||||
|
parser.execute name:'version', argText:'alpha'
|
||||||
|
modVersion = data.getCurrent 'modVersion'
|
||||||
|
modVersion.id.should.equal 'alpha'
|
||||||
|
modVersion.mod.id.should.equal 0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - multiblock_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class MultiblockParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, 'multiblock',
|
||||||
|
layer: @_command_layer
|
||||||
|
multiblock: @_command_multiblock
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_layer: (command)->
|
||||||
|
return if @missingCurrent command, 'multiblock'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
multiblock = @data.getCurrent 'multiblock'
|
||||||
|
multiblock.layers ?= []
|
||||||
|
multiblock.layers.push command.argText
|
||||||
|
|
||||||
|
_command_multiblock: (command)->
|
||||||
|
return if @missingCurrent command, 'item'
|
||||||
|
|
||||||
|
item = @data.getCurrent 'item'
|
||||||
|
return if @duplicateField command, item, 'multiblock'
|
||||||
|
return if @incompatibleField command, item, 'recipe'
|
||||||
|
return if @incompatibleField command, item, 'gatherable'
|
||||||
|
|
||||||
|
multiblock = @data.create command, 'multiblock'
|
||||||
|
multiblock.item = item
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - multiblock_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
MultiblockParserExtensionV1 = require './multiblock_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = data = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'multiblock_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
data = new ParserData
|
||||||
|
parser = new MultiblockParserExtensionV1 data
|
||||||
|
|
||||||
|
data.create {}, 'item', 1
|
||||||
|
|
||||||
|
describe 'layer', ->
|
||||||
|
|
||||||
|
it 'creates a new layer in the current multiblock', ->
|
||||||
|
data.create {}, 'multiblock', 2
|
||||||
|
|
||||||
|
parser.execute name:'layer', argText:'alpha'
|
||||||
|
multiblock = data.getCurrent 'multiblock'
|
||||||
|
multiblock.layers.length.should.equal 1
|
||||||
|
multiblock.layers[0].should.equal 'alpha'
|
||||||
|
|
||||||
|
it 'can create multiple layers', ->
|
||||||
|
data.create {}, 'multiblock', 2
|
||||||
|
|
||||||
|
parser.execute name:'layer', argText:'alpha'
|
||||||
|
parser.execute name:'layer', argText:'bravo'
|
||||||
|
multiblock = data.getCurrent 'multiblock'
|
||||||
|
multiblock.layers.should.eql ['alpha', 'bravo']
|
||||||
|
|
||||||
|
describe 'multiblock', ->
|
||||||
|
|
||||||
|
it 'creates a new multiblock', ->
|
||||||
|
parser.execute name:'multiblock'
|
||||||
|
multiblock = data.getCurrent 'multiblock'
|
||||||
|
multiblock.item.id.should.equal 1
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - recipe_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class RecipeParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, 'recipe',
|
||||||
|
extras: @_command_extras
|
||||||
|
ignoreDuringCrafting: @_command_ignoreDuringCrafting
|
||||||
|
input: @_command_input
|
||||||
|
onlyIf: @_command_onlyIf
|
||||||
|
pattern: @_command_pattern
|
||||||
|
recipe: @_command_recipe
|
||||||
|
quantity: @_command_quantity
|
||||||
|
tools: @_command_tools
|
||||||
|
|
||||||
|
# Class Methods ################################################################################
|
||||||
|
|
||||||
|
@::PATTERN = /[0-9. ]+/
|
||||||
|
|
||||||
|
@::STACK = /^([0-9]+) +(.+)$/
|
||||||
|
|
||||||
|
# Error Checking Helpers #######################################################################
|
||||||
|
|
||||||
|
invalidPattern: (command)->
|
||||||
|
match = command.argText.match @PATTERN
|
||||||
|
if not match?
|
||||||
|
@data.addError command, "invalid pattern: \"#{command.argText}\""
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_extras: (command)->
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
return if @missingArgs command
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
return if @duplicateField command, 'extras', recipe
|
||||||
|
|
||||||
|
recipe.extras = command.args
|
||||||
|
|
||||||
|
_command_ignoreDuringCrafting: (command)->
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
return if @duplicateField command, recipe, 'ignoreDuringCrafting'
|
||||||
|
|
||||||
|
ignoreDuringCrafting = @parseBoolean command, command.argText
|
||||||
|
return unless ignoreDuringCrafting?
|
||||||
|
|
||||||
|
recipe.ignoreDuringCrafting = ignoreDuringCrafting
|
||||||
|
|
||||||
|
_command_input: (command)->
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
return if @missingArgs command
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
multiblock = @data.getCurrent 'multiblock'
|
||||||
|
|
||||||
|
target = if multiblock? then multiblock else recipe
|
||||||
|
|
||||||
|
return if @duplicateField command, 'input', target
|
||||||
|
target.input = (@_parseStack(arg) for arg in command.args)
|
||||||
|
|
||||||
|
_command_onlyIf: (command)->
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
return if @duplicateField command, recipe, 'condition'
|
||||||
|
|
||||||
|
words = command.argText.split ' '
|
||||||
|
inverted = false
|
||||||
|
if words[0] is 'not'
|
||||||
|
inverted = true
|
||||||
|
words.shift()
|
||||||
|
|
||||||
|
verb = words[0]
|
||||||
|
noun = words[1..].join ' '
|
||||||
|
|
||||||
|
return if @invalidVerb command, verb
|
||||||
|
recipe.condition = verb:verb, noun:noun, inverted:inverted
|
||||||
|
|
||||||
|
_command_pattern: (command)->
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
return if @missingArgText command
|
||||||
|
return if @invalidPattern command
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
return if @duplicateField command, 'pattern', recipe
|
||||||
|
|
||||||
|
recipe.pattern = command.argText
|
||||||
|
|
||||||
|
_command_quantity: (command)->
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
value = @parseInt command, command.argText
|
||||||
|
return unless value?
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
return if @duplicateField command, 'quantity', recipe
|
||||||
|
|
||||||
|
recipe.quantity = value
|
||||||
|
|
||||||
|
_command_recipe: (command)->
|
||||||
|
return if @missingCurrent command, 'item'
|
||||||
|
|
||||||
|
recipe = @data.create command, 'recipe'
|
||||||
|
recipe.item = @data.getCurrent 'item'
|
||||||
|
|
||||||
|
_command_tools: (command)->
|
||||||
|
return if @missingCurrent command, 'recipe'
|
||||||
|
return if @missingArgs command
|
||||||
|
|
||||||
|
recipe = @data.getCurrent 'recipe'
|
||||||
|
return if @duplicateField command, 'tools', recipe
|
||||||
|
|
||||||
|
recipe.tools = (@_parseStack(arg) for arg in command.args)
|
||||||
|
|
||||||
|
# Custom Parsers ###############################################################################
|
||||||
|
|
||||||
|
_parseStack: (text)->
|
||||||
|
match = text.match @STACK
|
||||||
|
return name:text, quantity:1 unless match?
|
||||||
|
|
||||||
|
quantity = parseFloat match[1]
|
||||||
|
if isNaN quantity then quantity = 1
|
||||||
|
|
||||||
|
name = match[2]
|
||||||
|
|
||||||
|
return name:name, quantity:quantity
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - recipe_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
RecipeParserExtensionV1 = require './recipe_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = data = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'recipe_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
data = new ParserData
|
||||||
|
parser = new RecipeParserExtensionV1 data
|
||||||
|
|
||||||
|
data.create {}, 'item', 0
|
||||||
|
|
||||||
|
describe 'extras', ->
|
||||||
|
|
||||||
|
it 'assigns to the current recipe', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'extras', args:['bravo']
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.extras.should.eql ['bravo']
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'ignoreDuringCrafting', ->
|
||||||
|
|
||||||
|
it 'assigns to the current recipe', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'ignoreDuringCrafting', argText:'yes'
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.ignoreDuringCrafting.should.equal true
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'input', ->
|
||||||
|
|
||||||
|
it 'correctly reads a single item', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'input', args:['bravo']
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.input.should.eql [name:'bravo', quantity:1]
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
it 'correctly reads multiple items', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'input', args:['bravo', 'charlie']
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.input.should.eql [{name:'bravo', quantity:1}, {name:'charlie', quantity:1}]
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
it 'correctly reads a stack', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'input', args:['10 bravo']
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.input.should.eql [name:'bravo', quantity:10]
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'onlyIf', ->
|
||||||
|
|
||||||
|
it 'assigns to the current recipe', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'onlyIf', argText:'not mod Alpha'
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.condition.should.eql verb:'mod', noun:'Alpha', inverted:true
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'pattern', ->
|
||||||
|
|
||||||
|
it 'assigns to the current recipe', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'pattern', argText:'00. 00. ...'
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.pattern.should.equal '00. 00. ...'
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
it 'rejects an invalid pattern', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'pattern', argText:'alpha'
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
expect(recipe.pattern).to.be.undefined
|
||||||
|
(e.message for e in data.errors).should.eql ['invalid pattern: "alpha"']
|
||||||
|
|
||||||
|
describe 'quantity', ->
|
||||||
|
|
||||||
|
it 'assigns to the current recipe', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'quantity', argText:'42'
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.quantity.should.equal 42
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'recipe', ->
|
||||||
|
|
||||||
|
it 'creates a new recipe', ->
|
||||||
|
parser.execute name:'recipe'
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.item.id.should.equal 0
|
||||||
|
data.errors.should.eql []
|
||||||
|
|
||||||
|
describe 'tools', ->
|
||||||
|
|
||||||
|
it 'assigns to the current recipe', ->
|
||||||
|
data.create {}, 'recipe'
|
||||||
|
|
||||||
|
parser.execute name:'tools', args:['bravo']
|
||||||
|
recipe = data.getCurrent 'recipe'
|
||||||
|
recipe.tools.should.eql [name:'bravo', quantity:1]
|
||||||
|
data.errors.should.eql []
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - tutorial_pe_v1.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
ParserExtension = require '../parser_extension'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
module.exports = class TutorialParserExtensionV1 extends ParserExtension
|
||||||
|
|
||||||
|
constructor: (data)->
|
||||||
|
super data, 'tutorial',
|
||||||
|
content: @_command_content
|
||||||
|
section: @_command_section
|
||||||
|
title: @_command_title
|
||||||
|
tutorial: @_command_tutorial
|
||||||
|
|
||||||
|
# Command Methods ##############################################################################
|
||||||
|
|
||||||
|
_command_content: (command)->
|
||||||
|
return if @missingCurrent command, 'tutorial'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
section = @data.getCurrent 'section'
|
||||||
|
section.content = command.argText
|
||||||
|
|
||||||
|
_command_section: (command)->
|
||||||
|
return if @missingCurrent command, 'tutorial'
|
||||||
|
|
||||||
|
section = @data.create command, 'section'
|
||||||
|
section.tutorial = @data.getCurrent 'tutorial'
|
||||||
|
|
||||||
|
_command_title: (command)->
|
||||||
|
return if @missingCurrent command, 'section'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
section = @data.getCurrent 'section'
|
||||||
|
return if @duplicateField command, section, 'title'
|
||||||
|
|
||||||
|
section.title = command.argText
|
||||||
|
|
||||||
|
_command_tutorial: (command)->
|
||||||
|
return if @missingCurrent command, 'modVersion'
|
||||||
|
return if @missingArgText command
|
||||||
|
|
||||||
|
tutorial = @data.create command, 'tutorial', command.argText
|
||||||
|
tutorial.name = command.argText
|
||||||
|
tutorial.modVersion = @data.getCurrent 'modVersion'
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#
|
||||||
|
# Crafting Guide - tutorial_pe_v1.test.coffee
|
||||||
|
#
|
||||||
|
# Copyright © 2014-2016 by Redwood Labs
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
TutorialParserExtensionV1 = require './tutorial_pe_v1'
|
||||||
|
ParserData = require '../parser_data'
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
parser = data = null
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
describe 'tutorial_pe_v1.coffee', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
data = new ParserData
|
||||||
|
parser = new TutorialParserExtensionV1 data
|
||||||
|
|
||||||
|
data.create {}, 'modVersion', 1
|
||||||
|
|
||||||
|
describe 'content', ->
|
||||||
|
|
||||||
|
it 'assigns content to the current section', ->
|
||||||
|
data.create {}, 'tutorial', 2
|
||||||
|
data.create {}, 'section', 3
|
||||||
|
|
||||||
|
parser.execute name:'content', argText:'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
section = data.getCurrent 'section'
|
||||||
|
section.content.should.equal 'alpha'
|
||||||
|
|
||||||
|
describe 'section', ->
|
||||||
|
|
||||||
|
it 'creates a section in the current tutorial', ->
|
||||||
|
data.create {}, 'tutorial', 2
|
||||||
|
|
||||||
|
parser.execute name:'section'
|
||||||
|
data.errors.should.eql []
|
||||||
|
section = data.getCurrent 'section'
|
||||||
|
section.tutorial.id.should.equal 2
|
||||||
|
|
||||||
|
describe 'title', ->
|
||||||
|
|
||||||
|
it 'assigns a title to the current tutorial', ->
|
||||||
|
data.create {}, 'tutorial', 2
|
||||||
|
data.create {}, 'section', 3
|
||||||
|
|
||||||
|
parser.execute name:'title', argText:'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
section = data.getCurrent 'section'
|
||||||
|
section.title.should.equal 'alpha'
|
||||||
|
|
||||||
|
describe 'tutorial', ->
|
||||||
|
|
||||||
|
it 'creates a new tutorial', ->
|
||||||
|
parser.execute name:'tutorial', argText:'alpha'
|
||||||
|
data.errors.should.eql []
|
||||||
|
tutorial = data.getCurrent 'tutorial'
|
||||||
|
tutorial.name.should.equal 'alpha'
|
||||||
@@ -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,46 @@
|
|||||||
#
|
#
|
||||||
# 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
|
||||||
|
@craftsman.want.add stack.itemSlug, stack.quantity
|
||||||
|
inventory.remove stack.itemSlug
|
||||||
|
|
||||||
_onHaveChanged: ->
|
if inventory.isEmpty then @params = null
|
||||||
@triggerPropertyChange "isDirty", @_isDirty, true
|
|
||||||
|
|
||||||
_onWantChanged: ->
|
|
||||||
@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,88 +0,0 @@
|
|||||||
#
|
|
||||||
# Crafting Guide - file_cache.coffee
|
|
||||||
#
|
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
|
|
||||||
w = require "when"
|
|
||||||
|
|
||||||
########################################################################################################################
|
|
||||||
|
|
||||||
module.exports = class FileCache extends Backbone.Events
|
|
||||||
|
|
||||||
@::FILE_TOKEN = '#file='
|
|
||||||
|
|
||||||
constructor: (url)->
|
|
||||||
@_ajax = $.ajax
|
|
||||||
@_files = {}
|
|
||||||
@_loading = w.resolve(true)
|
|
||||||
|
|
||||||
if url? then @loadArchive(url).catch(->)
|
|
||||||
|
|
||||||
# Public Methods ###############################################################################
|
|
||||||
|
|
||||||
getFile: (file)->
|
|
||||||
return null unless @hasFile file
|
|
||||||
return @_files[file]
|
|
||||||
|
|
||||||
hasFile: (file)->
|
|
||||||
return @_files[file]?
|
|
||||||
|
|
||||||
loadArchive: (url)->
|
|
||||||
if not _.isFunction(@_ajax) then throw new Error '$.ajax is required'
|
|
||||||
|
|
||||||
newLoading = w.promise (resolve, reject)=>
|
|
||||||
@_ajax
|
|
||||||
url: url
|
|
||||||
dataType: 'text'
|
|
||||||
success: (text, status, xhr)=> resolve @_onLoadSuccess text, status, xhr
|
|
||||||
error: (xhr, status, error)=> resolve @_onLoadError error, status, xhr
|
|
||||||
|
|
||||||
@_loading = w.join @_loading, newLoading
|
|
||||||
|
|
||||||
# Property Methods #############################################################################
|
|
||||||
|
|
||||||
Object.defineProperties @prototype,
|
|
||||||
|
|
||||||
loading:
|
|
||||||
get: -> return @_loading
|
|
||||||
|
|
||||||
# Private Methods ##############################################################################
|
|
||||||
|
|
||||||
_onLoadError: (error, status, xhr)->
|
|
||||||
console.error "Failed to load archive: #{xhr.responseText}"
|
|
||||||
return false
|
|
||||||
|
|
||||||
_onLoadSuccess: (text, status, xhr)->
|
|
||||||
return @_parseArchive text
|
|
||||||
|
|
||||||
_parseArchive: (text)->
|
|
||||||
file = null
|
|
||||||
tokenIndex = nameStartIndex = nameEndIndex = fileStartIndex = fileEndIndex = 0
|
|
||||||
fileCount = 0
|
|
||||||
|
|
||||||
while true
|
|
||||||
tokenIndex = text.indexOf @FILE_TOKEN, fileStartIndex
|
|
||||||
if tokenIndex is -1
|
|
||||||
if file?
|
|
||||||
@_files[file] = text.substring fileStartIndex, text.length
|
|
||||||
fileCount += 1
|
|
||||||
break
|
|
||||||
|
|
||||||
if file?
|
|
||||||
fileEndIndex = tokenIndex
|
|
||||||
@_files[file] = text.substring fileStartIndex, fileEndIndex
|
|
||||||
fileCount += 1
|
|
||||||
|
|
||||||
nameStartIndex = tokenIndex + @FILE_TOKEN.length
|
|
||||||
nameEndIndex = text.indexOf '\n', nameStartIndex
|
|
||||||
if nameEndIndex is -1
|
|
||||||
file = text.substring nameStartIndex, text.length
|
|
||||||
@_files[file] = ''
|
|
||||||
break
|
|
||||||
|
|
||||||
file = text.substring nameStartIndex, nameEndIndex
|
|
||||||
fileStartIndex = nameEndIndex + '\n'.length
|
|
||||||
|
|
||||||
return fileCount > 0
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#
|
|
||||||
# Crafting Guide - file_cache.test.coffee
|
|
||||||
#
|
|
||||||
# Copyright © 2014-2017 by Redwood Labs
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
|
|
||||||
FileCache = require './file_cache'
|
|
||||||
|
|
||||||
########################################################################################################################
|
|
||||||
|
|
||||||
describe 'file_cache.coffee', ->
|
|
||||||
|
|
||||||
beforeEach ->
|
|
||||||
@cache = new FileCache
|
|
||||||
|
|
||||||
makeTestAjax = (text)->
|
|
||||||
return (options)->
|
|
||||||
options.success text, 'success', {url:options.url}
|
|
||||||
|
|
||||||
it 'can load an archive with a single file', ->
|
|
||||||
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie delta'
|
|
||||||
@cache.loadArchive 'http://test'
|
|
||||||
.then =>
|
|
||||||
@cache.hasFile('alpha').should.equal true
|
|
||||||
@cache.getFile('alpha').should.equal 'bravo charlie delta'
|
|
||||||
|
|
||||||
it 'can load an archive with multiple files', ->
|
|
||||||
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\necho foxtrot'
|
|
||||||
@cache.loadArchive 'http://test'
|
|
||||||
.then =>
|
|
||||||
@cache.hasFile('alpha').should.equal true
|
|
||||||
@cache.getFile('alpha').should.equal 'bravo charlie\n'
|
|
||||||
@cache.hasFile('delta').should.equal true
|
|
||||||
@cache.getFile('delta').should.equal 'echo foxtrot'
|
|
||||||
|
|
||||||
it 'can load an archive without newlines', ->
|
|
||||||
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie#file=delta\necho foxtrot'
|
|
||||||
@cache.loadArchive 'http://test'
|
|
||||||
.then =>
|
|
||||||
@cache.hasFile('alpha').should.equal true
|
|
||||||
@cache.getFile('alpha').should.equal 'bravo charlie'
|
|
||||||
@cache.hasFile('delta').should.equal true
|
|
||||||
@cache.getFile('delta').should.equal 'echo foxtrot'
|
|
||||||
|
|
||||||
it 'can cope with an empty file in the middle of an archive', ->
|
|
||||||
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\n#file=echo\nfoxtrot golf'
|
|
||||||
@cache.loadArchive 'http://test'
|
|
||||||
.then =>
|
|
||||||
@cache.hasFile('alpha').should.equal true
|
|
||||||
@cache.getFile('alpha').should.equal 'bravo charlie\n'
|
|
||||||
@cache.hasFile('delta').should.equal true
|
|
||||||
@cache.getFile('delta').should.equal ''
|
|
||||||
@cache.hasFile('echo').should.equal true
|
|
||||||
@cache.getFile('echo').should.equal 'foxtrot golf'
|
|
||||||
|
|
||||||
it 'can cope with an empty file at the end of an archive', ->
|
|
||||||
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta'
|
|
||||||
@cache.loadArchive 'http://test'
|
|
||||||
.then =>
|
|
||||||
@cache.hasFile('alpha').should.equal true
|
|
||||||
@cache.getFile('alpha').should.equal 'bravo charlie\n'
|
|
||||||
@cache.hasFile('delta').should.equal true
|
|
||||||
@cache.getFile('delta').should.equal ''
|
|
||||||
|
|
||||||
it 'can load multiple, non-overlapping archives', ->
|
|
||||||
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\necho foxtrot'
|
|
||||||
@cache.loadArchive 'http://test'
|
|
||||||
|
|
||||||
@cache._ajax = makeTestAjax '#file=golf\nhotel india'
|
|
||||||
@cache.loadArchive 'http://test2'
|
|
||||||
.then =>
|
|
||||||
@cache.hasFile('alpha').should.equal true
|
|
||||||
@cache.getFile('alpha').should.equal 'bravo charlie\n'
|
|
||||||
@cache.hasFile('delta').should.equal true
|
|
||||||
@cache.getFile('delta').should.equal 'echo foxtrot'
|
|
||||||
@cache.hasFile('golf').should.equal true
|
|
||||||
@cache.getFile('golf').should.equal 'hotel india'
|
|
||||||
@@ -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,56 @@
|
|||||||
#
|
#
|
||||||
# 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
|
||||||
|
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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user