Author SHA1 Message Date
Andrew Miner c553924dd8 Fix typo in README
fixes #319
2018-04-03 22:08:58 -06:00
Andrew Miner 248cb30703 Announce Steve's Factory Manager 2018-03-27 21:14:14 -06:00
Andrew Miner 40218c6a16 Add Steve's Factory Manager (contributed by Cooltimmetje) 2018-03-27 20:56:13 -06:00
Andrew Miner 7fdec4720e Remove Amazon Associates trial code 2017-11-10 09:02:28 -08:00
Andrew Miner 510bd27742 Remove reference to Trello 2017-08-18 21:24:46 -07:00
Andrew Miner 3192c50870 Update the news page 2017-07-24 21:39:35 -07:00
Andrew Miner 6fc9a83948 Merge branch 'andrewminer/new-model-objects' 2017-07-23 20:07:59 -07:00
Andrew Miner c45572997f Make adjustments for mobile 2017-07-23 20:06:02 -07:00
Andrew Miner 59217a5fea Quantity change should cause step count change
fixes #294
2017-07-23 15:51:13 -07:00
Andrew Miner 0b94cc4a1f Update wiki editing to use new data format
fixes: #292
2017-07-23 15:16:21 -07:00
Andrew Miner cbf66bf5bb Fix bug where similar items weren't shown 2017-07-19 19:40:16 -07:00
Andrew Miner 8b0cd71bb5 Allow recipe comp. to show I/O inventories 2017-07-19 19:35:16 -07:00
Andrew Miner 0e6d75ec9b Recipe component can show multiblock recipes 2017-07-16 13:07:49 -04:00
Andrew Miner 3ca0dcceed Recipe compoent can show many types of recipes 2017-07-16 07:44:55 -04:00
Andrew Miner 89ba3ac48e Fix support for multiblock recipes 2017-07-16 04:58:16 -04:00
Andrew Miner ab96166601 Add missing detail data to item pages 2017-07-13 16:52:51 -04:00
Andrew Miner dac1a8dceb Ensure changing quantities outdates plans 2017-07-11 14:24:13 -07:00
Andrew Miner ccb2cad516 Fix search box to return better results 2017-07-11 09:58:56 -07:00
Andrew Miner 33479051c8 Ensure step helper buttons work
fixes #288
2017-07-11 09:26:09 -07:00
Andrew Miner b8a2dae9f2 Ensure inventory changes work on craft page 2017-07-11 05:51:52 -07:00
Andrew Miner 2fd1de40da Improve render time on long crafting plans 2017-07-04 23:28:56 -07:00
Andrew Miner f6f736f8be Update item detail page to new models 2017-07-04 22:52:01 -07:00
Andrew Miner 20119cf8e4 Initial effort to convert craft page to new models 2017-07-01 18:06:11 -07:00
Andrew Miner 997ff114b4 Move Amazon ad block lower on the page 2017-07-01 18:06:11 -07:00
Andrew Miner 6d62375c46 Add ad unit div for amazon ads 2017-07-01 18:06:11 -07:00
Andrew Miner b4cde5a37b Add Amazon ad unit 2017-07-01 18:06:10 -07:00
Andrew Miner 5f65f77222 Add support for videos on mod pages 2017-07-01 18:06:10 -07:00
Andrew Miner 5056708bdc Move Amazon ad block lower on the page 2017-06-23 19:38:47 -07:00
Andrew Miner df3dd8d9ef Add ad unit div for amazon ads 2017-06-18 10:35:44 -07:00
Andrew Miner 526af52b07 Add Amazon ad unit 2017-06-18 10:15:12 -07:00
Andrew Miner c9bc7371d9 Add support for videos on mod pages 2017-06-17 23:25:18 -07:00
Andrew Miner ac87fedca8 Update CraftingGuideCommon version 2017-06-15 22:42:22 -07:00
Andrew Miner 8b43c53a9e WIP 2017-06-15 22:39:17 -07:00
Andrew Miner 3bc7c4cf89 Fix bugs 2017-05-07 16:05:16 -07:00
Andrew Miner c4ba36b60a Convert new files on master to use CGC 2017-05-02 19:26:35 -07:00
Andrew Miner b05e237aee Merge branch 'crafting-algo-3' 2017-05-02 19:25:08 -07:00
Andrew Miner 7865ba98c3 Update copyright notice 2017-05-02 19:19:35 -07:00
Andrew Miner 69e4d968b5 Upgrade packages to most recent versions & lock 2017-05-02 19:18:02 -07:00
Andrew Miner bd96bc6575 Move model files to crafting-guide-common 2017-05-02 18:34:53 -07:00
Andrew Miner 90a41a59f0 Update link to YouTube channel instead of playlist 2017-04-05 18:40:29 -07:00
Andrew Miner ebdb6158ba Update news page about new feature 2017-03-31 20:53:45 -07:00
Andrew Miner 407e3fca5d Add mod version selector to browse page tiles 2017-03-31 20:47:39 -07:00
Andrew Miner 3d9895a6d5 Refactor mod version selector to a control 2017-03-31 20:32:28 -07:00
Andrew Miner ee6887e803 Add YouTube link 2017-03-21 19:39:06 -07:00
Andrew Miner fb48362a59 Add Google verification for YouTube 2017-03-21 18:06:11 -07:00
Andrew Miner 86a547f422 Make sure samples update craft page URL
fixes #241
2017-01-28 21:02:49 -08:00
Andrew Miner 0131c8ff3b Add handling for browsers without JavaScript
fixes #243
2017-01-28 20:55:24 -08:00
Andrew Miner 478364418b Consider item craftable if multiblock
Because the item page use the `craftable` attribute of the item to
decide whether to show the "Show Crafting Plan" button, it was not
showing that button for multiblocks (which don't have any normal
recipes). This change make it so that an item is also considered to
be craftable if it has a multiblock recipe.

fixes #256
2017-01-28 20:38:58 -08:00
Andrew Miner ed96cf0c71 Update SAYT algo to give better matches
The algorithm will now give higher rankings to items containing
multiple sequential letters which match, and higher rankings to
those whose length more closely matches the given term.
2017-01-28 19:59:18 -08:00
Andrew Miner 5d8251bebb Fix algo. for new Agricraft version 2016-12-28 18:14:34 -08:00
Andrew Miner 69ca6b06b9 wip 2016-12-27 15:25:33 -08:00
Andrew Miner 7377559f7b Initial take on crafting algo Mk.III 2016-09-25 13:14:42 -07:00
Andrew Miner c0472b268d Don't hang the site if a mod doesn't load 2016-09-16 19:53:37 -07:00
Andrew Miner 13b717836a 3.2.6 2016-09-15 19:39:13 -07:00
Andrew Miner 016c61e983 Add Solar Expansion 2016-09-15 19:30:19 -07:00
Andrew Miner d7743285cb Updated version & add Thermal Foundation 2016-09-14 20:43:21 -07:00
Andrew Miner cc9232d143 Updated version & add Thermal Foundation 2016-09-14 18:01:32 -07:00
Andrew Miner a1e7fb4fa0 Add news entry about exporter mod 2016-09-10 20:19:36 -07:00
Andrew Miner 9ac25efb0c Add more debugging data when crafting can't craft 2016-09-10 20:12:22 -07:00
Andrew Miner 19a3e9846c 3.2.5 2016-08-21 19:27:38 -07:00
Andrew Miner c5e9920473 Add Draconic Evolution 2016-08-21 19:12:18 -07:00
Andrew Miner 96b5beeb8c Prune invalid recipes 2016-08-21 16:06:48 -07:00
Andrew Miner d0bca0d2ac Include Twitter script after Twitter button
The Twitter button would frequently fail to render properly because
the script which dynamically creates the proper widget would load
before the DOM element which serves as the placeholder for the widget
hadn't yet been created.  This change ensures that the widget script
is always loaded after the placeholder has been created.
2016-06-02 11:50:58 -06:00
Andrew Miner d102834408 Fix mod ballots on mobile 2016-06-01 18:40:45 -06:00
Andrew Miner 00ca88500e Add announcement for new voting feature 2016-06-01 18:09:05 -06:00
Andrew Miner 6a2b3805a2 Don't change wait image when setting icon 2016-06-01 17:33:19 -06:00
Andrew Miner eba8c99ac0 Show mod icons in mod ballot 2016-06-01 17:22:01 -06:00
Andrew Miner b82bfea8f1 Add client ID for staging env 2016-06-01 13:10:08 -06:00
Andrew Miner d0a2292094 Add mod voting feature 2016-06-01 12:49:29 -06:00
Andrew Miner cb962f6175 Ensure page view preceeds any event tracking 2016-05-19 20:22:51 -07:00
Andrew Miner 083cc30eb2 Update version of crafting-guide-common 2016-05-18 19:58:54 -07:00
Andrew Miner 22009557d2 Use new server configuration for logging in 2016-05-15 19:52:21 -07:00
Andrew Miner b34ca26d0a Include item slug in inventory tracking events 2016-05-12 22:33:39 -07:00
Andrew Miner 23b3f1c7e1 Add mod suggestion panel 2016-05-12 22:27:44 -07:00
Andrew Miner 6b84698802 Fix mod page to properly transmit page events 2016-05-12 17:33:26 -07:00
Andrew Miner c85fca8357 Remove multiblock hover tracking event 2016-05-11 22:52:17 -07:00
Andrew Miner 16848772a3 Add GoogleAnalytics event tracking 2016-05-11 21:39:43 -07:00
Andrew Miner c441d75529 Fix layout issues on News Page 2016-05-07 14:50:06 -07:00
Andrew Miner 1bc89c1c89 Fix layout issues for News page 2016-05-07 14:47:07 -07:00
Andrew Miner 43202e9917 Add link to Twitter account
fixes #226
2016-05-07 14:41:47 -07:00
Andrew Miner 05bc1a271d Add support for Google Sitelinks
fixes #187
2016-05-07 13:36:49 -07:00
Andrew Miner da1419d6b6 Add a "Random Item" link to the breadcrumb bar
fixes #216
2016-05-07 12:25:21 -07:00
Andrew Miner 841fac38d3 Correct errors on mobile search feature
fixes #235, #233
2016-05-07 09:49:51 -07:00
Andrew Miner 2b8e23b32f Fix layout of video sections on mobile 2016-05-07 09:39:11 -07:00
Andrew Miner 5eae2bb488 Start editing after login needed for editing
When a user isn't logged in, click the "Edit" button on an item
description will initiate the login process.  After it is complete,
the user is placed back on the page where they started.  However,
the edit box remains closed.  So, after all the activity which
resulted in trying to edit, they user still has to click the edit
button one more time to actually start editing.

This change makes it so that the user returning from the login
process is placed directly into edit mode without having to click the
button a second time.

fixes #237
2016-05-07 08:12:39 -07:00
Andrew Miner 6fa18c764b Correct keyboard handling errors in item selector
fixes #227, #218
2016-05-06 21:30:41 -07:00
Andrew Miner c9796a142d Improve scrolling behavior on mobile
fixes #221
2016-05-05 21:20:41 -07:00
Andrew Miner 0718590f56 Don't use large inventory on mobile crafting page
fixes #219
2016-05-04 21:50:48 -07:00
Andrew Miner 51f566ebf1 Mark button for current page as active
fixes #232
2016-05-04 21:27:23 -07:00
Andrew Miner d2d904be7f Don't override index.html from prerendered pages 2016-05-04 20:20:44 -07:00
Andrew Miner 088994f366 Correct bug where prerendered pages don't display 2016-05-03 22:22:03 -07:00
Andrew Miner 184b64a6af No sourcemaps for CSS 2016-05-03 22:06:36 -07:00
Andrew Miner 4aca035709 Correct sizing problem with mod tiles 2016-05-03 20:42:36 -07:00
Andrew Miner ebf3443e96 Update Adsense to place ads on all devices
The current adsense implementation only places ads on desktop-sized
screens.  This change allows ads of the appropriate size to be
displayed on any device.

fixes #224
2016-05-03 20:25:56 -07:00
Andrew Miner fd5372b35d Add announcement of mobile site 2016-04-30 13:02:41 -07:00
Andrew Miner f464558cd2 Don't show "login" button on mobile 2016-04-30 13:00:14 -07:00
Andrew Miner e7cd3f0549 Complete mobile reactiveness in CSS layer 2016-04-30 11:31:49 -07:00
Andrew Miner 545408a7e7 Fixes for reactivity in Safari 2016-04-29 23:08:08 -07:00
Andrew Miner 83156c30c7 Initial progress towards responsive for mobile 2016-04-29 22:30:37 -07:00
Andrew Miner 636a8d061c Don't show page until fully loaded 2016-04-28 22:38:04 -07:00
Andrew Miner 2dbda99986 Allow FileCache to operate on file markers midline 2016-04-28 20:22:54 -07:00
Andrew Miner 7d83552013 Load all data files from a single archive 2016-04-27 22:47:44 -07:00
Andrew Miner 35bb12de6a Show crafting plan button only when craftable 2016-04-26 21:53:39 -07:00
Andrew Miner 1d1772e961 Make the mod version selector more clear
This change the single select box into a checkbox which toggles the
mod on/off.  If the mod is "on" and it has multiple versions, then
a selection list will be shown with the various possible versions.
If there is only a single available version, then the selector is
not shown.
2016-04-26 21:40:52 -07:00
Andrew Miner 6787063ac7 Prevent border around tool buttons on craft page 2016-04-26 20:31:51 -07:00
Andrew Miner 9e09b8d87c Make "outdated" background more pronounced 2016-04-26 20:25:20 -07:00
Andrew Miner 2c77972073 Correct several small bugs found in user testing
* The "Show Plan for Tools" link should open a new window each time
    it is clicked (i.e., don't re-use the same window)
  * Further customize the title of the crafting page to give some
    indication of what is being crafted
2016-04-25 21:54:28 -07:00
Andrew Miner 31f4594928 Don't show border on a pretending to be buttons 2016-04-24 15:21:12 -07:00
Andrew Miner eb9ac15de8 Show stack quantity tooltip for large quantities
When hovering over a quantity in an inventory display, if the
quantity is larger than one stack (i.e., 64 items), display a
tooltip showing the quantity in stacks (plus the remainder).
2016-04-24 15:17:46 -07:00
Andrew Miner e922f3c5cc Fix iOS viewport meta tag 2016-04-23 20:22:05 -07:00
Andrew Miner a8d08b93c5 Add reactive styles for Tablets 2016-04-23 20:10:09 -07:00
Andrew Miner 64e118a2c8 Open crafting plan for tools in a new window 2016-04-23 16:54:50 -07:00
Andrew Miner 6671541dcb Announce changes to crafting page 2016-04-23 11:58:48 -07:00
Andrew Miner 962165dc8e Don't automatically recalculate crafting plans
Instead, any time a change is made which invalidates a crafting plan,
update the visual display to show that the plan is out of date, and
add some extra affordances to recalculate.
2016-04-23 11:46:35 -07:00
Andrew Miner f9b65dd892 3.2.4 2016-04-21 22:59:37 -07:00
Andrew Miner 979586d9d3 Add Tinker's Construct 2016-04-21 22:59:00 -07:00
Andrew Miner c522e60e28 Fix router error on item page 2016-04-20 22:58:58 -07:00
Andrew Miner f9cd38947d Create item group controllers off main event loop 2016-04-20 22:30:55 -07:00
Andrew Miner 99b6cf2861 Don't clear user inventory between crafting plans 2016-04-20 22:30:14 -07:00
Andrew Miner d925a17940 Do not show pass-through items in derived items
For the various derived item groups on the item detail page, do not
show pass through items as being related (e.g., "Used to Make").
2016-04-20 22:28:42 -07:00
Andrew Miner a2bbdf4645 Improve perf. looking up item display data 2016-04-20 22:27:22 -07:00
Andrew Miner 93c98e16ac Further fixes of how passthrough items are handled
* When evaluating a plan, only give the pass-through treatment to
  items if the step they're part of was actually executed
* When marking a step "complete", ignore passthrough items
2016-04-16 20:33:05 -07:00
Andrew Miner 88e57b1200 Improve handling of pass-through items in recipes 2016-04-16 12:20:29 -07:00
Andrew Miner 1e96cf2d6c 3.2.3 2016-04-14 20:59:20 -07:00
Andrew Miner ccaf8e9842 Add ProjectRed 2016-04-14 20:54:44 -07:00
Andrew Miner e1f8282049 Fix bug with evaluating mod conditions 2016-04-10 14:10:23 -07:00
Andrew Miner d873c4b7f1 Fix layout bug on browse page in Safari 2016-04-10 14:09:56 -07:00
Andrew Miner 3529bccf3a Add back mods lost in website upgrade 2016-04-10 14:08:38 -07:00
244 changed files with 8032 additions and 9037 deletions
+2
View File
@@ -1,5 +1,7 @@
*.log
*.swp
.DS_Store
.env
.sass-cache
.tm_properties
aws.config
+18 -44
View File
@@ -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()
+1 -3
View File
@@ -1,8 +1,6 @@
# Crafting Guide
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From the your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
**Check out the [Trello Board](https://trello.com/b/s9N0aTYd/crafting-guide) to see what's coming up!**
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
## Adding new features
+1
View File
@@ -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

+2917
View File
File diff suppressed because it is too large Load Diff
+34 -34
View File
@@ -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"
}
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
clear && printf '\e[3J'
+2
View File
@@ -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>
+50 -35
View File
@@ -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'
apiBaseUrl = "http://api-staging.crafting-guide.com"
when "crafting-guide.com"
global.env = "production"
logger.level = Logger.INFO
apiBaseUrl = 'http://api.crafting-guide.com'
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"
+3 -1
View File
@@ -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"
}
}
-88
View File
@@ -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
-151
View File
@@ -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}"
-36
View File
@@ -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}
-227
View File
@@ -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]
-90
View File
@@ -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()
-96
View File
@@ -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'
-224
View File
@@ -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
-30
View File
@@ -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
-176
View File
@@ -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
-103
View File
@@ -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'
-196
View File
@@ -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
-85
View File
@@ -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'
-252
View File
@@ -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
-90
View File
@@ -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}"
-23
View File
@@ -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
-112
View File
@@ -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]
+77 -29
View File
@@ -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
+3 -2
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
#
# Crafting Guide - email_client.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# Copyright © 2014-2017 by Redwood Labs
# All rights reserved.
#
+88
View File
@@ -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'
+2 -2
View File
@@ -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"
+72 -33
View File
@@ -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
+60 -45
View File
@@ -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
set: (newHint)->
oldHint = @_hint
return if newHint is oldHint
@_hint = newHint.toLowerCase()
@trigger c.event.change + ':hint', this, oldHint, newHint
get: -> return @_hint
set: (hint)->
@triggerPropertyChange "hint", @_hint, hint, ->
@_hint = hint?.toLowerCase()
@_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 -1
View File
@@ -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
@_images = []
@muted =>
@client = options.client
@_images = {}
@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,7 +42,10 @@ module.exports = class MarkdownImageList extends BaseModel
# Property Methods #############################################################################
getAll: ->
Object.defineProperties @prototype,
all:
get: ->
fileNames = (fileName for fileName, image of @_images).sort()
result = []
for fileName in fileNames
@@ -54,15 +53,33 @@ module.exports = class MarkdownImageList extends BaseModel
return result
isValid: ->
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
Object.defineProperties @prototype,
all: {get:@prototype.getAll}
valid: {get:@prototype.isValid}
# Private Methods ##############################################################################
_analyzeMarkdownText: ->
@@ -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

Some files were not shown because too many files have changed in this diff Show More