Compare commits
116
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c45572997f | ||
|
|
59217a5fea | ||
|
|
0b94cc4a1f | ||
|
|
cbf66bf5bb | ||
|
|
8b0cd71bb5 | ||
|
|
0e6d75ec9b | ||
|
|
3ca0dcceed | ||
|
|
89ba3ac48e | ||
|
|
ab96166601 | ||
|
|
dac1a8dceb | ||
|
|
ccb2cad516 | ||
|
|
33479051c8 | ||
|
|
b8a2dae9f2 | ||
|
|
2fd1de40da | ||
|
|
f6f736f8be | ||
|
|
20119cf8e4 | ||
|
|
997ff114b4 | ||
|
|
6d62375c46 | ||
|
|
b4cde5a37b | ||
|
|
5f65f77222 | ||
|
|
8b43c53a9e | ||
|
|
3bc7c4cf89 | ||
|
|
c4ba36b60a | ||
|
|
b05e237aee | ||
|
|
7865ba98c3 | ||
|
|
69e4d968b5 | ||
|
|
bd96bc6575 | ||
|
|
90a41a59f0 | ||
|
|
ebdb6158ba | ||
|
|
407e3fca5d | ||
|
|
3d9895a6d5 | ||
|
|
ee6887e803 | ||
|
|
fb48362a59 | ||
|
|
86a547f422 | ||
|
|
0131c8ff3b | ||
|
|
478364418b | ||
|
|
ed96cf0c71 | ||
|
|
5d8251bebb | ||
|
|
69ca6b06b9 | ||
|
|
7377559f7b | ||
|
|
c0472b268d | ||
|
|
13b717836a | ||
|
|
016c61e983 | ||
|
|
d7743285cb | ||
|
|
cc9232d143 | ||
|
|
a1e7fb4fa0 | ||
|
|
9ac25efb0c | ||
|
|
19a3e9846c | ||
|
|
c5e9920473 | ||
|
|
96b5beeb8c | ||
|
|
d0bca0d2ac | ||
|
|
d102834408 | ||
|
|
00ca88500e | ||
|
|
6a2b3805a2 | ||
|
|
eba8c99ac0 | ||
|
|
b82bfea8f1 | ||
|
|
d0a2292094 | ||
|
|
cb962f6175 | ||
|
|
083cc30eb2 | ||
|
|
22009557d2 | ||
|
|
b34ca26d0a | ||
|
|
23b3f1c7e1 | ||
|
|
6b84698802 | ||
|
|
c85fca8357 | ||
|
|
16848772a3 | ||
|
|
c441d75529 | ||
|
|
1bc89c1c89 | ||
|
|
43202e9917 | ||
|
|
05bc1a271d | ||
|
|
da1419d6b6 | ||
|
|
841fac38d3 | ||
|
|
2b8e23b32f | ||
|
|
5eae2bb488 | ||
|
|
6fa18c764b | ||
|
|
c9796a142d | ||
|
|
0718590f56 | ||
|
|
51f566ebf1 | ||
|
|
d2d904be7f | ||
|
|
088994f366 | ||
|
|
184b64a6af | ||
|
|
4aca035709 | ||
|
|
ebf3443e96 | ||
|
|
fd5372b35d | ||
|
|
f464558cd2 | ||
|
|
e7cd3f0549 | ||
|
|
545408a7e7 | ||
|
|
83156c30c7 | ||
|
|
636a8d061c | ||
|
|
2dbda99986 | ||
|
|
7d83552013 | ||
|
|
35bb12de6a | ||
|
|
1d1772e961 | ||
|
|
6787063ac7 | ||
|
|
9e09b8d87c | ||
|
|
2c77972073 | ||
|
|
31f4594928 | ||
|
|
eb9ac15de8 | ||
|
|
e922f3c5cc | ||
|
|
a8d08b93c5 | ||
|
|
64e118a2c8 | ||
|
|
6671541dcb | ||
|
|
962165dc8e | ||
|
|
f9b65dd892 | ||
|
|
979586d9d3 | ||
|
|
c522e60e28 | ||
|
|
f9cd38947d | ||
|
|
99b6cf2861 | ||
|
|
d925a17940 | ||
|
|
a2bbdf4645 | ||
|
|
93c98e16ac | ||
|
|
88e57b1200 | ||
|
|
1e96cf2d6c | ||
|
|
ccaf8e9842 | ||
|
|
e1f8282049 | ||
|
|
d873c4b7f1 | ||
|
|
3529bccf3a |
@@ -1,5 +1,7 @@
|
||||
*.log
|
||||
*.swp
|
||||
.DS_Store
|
||||
.env
|
||||
.sass-cache
|
||||
.tm_properties
|
||||
aws.config
|
||||
|
||||
+18
-44
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - Gruntfile.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
@@ -17,29 +17,6 @@ EXTERNAL_LIBS = [
|
||||
'./vendor/email.js:emailjs'
|
||||
]
|
||||
|
||||
PUBLISHED_FILES = [
|
||||
'./client/models/base_model.coffee'
|
||||
'./client/models/game/inventory.coffee'
|
||||
'./client/models/game/item.coffee'
|
||||
'./client/models/game/item_slug.coffee'
|
||||
'./client/models/game/mod.coffee'
|
||||
'./client/models/game/mod_version.coffee'
|
||||
'./client/models/game/multiblock.coffee'
|
||||
'./client/models/game/recipe.coffee'
|
||||
'./client/models/game/stack.coffee'
|
||||
'./client/models/game/simple_stack.coffee'
|
||||
'./client/models/parsing/command_parser_version_base.coffee'
|
||||
'./client/models/parsing/mod_parser.coffee'
|
||||
'./client/models/parsing/mod_parser_v1.coffee'
|
||||
'./client/models/parsing/mod_version_parser.coffee'
|
||||
'./client/models/parsing/mod_version_parser_v1.coffee'
|
||||
'./client/models/parsing/versioned_parser_base.coffee'
|
||||
'./client/models/site/tutorial.coffee'
|
||||
'./common/constants.coffee'
|
||||
'./common/underscore.coffee'
|
||||
'./index.coffee'
|
||||
]
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = (grunt)->
|
||||
@@ -54,30 +31,28 @@ module.exports = (grunt)->
|
||||
mode: 'gzip'
|
||||
files: [
|
||||
{expand: true, cwd:'./build/static', src:'**/*.css', dest:'./dist/'}
|
||||
{expand: true, cwd:'./build/static', src:'**/*.html', dest:'./dist/'}
|
||||
{expand: true, cwd:'./build/static', src:['**/*.html', '!**/index.html'], dest:'./dist/'}
|
||||
{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:'**/*.ttf', 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:
|
||||
assets_build:
|
||||
files: [
|
||||
{expand:true, cwd:'./assets/', src:'**/*', dest:'./build/static'}
|
||||
]
|
||||
env_build:
|
||||
files: [
|
||||
{expand:true, cwd:'.', src:'.env', dest:'./build'}
|
||||
]
|
||||
build_to_dist:
|
||||
files: [
|
||||
{expand:true, cwd:'./build/static/', src:['**/*', '!**/*.map', '!**/*.js'], dest:'./dist'}
|
||||
expand: true
|
||||
cwd: './build/static/'
|
||||
src: ['**/*', '!**/*.map', '!**/*.js', '!index.html']
|
||||
dest: './dist'
|
||||
]
|
||||
common_source:
|
||||
files: [
|
||||
@@ -123,9 +98,9 @@ module.exports = (grunt)->
|
||||
|
||||
mochaTest:
|
||||
options:
|
||||
bail: true
|
||||
bail: false
|
||||
color: true
|
||||
reporter: 'dot'
|
||||
reporter: 'list'
|
||||
require: [
|
||||
'coffee-script/register'
|
||||
'./src/test_helper.coffee'
|
||||
@@ -135,6 +110,8 @@ module.exports = (grunt)->
|
||||
|
||||
sass:
|
||||
all:
|
||||
options:
|
||||
sourcemap: 'none'
|
||||
files:
|
||||
'./build/static/main.css': [ './build/imports.scss' ]
|
||||
|
||||
@@ -187,7 +164,7 @@ module.exports = (grunt)->
|
||||
['jade', 'build:copy', 'build:css', 'browserify:external', 'browserify:internal']
|
||||
|
||||
grunt.registerTask 'build:copy', 'helper task for build',
|
||||
['copy:assets_build', 'copy:common_source', 'copy:server_source']
|
||||
['copy:assets_build', 'copy:env_build', 'copy:common_source', 'copy:server_source']
|
||||
|
||||
grunt.registerTask 'build:css', 'helper task for build',
|
||||
['sass_globbing', 'sass']
|
||||
@@ -207,14 +184,11 @@ module.exports = (grunt)->
|
||||
grunt.registerTask 'dist', 'build the project to be run from Amazon S3',
|
||||
['build', 'copy:build_to_dist', 'uglify', 'compress']
|
||||
|
||||
grunt.registerTask 'prepublish', 'build the project to be published to NPM as shared code',
|
||||
['clean', 'coffee']
|
||||
|
||||
grunt.registerTask 'publish', 'publish the project to NPM',
|
||||
['prepublish', 'script:publish', 'clean', 'build']
|
||||
|
||||
grunt.registerTask 'start', 'build the project and start a local HTTP server',
|
||||
['build', 'script:start']
|
||||
['script:start']
|
||||
|
||||
grunt.registerTask 'test', 'run unit tests',
|
||||
['mochaTest']
|
||||
@@ -225,7 +199,7 @@ module.exports = (grunt)->
|
||||
grunt.registerTask 'upload:staging', 'upload the project the staging Amazon S3 environment',
|
||||
['dist', 'script:s3_upload:staging']
|
||||
|
||||
grunt.registerTask 'useLocalDeps', 'use local dependencies instead of NPM dependencies',
|
||||
grunt.registerTask 'use-local-deps', 'use local dependencies instead of NPM dependencies',
|
||||
['script:use_local_deps']
|
||||
|
||||
# Code Tasks #######################################################################################################
|
||||
@@ -268,7 +242,7 @@ module.exports = (grunt)->
|
||||
|
||||
grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
|
||||
done = this.async()
|
||||
grunt.util.spawn cmd:'clear', opts:{stdio:'inherit'}, (error)-> done(error)
|
||||
grunt.util.spawn cmd:'./scripts/clear_buffer', opts:{stdio:'inherit'}, (error)-> done(error)
|
||||
|
||||
grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", ->
|
||||
done = this.async()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
google-site-verification: google077275160ac09f2f.html
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 267 B |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 723 B |
Binary file not shown.
|
After 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",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.6",
|
||||
"description": "A guide for building anything in Minecraft or its many mods",
|
||||
"main": "./dist/index.js",
|
||||
"repository": "http://github.com/andrewminer/crafting-guide",
|
||||
"scripts": {
|
||||
"build": "grunt build",
|
||||
"prepublish": "grunt prepublish",
|
||||
"start": "grunt start",
|
||||
"test": "grunt test"
|
||||
},
|
||||
"author": "Andrew Miner",
|
||||
"license": "none",
|
||||
"license": "UNLICENSED",
|
||||
"devDependencies": {
|
||||
"chai": "^1.10.0",
|
||||
"coffee-script": "^1.9.3",
|
||||
"coffeeify": "^1.1.0",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-coffee": "^1.0.0",
|
||||
"grunt-contrib-compress": "^0.14.0",
|
||||
"grunt-contrib-copy": "^0.8.2",
|
||||
"grunt-contrib-jade": "^0.15.0",
|
||||
"grunt-contrib-sass": "^0.9.2",
|
||||
"grunt-contrib-uglify": "^0.11.0",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"grunt-sass-globbing": "^1.4.0",
|
||||
"mocha": "^2.4.5",
|
||||
"sass": "^0.5.0",
|
||||
"sinon": "^1.14.1",
|
||||
"sinon-chai": "^2.7.0"
|
||||
"chai": "3.5.0",
|
||||
"coffee-script": "1.12.5",
|
||||
"coffeeify": "2.1.0",
|
||||
"grunt": "1.0.1",
|
||||
"grunt-contrib-clean": "1.1.0",
|
||||
"grunt-contrib-coffee": "1.0.0",
|
||||
"grunt-contrib-compress": "1.4.1",
|
||||
"grunt-contrib-copy": "1.0.0",
|
||||
"grunt-contrib-jade": "1.0.0",
|
||||
"grunt-contrib-sass": "1.0.0",
|
||||
"grunt-contrib-uglify": "2.3.0",
|
||||
"grunt-contrib-watch": "1.0.0",
|
||||
"grunt-mocha-test": "0.13.2",
|
||||
"grunt-sass-globbing": "1.5.1",
|
||||
"mocha": "3.3.0",
|
||||
"sass": "0.5.0",
|
||||
"sinon": "2.2.0",
|
||||
"sinon-chai": "2.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"backbone": "^1.2.3",
|
||||
"body-parser": "^1.13.3",
|
||||
"client-sessions": "^0.7.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"crafting-guide-common": "^1.5.1",
|
||||
"express": "^4.13.3",
|
||||
"express-session": "^1.11.3",
|
||||
"jade": "^1.11.0",
|
||||
"jquery": "^2.1.4",
|
||||
"marked": "^0.3.5",
|
||||
"underscore": "^1.8.3",
|
||||
"underscore.inflections": "^0.2.1",
|
||||
"when": "^3.7.3"
|
||||
"backbone": "1.3.3",
|
||||
"body-parser": "1.17.1",
|
||||
"client-sessions": "0.8.0",
|
||||
"cookie-parser": "1.4.3",
|
||||
"crafting-guide-common": ">=5.0.0",
|
||||
"dotenv": "4.0.0",
|
||||
"express": "4.15.2",
|
||||
"express-session": "1.15.2",
|
||||
"jade": "1.11.0",
|
||||
"jquery": "3.2.1",
|
||||
"marked": "0.3.6",
|
||||
"underscore": "1.8.3",
|
||||
"underscore.inflections": "0.2.1",
|
||||
"when": "3.7.8"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
clear && printf '\e[3J'
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
grunt watch &
|
||||
|
||||
pushd './build/' &>/dev/null
|
||||
clear
|
||||
nodemon ./server.coffee $*
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
<?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>
|
||||
+51
-36
@@ -1,78 +1,93 @@
|
||||
#
|
||||
# Crafting Guide - client.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
############################################################################################################
|
||||
|
||||
# Allow Node.js-style `global` in addition to `window`
|
||||
if typeof(global) is 'undefined'
|
||||
if typeof(global) is "undefined"
|
||||
window.global = window
|
||||
|
||||
global._ = require '../common/underscore'
|
||||
global.$ = require 'jquery'
|
||||
global.c = require '../common/constants'
|
||||
global._ = require "../common/underscore"
|
||||
global.$ = require "jquery"
|
||||
global.c = require "../common/constants"
|
||||
global.π = Math.PI
|
||||
global.ε = 0.0001
|
||||
global.w = require 'when'
|
||||
global.w = require "when"
|
||||
|
||||
global.Backbone = Backbone = require 'backbone'
|
||||
Backbone.$ = $
|
||||
|
||||
{Logger} = require 'crafting-guide-common'
|
||||
{Logger} = require("crafting-guide-common").util
|
||||
global.logger = new Logger
|
||||
|
||||
marked = require 'marked'
|
||||
Tracker = require "./tracker"
|
||||
global.tracker = new Tracker
|
||||
|
||||
marked = require "marked"
|
||||
marked.setOptions sanitize:true
|
||||
|
||||
global.Backbone = Backbone = require "backbone"
|
||||
Backbone.$ = $
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
global.hostName = window.location.hostname
|
||||
switch global.hostName
|
||||
when 'prerender.crafting-guide.com'
|
||||
global.env = 'prerender'
|
||||
when "prerender.crafting-guide.com"
|
||||
global.env = "prerender"
|
||||
logger.level = Logger.FATAL
|
||||
apiBaseUrl = 'http://local.crafting-guide.com:8001'
|
||||
when 'local.crafting-guide.com', 'localhost'
|
||||
global.env = 'local'
|
||||
apiBaseUrl = "http://prerender.crafting-guide.com:4347"
|
||||
when "local.crafting-guide.com", "localhost"
|
||||
global.env = "local"
|
||||
logger.level = Logger.DEBUG
|
||||
apiBaseUrl = 'http://local.crafting-guide.com:8001'
|
||||
when 'staging.crafting-guide.com'
|
||||
global.env = 'staging'
|
||||
apiBaseUrl = "http://#{global.hostName}:4347"
|
||||
when "staging.crafting-guide.com"
|
||||
global.env = "staging"
|
||||
logger.level = Logger.VERBOSE
|
||||
apiBaseUrl = 'http://api-staging.crafting-guide.com'
|
||||
when 'crafting-guide.com'
|
||||
global.env = 'production'
|
||||
logger.level = Logger.INFO
|
||||
apiBaseUrl = 'http://api.crafting-guide.com'
|
||||
apiBaseUrl = "http://api-staging.crafting-guide.com"
|
||||
when "crafting-guide.com"
|
||||
global.env = "production"
|
||||
logger.level = Logger.INFO
|
||||
tracker.enabled = true
|
||||
apiBaseUrl = "http://api.crafting-guide.com"
|
||||
else
|
||||
throw new Error "cannot determine the environment of: #{window.location.hostname}"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
Storage = require './storage'
|
||||
Storage = require "./storage"
|
||||
storage = new Storage storage:global.localStorage
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
{CraftingGuideClient} = require 'crafting-guide-common'
|
||||
tracker.trackPageView()
|
||||
|
||||
{CraftingGuideClient} = require("crafting-guide-common").api
|
||||
client = _(new CraftingGuideClient(baseUrl:apiBaseUrl)).extend Backbone.Events
|
||||
client.onStatusChanged = (client, oldStatus, newStatus)->
|
||||
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
|
||||
client.trigger 'change:status', client, oldStatus, newStatus
|
||||
client.trigger 'change', client
|
||||
client.trigger "change:status", client, oldStatus, newStatus
|
||||
client.trigger "change", client
|
||||
|
||||
client.checkStatus()
|
||||
client.startMonitoringStatus()
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
SiteController = require './site/site_controller'
|
||||
global.site = site = new SiteController client:client, storage:storage
|
||||
site.render()
|
||||
site.loadDefaultModPack()
|
||||
site.loadCurrentUser()
|
||||
{http, ModPackStore, ItemDetailStore} = require("crafting-guide-common").api
|
||||
storeOptions = http:http, baseUrl:"#{location.protocol}//#{location.hostname}:#{location.port}"
|
||||
global.stores =
|
||||
modPack: new ModPackStore storeOptions
|
||||
itemDetail: new ItemDetailStore storeOptions
|
||||
|
||||
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
|
||||
//-
|
||||
//- Copyright © 2014-2016 by Redwood Labs
|
||||
//- Copyright © 2014-2017 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
@@ -15,4 +15,6 @@ script.
|
||||
ga('create', 'UA-54970416-2', 'crafting-guide.com');
|
||||
ga('require', 'displayfeatures');
|
||||
ga('require', 'linkid', 'linkid.js');
|
||||
} else {
|
||||
window.ga = function() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//-
|
||||
//- 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"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
#
|
||||
# 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}"
|
||||
@@ -1,122 +0,0 @@
|
||||
#
|
||||
# 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"
|
||||
@@ -1,44 +0,0 @@
|
||||
#
|
||||
# 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"
|
||||
]
|
||||
@@ -1,164 +0,0 @@
|
||||
#
|
||||
# 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++
|
||||
@@ -1,107 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,70 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,151 +0,0 @@
|
||||
#
|
||||
# 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()
|
||||
@@ -1,166 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,69 +0,0 @@
|
||||
#
|
||||
# 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 + ' ')}"
|
||||
@@ -1,77 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,55 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,98 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,127 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,43 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,98 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,80 +0,0 @@
|
||||
#
|
||||
# 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]
|
||||
@@ -1,87 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,212 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
#
|
||||
# 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}"
|
||||
@@ -1,36 +0,0 @@
|
||||
###
|
||||
# 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}
|
||||
@@ -1,227 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,213 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - inventory.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
EventRecorder = require '../event_recorder'
|
||||
Inventory = require './inventory'
|
||||
Item = require './item'
|
||||
ItemSlug = require './item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
inventory = modPack = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'inventory.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
inventory = new Inventory {}, silent:false
|
||||
inventory.add ItemSlug.slugify('wool'), 4
|
||||
inventory.add ItemSlug.slugify('string'), 20
|
||||
inventory.add ItemSlug.slugify('boat')
|
||||
|
||||
describe 'add', ->
|
||||
|
||||
it 'can add to an empty inventory', ->
|
||||
inventory.add ItemSlug.slugify('iron_ingot'), 4
|
||||
stack = inventory._stacks['iron_ingot']
|
||||
stack.constructor.name.should.equal 'Stack'
|
||||
stack.itemSlug.qualified.should.equal 'iron_ingot'
|
||||
stack.quantity.should.equal 4
|
||||
|
||||
it 'can augment quantity of existing items', ->
|
||||
inventory.add ItemSlug.slugify('wool'), 2
|
||||
inventory.unparse().should.equal 'boat:20.string:6.wool'
|
||||
|
||||
it 'can add zero quantity', ->
|
||||
inventory.add ItemSlug.slugify('wool'), 0
|
||||
inventory.unparse().should.equal 'boat:20.string:4.wool'
|
||||
|
||||
it 'emits the proper events', ->
|
||||
events = new EventRecorder inventory
|
||||
inventory.add ItemSlug.slugify('iron_ingot'), 10
|
||||
events.names.should.eql [c.event.add, c.event.change]
|
||||
|
||||
describe 'addInventory', ->
|
||||
|
||||
it 'can add to an empty inventory', ->
|
||||
newInventory = new Inventory
|
||||
newInventory.addInventory inventory
|
||||
newInventory.unparse().should.equal 'boat:20.string:4.wool'
|
||||
|
||||
it 'can add a mix of new and existing items', ->
|
||||
newInventory = new Inventory
|
||||
newInventory.add ItemSlug.slugify('string'), 2
|
||||
newInventory.addInventory inventory
|
||||
newInventory.unparse().should.equal 'boat:22.string:4.wool'
|
||||
|
||||
describe 'clone', ->
|
||||
|
||||
it 'creates an empty inventory from an empty inventory', ->
|
||||
a = new Inventory
|
||||
b = a.clone()
|
||||
b._itemSlugs.should.eql []
|
||||
|
||||
it 'faithfully copies an existing inventory', ->
|
||||
copy = inventory.clone()
|
||||
copy.unparse().should.equal 'boat:20.string:4.wool'
|
||||
|
||||
describe 'each', ->
|
||||
|
||||
it 'works with an empty inventory', ->
|
||||
inventory = new Inventory
|
||||
result = []
|
||||
inventory.each (item)-> result.push item.name
|
||||
result.should.eql []
|
||||
|
||||
it 'works when items have only been added', ->
|
||||
result = []
|
||||
inventory.each (stack)-> result.push stack.itemSlug.qualified
|
||||
result.should.eql ['boat', 'string', 'wool']
|
||||
|
||||
it 'works when items have been augmented', ->
|
||||
inventory.add ItemSlug.slugify 'iron_ingot'
|
||||
inventory.add ItemSlug.slugify 'boat'
|
||||
inventory.add ItemSlug.slugify('wool'), 2
|
||||
|
||||
result = []
|
||||
inventory.each (stack)-> result.push stack.itemSlug.qualified
|
||||
result.should.eql ['boat', 'iron_ingot', 'string', 'wool']
|
||||
|
||||
describe 'hasAtLeast', ->
|
||||
|
||||
it 'works when the item is completely absent', ->
|
||||
answer = inventory.hasAtLeast 'chicken', 1
|
||||
answer.should.be.false
|
||||
|
||||
it 'always returns true for zero quantity', ->
|
||||
inventory.hasAtLeast('chicken', 0).should.be.true
|
||||
inventory.hasAtLeast('wool', 0).should.be.true
|
||||
|
||||
it 'works for a quantity above 1', ->
|
||||
inventory.hasAtLeast('wool', 3).should.be.true
|
||||
inventory.hasAtLeast('wool', 4).should.be.true
|
||||
inventory.hasAtLeast('wool', 5).should.be.false
|
||||
|
||||
describe 'localize', ->
|
||||
|
||||
before ->
|
||||
modPack =
|
||||
modSlug:
|
||||
wool: 'minecraft'
|
||||
string: 'minecraft'
|
||||
boat: 'minecraft'
|
||||
stone_gear: 'buildcraft'
|
||||
findItem: (slug)->
|
||||
return slug:new ItemSlug @modSlug[slug.item], slug.item
|
||||
|
||||
it 'replaces item slugs with qualified slugs', ->
|
||||
inventory.add ItemSlug.slugify 'stone_gear'
|
||||
inventory.modPack = modPack
|
||||
inventory.localize()
|
||||
|
||||
slugs = []
|
||||
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
|
||||
slugs.should.eql [
|
||||
'minecraft__boat'
|
||||
'buildcraft__stone_gear'
|
||||
'minecraft__string'
|
||||
'minecraft__wool'
|
||||
]
|
||||
|
||||
it 'ignores qualified slugs', ->
|
||||
inventory.add ItemSlug.slugify 'buildcraft__stone_gear'
|
||||
inventory.modPack = modPack
|
||||
inventory.localize()
|
||||
|
||||
slugs = []
|
||||
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
|
||||
slugs.should.eql [
|
||||
'minecraft__boat'
|
||||
'buildcraft__stone_gear'
|
||||
'minecraft__string'
|
||||
'minecraft__wool'
|
||||
]
|
||||
|
||||
describe 'parse', ->
|
||||
|
||||
beforeEach ->
|
||||
inventory = new Inventory {}, silent:false
|
||||
|
||||
it 'ignores an empty string', ->
|
||||
result = inventory.parse ''
|
||||
result.unparse().should.eql ''
|
||||
|
||||
it 'can parse a single item without quantity', ->
|
||||
result = inventory.parse 'wool'
|
||||
result.unparse().should.equal 'wool'
|
||||
|
||||
it 'can parse a single item with quantity', ->
|
||||
result = inventory.parse '4.wool'
|
||||
result.unparse().should.equal '4.wool'
|
||||
|
||||
it 'can parse multiple mixed-type items', ->
|
||||
result = inventory.parse '4.wool:10.string:boat'
|
||||
result.unparse().should.equal 'boat:10.string:4.wool'
|
||||
|
||||
describe 'pop', ->
|
||||
|
||||
it 'returns null for an empty inventory', ->
|
||||
inventory = new Inventory
|
||||
result = inventory.pop()
|
||||
expect(result).to.be.null
|
||||
|
||||
it 'completely removes the last item', ->
|
||||
stack = inventory.pop()
|
||||
stack.itemSlug.qualified.should.equal 'wool'
|
||||
stack.quantity.should.equal 4
|
||||
inventory.unparse().should.equal 'boat:20.string'
|
||||
|
||||
it 'triggers the right events', ->
|
||||
events = new EventRecorder inventory
|
||||
result = inventory.pop()
|
||||
events.names.should.eql [c.event.remove, c.event.change]
|
||||
|
||||
describe 'remove', ->
|
||||
|
||||
it 'does nothing when the item is absent', ->
|
||||
before = inventory.unparse()
|
||||
inventory.remove 'foo'
|
||||
after = inventory.unparse()
|
||||
|
||||
before.should.equal after
|
||||
|
||||
it 'throws when the item has insufficient quantity', ->
|
||||
expect(-> inventory.remove('wool', 10)).to.throw Error,
|
||||
'cannot remove 10: only 4 wool in this inventory'
|
||||
|
||||
it 'removes all items by default', ->
|
||||
inventory.remove 'wool'
|
||||
expect(inventory._stacks.wool).to.be.empty
|
||||
|
||||
it 'removes a quantity above 1', ->
|
||||
inventory.remove 'wool', 3
|
||||
inventory._stacks.wool.quantity.should.equal 1
|
||||
|
||||
it 'emits the proper events', ->
|
||||
events = new EventRecorder inventory
|
||||
inventory.remove 'wool'
|
||||
events.names.should.eql [c.event.change, c.event.remove, c.event.change]
|
||||
@@ -1,90 +0,0 @@
|
||||
#
|
||||
# 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()
|
||||
@@ -1,96 +0,0 @@
|
||||
#
|
||||
# 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()
|
||||
@@ -1,122 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - item_slug.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
ItemSlug = require './item_slug'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'item_slug.coffee', ->
|
||||
|
||||
describe 'constructor', ->
|
||||
|
||||
it 'can handle one argument', ->
|
||||
slug = new ItemSlug 'alpha'
|
||||
slug.item.should.equal 'alpha'
|
||||
expect(slug.mod).to.be.null
|
||||
slug.qualified.should.equal 'alpha'
|
||||
|
||||
it 'can handle two arguments', ->
|
||||
slug = new ItemSlug 'alpha', 'bravo'
|
||||
slug.mod.should.equal 'alpha'
|
||||
slug.item.should.equal 'bravo'
|
||||
slug.qualified.should.equal 'alpha__bravo'
|
||||
|
||||
it 'throws with zero arguments', ->
|
||||
f = -> new ItemSlug
|
||||
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
|
||||
|
||||
it 'throws with more arguments', ->
|
||||
f = -> new ItemSlug 'alpha', 'bravo', 'charlie'
|
||||
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
|
||||
|
||||
describe 'ItemSlug.compare', ->
|
||||
|
||||
it 'sorts by item when both are qualified in the same mod', ->
|
||||
a = new ItemSlug 'alpha', 'bravo'
|
||||
b = new ItemSlug 'charlie', 'bravo'
|
||||
|
||||
ItemSlug.compare(a, b).should.equal -1
|
||||
ItemSlug.compare(b, a).should.equal +1
|
||||
|
||||
it 'sorts by item when not qualified', ->
|
||||
a = new ItemSlug 'alpha'
|
||||
b = new ItemSlug 'bravo'
|
||||
|
||||
ItemSlug.compare(a, b).should.equal -1
|
||||
ItemSlug.compare(b, a).should.equal +1
|
||||
|
||||
describe 'ItemSlug.equal', ->
|
||||
|
||||
it 'requires both to have the same mod', ->
|
||||
a = new ItemSlug 'alpha', 'bravo'
|
||||
b = new ItemSlug 'alpha', 'charlie'
|
||||
c = new ItemSlug 'alpha', 'bravo'
|
||||
|
||||
ItemSlug.equal(a, b).should.be.false
|
||||
ItemSlug.equal(a, c).should.be.true
|
||||
|
||||
it 'requires both to have the same item', ->
|
||||
a = new ItemSlug 'alpha', 'bravo'
|
||||
b = new ItemSlug 'alpha', 'charlie'
|
||||
c = new ItemSlug 'alpha', 'bravo'
|
||||
|
||||
ItemSlug.equal(a, b).should.be.false
|
||||
ItemSlug.equal(a, c).should.be.true
|
||||
|
||||
describe 'ItemSlug.slugify', ->
|
||||
|
||||
it 'can slugify a pure name', ->
|
||||
slug = ItemSlug.slugify 'Alpha Bravo (Charlie)'
|
||||
slug.item.should.equal 'alpha_bravo_charlie'
|
||||
expect(slug.mod).to.be.null
|
||||
|
||||
it 'can slugify a simple item slug', ->
|
||||
slug = ItemSlug.slugify 'alpha_bravo_charlie'
|
||||
slug.item.should.equal 'alpha_bravo_charlie'
|
||||
expect(slug.mod).to.be.null
|
||||
|
||||
it 'can slugify a fully-qualified slug', ->
|
||||
slug = ItemSlug.slugify 'alpha_bravo__charlie_delta'
|
||||
slug.mod.should.equal 'alpha_bravo'
|
||||
slug.item.should.equal 'charlie_delta'
|
||||
|
||||
describe 'matches', ->
|
||||
|
||||
it 'ignores mod when either is unqualified', ->
|
||||
a = new ItemSlug 'alpha', 'bravo'
|
||||
b = new ItemSlug 'bravo'
|
||||
c = new ItemSlug 'charlie'
|
||||
|
||||
a.matches(b).should.be.true
|
||||
a.matches(c).should.be.false
|
||||
|
||||
it 'observes differences in mod when all are qualified', ->
|
||||
a = new ItemSlug 'alpha', 'bravo'
|
||||
b = new ItemSlug 'charlie', 'bravo'
|
||||
c = new ItemSlug 'delta', 'echo'
|
||||
d = new ItemSlug 'alpha', 'bravo'
|
||||
|
||||
a.matches(b).should.be.false
|
||||
a.matches(c).should.be.false
|
||||
a.matches(d).should.be.true
|
||||
|
||||
describe 'isQualified', ->
|
||||
|
||||
it 'returns true only when the mod slug is set', ->
|
||||
a = new ItemSlug 'alpha', 'bravo'
|
||||
b = new ItemSlug 'charlie'
|
||||
a.isQualified.should.be.true
|
||||
b.isQualified.should.be.false
|
||||
|
||||
describe '[]', ->
|
||||
|
||||
it 'allows slugs as a key', ->
|
||||
slug = new ItemSlug 'alpha', 'bravo'
|
||||
data = {}
|
||||
data[slug] = 'foo'
|
||||
data['alpha__bravo'].should.equal 'foo'
|
||||
data[slug].should.equal 'foo'
|
||||
@@ -1,224 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,30 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - mod.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
Mod = require './mod'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
mod = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'mod.coffee', ->
|
||||
|
||||
beforeEach -> mod = new Mod name:'Test', slug:'test'
|
||||
|
||||
describe 'compareTo', ->
|
||||
|
||||
it 'lists required mods first', ->
|
||||
minecraft = new Mod name:'Minecraft', slug:'minecraft'
|
||||
mod.compareTo(minecraft).should.equal +1
|
||||
minecraft.compareTo(mod).should.equal -1
|
||||
|
||||
it 'sorts by name second', ->
|
||||
buildcraft = new Mod name:'Buildcraft', slug:'buildcraft'
|
||||
mod.compareTo(buildcraft).should.equal +1
|
||||
buildcraft.compareTo(mod).should.equal -1
|
||||
@@ -1,176 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,103 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - mod_pack.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
Item = require './item'
|
||||
ItemSlug = require './item_slug'
|
||||
Mod = require './mod'
|
||||
ModPack = require './mod_pack'
|
||||
ModVersion = require './mod_version'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
buildcraft = industrialCraft = minecraft = modPack = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'mod_pack.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
minecraft = new Mod slug:'minecraft', name:'Minecraft'
|
||||
minecraft.addModVersion new ModVersion modSlug:minecraft.slug, version:'1.7.10'
|
||||
minecraft.activeModVersion.addItem new Item name:'Wool'
|
||||
minecraft.activeModVersion.addItem new Item name:'Bed', recipes:['']
|
||||
minecraft.activeModVersion.registerName ItemSlug.slugify('iron_chestplate'), 'Iron Chestplate'
|
||||
|
||||
buildcraft = new Mod slug:'buildcraft', name:'Buildcraft'
|
||||
buildcraft.addModVersion new ModVersion modSlug:buildcraft.slug, version:'6.2.6'
|
||||
buildcraft.activeModVersion.addItem new Item name:'Stone Gear', recipes:['']
|
||||
buildcraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
|
||||
buildcraft.activeVersion = Mod.Version.None
|
||||
|
||||
industrialCraft = new Mod slug:'industrial_craft', name:'Industrial Craft'
|
||||
industrialCraft.addModVersion new ModVersion modSlug:industrialCraft.slug, version:'2.0'
|
||||
industrialCraft.activeModVersion.addItem new Item name:'Resin'
|
||||
industrialCraft.activeModVersion.addItem new Item name:'Rubber'
|
||||
industrialCraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
|
||||
industrialCraft.activeVersion = Mod.Version.None
|
||||
|
||||
modPack = new ModPack
|
||||
modPack.addMod minecraft
|
||||
modPack.addMod buildcraft
|
||||
modPack.addMod industrialCraft
|
||||
|
||||
describe 'findItem', ->
|
||||
|
||||
it 'can find an item by partial slug', ->
|
||||
item = modPack.findItem ItemSlug.slugify 'wool'
|
||||
item.slug.qualified.should.equal 'minecraft__wool'
|
||||
|
||||
it 'can find an item by full slug', ->
|
||||
item = modPack.findItem ItemSlug.slugify 'minecraft__wool'
|
||||
item.name.should.equal 'Wool'
|
||||
|
||||
it 'can find an ambiguous item by full slug', ->
|
||||
buildcraft.activeVersion = Mod.Version.Latest
|
||||
industrialCraft.activeVersion = Mod.Version.Latest
|
||||
item = modPack.findItem ItemSlug.slugify 'industrial_craft__wrench'
|
||||
item.name.should.equal 'Wrench'
|
||||
item.modVersion.mod.name.should.equal 'Industrial Craft'
|
||||
|
||||
it 'can find an ambiguous item by partial slug', ->
|
||||
buildcraft.activeVersion = Mod.Version.Latest
|
||||
industrialCraft.activeVersion = Mod.Version.Latest
|
||||
item = modPack.findItem ItemSlug.slugify 'wrench'
|
||||
item.name.should.equal 'Wrench'
|
||||
item.modVersion.mod.name.should.equal 'Buildcraft'
|
||||
|
||||
describe 'findItemByName', ->
|
||||
|
||||
it 'finds the requested item', ->
|
||||
item = modPack.findItemByName 'Bed'
|
||||
item.name.should.equal 'Bed'
|
||||
|
||||
it 'ignores disabled mod versions', ->
|
||||
item = modPack.findItemByName 'Stone Gear'
|
||||
expect(item).to.be.null
|
||||
|
||||
describe 'findItemDisplay', ->
|
||||
|
||||
it 'returns all data for a regular Minecraft item', ->
|
||||
display = modPack.findItemDisplay ItemSlug.slugify 'bed'
|
||||
display.iconUrl.should.equal '/data/minecraft/items/bed/icon.png'
|
||||
display.itemUrl.should.equal '/browse/minecraft/bed/'
|
||||
display.itemName.should.equal 'Bed'
|
||||
display.modSlug.should.equal 'minecraft'
|
||||
|
||||
it 'returns all data for an item in an enabled mod', ->
|
||||
buildcraft.activeVersion = '6.2.6'
|
||||
display = modPack.findItemDisplay ItemSlug.slugify 'stone_gear'
|
||||
display.iconUrl.should.equal '/data/buildcraft/items/stone_gear/icon.png'
|
||||
display.itemUrl.should.equal '/browse/buildcraft/stone_gear/'
|
||||
display.itemName.should.equal 'Stone Gear'
|
||||
display.modSlug.should.equal 'buildcraft'
|
||||
|
||||
it 'assumes an unfound item is from Minecraft', ->
|
||||
display = modPack.findItemDisplay ItemSlug.slugify 'iron_chestplate'
|
||||
display.iconUrl.should.equal '/data/minecraft/items/iron_chestplate/icon.png'
|
||||
display.itemUrl.should.equal '/browse/minecraft/iron_chestplate/'
|
||||
display.itemName.should.equal 'Iron Chestplate'
|
||||
display.modSlug.should.equal 'minecraft'
|
||||
@@ -1,196 +0,0 @@
|
||||
#
|
||||
# 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»
|
||||
}"
|
||||
@@ -1,101 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - mod_version.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
Item = require './item'
|
||||
ItemSlug = require './item_slug'
|
||||
ModVersion = require './mod_version'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
modVersion = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'mod_version.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
modVersion = new ModVersion modSlug:'test', version:'0.0'
|
||||
modVersion.addItem new Item name:'underscore', group:'punctuation'
|
||||
modVersion.addItem new Item name:'bravo', group:'letter'
|
||||
modVersion.addItem new Item name:'alpha', group:'letter'
|
||||
modVersion.addItem new Item name:'one', group:'number'
|
||||
modVersion.addItem new Item name:'two', group:'number'
|
||||
|
||||
describe 'constructor', ->
|
||||
|
||||
it 'requires a mod slug', ->
|
||||
expect(-> new ModVersion version:'0.0').to.throw Error, 'attributes.modSlug is required'
|
||||
|
||||
it 'requires a mod version', ->
|
||||
expect(-> new ModVersion modSlug:'test').to.throw Error, 'attributes.version is required'
|
||||
|
||||
describe 'addItem', ->
|
||||
|
||||
it 'refuses to add duplicates', ->
|
||||
modVersion.addItem new Item name:'Wool'
|
||||
expect(-> modVersion.addItem new Item name:'Wool').to.throw Error, 'duplicate item for Wool'
|
||||
|
||||
it 'adds an item indexed by its slug', ->
|
||||
modVersion.addItem new Item name:'Wool'
|
||||
modVersion._items.wool.name.should.equal 'Wool'
|
||||
|
||||
it 'sets the modVersion', ->
|
||||
modVersion.addItem new Item name:'Wool'
|
||||
modVersion._items.wool.modVersion.should.equal modVersion
|
||||
|
||||
describe 'eachGroup', ->
|
||||
|
||||
it 'returns all the groups in order', ->
|
||||
groupNames = []
|
||||
modVersion.eachGroup (groupName)-> groupNames.push groupName
|
||||
groupNames.should.eql ['letter', 'number', 'punctuation']
|
||||
|
||||
describe 'eachItemInGroup', ->
|
||||
|
||||
it 'returns immediately for unknown group', ->
|
||||
slugs = []
|
||||
modVersion.eachItemInGroup 'foobar', (item)-> slugs.push item.slug.qualified
|
||||
slugs.should.eql []
|
||||
|
||||
it 'calls callback for exactly the items in a group in order', ->
|
||||
slugs = []
|
||||
modVersion.eachItemInGroup 'letter', (item)-> slugs.push item.slug.qualified
|
||||
slugs.should.eql ['test__alpha', 'test__bravo']
|
||||
|
||||
slugs = []
|
||||
modVersion.eachItemInGroup 'number', (item)-> slugs.push item.slug.qualified
|
||||
slugs.should.eql ['test__one', 'test__two']
|
||||
|
||||
|
||||
describe 'findItemByName', ->
|
||||
|
||||
it 'locates items by slugified name', ->
|
||||
modVersion.addItem new Item name:'Crafting Table'
|
||||
modVersion.findItemByName('Crafting Table').slug.qualified.should.equal 'test__crafting_table'
|
||||
|
||||
describe 'findRecipes', ->
|
||||
|
||||
beforeEach ->
|
||||
modVersion = new ModVersion modSlug:'test', version:'1.0'
|
||||
modVersion.parse """
|
||||
schema:1
|
||||
|
||||
item: Cake
|
||||
recipe:; input: Milk, Sugar, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
|
||||
recipe:; input: Milk, Cocoa Beans, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
|
||||
recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice
|
||||
item: Bucket
|
||||
recipe:; input: Iron Ingot; pattern: ... 0.0 .0.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -1,85 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,85 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - multiblock.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
ItemSlug = require './item_slug'
|
||||
Mod = require './mod'
|
||||
Multiblock = require './multiblock'
|
||||
Stack = require './stack'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'multiblock.coffee', ->
|
||||
|
||||
describe 'with a single block type', ->
|
||||
|
||||
beforeEach ->
|
||||
@input = [ new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1 ]
|
||||
|
||||
it 'correctly parses a 1x1x1 cube', ->
|
||||
model = new Multiblock input:@input, layers:['0']
|
||||
model.depth.should.equal 1
|
||||
model.height.should.equal 1
|
||||
model.width.should.equal 1
|
||||
model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone'
|
||||
|
||||
it 'correctly parses a solid 3x3x3 cube', ->
|
||||
model = new Multiblock input:@input, layers:['000 000 000', '000 000 000', '000 000 000']
|
||||
model.depth.should.equal 3
|
||||
model.height.should.equal 3
|
||||
model.width.should.equal 3
|
||||
|
||||
for x in [0..2]
|
||||
for y in [0..2]
|
||||
for z in [0..2]
|
||||
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
|
||||
|
||||
it 'correctly parses a hollow 3x3x3 cube', ->
|
||||
model = new Multiblock input:@input, layers:['000 000 000', '000 0.0 000', '000 000 000']
|
||||
model.depth.should.equal 3
|
||||
model.height.should.equal 3
|
||||
model.width.should.equal 3
|
||||
|
||||
for x in [0..2]
|
||||
for y in [0..2]
|
||||
for z in [0..2]
|
||||
if x isnt 1 or y isnt 1 or z isnt 1
|
||||
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
|
||||
else
|
||||
expect(model.getStackAt(x, y, z)).to.be.null
|
||||
|
||||
it 'correctly parses a 3x2x3 pyramid', ->
|
||||
model = new Multiblock input:@input, layers:['000 000 000', '.. .0 ..']
|
||||
model.depth.should.equal 3
|
||||
model.height.should.equal 2
|
||||
model.width.should.equal 3
|
||||
|
||||
for y in [0..1]
|
||||
for z in [0..2]
|
||||
for x in [0..2]
|
||||
if y is 1 and (x isnt 1 or z isnt 1)
|
||||
expect(model.getStackAt(x, y, z)).to.be.null
|
||||
else
|
||||
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
|
||||
|
||||
describe 'with multiple block types', ->
|
||||
|
||||
beforeEach ->
|
||||
@input = [
|
||||
new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1
|
||||
new Stack itemSlug:ItemSlug.slugify('stone'), quantity:1
|
||||
new Stack itemSlug:ItemSlug.slugify('oak wood'), quantity:1
|
||||
]
|
||||
|
||||
it 'correctly parses a 1x3x1 column of different types', ->
|
||||
model = new Multiblock input:@input, layers:['0', '1', '2']
|
||||
model.depth.should.equal 1
|
||||
model.height.should.equal 3
|
||||
model.width.should.equal 1
|
||||
|
||||
model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone'
|
||||
model.getStackAt(0, 1, 0).toString().should.equal '1 stone'
|
||||
model.getStackAt(0, 2, 0).toString().should.equal '1 oak_wood'
|
||||
@@ -1,252 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,90 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,33 +0,0 @@
|
||||
#
|
||||
# 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}"
|
||||
@@ -1,23 +0,0 @@
|
||||
#
|
||||
# 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}"
|
||||
@@ -1,143 +0,0 @@
|
||||
#
|
||||
# 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]
|
||||
@@ -1,121 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,112 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,134 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,55 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,165 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,50 +0,0 @@
|
||||
#
|
||||
# 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(', ')
|
||||
@@ -1,59 +0,0 @@
|
||||
#
|
||||
# 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 []
|
||||
@@ -1,67 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,54 +0,0 @@
|
||||
#
|
||||
# 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 []
|
||||
@@ -1,62 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,58 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,33 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,41 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,38 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,48 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,143 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,125 +0,0 @@
|
||||
#
|
||||
# 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 []
|
||||
@@ -1,51 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,63 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,138 +0,0 @@
|
||||
#
|
||||
# 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')]
|
||||
@@ -1,48 +0,0 @@
|
||||
#
|
||||
# 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'
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,72 +0,0 @@
|
||||
#
|
||||
# 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()
|
||||
@@ -1,99 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,94 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,380 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,344 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,65 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,51 +0,0 @@
|
||||
#
|
||||
# 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,46 +1,94 @@
|
||||
#
|
||||
# Crafting Guide - craft_page.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
Craftsman = require '../crafting/craftsman'
|
||||
Inventory = require '../game/inventory'
|
||||
ModPack = require '../game/mod_pack'
|
||||
{Inventory} = require("crafting-guide-common").models
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
{PlanBuilder} = require("crafting-guide-common").crafting
|
||||
{ResourcesEvaluator} = require("crafting-guide-common").crafting
|
||||
{StepsEvaluator} = require("crafting-guide-common").crafting
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftPage extends BaseModel
|
||||
module.exports = class CraftPage extends Observable
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.modPack then throw new Error 'attributes.modPack is required'
|
||||
attributes.params ?= null
|
||||
attributes.craftsman ?= new Craftsman attributes.modPack
|
||||
super attributes, options
|
||||
constructor: (attributes={})->
|
||||
super
|
||||
|
||||
@modPack.on c.event.change, => @_consumeParams()
|
||||
@on c.event.change + ':params', => @_consumeParams()
|
||||
@craftsman.on c.event.change + ':stage', => @trigger c.event.change, this
|
||||
@craftsman.on c.event.change + ':complete', => @trigger c.event.change, this
|
||||
@muted -> @modPack = attributes.modPack
|
||||
|
||||
@_currentPlan = null
|
||||
@_have = new Inventory
|
||||
@_planBuilder = new PlanBuilder new StepsEvaluator
|
||||
@_want = new Inventory
|
||||
|
||||
@_consumeParams attributes.params
|
||||
|
||||
@_have.on Observable::ANY, this, "_onHaveChanged"
|
||||
@_want.on Observable::ANY, this, "_onWantChanged"
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
currentPlan:
|
||||
get: -> return @_currentPlan
|
||||
set: -> throw new Error "currentPlan cannot be assigned"
|
||||
|
||||
have:
|
||||
get: -> return @_have
|
||||
set: -> throw new "have cannot be assigned"
|
||||
|
||||
isDirty:
|
||||
get: -> return @_isDirty
|
||||
set: -> throw new Error "isDirty cannot be assigned"
|
||||
|
||||
isOutdated:
|
||||
get: -> return @currentPlan? and @isDirty
|
||||
set: -> throw new Error "isOutdated cannot be assigned"
|
||||
|
||||
modPack:
|
||||
get: -> return @_modPack
|
||||
set: (modPack)->
|
||||
if @_modPack? then throw new Error "modPack cannot be reassigned"
|
||||
if not modPack? then throw new Error "modPack is required"
|
||||
@_modPack = modPack
|
||||
|
||||
planBuilder:
|
||||
get: -> return @_planBuilder
|
||||
set: -> throw new Error "planBuilder cannot be assigned"
|
||||
|
||||
want:
|
||||
get: -> return @_want
|
||||
set: -> throw new Error "want cannot be assigned"
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
createPlan: ->
|
||||
newPlan = @_planBuilder.createPlan @_want, @_have
|
||||
@triggerPropertyChange "currentPlan", @_currentPlan, newPlan, ->
|
||||
@_currentPlan = newPlan
|
||||
@triggerPropertyChange "isDirty", @_isDirty, false
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_consumeParams: ->
|
||||
return unless @params?
|
||||
_consumeParams: (params)->
|
||||
return unless params?.inventoryText?
|
||||
|
||||
@craftsman.want.clear()
|
||||
if not @params.inventoryText?
|
||||
@params = null
|
||||
else
|
||||
inventory = new Inventory
|
||||
inventory.parse @params.inventoryText
|
||||
newWant = Inventory.fromUrlString params.inventoryText, @modPack
|
||||
@_want.clear()
|
||||
|
||||
inventory.each (stack)=>
|
||||
item = @modPack.findItem stack.itemSlug, enableAsNeeded:true
|
||||
return unless item? and item.isCraftable
|
||||
@craftsman.want.add stack.itemSlug, stack.quantity
|
||||
inventory.remove stack.itemSlug
|
||||
for itemId, stack of newWant.stacks
|
||||
continue unless stack.item.isCraftable
|
||||
@_want.add stack.item, stack.quantity
|
||||
|
||||
if inventory.isEmpty then @params = null
|
||||
_onHaveChanged: ->
|
||||
@triggerPropertyChange "isDirty", @_isDirty, true
|
||||
|
||||
_onWantChanged: ->
|
||||
@triggerPropertyChange "isDirty", @_isDirty, true, ->
|
||||
@_isDirty = true
|
||||
if @want.isEmpty then @triggerPropertyChange "currentPlan", @_currentPlan, null
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#
|
||||
# Crafting Guide - editable_file.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
{BaseModel} = require("crafting-guide-common").deprecated
|
||||
w = require "when"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - email_client.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,78 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
{BaseModel} = require("crafting-guide-common").deprecated
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#
|
||||
# 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,56 +1,95 @@
|
||||
#
|
||||
# Crafting Guide - item_page.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
{Item} = require("crafting-guide-common").models
|
||||
ItemDisplay = require "./item_display"
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemPage extends BaseModel
|
||||
module.exports = class ItemPage extends Observable
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not attributes.modPack? then throw new Error 'attributes.modPack is required'
|
||||
attributes.item ?= null
|
||||
super attributes, options
|
||||
constructor: (item)->
|
||||
if item.constructor isnt Item then throw new Error "item must be an Item instance"
|
||||
@item = item
|
||||
super
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
item:
|
||||
get: -> return @_item
|
||||
set: (item)->
|
||||
if @_item? then throw new Error "item cannot be reassigned"
|
||||
if not item? then throw new Error "item is required"
|
||||
if @_item? then @_item.off Observable::ANY, this
|
||||
@_item = item
|
||||
@_item.on Observable::ANY, this, "_onItemChanged"
|
||||
|
||||
itemDisplay:
|
||||
get: -> return @_itemDisplay ?= new ItemDisplay @item
|
||||
|
||||
mod:
|
||||
get: -> return @_item.mod
|
||||
set: -> throw new Error "mod cannot be assigned"
|
||||
|
||||
modPack:
|
||||
get: -> return @mod.modPack
|
||||
set: -> throw new Error "modPack cannot be assigned"
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
findComponentInItems: ->
|
||||
return @_findRecipesMatching (recipe)=> recipe.requires @item.slug
|
||||
|
||||
findSimilarItems: ->
|
||||
return null unless @item?.modVersion?
|
||||
|
||||
result = []
|
||||
@item.modVersion.eachItemInGroup @item.group, (item)=>
|
||||
result.push item
|
||||
|
||||
return null unless result.length > 0
|
||||
return result
|
||||
return @_findItemsWithMatchingRecipes (recipe)=>
|
||||
recipe.computeQuantityRequired @item
|
||||
|
||||
findRecipes: ->
|
||||
return @modPack.findRecipes @item?.slug, [], alwaysFromOwningMod:true
|
||||
return (recipe for recipeId, recipe of @item.recipes)
|
||||
|
||||
findToolForRecipes: ->
|
||||
return @_findRecipesMatching (recipe)=> recipe.requiresTool @item.slug
|
||||
findSimilarItems: ->
|
||||
group = @mod.itemGroups[@item.groupName]
|
||||
return unless group?
|
||||
|
||||
return null unless group.length > 0
|
||||
return group
|
||||
|
||||
findToolForItems: ->
|
||||
return @_findItemsWithMatchingRecipes (recipe)=>
|
||||
return recipe.tools[@item.id]?
|
||||
|
||||
loadDetails: ->
|
||||
stores.itemDetail.loadDetailFor @item
|
||||
.catch (error)=>
|
||||
if error.message.indexOf("404") is -1
|
||||
logger.error "Could not load details for item #{@item.id}: #{error.message}"
|
||||
else
|
||||
logger.info "Item #{@item.id} has no details file."
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_findRecipesMatching: (isAcceptable)->
|
||||
return null unless @item?
|
||||
|
||||
_findItemsWithMatchingRecipes: (isMatching)->
|
||||
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
|
||||
|
||||
result = _.values result
|
||||
return null unless result.length > 0
|
||||
for modId, mod of @modPack.mods
|
||||
for itemId, item of mod.items
|
||||
for recipeId, recipe of item.recipes
|
||||
if isMatching(recipe)
|
||||
result[item.id] = item
|
||||
|
||||
result = (item for itemId, item of result)
|
||||
result.sort (a, b)->
|
||||
if not a?.displayName? and not b?.displayName? then return 0
|
||||
if not b?.displayName? then return +1
|
||||
if not a?.displayName? then return +1
|
||||
return a.displayName.localeCompare b.displayName
|
||||
|
||||
return result
|
||||
|
||||
_onItemChanged: ->
|
||||
@trigger Observable::CHANGE
|
||||
|
||||
return result.sort (a, b)-> a.compareTo b
|
||||
|
||||
@@ -1,89 +1,104 @@
|
||||
#
|
||||
# Crafting Guide - item_selector.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class ItemSelector extends BaseModel
|
||||
module.exports = class ItemSelector extends Observable
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
if not options.modPack? then throw new Error 'options.modPack is required'
|
||||
options.isAcceptable ?= (item)-> return true # accept everything by default
|
||||
super attributes, options
|
||||
constructor: (modPack, options={})->
|
||||
super
|
||||
|
||||
@_isAcceptable = options.isAcceptable
|
||||
@_isAcceptable = options.isAcceptable or (item)-> return true # accept everything by default
|
||||
@_maxResults = 100
|
||||
@_minHintLength = 3
|
||||
@_modPack = options.modPack
|
||||
@_results = []
|
||||
|
||||
@modPack = modPack
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
hint:
|
||||
get: ->
|
||||
return @_hint
|
||||
get: -> return @_hint
|
||||
set: (hint)->
|
||||
@triggerPropertyChange "hint", @_hint, hint, ->
|
||||
@_hint = hint?.toLowerCase()
|
||||
@_refreshResults()
|
||||
|
||||
set: (newHint)->
|
||||
oldHint = @_hint
|
||||
return if newHint is oldHint
|
||||
|
||||
@_hint = newHint.toLowerCase()
|
||||
|
||||
@trigger c.event.change + ':hint', this, oldHint, newHint
|
||||
@_refreshResults()
|
||||
@trigger c.event.change, this
|
||||
modPack:
|
||||
get: -> return @_modPack
|
||||
set: (modPack)->
|
||||
if not modPack? then throw new Error "modPack is required"
|
||||
if @_modPack? then throw new Error "modPack cannot be reassigned"
|
||||
@_modPack = modPack
|
||||
|
||||
results:
|
||||
get: ->
|
||||
return @_results
|
||||
get: -> return @_results
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_isMatch: (name, itemSlug)->
|
||||
_computeScore: (item)->
|
||||
hintIndex = 0
|
||||
hintLetter = @_hint[hintIndex]
|
||||
name = name.toLowerCase()
|
||||
name = item.displayName.toLowerCase()
|
||||
nextScore = 1
|
||||
totalScore = 0
|
||||
|
||||
for nameIndex in [0...name.length] by 1
|
||||
if name.charAt(nameIndex) is hintLetter
|
||||
totalScore += nextScore
|
||||
nextScore += 1
|
||||
hintIndex += 1
|
||||
if hintIndex is @_hint.length
|
||||
item = @_modPack.findItem itemSlug
|
||||
return false unless item?
|
||||
return @_isAcceptable item
|
||||
hintLetter = @_hint[hintIndex]
|
||||
return 0 unless @_isAcceptable item
|
||||
break
|
||||
|
||||
return false
|
||||
hintLetter = @_hint[hintIndex]
|
||||
else
|
||||
nextScore = 1
|
||||
|
||||
return 0 if hintIndex < @_hint.length
|
||||
return totalScore
|
||||
|
||||
_refreshResults: ->
|
||||
oldResults = @_results
|
||||
newResults = {}
|
||||
scoredItems = []
|
||||
count = 0
|
||||
|
||||
logger.verbose => "Looking for items which match: #{@_hint}"
|
||||
if @_hint.length >= @_minHintLength
|
||||
@_modPack.eachMod (mod)=>
|
||||
for modId, mod of @_modPack.mods
|
||||
return if count >= @_maxResults
|
||||
return unless mod.enabled
|
||||
return unless mod.isEnabled
|
||||
|
||||
mod.eachName (name, itemSlug)=>
|
||||
return if count >= @_maxResults
|
||||
for itemId, item of mod.items
|
||||
score = @_computeScore item
|
||||
if score > 0
|
||||
scoredItems.push score:score, item:item
|
||||
|
||||
if @_isMatch name, itemSlug
|
||||
if itemSlug.isQualified
|
||||
newResults[itemSlug] = itemSlug
|
||||
count += 1
|
||||
scoredItems.sort (a, b)->
|
||||
if a.score isnt b.score
|
||||
return if a.score > b.score then -1 else +1
|
||||
|
||||
newResults = _.values(newResults)
|
||||
newResults.sort ItemSlug.compare
|
||||
nameA = a.item.displayName
|
||||
nameB = b.item.displayName
|
||||
if nameA.length isnt nameB.length
|
||||
return if nameA.length < nameB.length then -1 else +1
|
||||
|
||||
if nameA isnt nameB
|
||||
return if nameA < nameB then -1 else +1
|
||||
|
||||
return 0
|
||||
|
||||
@_results = []
|
||||
for element in scoredItems
|
||||
@_results.push element.item
|
||||
break if @_results.length >= @_maxResults
|
||||
|
||||
@trigger c.event.change + ':results', this
|
||||
|
||||
@_results = newResults
|
||||
@trigger c.event.change + ':results', this, oldResults, newResults
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - markdown_image.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
#
|
||||
# Crafting Guide - markdown_image_list.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseModel = require '../base_model'
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
MarkdownImage = require './markdown_image'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class MarkdownImageList extends BaseModel
|
||||
module.exports = class MarkdownImageList extends Observable
|
||||
|
||||
constructor: (attributes={}, options={})->
|
||||
attributes.imageBase ?= ''
|
||||
attributes.markdownText ?= null
|
||||
super attributes, options
|
||||
constructor: (options={})->
|
||||
super
|
||||
|
||||
@client = options.client
|
||||
@_images = {}
|
||||
@_images = []
|
||||
@muted =>
|
||||
@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()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
@@ -46,22 +42,43 @@ module.exports = class MarkdownImageList extends BaseModel
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
getAll: ->
|
||||
fileNames = (fileName for fileName, image of @_images).sort()
|
||||
result = []
|
||||
for fileName in fileNames
|
||||
result.push @_images[fileName]
|
||||
|
||||
return result
|
||||
|
||||
isValid: ->
|
||||
for fileName, image of @_images
|
||||
return false unless image.valid
|
||||
return true
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
all: {get:@prototype.getAll}
|
||||
valid: {get:@prototype.isValid}
|
||||
|
||||
all:
|
||||
get: ->
|
||||
fileNames = (fileName for fileName, image of @_images).sort()
|
||||
result = []
|
||||
for fileName in fileNames
|
||||
result.push @_images[fileName]
|
||||
|
||||
return result
|
||||
|
||||
client:
|
||||
get: -> return @_client
|
||||
set: (client)->
|
||||
if not client? then throw new Error "client is required"
|
||||
@_client = client
|
||||
|
||||
imageBase: ->
|
||||
get: -> return @_imageBase
|
||||
set: (imageBase)->
|
||||
@triggerPropertyChange "imageBase", @_imageBase, imageBase, ->
|
||||
@_imageBase = imageBase
|
||||
for fileName, image of @_images
|
||||
image.path = @imageBase
|
||||
|
||||
markdownText:
|
||||
get: -> return @_markdownText
|
||||
set: (markdownText)->
|
||||
@triggerPropertyChange "markdownText", @_markdownText, markdownText, ->
|
||||
@_markdownText = markdownText
|
||||
@_analyzeMarkdownText()
|
||||
|
||||
valid:
|
||||
get: ->
|
||||
for fileName, image of @_images
|
||||
return false unless image.valid
|
||||
return true
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
@@ -79,8 +96,8 @@ module.exports = class MarkdownImageList extends BaseModel
|
||||
image = @_images[fileName]
|
||||
if not image?
|
||||
image = new MarkdownImage {fileName:fileName, path:@imageBase}, {client:@client}
|
||||
@listenTo image, c.event.change, => @trigger c.event.change, this
|
||||
@trigger c.event.add, this, image
|
||||
image.on Observable::CHANGE, this, "_onImageChanged"
|
||||
@trigger Observable::ADD, image
|
||||
changed = true
|
||||
|
||||
newImages[fileName] = image
|
||||
@@ -92,4 +109,7 @@ module.exports = class MarkdownImageList extends BaseModel
|
||||
|
||||
if changed
|
||||
@_images = newImages
|
||||
@trigger c.event.change, this
|
||||
@trigger Observable::CHANGE
|
||||
|
||||
_onImageChanged: ->
|
||||
@trigger Observable::CHANGE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Crafting Guide - markdown_image_list.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
@@ -16,7 +16,7 @@ list = null
|
||||
describe 'markdown_image_list.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
list = new MarkdownImageList {}, client:{fetchFile:sinon.stub().returns(w.promise(->))}
|
||||
list = new MarkdownImageList client:{fetchFile:sinon.stub().returns(w.promise(->))}
|
||||
|
||||
describe '_analyzeMarkdownText', ->
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,134 @@
|
||||
#
|
||||
# Crafting Guide - recipe_display.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
_ = require "../../../common/underscore"
|
||||
c = require "../../../common/constants"
|
||||
{Inventory} = require("crafting-guide-common").models
|
||||
{Observable} = require("crafting-guide-common").util
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class RecipeDisplay extends Observable
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.recipe? then throw new Error "options.recipe is required"
|
||||
super
|
||||
|
||||
@_layerInventories = []
|
||||
|
||||
@multiplier = options.multiplier or 1
|
||||
@recipe = options.recipe
|
||||
@showInputInventory = if options.showInputInventory then options.showInputInventory or false
|
||||
@showOutputInventory = if options.showOutputInventory then options.showOutputInventory or false
|
||||
@showOutputSlot = if options.showOutputSlot then options.showOutputSlot or false
|
||||
|
||||
# Class Methods ################################################################################
|
||||
|
||||
@::RECIPE_TYPE =
|
||||
CRAFTING_TABLE: "crafting_table"
|
||||
MULTIBLOCK: "multiblock"
|
||||
|
||||
@::ALL_RECIPE_TYPES = (value for key, value of @::RECIPE_TYPE)
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
depth:
|
||||
get: -> return @recipe.depth
|
||||
set: -> throw new Error "depth cannot be assigned"
|
||||
|
||||
height:
|
||||
get: -> return @recipe.height
|
||||
set: -> throw new Error "height cannot be assigned"
|
||||
|
||||
multiplier:
|
||||
get: -> return @_multiplier
|
||||
set: (multiplier)->
|
||||
if not _.isNumber(multiplier) then multiplier = 1
|
||||
@triggerPropertyChange "multiplier", @_multiplier, multiplier
|
||||
|
||||
recipe:
|
||||
get: -> return @_recipe
|
||||
set: (recipe)->
|
||||
return if recipe is @_recipe
|
||||
if not recipe? then throw new Error "recipe is required"
|
||||
if @_recipe? then throw new Error "recipe cannot be reassigned"
|
||||
@_recipe = recipe
|
||||
@_computeType()
|
||||
|
||||
showInputInventory:
|
||||
get: -> return @_showInputInventory
|
||||
set: (showInputInventory)->
|
||||
@triggerPropertyChange "showInputInventory", @_showInputInventory, !!showInputInventory
|
||||
|
||||
showOutputInventory:
|
||||
get: -> return @_showOutputInventory
|
||||
set: (showOutputInventory)->
|
||||
@triggerPropertyChange "showOutputInventory", @_showOutputInventory, !!showOutputInventory
|
||||
|
||||
showOutputSlot:
|
||||
get: -> return @_showOutputSlot
|
||||
set: (showOutputSlot)->
|
||||
@triggerPropertyChange "showOutputSlot", @_showOutputSlot, !!showOutputSlot
|
||||
|
||||
tools:
|
||||
get: -> return @recipe.tools
|
||||
set: -> throw new Error "tools cannot be assigned"
|
||||
|
||||
type:
|
||||
get: -> return @_type
|
||||
set: -> throw new Error "type cannot be assigned"
|
||||
|
||||
width:
|
||||
get: -> return @recipe.width
|
||||
set: -> throw new Error "width cannot be assigned"
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
getInputAt: (x, y, z)->
|
||||
return @recipe.getInputAt x, y, z
|
||||
|
||||
getInventory: (z)->
|
||||
if not z?
|
||||
return @_fullInventory ?= @_createFullInventory()
|
||||
else
|
||||
return @_layerInventories[z] ?= @_addLayerInventory z
|
||||
|
||||
getOutputInventory: ->
|
||||
if not @_outputInventory?
|
||||
@_outputInventory = new Inventory
|
||||
for stack in @recipe.allProducts
|
||||
@_outputInventory.add stack.item, stack.quantity
|
||||
|
||||
return @_outputInventory
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_addLayerInventory: (z, inventory=null)->
|
||||
inventory ?= new Inventory
|
||||
for x in [0..@recipe.width]
|
||||
for y in [0..@recipe.height]
|
||||
stack = @recipe.getInputAt x, y, z
|
||||
continue unless stack?
|
||||
inventory.add stack.item, stack.quantity * @multiplier
|
||||
return inventory
|
||||
|
||||
_computeType: ->
|
||||
if not @recipe?
|
||||
type = null
|
||||
else if @recipe.depth > 1
|
||||
type = @RECIPE_TYPE.MULTIBLOCK
|
||||
else
|
||||
type = @RECIPE_TYPE.CRAFTING_TABLE
|
||||
@triggerPropertyChange "type", @_type, type
|
||||
|
||||
_createFullInventory: (inventory=null)->
|
||||
inventory ?= new Inventory()
|
||||
for z in [0..@recipe.depth]
|
||||
@_addLayerInventory z, inventory
|
||||
return inventory
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user