Author SHA1 Message Date
Andrew Miner 9ddbd5cda7 Add remaining commands 2016-04-15 21:22:10 -07:00
Andrew Miner 31f6e12844 wip 2016-04-13 22:31:11 -07:00
Andrew Miner 14278c81df Add first stage of a new single-file parser
This first stage consists of the general framework for the parser as
a whole, a lexical analyzers to converts text into commands, and the
semantic anlyzer to convert commands into a complete data structure
representing the cumulative effect of running the commands.
2016-04-10 13:37:17 -07:00
244 changed files with 9032 additions and 8027 deletions
-2
View File
@@ -1,7 +1,5 @@
*.log
*.swp
.DS_Store
.env
.sass-cache
.tm_properties
aws.config
+44 -18
View File
@@ -1,7 +1,7 @@
#
# Crafting Guide - Gruntfile.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
@@ -17,6 +17,29 @@ EXTERNAL_LIBS = [
'./vendor/email.js:emailjs'
]
PUBLISHED_FILES = [
'./client/models/base_model.coffee'
'./client/models/game/inventory.coffee'
'./client/models/game/item.coffee'
'./client/models/game/item_slug.coffee'
'./client/models/game/mod.coffee'
'./client/models/game/mod_version.coffee'
'./client/models/game/multiblock.coffee'
'./client/models/game/recipe.coffee'
'./client/models/game/stack.coffee'
'./client/models/game/simple_stack.coffee'
'./client/models/parsing/command_parser_version_base.coffee'
'./client/models/parsing/mod_parser.coffee'
'./client/models/parsing/mod_parser_v1.coffee'
'./client/models/parsing/mod_version_parser.coffee'
'./client/models/parsing/mod_version_parser_v1.coffee'
'./client/models/parsing/versioned_parser_base.coffee'
'./client/models/site/tutorial.coffee'
'./common/constants.coffee'
'./common/underscore.coffee'
'./index.coffee'
]
########################################################################################################################
module.exports = (grunt)->
@@ -31,28 +54,30 @@ module.exports = (grunt)->
mode: 'gzip'
files: [
{expand: true, cwd:'./build/static', src:'**/*.css', dest:'./dist/'}
{expand: true, cwd:'./build/static', src:['**/*.html', '!**/index.html'], dest:'./dist/'}
{expand: true, cwd:'./build/static', src:'**/*.html', dest:'./dist/'}
{expand: true, cwd:'./build/static', src:'**/*.ugly.js', dest:'./dist/', ext: '.js'}
{expand: true, cwd:'./build/static', src:'**/*.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', '!index.html']
dest: './dist'
{expand:true, cwd:'./build/static/', src:['**/*', '!**/*.map', '!**/*.js'], dest:'./dist'}
]
common_source:
files: [
@@ -98,9 +123,9 @@ module.exports = (grunt)->
mochaTest:
options:
bail: false
bail: true
color: true
reporter: 'list'
reporter: 'dot'
require: [
'coffee-script/register'
'./src/test_helper.coffee'
@@ -110,8 +135,6 @@ module.exports = (grunt)->
sass:
all:
options:
sourcemap: 'none'
files:
'./build/static/main.css': [ './build/imports.scss' ]
@@ -164,7 +187,7 @@ module.exports = (grunt)->
['jade', 'build:copy', 'build:css', 'browserify:external', 'browserify:internal']
grunt.registerTask 'build:copy', 'helper task for build',
['copy:assets_build', 'copy:env_build', 'copy:common_source', 'copy:server_source']
['copy:assets_build', 'copy:common_source', 'copy:server_source']
grunt.registerTask 'build:css', 'helper task for build',
['sass_globbing', 'sass']
@@ -184,11 +207,14 @@ module.exports = (grunt)->
grunt.registerTask 'dist', 'build the project to be run from Amazon S3',
['build', 'copy:build_to_dist', 'uglify', 'compress']
grunt.registerTask 'prepublish', 'build the project to be published to NPM as shared code',
['clean', 'coffee']
grunt.registerTask 'publish', 'publish the project to NPM',
['prepublish', 'script:publish', 'clean', 'build']
grunt.registerTask 'start', 'build the project and start a local HTTP server',
['script:start']
['build', 'script:start']
grunt.registerTask 'test', 'run unit tests',
['mochaTest']
@@ -199,7 +225,7 @@ module.exports = (grunt)->
grunt.registerTask 'upload:staging', 'upload the project the staging Amazon S3 environment',
['dist', 'script:s3_upload:staging']
grunt.registerTask 'use-local-deps', 'use local dependencies instead of NPM dependencies',
grunt.registerTask 'useLocalDeps', 'use local dependencies instead of NPM dependencies',
['script:use_local_deps']
# Code Tasks #######################################################################################################
@@ -242,7 +268,7 @@ module.exports = (grunt)->
grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
done = this.async()
grunt.util.spawn cmd:'./scripts/clear_buffer', opts:{stdio:'inherit'}, (error)-> done(error)
grunt.util.spawn cmd:'clear', opts:{stdio:'inherit'}, (error)-> done(error)
grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", ->
done = this.async()
+3 -1
View File
@@ -1,6 +1,8 @@
# Crafting Guide
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
Crafting Guide is the ultimate step-by-step guide for making anything in Minecraft. From the your first [crafting table](http://crafting-guide.com/item/crafting_table) all the way up to your IndustrialCraft2 [QuantumSuit Bodyarmor](http://crafting-guide.com/item/quantumsuit_bodyarmor), Crafting Guide breaks down the recipes to raw ingredients and displays step-by-step instructions for what to craft, how much to craft, and in what order. It also supports crafting multiple items at once, and can even include crafting the necessary tools in the step-by-step instructions.
**Check out the [Trello Board](https://trello.com/b/s9N0aTYd/crafting-guide) to see what's coming up!**
## Adding new features
-1
View File
@@ -1 +0,0 @@
google-site-verification: google077275160ac09f2f.html
Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

-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.6",
"version": "3.2.2",
"description": "A guide for building anything in Minecraft or its many mods",
"main": "./dist/index.js",
"repository": "http://github.com/andrewminer/crafting-guide",
"scripts": {
"build": "grunt build",
"prepublish": "grunt prepublish",
"start": "grunt start",
"test": "grunt test"
},
"author": "Andrew Miner",
"license": "UNLICENSED",
"license": "none",
"devDependencies": {
"chai": "3.5.0",
"coffee-script": "1.12.5",
"coffeeify": "2.1.0",
"grunt": "1.0.1",
"grunt-contrib-clean": "1.1.0",
"grunt-contrib-coffee": "1.0.0",
"grunt-contrib-compress": "1.4.1",
"grunt-contrib-copy": "1.0.0",
"grunt-contrib-jade": "1.0.0",
"grunt-contrib-sass": "1.0.0",
"grunt-contrib-uglify": "2.3.0",
"grunt-contrib-watch": "1.0.0",
"grunt-mocha-test": "0.13.2",
"grunt-sass-globbing": "1.5.1",
"mocha": "3.3.0",
"sass": "0.5.0",
"sinon": "2.2.0",
"sinon-chai": "2.10.0"
"chai": "^1.10.0",
"coffee-script": "^1.9.3",
"coffeeify": "^1.1.0",
"grunt": "^0.4.5",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-coffee": "^1.0.0",
"grunt-contrib-compress": "^0.14.0",
"grunt-contrib-copy": "^0.8.2",
"grunt-contrib-jade": "^0.15.0",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-uglify": "^0.11.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-mocha-test": "^0.12.7",
"grunt-sass-globbing": "^1.4.0",
"mocha": "^2.4.5",
"sass": "^0.5.0",
"sinon": "^1.14.1",
"sinon-chai": "^2.7.0"
},
"dependencies": {
"backbone": "1.3.3",
"body-parser": "1.17.1",
"client-sessions": "0.8.0",
"cookie-parser": "1.4.3",
"crafting-guide-common": ">=5.0.0",
"dotenv": "4.0.0",
"express": "4.15.2",
"express-session": "1.15.2",
"jade": "1.11.0",
"jquery": "3.2.1",
"marked": "0.3.6",
"underscore": "1.8.3",
"underscore.inflections": "0.2.1",
"when": "3.7.8"
"backbone": "^1.2.3",
"body-parser": "^1.13.3",
"client-sessions": "^0.7.0",
"cookie-parser": "^1.3.5",
"crafting-guide-common": "^1.5.1",
"express": "^4.13.3",
"express-session": "^1.11.3",
"jade": "^1.11.0",
"jquery": "^2.1.4",
"marked": "^0.3.5",
"underscore": "^1.8.3",
"underscore.inflections": "^0.2.1",
"when": "^3.7.3"
}
}
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
clear && printf '\e[3J'
-2
View File
@@ -1,7 +1,5 @@
#!/bin/bash
grunt watch &
pushd './build/' &>/dev/null
clear
nodemon ./server.coffee $*
@@ -1,353 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ActiveLayerIndex</key>
<integer>0</integer>
<key>ApplicationVersion</key>
<array>
<string>com.omnigroup.OmniGraffle.MacAppStore</string>
<string>139.18</string>
</array>
<key>AutoAdjust</key>
<false/>
<key>BackgroundGraphic</key>
<dict>
<key>Bounds</key>
<string>{{0, 0}, {320, 320}}</string>
<key>Class</key>
<string>SolidGraphic</string>
<key>ID</key>
<integer>2</integer>
<key>Style</key>
<dict>
<key>shadow</key>
<dict>
<key>Draws</key>
<string>NO</string>
</dict>
<key>stroke</key>
<dict>
<key>Draws</key>
<string>NO</string>
</dict>
</dict>
</dict>
<key>BaseZoom</key>
<integer>0</integer>
<key>CanvasOrigin</key>
<string>{0, 0}</string>
<key>CanvasSize</key>
<string>{320, 320}</string>
<key>ColumnAlign</key>
<integer>1</integer>
<key>ColumnSpacing</key>
<real>36</real>
<key>CreationDate</key>
<string>2016-04-23 05:39:56 +0000</string>
<key>Creator</key>
<string>Andrew Miner</string>
<key>DisplayScale</key>
<string>1 pt = 1 px</string>
<key>GraphDocumentVersion</key>
<integer>8</integer>
<key>GraphicsList</key>
<array>
<dict>
<key>AllowConnections</key>
<string>NO</string>
<key>AllowLabelDrop</key>
<false/>
<key>AllowToConnect</key>
<false/>
<key>Class</key>
<string>LineGraphic</string>
<key>ID</key>
<integer>5</integer>
<key>Points</key>
<array>
<string>{160, 480}</string>
<string>{480, 160}</string>
</array>
<key>Style</key>
<dict>
<key>stroke</key>
<dict>
<key>Cap</key>
<integer>0</integer>
<key>Color</key>
<dict>
<key>a</key>
<string>0.2</string>
<key>b</key>
<string>0</string>
<key>g</key>
<string>1</string>
<key>r</key>
<string>1</string>
</dict>
<key>HeadArrow</key>
<string>0</string>
<key>Legacy</key>
<true/>
<key>TailArrow</key>
<string>0</string>
<key>Width</key>
<real>120</real>
</dict>
</dict>
</dict>
<dict>
<key>AllowConnections</key>
<string>NO</string>
<key>AllowLabelDrop</key>
<false/>
<key>AllowToConnect</key>
<false/>
<key>Class</key>
<string>LineGraphic</string>
<key>ID</key>
<integer>4</integer>
<key>Points</key>
<array>
<string>{-160, 160}</string>
<string>{160, -160}</string>
</array>
<key>Style</key>
<dict>
<key>stroke</key>
<dict>
<key>Cap</key>
<integer>0</integer>
<key>Color</key>
<dict>
<key>a</key>
<string>0.2</string>
<key>b</key>
<string>0</string>
<key>g</key>
<string>1</string>
<key>r</key>
<string>1</string>
</dict>
<key>HeadArrow</key>
<string>0</string>
<key>Legacy</key>
<true/>
<key>TailArrow</key>
<string>0</string>
<key>Width</key>
<real>120</real>
</dict>
</dict>
</dict>
<dict>
<key>AllowConnections</key>
<string>NO</string>
<key>AllowLabelDrop</key>
<false/>
<key>AllowToConnect</key>
<false/>
<key>Class</key>
<string>LineGraphic</string>
<key>ID</key>
<integer>3</integer>
<key>Points</key>
<array>
<string>{0, 320}</string>
<string>{320, 0}</string>
</array>
<key>Style</key>
<dict>
<key>stroke</key>
<dict>
<key>Cap</key>
<integer>0</integer>
<key>Color</key>
<dict>
<key>a</key>
<string>0.2</string>
<key>b</key>
<string>0</string>
<key>g</key>
<string>1</string>
<key>r</key>
<string>1</string>
</dict>
<key>HeadArrow</key>
<string>0</string>
<key>Legacy</key>
<true/>
<key>TailArrow</key>
<string>0</string>
<key>Width</key>
<real>120</real>
</dict>
</dict>
</dict>
</array>
<key>GridInfo</key>
<dict>
<key>GridSpacing</key>
<real>10</real>
<key>MajorGridSpacing</key>
<integer>10</integer>
<key>ShowsGrid</key>
<string>YES</string>
<key>SnapsToGrid</key>
<string>YES</string>
</dict>
<key>GuidesLocked</key>
<string>NO</string>
<key>GuidesVisible</key>
<string>YES</string>
<key>HPages</key>
<integer>1</integer>
<key>ImageCounter</key>
<integer>1</integer>
<key>KeepToScale</key>
<false/>
<key>Layers</key>
<array>
<dict>
<key>Lock</key>
<string>NO</string>
<key>Name</key>
<string>Layer 1</string>
<key>Print</key>
<string>YES</string>
<key>View</key>
<string>YES</string>
</dict>
</array>
<key>LayoutInfo</key>
<dict>
<key>Animate</key>
<string>NO</string>
<key>circoMinDist</key>
<real>18</real>
<key>circoSeparation</key>
<real>0.0</real>
<key>layoutEngine</key>
<string>dot</string>
<key>neatoSeparation</key>
<real>0.0</real>
<key>twopiSeparation</key>
<real>0.0</real>
</dict>
<key>LinksVisible</key>
<string>NO</string>
<key>MagnetsVisible</key>
<string>NO</string>
<key>MasterSheets</key>
<array/>
<key>ModificationDate</key>
<string>2016-04-27 03:11:38 +0000</string>
<key>Modifier</key>
<string>Andrew Miner</string>
<key>NotesVisible</key>
<string>NO</string>
<key>Orientation</key>
<integer>2</integer>
<key>OriginVisible</key>
<string>NO</string>
<key>PageBreaks</key>
<string>NO</string>
<key>PrintInfo</key>
<dict>
<key>NSBottomMargin</key>
<array>
<string>float</string>
<string>41</string>
</array>
<key>NSHorizonalPagination</key>
<array>
<string>coded</string>
<string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
</array>
<key>NSLeftMargin</key>
<array>
<string>float</string>
<string>18</string>
</array>
<key>NSPaperSize</key>
<array>
<string>size</string>
<string>{611.99998474121094, 792}</string>
</array>
<key>NSPrintReverseOrientation</key>
<array>
<string>int</string>
<string>0</string>
</array>
<key>NSRightMargin</key>
<array>
<string>float</string>
<string>18</string>
</array>
<key>NSTopMargin</key>
<array>
<string>float</string>
<string>18</string>
</array>
</dict>
<key>PrintOnePage</key>
<false/>
<key>ReadOnly</key>
<string>NO</string>
<key>RowAlign</key>
<integer>1</integer>
<key>RowSpacing</key>
<real>36</real>
<key>SheetTitle</key>
<string>Canvas 1</string>
<key>SmartAlignmentGuidesActive</key>
<string>YES</string>
<key>SmartDistanceGuidesActive</key>
<string>YES</string>
<key>UniqueID</key>
<integer>1</integer>
<key>UseEntirePage</key>
<false/>
<key>VPages</key>
<integer>1</integer>
<key>WindowInfo</key>
<dict>
<key>CurrentSheet</key>
<integer>0</integer>
<key>ExpandedCanvases</key>
<array>
<dict>
<key>name</key>
<string>Canvas 1</string>
</dict>
</array>
<key>Frame</key>
<string>{{215, 0}, {2134, 1417}}</string>
<key>ListView</key>
<true/>
<key>OutlineWidth</key>
<integer>142</integer>
<key>RightSidebar</key>
<false/>
<key>ShowRuler</key>
<true/>
<key>Sidebar</key>
<true/>
<key>SidebarWidth</key>
<integer>120</integer>
<key>VisibleRegion</key>
<string>{{-840, -477}, {1999, 1275}}</string>
<key>Zoom</key>
<real>1</real>
<key>ZoomValues</key>
<array>
<array>
<string>Canvas 1</string>
<real>1</real>
<real>1</real>
</array>
</array>
</dict>
</dict>
</plist>
+36 -51
View File
@@ -1,93 +1,78 @@
#
# Crafting Guide - client.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
############################################################################################################
# Allow Node.js-style `global` in addition to `window`
if typeof(global) is "undefined"
if typeof(global) is 'undefined'
window.global = window
global._ = require "../common/underscore"
global.$ = require "jquery"
global.c = require "../common/constants"
global._ = require '../common/underscore'
global.$ = require 'jquery'
global.c = require '../common/constants'
global.π = Math.PI
global.ε = 0.0001
global.w = require "when"
global.w = require 'when'
{Logger} = require("crafting-guide-common").util
global.Backbone = Backbone = require 'backbone'
Backbone.$ = $
{Logger} = require 'crafting-guide-common'
global.logger = new Logger
Tracker = require "./tracker"
global.tracker = new Tracker
marked = require "marked"
marked = require 'marked'
marked.setOptions sanitize:true
global.Backbone = Backbone = require "backbone"
Backbone.$ = $
########################################################################################################################
global.hostName = window.location.hostname
switch global.hostName
when "prerender.crafting-guide.com"
global.env = "prerender"
when 'prerender.crafting-guide.com'
global.env = 'prerender'
logger.level = Logger.FATAL
apiBaseUrl = "http://prerender.crafting-guide.com:4347"
when "local.crafting-guide.com", "localhost"
global.env = "local"
apiBaseUrl = 'http://local.crafting-guide.com:8001'
when 'local.crafting-guide.com', 'localhost'
global.env = 'local'
logger.level = Logger.DEBUG
apiBaseUrl = "http://#{global.hostName}:4347"
when "staging.crafting-guide.com"
global.env = "staging"
apiBaseUrl = 'http://local.crafting-guide.com:8001'
when 'staging.crafting-guide.com'
global.env = 'staging'
logger.level = Logger.VERBOSE
apiBaseUrl = "http://api-staging.crafting-guide.com"
when "crafting-guide.com"
global.env = "production"
logger.level = Logger.INFO
tracker.enabled = true
apiBaseUrl = "http://api.crafting-guide.com"
apiBaseUrl = 'http://api-staging.crafting-guide.com'
when 'crafting-guide.com'
global.env = 'production'
logger.level = Logger.INFO
apiBaseUrl = 'http://api.crafting-guide.com'
else
throw new Error "cannot determine the environment of: #{window.location.hostname}"
########################################################################################################################
Storage = require "./storage"
Storage = require './storage'
storage = new Storage storage:global.localStorage
########################################################################################################################
tracker.trackPageView()
{CraftingGuideClient} = require("crafting-guide-common").api
{CraftingGuideClient} = require 'crafting-guide-common'
client = _(new CraftingGuideClient(baseUrl:apiBaseUrl)).extend Backbone.Events
client.onStatusChanged = (client, oldStatus, newStatus)->
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
client.trigger "change:status", client, oldStatus, newStatus
client.trigger "change", client
client.trigger 'change:status', client, oldStatus, newStatus
client.trigger 'change', client
client.startMonitoringStatus()
client.checkStatus()
########################################################################################################################
{http, ModPackStore, ItemDetailStore} = require("crafting-guide-common").api
storeOptions = http:http, baseUrl:"#{location.protocol}//#{location.hostname}:#{location.port}"
global.stores =
modPack: new ModPackStore storeOptions
itemDetail: new ItemDetailStore storeOptions
SiteController = require './site/site_controller'
global.site = site = new SiteController client:client, storage:storage
site.render()
site.loadDefaultModPack()
site.loadCurrentUser()
########################################################################################################################
global.stores.modPack.load c.modPacks.default
.then (modPack)->
SiteController = require "./site/site_controller"
global.site = site = new SiteController client:client, storage:storage, modPack:modPack
site.render()
site.loadCurrentUser()
Backbone.history.start pushState:true
logger.info -> "CraftingGuide is ready"
Backbone.history.start pushState:true
logger.info -> "CraftingGuide is ready"
+1 -3
View File
@@ -1,7 +1,7 @@
//-
//- Crafting Guide - _google_analytics.jade
//-
//- Copyright © 2014-2017 by Redwood Labs
//- Copyright © 2014-2016 by Redwood Labs
//- All rights reserved.
//-
@@ -15,6 +15,4 @@ script.
ga('create', 'UA-54970416-2', 'crafting-guide.com');
ga('require', 'displayfeatures');
ga('require', 'linkid', 'linkid.js');
} else {
window.ga = function() {}
}
@@ -1,18 +0,0 @@
//-
//- Crafting Guide - _google_sitelinks.jade
//-
//- Copyright © 2014-2017 by Redwood Labs
//- All rights reserved.
//-
script(type="application/ld+json").
{
"@context": "http://schema.org",
"@type": "WebSite",
"url": "http://crafting-guide.com/",
"potentialAction": {
"@type": "SearchAction",
"target": "http://crafting-guide.com?search={search_term}",
"query-input": "required name=search_term"
}
}
+88
View File
@@ -0,0 +1,88 @@
#
# Crafting Guide - base_model.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class BaseModel extends Backbone.Model
constructor: (attributes={}, options={})->
options.logEvents ?= true
super attributes, options
makeGetter = (name)-> return -> @get name
makeSetter = (name)-> return (value)-> @set name, value
for name, value of attributes
continue if name is 'id'
Object.defineProperty this, name, get:makeGetter(name), set:makeSetter(name)
@logEvents = options.logEvents or false
@state = c.modelState.unloaded
@loading = null
Object.defineProperties this,
isUnloaded: { get:-> @state is c.modelState.unloaded }
isLoading: { get:-> @state is c.modelState.loading }
isLoaded: { get:-> @state is c.modelState.loaded }
isError: { get:-> @state is c.modelState.error }
# Event Methods ################################################################################
onLoadSucceeded: (text, status, xhr)->
try
@set @parse text
@state = c.modelState.loaded
@trigger c.event.change, this
@trigger c.event.sync, this
logger.info => "#{@constructor.name}.#{@cid} loaded successfully"
catch e
logger.error -> "A parsing error occured: #{e.stack}"
@onLoadFailed e.message, 'parsing failed', xhr
onLoadFailed: (error, status, xhr)->
@state = c.modelState.error
logger.error => "#{@constructor.name}.#{@cid} failed to load: status:#{status}, message:#{error}"
@trigger c.event.error, this, error
# Backbone.Model Overrides #####################################################################
fetch: (options={})->
options.force ?= false
return if (@isLoading or @isLoaded) and not options.force
url = @url()
logger.info => "#{@constructor.name}.#{@cid} reading from url: #{url}"
@state = c.modelState.loading
@trigger c.event.request, this
@loading = w.promise (resolve, reject)=>
$.ajax
url: url
dataType: 'text'
success: (text, status, xhr)=> resolve @onLoadSucceeded text, status, xhr
error: (xhr, status, error)=> reject @onLoadFailed error, status, xhr
@loading.catch (e)-> # do nothing
return @loading
parse: (text)->
return JSON.parse text
sync: (method, model)->
throw new Error "#{@constructor.name}.#{@cid} is not permitted to #{method}"
trigger: (name, model, args...)->
if @logEvents
argText = ("#{arg}"[0..50] for arg in args).join ", "
logger.trace => "#{@constructor.name}.#{@cid} triggered event #{name} with args: #{argText}"
super
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name}.#{@cid}"
@@ -0,0 +1,122 @@
#
# Crafting Guide - crafting_node.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class CraftingNode
@::TYPES =
INVENTORY: 0
ITEM: 1
RECIPE: 2
@::TYPE = null
constructor: (options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
@modPack = options.modPack
@_children = []
@_complete = null
@_id = _.uniqueId 'node_'
@_rotations = 0
@_valid = null
# Public Methods ###############################################################################
expand: (queue=[])->
return unless @valid
return queue if @children.length > 0
for child in @_createChildren()
child.parent = this
@_children.push child
queue.push child
return queue
acceptVisitor: (visitor)->
enter = visitor[@ENTER_METHOD]
if enter?
enter.call visitor, this
else
enter = visitor['onEnterOtherNode']
if enter? then enter.call visitor, this
for child in @_children
child.acceptVisitor visitor
leave = visitor[@LEAVE_METHOD]
if leave?
leave.call visitor, this
else
leave = visitor['onLeaveOtherNode']
if leave? then leave.call visitor, this
rotateChildren: ->
@_children.push @_children.shift()
@_rotations += 1
# Property Methods #############################################################################
Object.defineProperties @prototype,
children:
get: -> return @_children
complete:
get: ->
if not @_complete?
@_complete = @_checkCompleteness()
return @_complete
completeText:
get: -> if @complete then "◼︎" else "◻︎"
depth:
get: ->
maxDepth = 1
for child in @children
maxDepth = Math.max maxDepth, child.depth + 1
return maxDepth
id:
get: -> return @_id
rotations:
get: -> return @_rotations
size:
get: ->
size = 1
for child in @children
size += child.size
return size
valid:
get: -> @_checkValidity()
validText:
get: -> if @valid then "" else ""
# Virtual Methods ##############################################################################
# Subclasses must override this method to create such children as are appropriate for that kind of node. The new
# nodes should be appended to the given array.
_createChildren: (result)->
throw new Error "#{@constructor.name} must override the _createChildren method"
# Subclasses must override this method to indicate whether the node has at least one "complete" subtree rooted with
# itself. Completeness may be defined differently by different subclasses.
_checkCompleteness: ->
throw new Error "#{@constructor.name} must override the _checkCompletness method"
# Subclasses must override this method to determine whether the subtree represented by a node should still be
# explored for having a useful crafting plan.
_checkValidity: ->
throw new Error "#{@constructor.name} must override the _checkValidity method"
@@ -0,0 +1,44 @@
#
# Crafting Guide - crafting_node.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
fixtures = require './fixtures.test'
########################################################################################################################
root = null
########################################################################################################################
describe 'crafting_node.coffee', ->
describe 'acceptVisitor', ->
it 'can provide a prefix, depth-first traversal', ->
root = fixtures.makeTree 'test__iron_ingot'
visitor =
nodes: []
onEnterOtherNode: (node)-> visitor.nodes.push node
root.acceptVisitor visitor
(v.constructor.name.replace('Node', '') for v in visitor.nodes).should.eql [
"Inventory", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Item"
]
it 'can provide a postfix, depth-first traversal', ->
root = fixtures.makeTree 'test__iron_ingot'
visitor =
nodes: []
onLeaveOtherNode: (node)-> visitor.nodes.push node
root.acceptVisitor visitor
(n.constructor.name.replace('Node', '') for n in visitor.nodes).should.eql [
"Item", "Item", "Item", "Recipe", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Inventory"
]
@@ -0,0 +1,164 @@
#
# Crafting Guide - crafting_plan.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
SimpleInventory = require './simple_inventory'
########################################################################################################################
module.exports = class CraftingPlan
constructor: (modPack, want, have, steps)->
if not modPack? then throw new Error 'modPack is required'
if not want? then throw new Error 'want is required'
if not have? then throw new Error 'have is required'
if not steps? then throw new Error 'steps is required'
@_have = have
@_made = null
@_modPack = modPack
@_need = null
@_rawScores = {}
@_scores = {}
@_steps = steps
@_want = want
@_numberSteps()
# Public Methods ###############################################################################
computeRequired: ->
@_need = new SimpleInventory modPack:@_modPack
@_need.addInventory @_want
@_made = new SimpleInventory modPack:@_modPack
@_made.addInventory @_have
for i in [@_steps.length-1..0] by -1
step = @_steps[i]
step.multiplier = 0
for stack in step.recipe.output
if not stack?
throw new Error 'stack should not be null here'
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
while @_need.quantityOf(qualifiedSlug) > 0
@_executeStep step
@_made.addInventory @_want
@_pruneEmptySteps()
@_numberSteps()
hasRawScore: (name)->
return @_rawScores[name]?
getRawScore: (name)->
return @_rawScores[name]
setRawScore: (name, rawScore)->
@_rawScores[name] = rawScore
hasScore: (name)->
return @_scores[name]?
getScore: (name)->
return @_scores[name]
setScore: (name, score)->
@_scores[name] = score
# Property Methods #############################################################################
Object.defineProperties @prototype,
have:
get: -> @_have
length:
get: -> @steps.length
made:
get: -> @_made
need:
get: -> @_need
steps:
get: -> @_steps
want:
get: -> @_want
# Object Overrides #############################################################################
toString: ->
result = ["To Make:"]
@_want.each (stack)->
result.push " #{stack}"
result.push "When you already have:"
@_have.each (stack)->
result.push " #{stack}"
result.push "Start with:"
if @_need?
@_need.each (stack)->
result.push " #{stack}"
result.push "Use these recipes:"
for step in @_steps
result.push " #{step}"
result.push "To produce:"
if @_made?
@_made.each (stack)->
result.push " #{stack}"
if _.keys(@_rawScores).length > 0
result.push "Scores:"
for criteria, score of @_rawScores
if @_scores[criteria]?
result.push " #{criteria}: #{score} (#{@_scores[criteria]})"
else
result.push " #{criteria}: #{score}"
return result.join '\n'
# Private Methods ##############################################################################
_executeStep: (step)->
step.multiplier += 1
recipe = step.recipe
for stack in recipe.input
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
available = @_made.quantityOf qualifiedSlug
required = recipe.getQuantityRequired stack.itemSlug
consumed = Math.min required, available
deficit = required - consumed
@_made.remove qualifiedSlug, consumed
@_need.add qualifiedSlug, deficit
for stack in recipe.output
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
deficit = @_need.quantityOf qualifiedSlug
created = recipe.getQuantityProduced stack.itemSlug
replenished = Math.min deficit, created
surplus = created - replenished
@_made.add qualifiedSlug, surplus
@_need.remove qualifiedSlug, replenished
_numberSteps: ->
for step, i in @_steps
step.number = i + 1
_pruneEmptySteps: ->
index = 0
while index < @_steps.length
step = @_steps[index]
if step.multiplier is 0
@_steps.splice index, 1
else
index++
@@ -0,0 +1,107 @@
#
# Crafting Guide - crafting_plan.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingPlan = require './crafting_plan'
fixtures = require './fixtures.test'
ItemSlug = require '../game/item_slug'
########################################################################################################################
describe 'crafting_plan.coffee', ->
it 'requires wanted item if gatherable', ->
plans = fixtures.makePlans [1, 'test__coal']
plans.length.should.equal 1
plan = plans[0]
plan.computeRequired()
plan.need.unparse().should.equal 'coal'
plan.made.unparse().should.equal 'coal'
it 'can compute a single item with one single step plan', ->
plans = fixtures.makePlans [1, 'test__charcoal']
plans.length.should.equal 1
plan = plans[0]
plan.computeRequired()
plan.need.unparse().should.equal 'coal:8.oak_wood'
plan.made.unparse().should.equal '8.charcoal'
(s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
it 'can compute a large quantity of a single item with one single step plan', ->
plans = fixtures.makePlans [15, 'test__charcoal']
plans.length.should.equal 1
plan = plans[0]
plan.computeRequired()
plan.need.unparse().should.equal '2.coal:16.oak_wood'
plan.made.unparse().should.equal '16.charcoal'
(s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
it 'can compute a single item with multiple plans', ->
plans = fixtures.makePlans [1, 'test__iron_ingot']
plans.length.should.equal 2
plan = plans[0]
plan.computeRequired()
plan.need.unparse().should.equal 'coal:8.iron_ore:8.oak_wood'
plan.made.unparse().should.equal '7.charcoal:8.iron_ingot'
(s.toString() for s in plan.steps).should.eql [
'1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
]
plan = plans[1]
plan.computeRequired()
plan.need.unparse().should.equal 'coal:8.iron_ore'
plan.made.unparse().should.equal '8.iron_ingot'
(s.toString() for s in plan.steps).should.eql [
'1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
]
it 'can compute multiple items with multiple plans', ->
plans = fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword']
plans.length.should.equal 4
for plan in plans
plan.computeRequired()
plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/
plan.made.unparse().should.match /copper_block.*:iron_sword/
plan = plans[0]
plan.need.unparse().should.match /3.coal.*9.oak_wood/
plan.made.unparse().should.match /7.charcoal/
"#{plan.steps[3]}".should.equal \
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
"#{plan.steps[4]}".should.equal \
'2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot'
plan.steps.length.should.equal 7
plan = plans[1]
plan.need.unparse().should.match /3.coal.*:oak_wood/
plan.made.unparse().should.not.match /charcoal/
"#{plan.steps[2]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
"#{plan.steps[3]}".should.equal \
'2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot'
plan.steps.length.should.equal 6
plan = plans[2]
plan.need.unparse().should.match /^coal.*9.oak_wood/
plan.made.unparse().should.match /5.charcoal/
"#{plan.steps[3]}".should.equal \
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
"#{plan.steps[4]}".should.equal \
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
plan.steps.length.should.equal 7
plan = plans[3]
plan.need.unparse().should.match /2.coal.*9.oak_wood/
plan.made.unparse().should.match /6.charcoal/
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
"#{plan.steps[4]}".should.equal \
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
plan.steps.length.should.equal 7
@@ -0,0 +1,70 @@
#
# Crafting Guide - crafting_step.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
SimpleInventory = require './simple_inventory'
########################################################################################################################
module.exports = class CraftingStep
constructor: (recipe, modPack, multiplier=0)->
if not recipe? then throw new Error 'recipe is required'
if not modPack? then throw new Error 'modPack is required'
if multiplier < 0 then throw new Error 'multiplier must be at least 1'
@number = null
@_inventory = null
@_modPack = modPack
@_multiplier = multiplier
@_recipe = recipe
# Public Methods ###############################################################################
addToolsTo: (targetInventory)->
for stack in @_recipe.tools
targetInventory.add stack.itemSlug, stack.quantity
completeInto: (targetInventory)->
for stack in @_recipe.output
quantity = @_recipe.getQuantityProduced stack.itemSlug
targetInventory.add stack.itemSlug, quantity * @_multiplier
# Property Methods #############################################################################
Object.defineProperties @prototype,
inventory:
get: ->
if not @_inventory?
@_refreshInventory()
return @_inventory
recipe:
get: -> @_recipe
multiplier:
get: -> @_multiplier
set: (newMultiplier)->
@_multiplier = newMultiplier
@_refreshInventory() if @_inventory?
slug:
get: -> "#{@multiplier}x #{@_recipe.slug}"
# Object Overrides #############################################################################
toString: ->
return @slug
# Private Methods ##############################################################################
_refreshInventory: ->
@_inventory = new SimpleInventory
for stack in @_recipe.input
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
@_inventory.add qualifiedSlug, @multiplier * @_recipe.getQuantityRequired stack.itemSlug
+151
View File
@@ -0,0 +1,151 @@
#
# Crafting Guide - craftsman.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
GraphBuilder = require './graph_builder'
Inventory = require '../game/inventory'
PlanBuilder = require './plan_builder'
PlanEvaluator = require './plan_evaluator'
########################################################################################################################
module.exports = class Craftsman extends BaseModel
@::ANALYZE_STEP_INCREMENT = 29
@::GRAPH_STEP_INCREMENT = 59
@::PLAN_STEP_INCREMENT = 39
@::STAGE =
WAITING: 'waiting'
GRAPHING: 'examining recipes'
PLANNING: 'computing plans'
ANALYZING: 'analyzing plans'
COMPLETE: 'complete'
INVALID: 'invalid'
constructor: (modPack)->
if not modPack? then throw new Error 'modPack is required'
attributes =
paused: false
stage: @STAGE.WAITING
stageCount: 0
super attributes, {}
@_modPack = modPack
reset = _.debounce (=> @reset()), 100
reevaluatePlans = _.debounce (=> @reevaluatePlans()), 100
@_have = new Inventory modPack:@_modPack
@_have.on c.event.change, reevaluatePlans
@_want = new Inventory modPack:@_modPack
@_want.on c.event.change, reset
@on c.event.change + ':paused', reset
@on c.event.change + ':stage', => logger.info "Craftsman has started #{@stage}..."
@on 'scheduleNextWork', => @_scheduleNextWork()
@reset()
# Public Methods ###############################################################################
reevaluatePlans: ->
@_plans = null
@_planEvaluator = null
@stage = @STAGE.WAITING
@stageCount = 0
@_scheduleNextWork()
reset: ->
@_graphBuilder = null
@_planBuilder = null
@_planEvaluator = null
@_plans = null
@stage = @STAGE.WAITING
@stageCount = 0
@_scheduleNextWork()
work: ->
return if @_want.isEmpty
if not @_graphBuilder?
@_want.localize()
@_have.localize()
logger.info -> "Craftsman starting to build #{@_want} from #{@_have}"
@_graphBuilder = new GraphBuilder modPack:@_modPack, want:@_want, have:@_have
@stage = @STAGE.GRAPHING
@stageCount = 0
else if not @_graphBuilder.complete
@_graphBuilder.expandGraph @GRAPH_STEP_INCREMENT
@stageCount = @_graphBuilder.stepCount
logger.verbose => "Craftsman is expanding graph: #{@stageCount} nodes"
else if not @_graphBuilder.rootNode.valid
@stage = @STAGE.INVALID
@stageCount = 0
logger.warning => "Craftsman could not complete a crafting plan."
else if not @_planBuilder?
logger.debug => "Craftsman finished computing graph:\n#{@_graphBuilder.rootNode}"
@_planBuilder = new PlanBuilder @_graphBuilder.rootNode, @_modPack, have:@_have, want:@_want
@stage = @STAGE.PLANNING
@stageCount = 0
else if not @_planBuilder.complete
@_planBuilder.producePlans @PLAN_STEP_INCREMENT
@stageCount = @_planBuilder.plans.length
logger.verbose => "Craftsman is producing plans: #{@_planBuilder.plans.length} plans"
else if not @_planEvaluator?
logger.verbose => "Craftsman finished producing plans: #{@_planBuilder.plans.length} plans"
@_planEvaluator = new PlanEvaluator @_planBuilder.plans
@stage = @STAGE.ANALYZING
@stageCount = 0
else if not @_planEvaluator.complete
@_planEvaluator.scorePlans @ANALYZE_STEP_INCREMENT
@stageCount = @_planEvaluator.lastScored
logger.verbose => "Craftsman is rating plans: #{@stageCount} plans"
else
@_plans = [
@_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.FEWEST_STEPS
@_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.LEAST_MATERIALS
]
@stage = @STAGE.COMPLETE
@stageCount = 0
logger.info => "Craftsman has finished with plans: #{(p.toString() for p in @_plans).join('\n\n')}"
@trigger c.event.change, this
@trigger 'scheduleNextWork'
return @complete
# Property Methods #############################################################################
Object.defineProperties @prototype,
complete:
get: -> @stage in [@STAGE.COMPLETE, @STAGE.INVALID]
have:
get: -> @_have
plan:
get: -> @_plans?[0]
want:
get: -> @_want
# Private Methods ##############################################################################
_scheduleNextWork: ->
return if @paused
return if @want.isEmpty
return if @complete
_.defer => @work()
@@ -0,0 +1,166 @@
#
# Crafting Guide - fixtures.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
GraphBuilder = require './graph_builder'
Inventory = require '../game/inventory'
ItemSlug = require '../game/item_slug'
Mod = require '../game/mod'
ModPack = require '../game/mod_pack'
ModVersion = require '../game/mod_version'
PlanBuilder = require './plan_builder'
########################################################################################################################
MOD_VERSION_FILE =
"""
schema: 1
item: Bed
recipe:
input: Oak Planks, Wool
pattern: ... 000 111
item: Charcoal
recipe:
input: 8 Oak Wood, Coal
pattern: .0. ... .1.
quantity: 8
item: Crafting Table
recipe:
input: Oak Planks
pattern: .00 .00 ...
item: Coal
gatherable: yes
item: Cobblestone
gatherable: yes
item: Copper Block
recipe:
input: Copper Ingot
pattern: 000 000 000
tools: Crafting Table
item: Copper Ingot
recipe:
input: 8 Copper Ore, Coal
pattern: .0. ... .1.
quantity: 8
tools: Furnace
recipe:
input: 8 Copper Ore, Charcoal
pattern: .0. ... .1.
quantity: 8
tools: Furnace
recipe:
input: Copper Block
pattern: ... .0. ...
quantity: 9
item: Copper Ore
gatherable: yes
item: Furnace
recipe:
input: Cobblestone
pattern: 000 0.0 000
tools: Crafting Table
item: Iron Ore
gatherable: yes
item: Iron Ingot
recipe:
input: 8 Iron Ore, Charcoal
pattern: .0. ... .1.
quantity: 8
tools: Furnace
recipe:
input: 8 Iron Ore, Coal
pattern: .0. ... .1.
quantity: 8
tools: Furnace
item: Iron Sword
recipe:
input: Iron Ingot, Stick
pattern: .0. .0. .1.
tools: Crafting Table
item: Lever
recipe:
input: Stick, Cobblestone
pattern: .0. .1. ...
item: Oak Planks
recipe:
input: Oak Wood
pattern: ... .0. ...
quantity: 4
item: Oak Wood
gatherable: yes
item: Stick
recipe:
input: Oak Planks
pattern: .0. .0. ...
quantity: 4
item: String
gatherable: yes
item: Wool
gatherable: yes
recipe:
input: String
pattern: 00. 00. ...
"""
########################################################################################################################
module.exports = fixtures =
makeGraphBuilder: ->
return new GraphBuilder modPack:fixtures.makeModPack(), want:new Inventory
makeModPack: ->
modPack = new ModPack
mod = new Mod name:'Test', slug:'test'
modPack.addMod mod
modVersion = new ModVersion modSlug:'test', version:'0.0'
modVersion.parse MOD_VERSION_FILE
mod.addModVersion modVersion
return modPack
makePlans: (stacks...)->
modPack = fixtures.makeModPack()
have = new Inventory
want = new Inventory
for stack in stacks
want.add ItemSlug.slugify(stack[1]), stack[0]
graphBuilder = new GraphBuilder modPack:modPack, want:want
graphBuilder.expandGraph()
planBuilder = new PlanBuilder graphBuilder.rootNode, modPack, want:want, have:have
return planBuilder.producePlans()
makeTree: (itemSlug, quantity=1)->
want = new Inventory
want.add ItemSlug.slugify itemSlug
builder = new GraphBuilder modPack:fixtures.makeModPack(), want:want
builder.expandGraph()
return builder.rootNode
@@ -0,0 +1,69 @@
#
# Crafting Guide - graph_builder.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Inventory = require '../game/inventory'
InventoryNode = require './inventory_node'
########################################################################################################################
module.exports = class GraphBuilder
constructor: (options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
if not options.want? then throw new Error 'options.want is required'
@_maximumGraphSize = c.limits.maximumGraphSize
@_modPack = options.modPack
@_stepCount = 0
@_want = options.want
@_rootNode = new InventoryNode modPack:@_modPack, inventory:@_want
@_queue = [@_rootNode]
# Public Methods ###############################################################################
expandGraph: (steps=null)->
maxSteps = if steps? then @_stepCount + steps else Number.MAX_VALUE
@_queue ?= []
while true
break if @_queue.length is 0
break if @_stepCount >= maxSteps
node = @_queue.shift()
break unless node?
node.expand @_queue
@_stepCount += 1
reset: ->
# Property Methods #############################################################################
Object.defineProperties @prototype,
complete:
get: ->
return true if @_stepCount > @_maximumGraphSize
return false unless @_rootNode?
return false unless @_queue?
return false unless @_queue.length is 0
return true
rootNode:
get: -> @_rootNode
stepCount:
get: -> @_stepCount
want:
get: -> @_want
# Object Overrides ############################################################################
toString: (indent='')->
"Build Tree\n#{@_rootNode.toString(indent + ' ')}"
@@ -0,0 +1,77 @@
#
# Crafting Guide - graph_builder.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
fixtures = require './fixtures.test'
ItemSlug = require '../game/item_slug'
########################################################################################################################
builder = null
########################################################################################################################
describe 'GraphBuilder.coffee', ->
beforeEach ->
builder = fixtures.makeGraphBuilder()
describe 'expand', ->
it 'can work a few steps at a time', ->
builder.want.add ItemSlug.slugify 'test__iron_sword'
builder.expandGraph 9
builder.rootNode.depth.should.equal 6
builder.rootNode.size.should.equal 13
builder.complete.should.be.false
builder.expandGraph 9
builder.rootNode.depth.should.equal 8
builder.rootNode.size.should.equal 18
builder.complete.should.be.true
it 'works properly with an empty inventory', ->
builder.expandGraph 100
builder.rootNode.depth.should.equal 1
builder.rootNode.size.should.equal 1
builder.complete.should.be.true
describe 'can build a tree for', ->
runSingleItemTreeBuildingTest = (slug, depth, size)->
builder.want.add ItemSlug.slugify slug
builder.expandGraph 100
builder.rootNode.depth.should.equal depth
builder.rootNode.size.should.equal size
builder.complete.should.be.true
it 'a gatherable item', ->
runSingleItemTreeBuildingTest 'test__oak_wood', 2, 2
it 'a single-step item', ->
runSingleItemTreeBuildingTest 'test__crafting_table', 6, 6
it 'an item with multiple inputs', ->
runSingleItemTreeBuildingTest 'test__lever', 8, 9
it 'an item with multiple recipes', ->
runSingleItemTreeBuildingTest 'test__iron_ingot', 6, 11
it 'an item with multiple inputs and multiple recipes', ->
runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18
it 'an item with one recursive recipe', ->
runSingleItemTreeBuildingTest 'test__copper_ingot', 6, 15
it 'an item which gatherable and craftable', ->
runSingleItemTreeBuildingTest 'test__wool', 4, 4
it 'an item which requires a gatherable and craftable item', ->
runSingleItemTreeBuildingTest 'test__bed', 6, 7
@@ -0,0 +1,55 @@
#
# Crafting Guide - inventory_node.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingNode = require './crafting_node'
ItemNode = require './item_node'
########################################################################################################################
module.exports = class InventoryNode extends CraftingNode
@::ENTER_METHOD = 'onEnterInventoryNode'
@::LEAVE_METHOD = 'onLeaveInventoryNode'
@::TYPE = CraftingNode::TYPES.INVENTORY
constructor: (options={})->
if not options.inventory? then throw new Error 'options.inventory is required'
super options
@inventory = options.inventory
# CraftingNode Overrides #######################################################################
_createChildren: (result=[])->
@inventory.each (stack)=>
item = @modPack.findItem stack.itemSlug
if not item? then throw new Error "Could not find an item for slug: #{stack.itemSlug}"
result.push new ItemNode modPack:@modPack, item:item, ignoreGatherable:true
return result
_checkCompleteness: ->
for child in @children
return false unless child.complete
return true
_checkValidity: ->
for child in @children
return false unless child.valid
return true
# Object Overrides #############################################################################
toString: (options={})->
options.indent ?= ''
options.recursive ?= true
parts = ["#{options.indent}#{@completeText} #{@validText} InventoryNode for #{@inventory}"]
nextIndent = options.indent + ' '
if options.recursive
for child in @children
parts.push child.toString indent:nextIndent
return parts.join '\n'
@@ -0,0 +1,98 @@
#
# Crafting Guide - item_node.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingNode = require './crafting_node'
RecipeNode = require './recipe_node'
########################################################################################################################
module.exports = class ItemNode extends CraftingNode
@::ENTER_METHOD = 'onEnterItemNode'
@::LEAVE_METHOD = 'onLeaveItemNode'
@::TYPE = CraftingNode::TYPES.ITEM
constructor: (options={})->
if not options.item? then throw new Error 'options.item is required'
super options
@item = options.item
@_ignoreGatherable = options.ignoreGatherable ?= false
@_recipes = null
# Property Methods #############################################################################
getRecipes: ->
if not @_recipes?
@_recipes = @modPack.findRecipes @item.slug, forCrafting:true, onlyPrimary:true
if not @_recipes
@_recipes = @modPack.findRecipes @item.slug, forCrafting:true
return @_recipes or []
isGatherable: ->
if not @_ignoreGatherable
return true if @item.isGatherable
return true if @getRecipes().length is 0
return false
Object.defineProperties @prototype,
gatherable: { get:@prototype.isGatherable }
recipes: { get:@prototype.getRecipes }
# CraftingNode Overrides #######################################################################
_createChildren: (result=[])->
recipes = @getRecipes()
return [] if @gatherable
for recipe in recipes
child = new RecipeNode modPack:@modPack, recipe:recipe
child.parent = this
if child.valid
result.push child
return result
_checkCompleteness: ->
return true if @gatherable
return false unless @children?
for child in @children
return true if child.complete
return false
_checkValidity: ->
return false if @_isRepeatedItem()
return true unless @children.length > 0
for child in @children
return true if child.valid
return false
# Object Overrides #############################################################################
toString: (options={})->
options.indent ?= ''
options.recursive ?= true
parts = ["#{options.indent}#{@completeText} #{@validText} ItemNode for #{@item.name}"]
nextIndent = options.indent + ' '
if options.recursive
for child in @children
parts.push child.toString indent:nextIndent
return parts.join '\n'
# Private Methods ##############################################################################
_isRepeatedItem: ->
nextParent = @parent
while nextParent?
return true if nextParent.item is @item
nextParent = nextParent.parent
return false
@@ -0,0 +1,127 @@
#
# Crafting Guide - plan_builder.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingNode = require './crafting_node'
CraftingPlan = require './crafting_plan'
CraftingStep = require './crafting_step'
Inventory = require '../game/inventory'
########################################################################################################################
module.exports = class PlanBuilder
constructor: (rootNode, modPack, options={})->
if not rootNode? then throw new Error 'rootNode is required'
if not modPack? then throw new Error 'modPack is required'
@want = options.want
@have = options.have
@_choiceNodes = []
@_complete = false
@_maxPlanCount = c.limits.maximumPlanCount
@_modPack = modPack
@_plans = []
@_rootNode = rootNode
@_isolateChoiceNodes()
# Public Methods ###############################################################################
producePlans: (maxPlans=null)->
maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE
if @plans.length >= @_maxPlanCount
@_complete = true
while not @complete and (@plans.length < maxPlans)
plan = @_captureCurrentPlan()
if plan?
@plans.push plan
@_incrementChoiceNodes()
return @_plans
# Property Methods #############################################################################
Object.defineProperties @prototype,
complete:
get: -> @_complete
have:
get: -> @_have
set: (have)-> @_have = have or new Inventory
maxPlanCount:
get: -> @_maxPlanCount
set: (value)-> @_maxPlanCount = value
plans:
get: -> @_plans
want:
get: -> @_want
set: (want)-> @_want = want or new Inventory
# Private Methods ##############################################################################
_captureCurrentPlan: ->
toVisit = [@_rootNode]
stepNodes = []
while toVisit.length > 0
node = toVisit.shift()
if node.TYPE is CraftingNode::TYPES.INVENTORY
toVisit.push(c) for c in node.children
else if node.TYPE is CraftingNode::TYPES.ITEM
toVisit.push node.children[0] if node.children.length > 0
else if node.TYPE is CraftingNode::TYPES.RECIPE
stepNodes.push node
toVisit.push(c) for c in node.children
steps = []
seenRecipes = {}
index = stepNodes.length - 1
while index >= 0
node = stepNodes[index]
index -= 1
return null unless node.valid and node.complete
recipeSlug = node.recipe.slug
continue if seenRecipes[recipeSlug]?
seenRecipes[recipeSlug] = true
steps.push new CraftingStep node.recipe, @_modPack
plan = new CraftingPlan @_modPack, @_want, @_have, steps
return plan
_incrementChoiceNodes: ->
if @_choiceNodes.length is 0
@_complete = true
return
index = @_choiceNodes.length - 1
while true
if index is -1
@_complete = true
return
node = @_choiceNodes[index]
node.rotateChildren()
return unless node.rotations % node.children.length is 0
index -= 1
_isolateChoiceNodes: ->
@_rootNode.acceptVisitor
onEnterItemNode: (node)=>
if node.children.length > 1
@_choiceNodes.push node
@@ -0,0 +1,43 @@
#
# Crafting Guide - plan_builder.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
fixtures = require './fixtures.test'
PlanBuilder = require './plan_builder'
########################################################################################################################
describe 'plan_builder.coffee', ->
printPlan = (plan)->
return '' unless plan?
return ((s.recipe.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > '
it 'generates an empty plan for a gatherable item', ->
plans = fixtures.makePlans [1, 'test__oak_wood']
plans.length.should.equal 1
plans[0].length.should.equal 0
it 'can find a multi-step plan', ->
plans = fixtures.makePlans [1, 'test__lever']
printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever'
plans.length.should.equal 1
it 'can find multiple plans', ->
plans = fixtures.makePlans [1, 'test__iron_ingot']
printPlan(plans[0]).should.equal '8 test__charcoal > 8 test__iron_ingot'
printPlan(plans[1]).should.equal '8 test__iron_ingot'
plans.length.should.equal 2
it 'ignores invalid plans', ->
plans = fixtures.makePlans [1, 'test__copper_block']
printPlan(plans[0]).should.equal '8 test__copper_ingot > test__copper_block'
printPlan(plans[1]).should.equal '8 test__charcoal > 8 test__copper_ingot > test__copper_block'
plans.length.should.equal 2
@@ -0,0 +1,98 @@
#
# Crafting Guide - plan_evaluator.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class PlanEvaluator
@::CRITERIA =
FEWEST_STEPS: 'fewest steps'
LEAST_MATERIALS: 'least materials'
constructor: (plans)->
if not plans? then throw new Error 'plans is required'
@_plans = plans
@_lastScored = -1
@_normalized = false
# Public Methods ###############################################################################
findBestPlan: (mainCriteria)->
criteriaList = [mainCriteria].concat (criteria for key, criteria of @CRITERIA when criteria isnt mainCriteria)
@_plans.sort (a, b)->
for criteria in criteriaList
scoreA = a.getScore criteria
scoreB = b.getScore criteria
if scoreA isnt scoreB
return if scoreA > scoreB then -1 else +1
return 0
return @_plans[0]
scorePlans: (count)->
count ?= @_plans.length
return if @complete
maxPlanIndex = Math.min @_lastScored + count, @_plans.length - 1
for i in [(@_lastScored + 1)..maxPlanIndex]
@_plans[i].computeRequired()
@_scorePlan @_plans[i]
@_lastScored = i
if @complete then @_normalizeScores()
return @complete
# Property Methods #############################################################################
Object.defineProperties @prototype,
complete:
get: -> @_lastScored is @_plans.length - 1
lastScored:
get: -> @_lastScored
# Private Methods ##############################################################################
_computeFewestStepsScore: (plan)->
total = 0
for step in plan.steps
total += step.multiplier
plan.setRawScore @CRITERIA.FEWEST_STEPS, total
_computeLeastMaterialsScore: (plan)->
total = 0
plan.need.each (stack)->
total += stack.quantity
plan.setRawScore @CRITERIA.LEAST_MATERIALS, total
_normalizeScores: ->
for key, criteria of @CRITERIA
maxScore = 0
minScore = Number.MAX_VALUE
for plan in @_plans
continue unless plan.hasRawScore criteria
maxScore = Math.max maxScore, plan.getRawScore(criteria)
minScore = Math.min minScore, plan.getRawScore(criteria)
adjustedMaxScore = maxScore - minScore
for plan in @_plans
continue unless plan.hasRawScore criteria
if adjustedMaxScore > 0
adjustedRawScore = plan.getRawScore(criteria) - minScore
plan.setScore criteria, 1.0 - (1.0 * adjustedRawScore / adjustedMaxScore)
else
plan.setScore criteria, 1.0
_scorePlan: (plan)->
@_computeFewestStepsScore plan
@_computeLeastMaterialsScore plan
@@ -0,0 +1,80 @@
#
# Crafting Guide - plan_evaluator.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingPlan = require './crafting_plan'
fixtures = require './fixtures.test'
Inventory = require '../game/inventory'
PlanEvaluator = require './plan_evaluator'
########################################################################################################################
criteria = evaluator = planA = planB = planC = plans = wanted = null
########################################################################################################################
describe 'plan_evaluator.coffee', ->
beforeEach ->
criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS
describe 'findBestPlan', ->
it 'can find the shortest of two options', ->
evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block']
evaluator.scorePlans()
bestPlan = evaluator.findBestPlan criteria
bestPlan.getScore(criteria).should.equal 1.0
it 'can find the shortest of many options', ->
evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword']
evaluator.scorePlans()
bestPlan = evaluator.findBestPlan criteria
bestPlan.getScore(criteria).should.equal 1.0
it 'can break ties using the non-primary criteria', ->
evaluator = new PlanEvaluator fixtures.makePlans [2, 'test__iron_ingot'], [1, 'test__charcoal']
evaluator.scorePlans()
bestPlan = evaluator.findBestPlan criteria
bestPlan.getScore(criteria).should.equal 1.0
(p.getRawScore('fewest steps') for p in evaluator._plans).should.eql [2, 2]
(p.getRawScore('least materials') for p in evaluator._plans).should.eql [17, 18]
describe '_normalizeScores', ->
beforeEach ->
modPack = fixtures.makeModPack()
wanted = new Inventory modPack:modPack
have = new Inventory modPack:modPack
planA = new CraftingPlan modPack, wanted, have, []
planB = new CraftingPlan modPack, wanted, have, []
planC = new CraftingPlan modPack, wanted, have, []
plans = [planA, planB, planC]
evaluator = new PlanEvaluator plans
criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS
it 'does not set scores when none have been evaluated', ->
evaluator._normalizeScores()
(plan.hasRawScore(criteria) for plan in plans).should.eql [false, false, false]
it 'assigns a 1.0 when all plans equal', ->
for plan in plans
plan.setRawScore criteria, 5
evaluator._normalizeScores()
(plan.getScore(criteria) for plan in plans).should.eql [1.0, 1.0, 1.0]
it 'computes correct scores from raw scores', ->
planA.setRawScore criteria, 7
planB.setRawScore criteria, 8
planC.setRawScore criteria, 9
evaluator._normalizeScores()
(plan.getScore(criteria) for plan in plans).should.eql [1.0, 0.5, 0.0]
@@ -0,0 +1,87 @@
#
# Crafting Guide - recipe_node.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CraftingNode = require './crafting_node'
Item = require '../game/item'
# ItemNode = require './item_node' # don't include here, causes a cycle
ItemSlug = require '../game/item_slug'
########################################################################################################################
module.exports = class RecipeNode extends CraftingNode
@::ENTER_METHOD = 'onEnterRecipeNode'
@::LEAVE_METHOD = 'onLeaveRecipeNode'
@::TYPE = CraftingNode::TYPES.RECIPE
constructor: (options={})->
if not options.recipe? then throw new Error 'options.recipe is required'
super options
@recipe = options.recipe
# CraftingNode Overrides #######################################################################
_createChildren: (result=[])->
ItemNode = require './item_node' # include here to avoid a cycle
for stack in @recipe.input
item = @modPack.findItem stack.itemSlug
if not item?
name = @modPack.findName stack.itemSlug
item = new Item name:name, slug:stack.itemSlug, gatherable:true
result.push new ItemNode modPack:@modPack, item:item
return result
_checkCompleteness: ->
for child in @children
return false unless child.complete
return true
_checkValidity: ->
return false if @_isRepeatedRecipe()
return false if @_requiresToolBeingMade()
for child in @children
return false unless child.valid
return true
# Private Methods ##############################################################################
_isRepeatedRecipe: ->
nextParent = @parent
while nextParent?
return true if nextParent.recipe is @recipe
nextParent = nextParent.parent
return false
_requiresToolBeingMade: ->
for toolStack in @recipe.tools
toolSlug = toolStack.itemSlug
nextParent = @parent
while nextParent?
if ItemSlug.equal toolSlug, nextParent.item?.slug
return true
nextParent = nextParent.parent
return false
# Object Overrides ############################################################################
toString: (options={})->
options.indent ?= ''
options.recursive ?= true
parts = ["#{options.indent}#{@completeText} #{@validText} RecipeNode for #{@recipe.slug}"]
nextIndent = options.indent + ' '
if options.recursive
for child in @children
parts.push child.toString indent:nextIndent
return parts.join '\n'
@@ -0,0 +1,212 @@
#
# Crafting Guide - simple_inventory.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require '../game/item_slug'
SimpleStack = require './simple_stack'
########################################################################################################################
module.exports = class SimpleInventory
constructor: (attributes={}, options={})->
@clear()
if options.modPack?
@modPack = options.modPack
if options.clone?
@addInventory options.clone
# Class Methods ################################################################################
@Delimiters =
Item: '.'
Stack: ':'
# Public Methods ###############################################################################
add: (itemSlug, quantity=1)->
return this unless quantity > 0
@_add itemSlug, quantity
return this
addInventory: (inventory)->
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
return this
clear: (options={})->
@_stacks = {}
@_itemSlugs = []
clone: ->
inventory = new Inventory
inventory.addInventory this
return inventory
each: (callback)->
for itemSlug in @_itemSlugs
callback @_stacks[itemSlug]
getSlugs: ->
return @_itemSlugs[..]
hasAtLeast: (itemSlug, quantity=1)->
if quantity is 0 then return true
stack = @_stacks[itemSlug]
return false unless stack?
return stack.quantity >= quantity
localize: ->
if not @modPack? then throw new Error 'localize requires @modPack'
changed = false
newSlugs = []
newStacks = []
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
continue unless stack?
qualifiedSlug = if itemSlug.isQualified then itemSlug else null
if not qualifiedSlug?
qualifiedSlug = @modPack.findItem(itemSlug)?.slug
changed = qualifiedSlug?
if qualifiedSlug?
newSlugs.push qualifiedSlug
newStacks.push new Stack itemSlug:qualifiedSlug, quantity:stack.quantity
else
newSlugs.push itemSlug
newStacks.push stack
if changed
@_itemSlugs = newSlugs
@_stacks = {}
for stack in newStacks
@_stacks[stack.itemSlug] = stack
@_sort()
pop: ->
itemSlug = @_itemSlugs.pop()
return null unless itemSlug?
stack = @_stacks[itemSlug]
delete @_stacks[itemSlug]
return stack
quantityOf: (itemSlug)->
stack = @_stacks[itemSlug]
return stack.quantity if stack
return 0
remove: (itemSlug, quantity=null)->
stack = @_stacks[itemSlug]
return this unless stack?
quantity ?= stack.quantity
return this unless quantity > 0
if stack.quantity < quantity
throw new Error "cannot remove #{quantity}: only #{stack.quantity} #{itemSlug} in this inventory"
stack.quantity -= quantity
if stack.quantity is 0
for currentItemSlug, index in @_itemSlugs
if ItemSlug.equal itemSlug, currentItemSlug
@_itemSlugs.splice index, 1
break
return this
# Parsing Methods ##############################################################################
parse: (data)->
return this if not data? or data.length is 0
stacks = data.split Inventory.Delimiters.Stack
for stackText in stacks
stackParts = stackText.split Inventory.Delimiters.Item
if stackParts.length is 2
quantity = parseInt stackParts[0], 10
itemSlug = ItemSlug.slugify stackParts[1]
else if stackParts.length is 1
quantity = 1
itemSlug = ItemSlug.slugify stackParts[0]
else
throw new Error "expected #{stackText} to have 0 or 1 parts"
if itemSlug.qualified.length > 0
@add itemSlug, quantity
return this
unparse: (options={})->
parts = []
@each (stack)=>
slugText = stack.itemSlug.item
if @modPack?
item = @modPack.findItem ItemSlug.slugify slugText
if item? and item.slug.qualified isnt stack.itemSlug.qualified
slugText = stack.itemSlug.qualified
if stack.quantity is 1
parts.push slugText
else
parts.push "#{stack.quantity}#{SimpleInventory.Delimiters.Item}#{slugText}"
return parts.join SimpleInventory.Delimiters.Stack
# Property Methods #############################################################################
getIsEmpty: ->
return @_itemSlugs.length is 0
getTotalQuantity: ->
total = 0
@each (stack)->
total += stack.quantity
return total
Object.defineProperties @prototype,
isEmpty: { get:@prototype.getIsEmpty }
totalQuantity: { get:@prototype.getTotalQuantity }
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") {items: ["]
needsDelimiter = false
@each (stack)->
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
result.push '}'
return result.join ''
# Private Methods ##############################################################################
_add: (itemSlug, quantity=1)->
return unless itemSlug?
return unless quantity > 0
stack = @_stacks[itemSlug]
if not stack?
stack = new SimpleStack itemSlug:itemSlug, quantity:quantity
@_stacks[itemSlug] = stack
@_itemSlugs.push itemSlug
@_sort()
else
stack.quantity += quantity
_sort: ->
@_itemSlugs.sort (a, b)-> ItemSlug.compare a, b
@@ -0,0 +1,33 @@
#
# Crafting Guide - simple_stack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require '../game/item_slug'
########################################################################################################################
module.exports = class Stack
constructor: (attributes={}, options={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
attributes.quantity ?= 1
@itemSlug = attributes.itemSlug
@quantity = attributes.quantity
# Class Methods ################################################################################
@compare: (a, b)->
if a? and not b? then return -1
if not a? and b? then return +1
if a.quantity isnt b.quantity
return if a.quantity > b.quantity then -1 else +1
return ItemSlug.compare a.itemSlug, b.itemSlug
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
+36
View File
@@ -0,0 +1,36 @@
###
# Crafting Guide - event_recorder.coffee
#
# Copyright (c) 2014-2015 by Redwood Labs
# All rights reserved.
###
util = require 'util'
########################################################################################################################
module.exports = class EventRecorder
constructor: (model)->
if not model? then throw new Error 'model is required'
@model = model
@events = []
@model.on 'all', (event, model, args...)=>
logger.verbose -> "#{model?.constructor?.name}(#{model?.cid}) emitted #{event}
with args: #{util.inspect(args)}"
@events.push id:model?.cid, event:event, args:args
# Public Methods ###############################################################################
reset: ->
@events = []
# Property Methods #############################################################################
getNames: ->
return (e.event for e in @events)
Object.defineProperties @prototype,
names: {get:@prototype.getNames}
+227
View File
@@ -0,0 +1,227 @@
#
# Crafting Guide - inventory.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Stack = require './stack'
########################################################################################################################
module.exports = class Inventory extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
attributes.modPack ?= null
@clear()
if options.clone?
@addInventory options.clone
# Class Methods ################################################################################
@Delimiters =
Item: '.'
Stack: ':'
# Public Methods ###############################################################################
add: (itemSlug, quantity=1)->
return this unless quantity > 0
@_add itemSlug, quantity
@trigger c.event.add, this, itemSlug, quantity
@trigger c.event.change, this
return this
addInventory: (inventory)->
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
@trigger c.event.change, this
return this
clear: (options={})->
@_stacks = {}
@_itemSlugs = []
@trigger c.event.change, this
clone: ->
inventory = new Inventory
inventory.addInventory this
return inventory
each: (callback)->
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
continue unless stack?
callback stack
getSlugs: ->
return @_itemSlugs[..]
hasAtLeast: (itemSlug, quantity=1)->
if quantity is 0 then return true
stack = @_stacks[itemSlug]
return false unless stack?
return stack.quantity >= quantity
localize: ->
if not @modPack? then throw new Error 'localize requires @modPack'
changed = false
newSlugs = []
newStacks = []
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
continue unless stack?
qualifiedSlug = if itemSlug.isQualified then itemSlug else null
if not qualifiedSlug?
qualifiedSlug = @modPack.findItem(itemSlug)?.slug
changed = qualifiedSlug?
if qualifiedSlug?
newSlugs.push qualifiedSlug
newStacks.push new Stack itemSlug:qualifiedSlug, quantity:stack.quantity
else
newSlugs.push itemSlug
newStacks.push stack
if changed
for itemSlug, stack of @_stacks
@stopListening stack
@_itemSlugs = newSlugs
@_stacks = {}
for stack in newStacks
@_stacks[stack.itemSlug] = stack
@listenTo stack, c.event.change, => @trigger c.event.change, this
@_sort()
@trigger c.event.change, this
pop: ->
itemSlug = @_itemSlugs.pop()
return null unless itemSlug?
stack = @_stacks[itemSlug]
delete @_stacks[itemSlug]
@trigger c.event.remove, this, stack.itemSlug, stack.quantity
@trigger c.event.change, this
return stack
quantityOf: (itemSlug)->
stack = @_stacks[itemSlug]
return stack.quantity if stack
return 0
remove: (itemSlug, quantity=null)->
stack = @_stacks[itemSlug]
return this unless stack?
quantity ?= stack.quantity
return this unless quantity > 0
if stack.quantity < quantity
throw new Error "cannot remove #{quantity}: only #{stack.quantity} #{itemSlug} in this inventory"
stack.quantity -= quantity
if stack.quantity is 0
@stopListening stack
delete @_stacks[itemSlug]
@_itemSlugs = (s for s in @_itemSlugs when not ItemSlug.equal(s, itemSlug))
@trigger c.event.remove, this, itemSlug, quantity
@trigger c.event.change, this
return this
# Parsing Methods ##############################################################################
parse: (data)->
return this if not data? or data.length is 0
stacks = data.split Inventory.Delimiters.Stack
for stackText in stacks
stackParts = stackText.split Inventory.Delimiters.Item
if stackParts.length is 2
quantity = parseInt stackParts[0], 10
itemSlug = ItemSlug.slugify stackParts[1]
else if stackParts.length is 1
quantity = 1
itemSlug = ItemSlug.slugify stackParts[0]
else
throw new Error "expected #{stackText} to have 0 or 1 parts"
if itemSlug.qualified.length > 0
@add itemSlug, quantity
return this
unparse: (options={})->
parts = []
@each (stack)=>
slugText = stack.itemSlug.item
if @modPack?
item = @modPack.findItem ItemSlug.slugify slugText
if item? and item.slug.qualified isnt stack.itemSlug.qualified
slugText = stack.itemSlug.qualified
if stack.quantity is 1
parts.push slugText
else
parts.push "#{stack.quantity}#{Inventory.Delimiters.Item}#{slugText}"
return parts.join Inventory.Delimiters.Stack
# Property Methods #############################################################################
Object.defineProperties @prototype,
isEmpty:
get: -> @_itemSlugs.length is 0
totalQuantity:
get: ->
total = 0
@each (stack)->
total += stack.quantity
return total
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") {items: ["]
needsDelimiter = false
@each (stack)->
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
result.push '}'
return result.join ''
# Private Methods ##############################################################################
_add: (itemSlug, quantity=1)->
return unless itemSlug?
return unless quantity > 0
stack = @_stacks[itemSlug]
if not stack?
stack = new Stack itemSlug:itemSlug, quantity:quantity
@listenTo stack, c.event.change, => @trigger c.event.change, this
@_stacks[itemSlug] = stack
@_itemSlugs.push itemSlug
@_sort()
else
stack.quantity += quantity
_sort: ->
@_itemSlugs.sort (a, b)-> ItemSlug.compare a, b
@@ -0,0 +1,213 @@
#
# Crafting Guide - inventory.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
EventRecorder = require '../event_recorder'
Inventory = require './inventory'
Item = require './item'
ItemSlug = require './item_slug'
########################################################################################################################
inventory = modPack = null
########################################################################################################################
describe 'inventory.coffee', ->
beforeEach ->
inventory = new Inventory {}, silent:false
inventory.add ItemSlug.slugify('wool'), 4
inventory.add ItemSlug.slugify('string'), 20
inventory.add ItemSlug.slugify('boat')
describe 'add', ->
it 'can add to an empty inventory', ->
inventory.add ItemSlug.slugify('iron_ingot'), 4
stack = inventory._stacks['iron_ingot']
stack.constructor.name.should.equal 'Stack'
stack.itemSlug.qualified.should.equal 'iron_ingot'
stack.quantity.should.equal 4
it 'can augment quantity of existing items', ->
inventory.add ItemSlug.slugify('wool'), 2
inventory.unparse().should.equal 'boat:20.string:6.wool'
it 'can add zero quantity', ->
inventory.add ItemSlug.slugify('wool'), 0
inventory.unparse().should.equal 'boat:20.string:4.wool'
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.add ItemSlug.slugify('iron_ingot'), 10
events.names.should.eql [c.event.add, c.event.change]
describe 'addInventory', ->
it 'can add to an empty inventory', ->
newInventory = new Inventory
newInventory.addInventory inventory
newInventory.unparse().should.equal 'boat:20.string:4.wool'
it 'can add a mix of new and existing items', ->
newInventory = new Inventory
newInventory.add ItemSlug.slugify('string'), 2
newInventory.addInventory inventory
newInventory.unparse().should.equal 'boat:22.string:4.wool'
describe 'clone', ->
it 'creates an empty inventory from an empty inventory', ->
a = new Inventory
b = a.clone()
b._itemSlugs.should.eql []
it 'faithfully copies an existing inventory', ->
copy = inventory.clone()
copy.unparse().should.equal 'boat:20.string:4.wool'
describe 'each', ->
it 'works with an empty inventory', ->
inventory = new Inventory
result = []
inventory.each (item)-> result.push item.name
result.should.eql []
it 'works when items have only been added', ->
result = []
inventory.each (stack)-> result.push stack.itemSlug.qualified
result.should.eql ['boat', 'string', 'wool']
it 'works when items have been augmented', ->
inventory.add ItemSlug.slugify 'iron_ingot'
inventory.add ItemSlug.slugify 'boat'
inventory.add ItemSlug.slugify('wool'), 2
result = []
inventory.each (stack)-> result.push stack.itemSlug.qualified
result.should.eql ['boat', 'iron_ingot', 'string', 'wool']
describe 'hasAtLeast', ->
it 'works when the item is completely absent', ->
answer = inventory.hasAtLeast 'chicken', 1
answer.should.be.false
it 'always returns true for zero quantity', ->
inventory.hasAtLeast('chicken', 0).should.be.true
inventory.hasAtLeast('wool', 0).should.be.true
it 'works for a quantity above 1', ->
inventory.hasAtLeast('wool', 3).should.be.true
inventory.hasAtLeast('wool', 4).should.be.true
inventory.hasAtLeast('wool', 5).should.be.false
describe 'localize', ->
before ->
modPack =
modSlug:
wool: 'minecraft'
string: 'minecraft'
boat: 'minecraft'
stone_gear: 'buildcraft'
findItem: (slug)->
return slug:new ItemSlug @modSlug[slug.item], slug.item
it 'replaces item slugs with qualified slugs', ->
inventory.add ItemSlug.slugify 'stone_gear'
inventory.modPack = modPack
inventory.localize()
slugs = []
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
slugs.should.eql [
'minecraft__boat'
'buildcraft__stone_gear'
'minecraft__string'
'minecraft__wool'
]
it 'ignores qualified slugs', ->
inventory.add ItemSlug.slugify 'buildcraft__stone_gear'
inventory.modPack = modPack
inventory.localize()
slugs = []
inventory.each (stack)-> slugs.push stack.itemSlug.qualified
slugs.should.eql [
'minecraft__boat'
'buildcraft__stone_gear'
'minecraft__string'
'minecraft__wool'
]
describe 'parse', ->
beforeEach ->
inventory = new Inventory {}, silent:false
it 'ignores an empty string', ->
result = inventory.parse ''
result.unparse().should.eql ''
it 'can parse a single item without quantity', ->
result = inventory.parse 'wool'
result.unparse().should.equal 'wool'
it 'can parse a single item with quantity', ->
result = inventory.parse '4.wool'
result.unparse().should.equal '4.wool'
it 'can parse multiple mixed-type items', ->
result = inventory.parse '4.wool:10.string:boat'
result.unparse().should.equal 'boat:10.string:4.wool'
describe 'pop', ->
it 'returns null for an empty inventory', ->
inventory = new Inventory
result = inventory.pop()
expect(result).to.be.null
it 'completely removes the last item', ->
stack = inventory.pop()
stack.itemSlug.qualified.should.equal 'wool'
stack.quantity.should.equal 4
inventory.unparse().should.equal 'boat:20.string'
it 'triggers the right events', ->
events = new EventRecorder inventory
result = inventory.pop()
events.names.should.eql [c.event.remove, c.event.change]
describe 'remove', ->
it 'does nothing when the item is absent', ->
before = inventory.unparse()
inventory.remove 'foo'
after = inventory.unparse()
before.should.equal after
it 'throws when the item has insufficient quantity', ->
expect(-> inventory.remove('wool', 10)).to.throw Error,
'cannot remove 10: only 4 wool in this inventory'
it 'removes all items by default', ->
inventory.remove 'wool'
expect(inventory._stacks.wool).to.be.empty
it 'removes a quantity above 1', ->
inventory.remove 'wool', 3
inventory._stacks.wool.quantity.should.equal 1
it 'emits the proper events', ->
events = new EventRecorder inventory
inventory.remove 'wool'
events.names.should.eql [c.event.change, c.event.remove, c.event.change]
+90
View File
@@ -0,0 +1,90 @@
#
# Crafting Guide - item.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Recipe = require './recipe'
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
module.exports = class Item extends BaseModel
@Group = Other:'Other'
constructor: (attributes={}, options={})->
if not attributes.name? then throw new Error 'attributes.name is required'
attributes.description ?= null
attributes.group ?= Item.Group.Other
attributes.ignoreDuringCrafting ?= false
attributes.isGatherable ?= false
attributes.modVersion ?= null
attributes.officialUrl ?= null
attributes.slug ?= ItemSlug.slugify attributes.name
attributes.videos ?= []
options.logEvents ?= false
super attributes, options
@on c.event.change + ':modVersion', =>
@_isCraftable = null
@slug.mod = @modVersion?.modSlug
# Public Methods ###############################################################################
compareTo: (that)->
if this.slug isnt that.slug
return if this.slug < that.slug then -1 else +1
if this.name isnt that.name
return if this.name < that.name then -1 else +1
return 0
unparse: ->
ItemParser = require '../parsing/item_parser' # to avoid require cycles
@_parser ?= new ItemParser model:this
return @_parser.unparse()
# Property Methods #############################################################################
getIsCraftable: ->
if not @_isCraftable?
@_isCraftable = false
if @modVersion?
@_isCraftable = @modVersion.hasRecipes @slug
return @_isCraftable
Object.defineProperties @prototype,
isCraftable: {get:@prototype.getIsCraftable}
# Backbone.Model Overrides #####################################################################
parse: (text)->
ItemParser = require '../parsing/item_parser' # to avoid require cycles
@_parser ?= new ItemParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.itemData modSlug:@slug.mod, itemSlug:@slug.item
# Object Overrides #############################################################################
toString: ->
builder = new StringBuilder
return builder
.push @constructor.name, ' (', @cid, ') { '
.push 'name:"', @name, '", '
.push 'isCraftable:', @isCraftable, ', '
.push 'isGatherable:', @isGatherable, ', '
.onlyIf (@group isnt Item.Group.Other), (b)=>
b.push 'group:"', @group, '", '
.push 'slug:"', @slug, '", '
.push '}'
.toString()
+96
View File
@@ -0,0 +1,96 @@
#
# Crafting Guide - item_slug.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ItemSlug
constructor: ->
@_item = @_mod = null
if arguments.length is 1
parts = _.decomposeSlug arguments[0]
@_mod = _.slugify parts[0]
@item = _.slugify parts[1]
else if arguments.length is 2
@_mod = arguments[0]
@item = arguments[1]
else
throw new Error 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
# Class Methods ################################################################################
@compare: (a, b)->
if a.item isnt b.item
return if a.item < b.item then -1 else +1
if a.mod isnt b.mod
return if a.mod < b.mod then -1 else +1
return 0
@equal: (a, b)->
return true if not a? and not b?
return false unless a? and b?
return false unless a.mod is b.mod
return false unless a.item is b.item
return true
@slugify: (arg)->
return arg if arg?.constructor?.name is 'ItemSlug'
[modSlug, itemSlug] = _.decomposeSlug arg
itemSlug = _.slugify itemSlug
if modSlug?
return new ItemSlug modSlug, itemSlug
else
return new ItemSlug itemSlug
# Public Methods ###############################################################################
compareTo: (that)->
return ItemSlug.compare this, that
matches: (slug, options={exact:false})->
return false unless slug?
return false unless typeof(slug.matches) is 'function'
if slug.isQualified and this.isQualified
return slug.qualified is this.qualified
else
return slug.item is this.item
# Property Methods #############################################################################
Object.defineProperties @prototype,
isQualified:
get: -> @_mod?
item:
get: -> @_item
set: (newItem)->
if not newItem? then throw new Error 'item is required'
@_item = newItem
@mod = @mod # reset @_qualified
mod:
get: -> @_mod
set: (newMod)->
@_mod = newMod
@_qualified = if @_mod? then _.composeSlugs(@_mod, @_item) else @_item
qualified:
get: -> @_qualified
# Object Overrides #############################################################################
toString: ->
return @_qualified
valueOf: ->
return @_qualified.valueOf()
@@ -0,0 +1,122 @@
#
# Crafting Guide - item_slug.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require './item_slug'
########################################################################################################################
describe 'item_slug.coffee', ->
describe 'constructor', ->
it 'can handle one argument', ->
slug = new ItemSlug 'alpha'
slug.item.should.equal 'alpha'
expect(slug.mod).to.be.null
slug.qualified.should.equal 'alpha'
it 'can handle two arguments', ->
slug = new ItemSlug 'alpha', 'bravo'
slug.mod.should.equal 'alpha'
slug.item.should.equal 'bravo'
slug.qualified.should.equal 'alpha__bravo'
it 'throws with zero arguments', ->
f = -> new ItemSlug
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
it 'throws with more arguments', ->
f = -> new ItemSlug 'alpha', 'bravo', 'charlie'
expect(f).to.throw Error, 'expected arguments to be "modSlug, itemSlug" or just "itemSlug"'
describe 'ItemSlug.compare', ->
it 'sorts by item when both are qualified in the same mod', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'bravo'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
it 'sorts by item when not qualified', ->
a = new ItemSlug 'alpha'
b = new ItemSlug 'bravo'
ItemSlug.compare(a, b).should.equal -1
ItemSlug.compare(b, a).should.equal +1
describe 'ItemSlug.equal', ->
it 'requires both to have the same mod', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'alpha', 'charlie'
c = new ItemSlug 'alpha', 'bravo'
ItemSlug.equal(a, b).should.be.false
ItemSlug.equal(a, c).should.be.true
it 'requires both to have the same item', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'alpha', 'charlie'
c = new ItemSlug 'alpha', 'bravo'
ItemSlug.equal(a, b).should.be.false
ItemSlug.equal(a, c).should.be.true
describe 'ItemSlug.slugify', ->
it 'can slugify a pure name', ->
slug = ItemSlug.slugify 'Alpha Bravo (Charlie)'
slug.item.should.equal 'alpha_bravo_charlie'
expect(slug.mod).to.be.null
it 'can slugify a simple item slug', ->
slug = ItemSlug.slugify 'alpha_bravo_charlie'
slug.item.should.equal 'alpha_bravo_charlie'
expect(slug.mod).to.be.null
it 'can slugify a fully-qualified slug', ->
slug = ItemSlug.slugify 'alpha_bravo__charlie_delta'
slug.mod.should.equal 'alpha_bravo'
slug.item.should.equal 'charlie_delta'
describe 'matches', ->
it 'ignores mod when either is unqualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'bravo'
c = new ItemSlug 'charlie'
a.matches(b).should.be.true
a.matches(c).should.be.false
it 'observes differences in mod when all are qualified', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie', 'bravo'
c = new ItemSlug 'delta', 'echo'
d = new ItemSlug 'alpha', 'bravo'
a.matches(b).should.be.false
a.matches(c).should.be.false
a.matches(d).should.be.true
describe 'isQualified', ->
it 'returns true only when the mod slug is set', ->
a = new ItemSlug 'alpha', 'bravo'
b = new ItemSlug 'charlie'
a.isQualified.should.be.true
b.isQualified.should.be.false
describe '[]', ->
it 'allows slugs as a key', ->
slug = new ItemSlug 'alpha', 'bravo'
data = {}
data[slug] = 'foo'
data['alpha__bravo'].should.equal 'foo'
data[slug].should.equal 'foo'
+224
View File
@@ -0,0 +1,224 @@
#
# Crafting Guide - mod.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class Mod extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.slug? then throw new Error 'attributes.slug is required'
attributes.author ?= ''
attributes.description ?= ''
attributes.documentationUrl ?= null
attributes.downloadUrl ?= null
attributes.homePageUrl ?= null
attributes.modPack ?= null
attributes.name ?= ''
super attributes, options
@_activeModVersion = null
@_activeVersion = null
@_modVersions = []
@_tutorials = []
@once c.event.sync, => @_verifyActiveModVersion()
# Class Methods ##################################################################################
@Version: Version =
None: 'none'
Latest: 'latest'
# Public Methods #################################################################################
compareTo: (that)->
thisRequired = this.slug in c.requiredMods
thatRequired = that.slug in c.requiredMods
if thisRequired isnt thatRequired
return -1 if thisRequired
return +1 if thatRequired
else if this.slug isnt that.slug
return if this.slug < that.slug then -1 else +1
return 0
# Property Methods #############################################################################
Object.defineProperties @prototype,
activeModVersion:
get: -> @_activeModVersion
activeVersion:
get: ->
return @_activeVersion
set: (version)->
return if version is @_activeVersion
version ?= Mod.Version.None
if version is Mod.Version.Latest then version = _.last(@_modVersions).version
if version is Mod.Version.None
@_activeVersion = version
@_activateModVersion null
@trigger c.event.change + ':activeVersion', this, @_activeVersion
@trigger c.event.change, this
else
for modVersion in @_modVersions
if version is modVersion.version
@_activateModVersion modVersion
break
@_activeVersion = version
@trigger c.event.change + ':activeVersion', this, @_activeVersion
@trigger c.event.change, this
enabled:
get: -> @_activeModVersion?
modVersions:
get: -> @_modVersions[..]
tutorials:
get: -> @getAllTutorials()
# Item Methods #################################################################################
eachItem: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachItem callback
findItem: (slug, options={})->
options.includeDisabled ?= false
options.enableAsNeeded ?= false
if not options.includeDisabled
return unless @_activeModVersion?
return @_activeModVersion.findItem slug
else
for modVersion in @_modVersions
modVersion.fetch()
item = modVersion.findItem slug
if item?
if options.enableAsNeeded then @setActiveVersion modVersion.version
return item
return null
findItemByName: (name)->
return unless @_activeModVersion?
@_activeModVersion.findItemByName name
# ModVersion Methods ###########################################################################
addModVersion: (modVersion)->
return unless modVersion?
return if @_modVersions.indexOf(modVersion) isnt -1
@_modVersions.push modVersion
@listenTo modVersion, c.event.change, => @trigger c.event.change, this
modVersion.mod = this
@trigger c.event.add + ':modVersion', modVersion, this
@trigger c.event.change + ':version', modVersion, this
@trigger c.event.change, this
if not @activeVersion? then @activeVersion = modVersion.version
if modVersion.version is @_activeVersion then @_activateModVersion modVersion
return this
eachModVersion: (callback)->
for modVersion in @_modVersions
callback modVersion
getModVersion: (version)->
return null if version is Mod.Version.None
return @_modVersions[0] if version is Mod.Version.Latest
for modVersion in @_modVersions
return modVersion if modVersion.version is version
return null
# Name Methods #################################################################################
eachName: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachName callback
findName: (itemSlug)->
return unless @_activeModVersion?
@_activeModVersion.findName itemSlug
# Recipe Methods ###############################################################################
eachRecipe: (callback)->
effectiveModVersion = @_activeModVersion or @getModVersion Mod.Version.Latest
effectiveModVersion.eachRecipe callback
findRecipes: (itemSlug, result=[], options={})->
options.alwaysFromOwningMod ?= false
if @_activeModVersion?
return @_activeModVersion.findRecipes itemSlug, result, options
else if options.alwaysFromOwningMod and itemSlug.mod is @slug
return @getModVersion(Mod.Version.Latest).findRecipes itemSlug, result, options
return null
# Tutorial Methods #############################################################################
addTutorial: (tutorial)->
return unless tutorial?
if @getTutorial(tutorial.slug)? then throw new Error "duplicate tutorial: #{tutorial.name}"
@_tutorials.push tutorial
tutorial.modSlug = @slug
getAllTutorials: ->
return @_tutorials[..]
getTutorial: (tutorialSlug)->
for tutorial in @_tutorials
return tutorial if tutorial.slug is tutorialSlug
return null
# Backbone.Model Overrides #####################################################################
parse: (text)->
ModParser = require '../parsing/mod_parser' # to avoid require cycles
@_parser ?= new ModParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.modData modSlug:@slug
# Private Methods ##############################################################################
_activateModVersion: (modVersion)->
if @_activeModVersion? then @stopListening @_activeModVersion
@_activeModVersion = modVersion
@trigger c.event.change + ':activeModVersion', this, @_activeModVersion
logger.verbose => "#{@slug} switched to version #{@_activeVersion}"
if @_activeModVersion?
@listenTo @_activeModVersion, 'all', -> @trigger.apply this, arguments
_verifyActiveModVersion: ->
if (@_activeVersion isnt Version.None) and (not @_activeModVersion?)
logger.warning => "#{@slug} no longer has a version #{@_activeVersion}, using latest instead"
@activeVersion = Version.Latest
+30
View File
@@ -0,0 +1,30 @@
#
# Crafting Guide - mod.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Mod = require './mod'
########################################################################################################################
mod = null
########################################################################################################################
describe 'mod.coffee', ->
beforeEach -> mod = new Mod name:'Test', slug:'test'
describe 'compareTo', ->
it 'lists required mods first', ->
minecraft = new Mod name:'Minecraft', slug:'minecraft'
mod.compareTo(minecraft).should.equal +1
minecraft.compareTo(mod).should.equal -1
it 'sorts by name second', ->
buildcraft = new Mod name:'Buildcraft', slug:'buildcraft'
mod.compareTo(buildcraft).should.equal +1
buildcraft.compareTo(mod).should.equal -1
+176
View File
@@ -0,0 +1,176 @@
#
# Crafting Guide - mod_pack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
Inventory = require './inventory'
ModVersionParser = require '../parsing/mod_version_parser'
Recipe = require './recipe'
########################################################################################################################
module.exports = class ModPack extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
@_mods = []
@_cache = {}
@on c.event.change, => @_cache = {}
# Item Methods #################################################################################
findItem: (itemSlug, options={})->
options.includeDisabled ?= false
key = "#{itemSlug}-#{options.includeDisabled}"
@_cache.itemBySlug ?= {}
item = @_cache.itemBySlug[key]
return item if item?
if itemSlug.isQualified
mod = @getMod itemSlug.mod
if mod?
item = mod.findItem itemSlug, options
if not item?
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
item = mod.findItem itemSlug, options
break if item?
if item?
@_cache.itemBySlug[key] = item
return item
findItemByName: (name, options={})->
options.enableAsNeeded ?= false
options.includeDisabled = true if options.enableAsNeeded
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
item = mod.findItemByName name, options
return item if item?
return null
findItemDisplay: (itemSlug)->
if not itemSlug? then throw new Error 'itemSlug is required'
result = {slug:itemSlug}
item = @findItem itemSlug, includeDisabled:true
if item?
result.itemName = item.name
result.itemSlug = item.slug.item
result.modSlug = item.slug.mod
result.modVersion = item.modVersion.version
else
result.itemName = @findName itemSlug, includeDisabled:true
result.itemSlug = itemSlug.item
result.modSlug = @_mods[0].slug
result.modVersion = @_mods[0].activeVersion
craftingUrlInventory = new Inventory modPack:this
if item?.multiblock?
craftingUrlInventory.addInventory item.multiblock.inventory
else
craftingUrlInventory.add itemSlug
result.craftingUrl = c.url.crafting inventoryText:craftingUrlInventory.unparse()
result.iconUrl = c.url.itemIcon result
result.itemUrl = c.url.item result
result.modName = @getMod(result.modSlug).name
return result
qualifySlug: (itemSlug)->
return itemSlug if itemSlug.isQualified
item = @findItem itemSlug
return item.slug if item?
return itemSlug
# Mod Methods ##################################################################################
addMod: (mod)->
if not mod? then throw new Error 'mod is required'
return if @_mods.indexOf(mod) isnt -1
mod.modPack = this
@_mods.push mod
@listenTo mod, c.event.change, (modVersion)=> @_onModVersionLoaded modVersion
@trigger c.event.add + ':mod', mod, this
@_mods.sort (a, b)-> a.compareTo b
@trigger c.event.sort + ':mod', this
@trigger c.event.change, this
return this
eachMod: (callback)->
for mod in @_mods
callback mod
getMod: (slug)->
for mod in @_mods
return mod if mod.slug is slug
return null
getAllMods: ->
return @_mods[..]
# Name Methods #################################################################################
findName: (slug, options={})->
options.includeDisabled ?= false
for mod in @_mods
continue unless mod.enabled or options.includeDisabled
name = mod.findName slug
return name if name
return null
# Recipe Methods ###############################################################################
findRecipes: (itemSlug, options={})->
options.alwaysFromOwningMod ?= false
return null unless itemSlug?
key = "#{itemSlug}-#{options.alwaysFromOwningMod}"
@_cache.recipesBySlug ?= {}
result = @_cache.recipesBySlug[key]
return result if result?
result = []
for mod in @_mods
if not mod.enabled
owningMod = itemSlug.isQualified and (itemSlug.mod is mod.slug)
continue unless owningMod and options.alwaysFromOwningMod
mod.findRecipes itemSlug, result, options
@_cache.recipesBySlug[key] = result
return if result.length > 0 then result else null
# Object Overrides #############################################################################
toString: ->
return "ModPack (#{@cid}) {modVersions:«#{@_mods.length} items»}"
# Private Methods ##############################################################################
_onModVersionLoaded: (modVersion)->
working = true
@eachMod (mod)->
mod.eachModVersion (modVersion)->
working = working and (modVersion.isUnloaded or modVersion.isLoading)
return if working
@trigger c.event.change, this
@trigger c.event.sync, this
+103
View File
@@ -0,0 +1,103 @@
#
# Crafting Guide - mod_pack.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require './item'
ItemSlug = require './item_slug'
Mod = require './mod'
ModPack = require './mod_pack'
ModVersion = require './mod_version'
########################################################################################################################
buildcraft = industrialCraft = minecraft = modPack = null
########################################################################################################################
describe 'mod_pack.coffee', ->
beforeEach ->
minecraft = new Mod slug:'minecraft', name:'Minecraft'
minecraft.addModVersion new ModVersion modSlug:minecraft.slug, version:'1.7.10'
minecraft.activeModVersion.addItem new Item name:'Wool'
minecraft.activeModVersion.addItem new Item name:'Bed', recipes:['']
minecraft.activeModVersion.registerName ItemSlug.slugify('iron_chestplate'), 'Iron Chestplate'
buildcraft = new Mod slug:'buildcraft', name:'Buildcraft'
buildcraft.addModVersion new ModVersion modSlug:buildcraft.slug, version:'6.2.6'
buildcraft.activeModVersion.addItem new Item name:'Stone Gear', recipes:['']
buildcraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
buildcraft.activeVersion = Mod.Version.None
industrialCraft = new Mod slug:'industrial_craft', name:'Industrial Craft'
industrialCraft.addModVersion new ModVersion modSlug:industrialCraft.slug, version:'2.0'
industrialCraft.activeModVersion.addItem new Item name:'Resin'
industrialCraft.activeModVersion.addItem new Item name:'Rubber'
industrialCraft.activeModVersion.addItem new Item name:'Wrench', recipes:['']
industrialCraft.activeVersion = Mod.Version.None
modPack = new ModPack
modPack.addMod minecraft
modPack.addMod buildcraft
modPack.addMod industrialCraft
describe 'findItem', ->
it 'can find an item by partial slug', ->
item = modPack.findItem ItemSlug.slugify 'wool'
item.slug.qualified.should.equal 'minecraft__wool'
it 'can find an item by full slug', ->
item = modPack.findItem ItemSlug.slugify 'minecraft__wool'
item.name.should.equal 'Wool'
it 'can find an ambiguous item by full slug', ->
buildcraft.activeVersion = Mod.Version.Latest
industrialCraft.activeVersion = Mod.Version.Latest
item = modPack.findItem ItemSlug.slugify 'industrial_craft__wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Industrial Craft'
it 'can find an ambiguous item by partial slug', ->
buildcraft.activeVersion = Mod.Version.Latest
industrialCraft.activeVersion = Mod.Version.Latest
item = modPack.findItem ItemSlug.slugify 'wrench'
item.name.should.equal 'Wrench'
item.modVersion.mod.name.should.equal 'Buildcraft'
describe 'findItemByName', ->
it 'finds the requested item', ->
item = modPack.findItemByName 'Bed'
item.name.should.equal 'Bed'
it 'ignores disabled mod versions', ->
item = modPack.findItemByName 'Stone Gear'
expect(item).to.be.null
describe 'findItemDisplay', ->
it 'returns all data for a regular Minecraft item', ->
display = modPack.findItemDisplay ItemSlug.slugify 'bed'
display.iconUrl.should.equal '/data/minecraft/items/bed/icon.png'
display.itemUrl.should.equal '/browse/minecraft/bed/'
display.itemName.should.equal 'Bed'
display.modSlug.should.equal 'minecraft'
it 'returns all data for an item in an enabled mod', ->
buildcraft.activeVersion = '6.2.6'
display = modPack.findItemDisplay ItemSlug.slugify 'stone_gear'
display.iconUrl.should.equal '/data/buildcraft/items/stone_gear/icon.png'
display.itemUrl.should.equal '/browse/buildcraft/stone_gear/'
display.itemName.should.equal 'Stone Gear'
display.modSlug.should.equal 'buildcraft'
it 'assumes an unfound item is from Minecraft', ->
display = modPack.findItemDisplay ItemSlug.slugify 'iron_chestplate'
display.iconUrl.should.equal '/data/minecraft/items/iron_chestplate/icon.png'
display.itemUrl.should.equal '/browse/minecraft/iron_chestplate/'
display.itemName.should.equal 'Iron Chestplate'
display.modSlug.should.equal 'minecraft'
+196
View File
@@ -0,0 +1,196 @@
#
# Crafting Guide - mod_version.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
Item = require './item'
ItemSlug = require './item_slug'
Recipe = require './recipe'
########################################################################################################################
module.exports = class ModVersion extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.modSlug? then throw new Error 'attributes.modSlug is required'
if not attributes.version? then throw new Error 'attributes.version is required'
attributes.mod ?= null
super attributes, options
@_groups = {}
@_items = {}
@_names = {}
@_recipes = {}
@_slugs = []
# Public Methods ###############################################################################
compareTo: (that)->
if this.mod? and that.mod?
return this.mod.compareTo that.mod
if this.modSlug isnt that.modSlug
return if this.modSlug < that.modSlug then -1 else +1
return 0
sort: ->
@_slugs.sort (a, b)-> ItemSlug.compare a, b
# Item Methods #################################################################################
addItem: (item)->
if @findItem(item.slug)? then throw new Error "duplicate item for #{item.name}"
item.modVersion = this
@_items[item.slug.item] = item
@_groups[item.group] ?= {}
@_groups[item.group][item.slug.item] = item
@registerName item.slug, item.name
return this
allItemsInGroup: (group)->
result = []
@eachItemInGroup group, (item)-> result.push item
return null if result.length is 0
return result
eachItem: (callback)->
for slug in @_slugs
item = @findItem slug
continue unless item?
callback item
return this
eachItemInGroup: (group, callback)->
itemMap = @_groups[group]
return unless itemMap?
items = _.values(itemMap).sort (a, b)-> a.compareTo b
for item in items
callback item
findItem: (itemSlug)->
return @_items[itemSlug.item]
findItemByName: (name)->
for itemSlug, item of @_items
return item if item.name is name
return null
# Group Methods ################################################################################
getAllGroups: ->
result = []
@eachGroup (group)-> result.push group
return result
eachGroup: (callback)->
groupNames = _.keys @_groups
groupNames.sort (a, b)->
if a is b then return 0
if a is Item.Group.Other then return -1
if b is Item.Group.Other then return +1
return if a < b then -1 else +1
for groupName in groupNames
callback groupName
# Name Methods #################################################################################
eachName: (callback)->
for slug in @_slugs
callback @_names[slug.item], slug
return this
findName: (itemSlug)->
return @_names[itemSlug.item]
registerName: (itemSlug, name)->
return if @_names[itemSlug.item]
@_names[itemSlug.item] = name
@_slugs.push itemSlug
return this
# Recipe Methods ###############################################################################
addRecipe: (recipe)->
return unless recipe?
recipe.modVersion = this
if @_recipes[recipe.slug]? then throw new Error "duplicate recipe: #{recipe.slug}"
@_recipes[recipe.slug] = recipe
return this
eachRecipe: (callback)->
recipes = _.values(@_recipes).sort (a, b)-> Recipe.compareFor a, b
for recipe in recipes
callback recipe
return this
findRecipes: (itemSlug, result=[], options={})->
options.onlyPrimary ?= false
options.forCrafting ?= false
primaryRecipes = []
otherRecipes = []
for recipe in _.values @_recipes
continue unless recipe.isConditionSatisfied()
continue unless recipe.hasAllTools()
continue if options.forCrafting and recipe.ignoreDuringCrafting
if recipe.itemSlug.matches itemSlug
primaryRecipes.push recipe
else if recipe.produces itemSlug
otherRecipes.push recipe
for recipe in primaryRecipes
result.push recipe
if not options.onlyPrimary
for recipe in otherRecipes
result.push recipe
return result
findExternalRecipes: ->
result = {}
for k, recipe of @_recipes
continue if recipe.itemSlug.isQualified
recipeList = result[recipe.itemSlug]
if not recipeList then recipeList = result[recipe.itemSlug] = []
recipeList.push recipe
return result
hasRecipes: (itemSlug)->
for k, recipe of @_recipes
return true if recipe.produces itemSlug
return false
# Backbone.Model Overrides #####################################################################
parse: (text)->
ModVersionParser = require '../parsing/mod_version_parser' # to avoid require cycles
@_parser ?= new ModVersionParser model:this
@_parser.parse text
return null # prevent calling `set`
url: ->
return c.url.modVersionData modSlug:@modSlug, modVersion:@version
# Object Overrides #############################################################################
toString: ->
return "ModVersion (#{@cid}) {
modSlug:#{@modSlug}, version:#{@version}, items:«#{@_slugs.length} items»
}"
@@ -0,0 +1,101 @@
#
# Crafting Guide - mod_version.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require './item'
ItemSlug = require './item_slug'
ModVersion = require './mod_version'
########################################################################################################################
modVersion = null
########################################################################################################################
describe 'mod_version.coffee', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'0.0'
modVersion.addItem new Item name:'underscore', group:'punctuation'
modVersion.addItem new Item name:'bravo', group:'letter'
modVersion.addItem new Item name:'alpha', group:'letter'
modVersion.addItem new Item name:'one', group:'number'
modVersion.addItem new Item name:'two', group:'number'
describe 'constructor', ->
it 'requires a mod slug', ->
expect(-> new ModVersion version:'0.0').to.throw Error, 'attributes.modSlug is required'
it 'requires a mod version', ->
expect(-> new ModVersion modSlug:'test').to.throw Error, 'attributes.version is required'
describe 'addItem', ->
it 'refuses to add duplicates', ->
modVersion.addItem new Item name:'Wool'
expect(-> modVersion.addItem new Item name:'Wool').to.throw Error, 'duplicate item for Wool'
it 'adds an item indexed by its slug', ->
modVersion.addItem new Item name:'Wool'
modVersion._items.wool.name.should.equal 'Wool'
it 'sets the modVersion', ->
modVersion.addItem new Item name:'Wool'
modVersion._items.wool.modVersion.should.equal modVersion
describe 'eachGroup', ->
it 'returns all the groups in order', ->
groupNames = []
modVersion.eachGroup (groupName)-> groupNames.push groupName
groupNames.should.eql ['letter', 'number', 'punctuation']
describe 'eachItemInGroup', ->
it 'returns immediately for unknown group', ->
slugs = []
modVersion.eachItemInGroup 'foobar', (item)-> slugs.push item.slug.qualified
slugs.should.eql []
it 'calls callback for exactly the items in a group in order', ->
slugs = []
modVersion.eachItemInGroup 'letter', (item)-> slugs.push item.slug.qualified
slugs.should.eql ['test__alpha', 'test__bravo']
slugs = []
modVersion.eachItemInGroup 'number', (item)-> slugs.push item.slug.qualified
slugs.should.eql ['test__one', 'test__two']
describe 'findItemByName', ->
it 'locates items by slugified name', ->
modVersion.addItem new Item name:'Crafting Table'
modVersion.findItemByName('Crafting Table').slug.qualified.should.equal 'test__crafting_table'
describe 'findRecipes', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'1.0'
modVersion.parse """
schema:1
item: Cake
recipe:; input: Milk, Sugar, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
recipe:; input: Milk, Cocoa Beans, Egg, Wheat; pattern: 000 121 333; extras: 3 Bucket
recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice
item: Bucket
recipe:; input: Iron Ingot; pattern: ... 0.0 .0.
"""
it 'finds all recipes which list item as output', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__bucket')
(r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'cake', 'cake']
it 'skip recipes whose conditions are not met', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__cake')
recipes.length.should.equal 2
+85
View File
@@ -0,0 +1,85 @@
#
# Crafting Guide - multiblock.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
Inventory = require './inventory'
########################################################################################################################
module.exports = class Multiblock extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.layers? then throw new Error 'attributes.layers is required'
if attributes.layers.length < 1 then throw new Error 'attributes.layers.length must be >= 1'
super attributes, options
@_analyzePattern()
# Public Methods ###############################################################################
getStackAt: (x, y, z)->
value = @_stackCache[y]?[z]?[x]
return null if value is undefined
return value
getLayerInventory: (y)->
@_layerInventories ?= []
result = @_layerInventories[y]
if not result? then result = @_layerInventories[y] = new Inventory
return result
# Property Methods #############################################################################
Object.defineProperties @prototype,
depth:
get: -> @_depth
height:
get: -> @_height
inventory:
get: -> @_inventory
width:
get: -> @_width
# Private Methods ##############################################################################
_analyzeLayer: (layer, y, stackCacheLayer)->
rows = layer.split ' '
@_depth = Math.max @_depth, rows.length
for row in rows
@_width = Math.max @_width, row.length
stackCacheRow = []
stackCacheLayer.push stackCacheRow
@_analyzeRow row, stackCacheRow, @getLayerInventory(y)
_analyzePattern: ->
@_depth = @_height = @_width = 0
@_inventory = new Inventory
@_stackCache = []
for layer, y in @layers
stackCacheLayer = []
@_stackCache.push stackCacheLayer
@_analyzeLayer layer, y, stackCacheLayer
@_height = @layers.length
_analyzeRow: (row, stackCacheRow, layerInventory)->
for cell, x in row.split ''
index = parseInt cell
stack = null
if not _.isNaN index
stack = @input[index]
@_inventory.add stack.itemSlug, stack.quantity
layerInventory.add stack.itemSlug, stack.quantity
stackCacheRow.push stack
@@ -0,0 +1,85 @@
#
# Crafting Guide - multiblock.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require './item_slug'
Mod = require './mod'
Multiblock = require './multiblock'
Stack = require './stack'
########################################################################################################################
describe 'multiblock.coffee', ->
describe 'with a single block type', ->
beforeEach ->
@input = [ new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1 ]
it 'correctly parses a 1x1x1 cube', ->
model = new Multiblock input:@input, layers:['0']
model.depth.should.equal 1
model.height.should.equal 1
model.width.should.equal 1
model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone'
it 'correctly parses a solid 3x3x3 cube', ->
model = new Multiblock input:@input, layers:['000 000 000', '000 000 000', '000 000 000']
model.depth.should.equal 3
model.height.should.equal 3
model.width.should.equal 3
for x in [0..2]
for y in [0..2]
for z in [0..2]
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
it 'correctly parses a hollow 3x3x3 cube', ->
model = new Multiblock input:@input, layers:['000 000 000', '000 0.0 000', '000 000 000']
model.depth.should.equal 3
model.height.should.equal 3
model.width.should.equal 3
for x in [0..2]
for y in [0..2]
for z in [0..2]
if x isnt 1 or y isnt 1 or z isnt 1
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
else
expect(model.getStackAt(x, y, z)).to.be.null
it 'correctly parses a 3x2x3 pyramid', ->
model = new Multiblock input:@input, layers:['000 000 000', '.. .0 ..']
model.depth.should.equal 3
model.height.should.equal 2
model.width.should.equal 3
for y in [0..1]
for z in [0..2]
for x in [0..2]
if y is 1 and (x isnt 1 or z isnt 1)
expect(model.getStackAt(x, y, z)).to.be.null
else
model.getStackAt(x, y, z).toString().should.equal '1 cobblestone'
describe 'with multiple block types', ->
beforeEach ->
@input = [
new Stack itemSlug:ItemSlug.slugify('cobblestone'), quantity:1
new Stack itemSlug:ItemSlug.slugify('stone'), quantity:1
new Stack itemSlug:ItemSlug.slugify('oak wood'), quantity:1
]
it 'correctly parses a 1x3x1 column of different types', ->
model = new Multiblock input:@input, layers:['0', '1', '2']
model.depth.should.equal 1
model.height.should.equal 3
model.width.should.equal 1
model.getStackAt(0, 0, 0).toString().should.equal '1 cobblestone'
model.getStackAt(0, 1, 0).toString().should.equal '1 stone'
model.getStackAt(0, 2, 0).toString().should.equal '1 oak_wood'
+252
View File
@@ -0,0 +1,252 @@
#
# Crafting Guide - recipe.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
ItemSlug = require './item_slug'
Stack = require './stack'
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.pattern? then throw new Error 'attributes.pattern is required'
if attributes.itemSlug? and not attributes.output?
attributes.output = [new Stack itemSlug:attributes.itemSlug, quantity:1]
else if attributes.output? and not attributes.itemSlug?
if attributes.output.length is 0 then throw new Error 'attributes.output cannot be empty'
attributes.itemSlug = attributes.output[0].itemSlug
else
throw new Error 'attributes.itemSlug or attributes.output is required'
attributes.pattern = @_parsePattern attributes.pattern
attributes.condition ?= null
attributes.ignoreDuringCrafting ?= false
attributes.modVersion ?= null
attributes.tools ?= []
options.logEvents ?= false
super attributes, options
@_computeQuantities attributes.pattern
@on c.event.change + ':modVersion', => @_slug = null
@on c.event.change + ':pattern', => @_patternCache = null
# Class Methods ################################################################################
@compareFor: (a, b, itemSlug)->
if itemSlug?
aValue = a.itemSlug.matches itemSlug
bValue = b.itemSlug.matches itemSlug
if aValue isnt bValue
return -1 if aValue
return +1 if bValue
aValue = a.getQuantityProduced itemSlug
bValue = b.getQuantityProduced itemSlug
if aValue isnt bValue
return if aValue > bValue then -1 else +1
return 0
# Public Methods ###############################################################################
getStackAtSlot: (patternSlot)->
trueIndex = 0:0, 1:1, 2:2, 3:4, 4:5, 5:6, 6:8, 7:9, 8:10
patternDigit = @pattern[trueIndex[patternSlot]]
return null unless patternDigit?
return null unless patternDigit.match /[0-9]/
stack = @input[parseInt(patternDigit)]
return null unless stack?
return stack
getQuantityProduced: (itemSlug)->
total = 0
for stack in @output
if stack.itemSlug.matches itemSlug
total += stack.quantity
return total
getQuantityRequired: (itemSlug)->
total = 0
for stack, index in @input
if ItemSlug.equal stack.itemSlug, itemSlug
total += @_quantities[index] * stack.quantity
return total
hasAllTools: (modPack)->
modPack ?= @modVersion?.mod?.modPack
return true unless modPack?
for stack in @tools
return false unless modPack.findItem stack.itemSlug
return true
isConditionSatisfied: (modPack)->
return true unless @condition?
modPack ?= @modVersion?.mod?.modPack
result = false
if @condition.verb is 'item'
if modPack?.findItemByName(@condition.noun)?
result = true
else if @condition.verb is 'mod'
modPack.eachMod (mod)->
if mod.name is @condition.noun
result = true
if @condition.inverted then result = not result
return result
isPassThroughFor: (itemSlug)->
amountCreated = 0
for stack in @output
if stack.itemSlug.matches itemSlug
amountCreated += stack.quantity
for stack in @input
if stack.itemSlug.matches itemSlug
amountCreated -= stack.quantity
return amountCreated <= 0
produces: (itemSlug)->
if not @_produces?
@_produces = {}
for stack in @output
actuallyProduces = not @isPassThroughFor stack.itemSlug
@_produces[stack.itemSlug.qualified] = actuallyProduces
result = @_produces[itemSlug.qualified] or @_produces[itemSlug.item]
return result
requires: (itemSlug)->
for stack in @input
if stack.itemSlug.matches itemSlug
return true
return false
requiresTool: (itemSlug)->
for stack in @tools
if stack.itemSlug.matches itemSlug
return true
# Property Methods #############################################################################
Object.defineProperties @prototype,
slug:
get: ->
if not @_slug?
builder = new StringBuilder
delimiterNeeded = false
for stack in @input
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
builder.push '>'
builder.push @pattern
builder.push '>'
for stack in @tools
builder.push stack.itemSlug.qualified
builder.push '>'
delimiterNeeded = false
for stack in @output
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
@_slug = builder.toString()
return @_slug
# Object Overrides #############################################################################
toString: ->
result = [@constructor.name, " (", @cid, ") { name:", @name]
result.push ", input:["
needsDelimiter = false
for stack in @input
if needsDelimiter then result.push ', '
result.push @getQuantityRequired stack.itemSlug
result.push ' '
result.push stack.itemSlug
needsDelimiter = true
result.push ']'
result.push ", output:["
needsDelimiter = false
for stack in @output
if needsDelimiter then result.push ', '
result.push @getQuantityProduced stack.itemSlug
result.push ' '
result.push stack.itemSlug
needsDelimiter = true
result.push ']'
if @tools.length > 0
result.push ", tools:["
needsDelimiter = false
for stack in @tools
if needsDelimiter then result.push ', '
result.push stack.toString()
needsDelimiter = true
result.push ']'
result.push '}'
return result.join ''
# Private Methods ##############################################################################
_computeQuantities: (pattern)->
quantityMap = {}
for c in pattern.split ''
continue if c is '.'
continue if c is ' '
if quantityMap[c]?
quantityMap[c] += 1
else
quantityMap[c] = 1
@_quantities = []
for i in [0...@input.length]
@_quantities.push quantityMap["#{i}"]
_parsePattern: (pattern)->
return unless pattern?
pattern = pattern.replace /\ /g, ''
return if pattern.length is 0
pattern = pattern.replace /[^0-9]/g, '.'
array = pattern.split ''
array = array[0...9]
while array.length isnt 9
array.push '.'
pattern = array.join ''
pattern = pattern.replace /(...)(...)(...)/, '$1 $2 $3'
return pattern
+90
View File
@@ -0,0 +1,90 @@
#
# Crafting Guide - recipe.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require './item'
ItemSlug = require './item_slug'
Recipe = require './recipe'
Stack = require './stack'
########################################################################################################################
input = output = pattern = recipe = null
########################################################################################################################
describe 'recipe.coffee', ->
describe 'constructor', ->
beforeEach ->
input = [
new Stack(itemSlug:new ItemSlug('iron_gear')),
new Stack(itemSlug:new ItemSlug('gold_ingot'), quantity:4)
]
pattern = '.1. 101 .1.'
it 'requires input', ->
expect(-> new Recipe slug:'gold_gear', pattern:pattern).to.throw Error, 'attributes.input is required'
it 'requires a pattern', ->
expect(-> new Recipe slug:'gold_gear', input:input).to.throw Error, 'attributes.pattern is required'
it 'requires either outputs or a slug', ->
f = -> new Recipe input:input, pattern:pattern
expect(f).to.throw 'attributes.itemSlug or attributes.output is required'
it 'creates default output', ->
recipe = new Recipe itemSlug:ItemSlug.slugify('gold_gear'), input:input, pattern:pattern
recipe.output.length.should.equal 1
recipe.output[0].itemSlug.qualified.should.equal 'gold_gear'
recipe.output[0].quantity.should.equal 1
it 'assigns a default slug', ->
recipe = new Recipe input:input, pattern:pattern, output:[new Stack itemSlug:ItemSlug.slugify('gold_gear')]
recipe.itemSlug.qualified.should.equal 'gold_gear'
describe 'getStackAtSlot', ->
beforeEach ->
input = [
new Stack itemSlug:ItemSlug.slugify('iron_gear')
new Stack itemSlug:ItemSlug.slugify('gold_ingot'), quantity:4
]
recipe = new Recipe itemSlug:'gold_gear', input:input, pattern:'.1. 101 .1.'
it 'returns the proper item for an early slot', ->
stack = recipe.getStackAtSlot(1)
stack.itemSlug.qualified.should.equal 'gold_ingot'
stack.quantity.should.equal 4
it 'returns the proper item for a late slot', ->
stack = recipe.getStackAtSlot(4)
stack.itemSlug.qualified.should.equal 'iron_gear'
stack.quantity.should.equal 1
it 'returns null for an invalid slot', ->
expect(recipe.getStackAtSlot(12)).to.be.null
describe '_parsePattern', ->
beforeEach ->
recipe = new Recipe
itemSlug: 'oak_wood_planks',
input: [new Stack itemSlug:new ItemSlug('oak_wood')],
pattern:'... .0. ...'
it 'normalizes invalid characters', ->
recipe._parsePattern('$$0 #() 010').should.equal '..0 ... 010'
it 'removes extra characters', ->
recipe._parsePattern('000 000 000 000').should.equal '000 000 000'
it 'fills in missing characters', ->
recipe._parsePattern('000000').should.equal '000 000 ...'
it 'fills in spaces', ->
recipe._parsePattern('000000000').should.equal '000 000 000'
@@ -0,0 +1,33 @@
#
# Crafting Guide - simple_stack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemSlug = require './item_slug'
########################################################################################################################
module.exports = class Stack
constructor: (attributes={}, options={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
attributes.quantity ?= 1
@itemSlug = attributes.itemSlug
@quantity = attributes.quantity
# Class Methods ################################################################################
@compare: (a, b)->
if a? and not b? then return -1
if not a? and b? then return +1
if a.quantity isnt b.quantity
return if a.quantity > b.quantity then -1 else +1
return ItemSlug.compare a.itemSlug, b.itemSlug
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
+23
View File
@@ -0,0 +1,23 @@
#
# Crafting Guide - stack.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class Stack extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
attributes.quantity ?= 1
options.logEvents ?= false
super attributes, options
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
@@ -0,0 +1,143 @@
#
# Crafting Guide - lexical_analyzer.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserCommand = require './parser_command'
########################################################################################################################
module.exports = class LexicalAnalyzer
constructor: (fileName, rawText)->
if not fileName? then throw new Error 'fileName is required'
if not rawText? then throw new Error 'rawText is required'
@_commands = []
@_fileName = fileName
@_rawText = rawText
@_lines = rawText.split '\n'
@_lineIndex = -1
@_lineNumber = 0
# Class Methods ################################################################################
@::COMMAND = /\ *([^:]*):?(.*)/
@::COMMENT = /([^\\]?)#.*/
@::FILE = /^#FILE +(.*)/
# Public Methods ###############################################################################
next: ->
command = @peek()
@_commands.shift()
return command
peek: ->
while @_commands.length is 0
break if @_lineNumber > @_lines.length
@_parseLine()
return null unless @_commands.length > 0
return @_commands[0]
push: (command)->
@_commands.unshift command
# Property Methods #############################################################################
getIsFinished: ->
return @peek() is null
Object.defineProperties @prototype,
isFinished: { get:@::getIsFinished }
# Private Methods ##############################################################################
_currentLine: ->
return null if @_lineIndex >= @_lines.length
return @_lines[@_lineIndex]
_nextLine: ->
if @_lineIndex < @_lines.length
while true
@_lineIndex += 1
@_lineNumber += 1
break if @_lineIndex >= @_lines.length
line = @_lines[@_lineIndex]
fileMatch = line.match @FILE, '$1'
if fileMatch?
@_fileName = fileMatch[1]
@_lineNumber = 0
else
line = line.replace @COMMENT, '$1'
@_lines[@_lineIndex] = line
break if line.trim().length > 0
return @_currentLine()
_parseHereDoc: ->
line = @_currentLine()
hereDocIndex = line.indexOf '<<-'
return [line, null] unless hereDocIndex isnt -1
hereDocStopText = line[hereDocIndex+3...line.length]
line = line[0...hereDocIndex]
hereDocLines = []
while true
nextLine = @_nextLine()
break unless nextLine?
break if nextLine.trim() is hereDocStopText
hereDocLines.push nextLine
shortestIndent = Number.MAX_VALUE
for hereDocLine in hereDocLines
continue if hereDocLine.trim().length is 0
shortestIndent = Math.min hereDocLine.match(/( *).*/)[1].length, shortestIndent
for i in [0...hereDocLines.length]
hereDocLines[i] = hereDocLines[i][shortestIndent..]
return [line, null] unless hereDocLines.length > 0
return [line, hereDocLines.join('\n')]
_parseLine: ->
line = @_nextLine()
return unless line?
startingLineNumber = @_lineNumber
[line, lineHereDoc] = @_parseHereDoc()
lineParts = (part.trim() for part in line.split(';'))
for linePart, index in lineParts
continue if linePart.length is 0
hereDoc = lineHereDoc if index is lineParts.length - 1
match = @COMMAND.exec linePart
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
argText = if match[2] then match[2].trim() else ''
args = (arg.trim() for arg in argText.split(','))
args = (arg for arg in args when arg.length > 0)
if hereDoc?
if argText.length > 0 then argText += ', '
argText += hereDoc
args.push hereDoc
@_commands.push new ParserCommand
argText: argText
args: args
fileName: @_fileName
hereDoc: hereDoc
lineNumber: startingLineNumber
name: match[1]
@@ -0,0 +1,121 @@
#
# Crafting Guide - lexical_analyzer.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
LexicalAnalyzer = require './lexical_analyzer'
########################################################################################################################
lexer = null
createLexer = (text)->
return new LexicalAnalyzer 'file.cg', text
########################################################################################################################
describe 'lexical_analyzer.coffee', ->
describe 'a document with', ->
it 'no content returns null immediately', ->
lexer = createLexer ''
expect(lexer.next()).to.be.null
it 'only whitespace returns null immediately', ->
lexer = createLexer '\n\n \n \n\n'
expect(lexer.next()).to.be.null
it 'a single command returns only that command', ->
lexer = createLexer 'alpha: bravo'
lexer.next().name.should.equal 'alpha'
expect(lexer.next()).to.be.null
it 'multiple commands on a single line returns all commands', ->
lexer = createLexer 'alpha: bravo; charlie: delta; echo: foxtrot'
lexer.next().name.should.equal 'alpha'
lexer.next().name.should.equal 'charlie'
lexer.next().name.should.equal 'echo'
expect(lexer.next()).to.be.null
it 'multiple commands on separate lines returns all commands', ->
lexer = createLexer 'alpha: bravo \n charlie: delta \n echo: foxtrot'
lexer.next().name.should.equal 'alpha'
lexer.next().name.should.equal 'charlie'
lexer.next().name.should.equal 'echo'
expect(lexer.next()).to.be.null
describe 'a command with', ->
it 'only a name has no arguments', ->
command = createLexer('alpha:').next()
command.argText.should.equal ''
command.args.should.eql []
it 'one argument has only that argument', ->
command = createLexer('alpha: bravo').next()
command.argText.should.equal 'bravo'
command.args.should.eql ['bravo']
it 'multiple arguments has all of them', ->
command = createLexer('alpha: bravo, charlie, delta').next()
command.argText.should.equal 'bravo, charlie, delta'
command.args.should.eql ['bravo', 'charlie', 'delta']
it 'only a heredoc has it recorded in the right places', ->
command = createLexer('alpha: <<-END\nbravo charlie\nEND').next()
command.hereDoc.should.equal 'bravo charlie'
command.args.should.eql ['bravo charlie']
command.argText.should.equal 'bravo charlie'
it 'multiple args and a hereDoc is built correctly', ->
command = createLexer('alpha: bravo, charlie <<-END\ndelta echo\nfoxtrot\nEND').next()
command.hereDoc.should.equal 'delta echo\nfoxtrot'
command.argText.should.equal 'bravo, charlie, delta echo\nfoxtrot'
command.args.should.eql ['bravo', 'charlie', 'delta echo\nfoxtrot']
describe 'file name is assigned correctly when', ->
it 'no file markers are given', ->
lexer = createLexer 'alpha: bravo\ncharlie: delta'
lexer.next().fileName.should.equal 'file.cg'
lexer.next().fileName.should.equal 'file.cg'
it 'commands appear before the first file marker', ->
lexer = createLexer 'alpha: bravo\n#FILE file2.cg\ncharlie: delta'
lexer.next().fileName.should.equal 'file.cg'
lexer.next().fileName.should.equal 'file2.cg'
it 'multiple file markers are used', ->
lexer = createLexer '#FILE file1.cg\na: b\n#FILE file2.cg\nc: d\n#FILE file3.cg\ne: f'
lexer.next().fileName.should.equal 'file1.cg'
lexer.next().fileName.should.equal 'file2.cg'
lexer.next().fileName.should.equal 'file3.cg'
describe 'line numbers are assigned correctly when', ->
it 'commands are listed one after another', ->
lexer = createLexer 'alpha: bravo\ncharlie: delta\necho: foxtrot'
lexer.next().lineNumber.should.equal 1
lexer.next().lineNumber.should.equal 2
lexer.next().lineNumber.should.equal 3
it 'commands are separated by blank lines and comments', ->
lexer = createLexer '# foo\n\nalpha: bravo\n\ncharlie: delta\n\n# bar\n\necho: foxtrot'
lexer.next().lineNumber.should.equal 3
lexer.next().lineNumber.should.equal 5
lexer.next().lineNumber.should.equal 9
it 'commands include hereDocs', ->
lexer = createLexer 'alpha: <<-END\nbravo charlie\nEND\ndelta: \n\necho: '
lexer.next().lineNumber.should.equal 1
lexer.next().lineNumber.should.equal 4
lexer.next().lineNumber.should.equal 6
it 'file markers are used', ->
lexer = createLexer '#FILE file1.cg\nalpha: \n#FILE file2.cg\n\nbravo: \n\n#FILE file3.cg\n\n\ncharlie: '
lexer.next().lineNumber.should.equal 1
lexer.next().lineNumber.should.equal 2
lexer.next().lineNumber.should.equal 3
+112
View File
@@ -0,0 +1,112 @@
#
# Crafting Guide - parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
LexicalAnalyzer = require './lexical_analyzer'
ModPack = require '../game/mod_pack'
ParserData = require './parser_data'
########################################################################################################################
module.exports = class Parser
constructor: (modPack, fileName, rawText)->
@_extensions = []
@_fileName = fileName
@_lexer = new LexicalAnalyzer fileName, rawText
@_modPack = new ModPack
@_data = new ParserData
modPackData = @_data.create {}, 'modPack'
modPackData.instance = @_modPack
@_loadSchema()
# Public Methods ###############################################################################
next: ->
if not @_lexer.isFinished
@_processNextCommand()
else if not @_data.isValidated
@_validateNextEntry()
else if not @_data.isBuilt
@_buildNextEntry()
else if not @_data.isAssembled
@_assembleNextEntry()
# Property Methods #############################################################################
getIsFinished: ->
return @_lexer.isFinished and @_data.isValidated and @_data.isBuilt and @_data.isAssembled
getData: ->
return @_data
getModPack: ->
return @_modPack
Object.defineProperties @prototype,
data: { get:@::getData }
isFinished: { get:@::getIsFinished }
modPack: { get:@::getModPack }
# Private Methods ##############################################################################
_assembleNextEntry: ->
entry = @_data.popNextToAssemble()
return unless entry?
for extensions in @_extensions
continue unless extension.canAssemble entry
extension.assemble entry
return
_buildNextEntry: ->
entry = @_data.popNextToBuild()
return unless entry?
for extensions in @_extensions
continue unless extension.canBuild entry
extension.build entry
return
_loadSchema: ->
command = @_lexer.next()
if command.name isnt 'schema' then throw new Error "#{@_fileName} must start with the \"schema\" command"
schema = command.argText
if schema is '1'
@_use new require './parser_extensions/current_pe_v1'
@_use new require './parser_extensions/item_pe_v1'
@_use new require './parser_extensions/mod_pe_v1'
@_use new require './parser_extensions/mod_version_pe_v1'
@_use new require './parser_extensions/recipe_pe_v1'
else
throw new Error "unknown schema version: #{schema}"
_processNextCommand: ->
command = @_lexer.next()
return unless command?
for extension in @_extensions
continue unless extension.canExecute command
extension.execute command
return
@_data.addError command, "unknown command: #{command.name}"
_use: (parserExtension)->
parserExtension.state = @_state
@_extensions.push parserExtension
_validateNextEntry: ->
entry = @_data.popNextToValidate()
return unless entry?
for extensions in @_extensions
continue unless extension.canValidate entry
extension.validate entry
return
@@ -0,0 +1,33 @@
#
# Crafting Guide - parser_command.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ParserCommand
constructor: (options={})->
if not options.name? then throw 'options.name is required'
if not options.fileName? then throw new 'options.fileName is required'
if not options.lineNumber? then throw 'options.lineNumber is required'
{@name, @fileName, @lineNumber, @argText, @args, @hereDoc} = options
# Property Methods #############################################################################
getLocation: ->
return "#{@fileName}:#{@lineNumber}"
Object.defineProperties @prototype,
location: { get:@::getLocation }
# Object Overrides #############################################################################
toString: ->
result = "#{@fileName}:#{@lineNumber} #{@name}: #{@argText}"
if @hereDoc?
result += " <<- END\n#{@hereDoc}\nEND"
return result
@@ -0,0 +1,134 @@
#
# Crafting Guide - parser_data.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ParserData
constructor: ->
@clear()
@_toAssemble = []
@_toBuild = []
@_toValidate = []
# Public Methods ###############################################################################
addError: (command, message)->
if not command? then throw new Error 'command is required'
message ?= 'is not valid'
@_errors.push command:command, message:message
create: (command, type, id=null)->
id ?= _.uniqueId "#{type}-"
@clearCurrent type
typeData = @_findOrCreateType type
itemData = @_current = typeData['current'] = typeData[id] = id:id, command:command, type:type
@_toAssemble.push itemData
@_toBuild.push itemData
@_toValidate.push itemData
return itemData
clear: ->
@_current = null
@_data =
mod: {}
modVersion: {}
tutorial: {}
itemGroup: {}
item: {}
multiblock: {}
recipe: {}
@_errors = []
clearCurrent: (type)->
switch type
when 'mod'
delete @_data.mod.current
delete @_data.modVersion.current
delete @_data.tutorial.current
delete @_data.itemGroup.current
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'modVersion'
delete @_data.modVersion.current
delete @_data.tutorial.current
delete @_data.itemGroup.current
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'tutorial'
delete @_data.tutorial.current
when 'itemGroup'
delete @_data.itemGroup.current
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'item'
delete @_data.item.current
delete @_data.multiblock.current
delete @_data.recipe.current
when 'multiblock'
delete @_data.multiblock.current
when 'recipe'
delete @_data.recipe.current
get: (type, id)->
typeData = @_findOrCreateType type
itemData = typeData[id] or null
return itemData
getCurrent: (type)->
return @_current unless type?
typeData = @_findOrCreateType type
current = typeData['current'] or null
return current
popNextToAssemble: ->
return @_toAssemble.pop() or null
popNextToBuild: ->
return @_toBuild.pop() or null
popNextToValidate: ->
return @_toValidate.pop() or null
# Property Methods #############################################################################
getErrors: ->
return @_errors[..]
getIsAssembled: ->
return @_toAssemble.length > 0
getIsBuilt: ->
return @_toBuild.length > 0
getIsValidated: ->
return @_toValidate.length > 0
Object.defineProperties @prototype,
errors: { get:@::getErrors }
isAssembled: { get:@::getIsAssembled }
isBuilt: { get:@::getIsBuilt }
isValidated: { get:@::getIsValidated }
# Private Methods ##############################################################################
_findOrCreateType: (type)->
typeData = @_data[type]
if not typeData?
typeData = @_data[type] = {}
typeData.type = type
return typeData
@@ -0,0 +1,55 @@
#
# Crafting Guide - parser_data.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserCommand = require './parser_command'
ParserData = require './parser_data'
########################################################################################################################
command = state = null
########################################################################################################################
describe 'parser_data.coffee', ->
beforeEach ->
state = new ParserData
command = new ParserCommand name:'alpha', fileName:'file.cg', lineNumber:1
describe 'create', ->
it 'returns an object with the given id', ->
alpha = state.create command, 'alpha', 0
alpha.id.should.equal 0
alpha.command.location.should.equal 'file.cg:1'
it 'returns an object with a newly created id when none is given', ->
alpha = state.create command, 'alpha'
alpha.id.should.not.be.null
alpha.id.should.match /^alpha-[0-9]+$/
alpha.command.location.should.equal 'file.cg:1'
describe 'get', ->
it 'returns a previously created item', ->
alpha1 = state.create command, 'alpha', 0
alpha2 = state.get 'alpha', 0
alpha1.should.equal alpha2
it 'returns null when no such item exists', ->
alpha = state.create command, 'alpha', 0
expect(state.get 'alpha', 1).to.be.null
expect(state.get 'bravo', 0).to.be.null
describe 'getCurrent', ->
it 'returns the new item each time one is created', ->
alpha1 = state.create command, 'alpha', 0
state.getCurrent('alpha').id.should.equal 0
alpha2 = state.create command, 'alpha', 1
state.getCurrent('alpha').id.should.equal 1
@@ -0,0 +1,165 @@
#
# Crafting Guide - parser_extension.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class ParserExtension
constructor: (data, type, commandFunctions)->
@data = data
@_commandFunctions = commandFunctions or {}
@_type = type or null
# Public Methods ###############################################################################
canAssemble: (entry)->
return entry.type is @_type
canBuild: (entry)->
return entry.type is @_type
canExecute: (command)->
_.isFunction @_commandFunctions[command.name]
canValidate: (entry)->
return entry.type is @_type
execute: (command)->
if not @_data? then throw new Error '@data must be defined before executing commands'
func = @_commandFunctions[command.name]
if not func then throw new Error "#{@constructor.name} cannot handle command: #{command.name}"
func.call this, command
return this
# Error Checking Helpers #######################################################################
alreadyExists: (command, type, id)->
obj = @data.get(type, id)
if obj?
@data.addError command, "a #{type} called #{id} has already been declared"
return true
return false
duplicateField: (command, obj, field)->
if obj[field]?
@data.addError command, "duplicate #{field}"
return true
return false
incompatibleField: (command, obj, field, otherCommandName)->
otherCommandName ?= field
if obj[field]?
@data.addError "#{command.name} cannot be used with #{otherCommandName}"
return true
return false
missingCurrent: (command, type)->
obj = @data.getCurrent type
if not obj?
@data.addError command, "you must declare a #{type} before using #{command.name}"
return true
return false
missingArgs: (command)->
if not command.args or command.args.length is 0
@data.addError command, "#{command.name} requires at least one item"
return true
return false
missingArgText: (command)->
if command.argText.length is 0
@data.addError command, "#{command.name} cannot be empty"
return true
return false
invalidVerb: (command, verb)->
if not verb in ['mod', 'item']
@data.addError command, "#{command.name} only applies to either `mod` or `item`"
return true
return false
tooFewArguments: (command, minimum)->
if command.args.length < minimum
@data.addError command, "#{command.name} requires at least #{minimum} arguments"
return true
return false
# Parsing Helpers ##############################################################################
parseBoolean: (command, text)->
return null unless text?
switch text.toLowerCase()
when "yes" then return true
when "no" then return false
@data.addError command, "#{command.name} requires \"yes\" or \"no\""
return null
parseInt: (command, text)->
return null unless text?
value = Number.parseInt text
if isNaN value
@addError command, "#{command.name} requires a number"
return null
return value
# Property Methods #############################################################################
getData: ->
return @_data
setData: (data)->
if not data? then throw new Error 'data cannot be undefined'
@_data = data
Object.defineProperties @prototype,
data: { get:@::getData, set:@::setData }
# Overridable Methods ##########################################################################
assmeble: (entry)->
throw new Error "#{@constructor.name} must override the `assemble` method"
build: (entry)->
throw new Error "#{@constructor.name} must override the `build` method"
validate: (entry)->
throw new Error "#{@constructor.name} must override the `validate` method"
# Private Methods ##############################################################################
_findBuildMethod: (entry)->
methodName = "_build_#{entry.type}"
method = this[methodName]
return null unless _.isFunction method
return method
_findCommandMethod: (command)->
methodName = "_command_#{command.name}"
method = this[methodName]
return null unless _.isFunction method
return method
_findValidateMethod: (command)->
methodName = "_validate_#{command.name}"
method = this[methodName]
return null unless _.isFunction method
return method
@@ -0,0 +1,50 @@
#
# Crafting Guide - current_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class CurrentParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, null,
description: @_command_description
name: @_command_name
officialUrl: @_command_officialUrl
video: @_command_video
# Command Methods ##############################################################################
_command_description: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'description'
return if @missingArgText command
current.description = command.argText
_command_name: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'name'
return if @missingArgText command
current.name = command.argText
_command_officialUrl: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'officialUrl'
return if @missingArgText command
current.officialUrl = command.argText
_command_video: (command)->
current = @data.getCurrent()
return if @missingArgs command
return if @tooFewArguments command, 2
current.videos ?= []
current.videos.push youTubeId:command.args[0], caption:command.args[1..].join(', ')
@@ -0,0 +1,59 @@
#
# Crafting Guide - current_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CurrentParserExtensionV1 = require './current_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = state = null
########################################################################################################################
describe 'current_pe_v1.coffee', ->
beforeEach ->
state = new ParserData
parser = new CurrentParserExtensionV1 state
state.create {}, 'alpha'
describe 'description', ->
it 'assigns to the current object', ->
parser.execute name:'description', argText:'bravo'
state.getCurrent().description.should.equal 'bravo'
state.errors.should.eql []
describe 'name', ->
it 'assigns to the current object', ->
parser.execute name:'name', argText:'bravo'
state.getCurrent().name.should.equal 'bravo'
state.errors.should.eql []
describe 'officialUrl', ->
it 'assigns to the current object', ->
parser.execute name:'officialUrl', argText:'bravo'
state.getCurrent().officialUrl.should.equal 'bravo'
state.errors.should.eql []
describe 'video', ->
it 'assigns to the current object', ->
parser.execute name:'video', args:['alpha', 'bravo']
videos = state.getCurrent().videos
videos[0].youTubeId.should.equal 'alpha'
videos[0].caption.should.equal 'bravo'
state.errors.should.eql []
it 'can assign multiple videos', ->
parser.execute name:'video', args:['alpha', 'bravo']
parser.execute name:'video', args:['charlie', 'delta']
state.getCurrent().videos.length.should.equal 2
state.errors.should.eql []
@@ -0,0 +1,67 @@
#
# Crafting Guide - item_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class ItemParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'item',
gatherable: @_command_gatherable
item: @_command_item
update: @_command_update
# ParserExtensions Overrides ###################################################################
assmeble: (entry)->
throw new Error "#{@constructor.name} must override the `assemble` method"
build: (entry)->
entry.instance = new
throw new Error "#{@constructor.name} must override the `build` method"
validate: (entry)-> # do nothing
# Command Methods ##############################################################################
_command_gatherable: (command)->
return if @missingArgText command
return if @missingCurrent command, 'item'
item = @data.getCurrent 'item'
return if @duplicateField command, item, 'gatherable'
gatherable = @parseBoolean command, command.argText
return unless gatherable?
item.gatherable = gatherable
_command_item: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
return if @alreadyExists command, 'item', command.argText
item = @data.create command, 'item', command.argText
item.name = command.argText
item.isUpdate = false
group = @data.getCurrent 'itemGroup'
if group? then item.group = group
item.modVersion = @data.getCurrent 'modVersion'
_command_update: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
return if @alreadyExists command, 'item', command.argText
item = @data.create command, 'item', command.argText
item.name = command.argText
item.isUpdate = true
item.modVersion = @data.getCurrent 'modVersion'
@@ -0,0 +1,54 @@
#
# Crafting Guide - item_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ItemParserExtensionV1 = require './item_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'item_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new ItemParserExtensionV1 data
data.create {}, 'modVersion', 0
describe 'gatherable', ->
it 'assigns to the current item', ->
data.create {}, 'item', 1
parser.execute name:'gatherable', argText:'yes'
data.getCurrent('item').gatherable.should.be.true
data.errors.should.eql []
describe 'item', ->
it 'creates a new item', ->
data.create {}, 'itemGroup', 2
parser.execute name:'item', argText:'alpha'
item = data.getCurrent 'item'
item.id.should.equal 'alpha'
item.isUpdate.should.equal false
item.name.should.equal 'alpha'
item.modVersion.id.should.equal 0
item.group.id.should.equal 2
data.getErrors().should.eql []
describe 'update', ->
it 'creates an item update', ->
parser.execute name:'update', argText:'alpha'
item = data.getCurrent 'item'
item.isUpdate.should.equal true
data.getErrors().should.eql []
@@ -0,0 +1,62 @@
#
# Crafting Guide - mod_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class ModParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'mod',
author: @_command_author
documentationUrl: @_command_documentationUrl
downloadUrl: @_command_downloadUrl
homePageUrl: @_command_homePageUrl
mod: @_command_mod
# Command Methods ##############################################################################
_command_author: (command)->
return if @missingCurrent command, 'mod'
mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'author'
return if @missingArgText command
mod.author = command.argText
_command_documentationUrl: (command)->
current = @data.getCurrent()
return if @duplicateField command, current, 'documentationUrl'
return if @missingArgText command
current.documentationUrl = command.argText
_command_downloadUrl: (command)->
return if @missingCurrent command, 'mod'
mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'downloadUrl'
return if @missingArgText command
mod.downloadUrl = command.argText
_command_homePageUrl: (command)->
return if @missingCurrent command, 'mod'
mod = @data.getCurrent 'mod'
return if @duplicateField command, mod, 'homePageUrl'
return if @missingArgText command
mod.homePageUrl = command.argText
_command_mod: (command)->
return if @missingArgText command
return if @alreadyExists command, 'mod', command.argText
@data.create command, 'mod', command.argText
@@ -0,0 +1,58 @@
#
# Crafting Guide - mod_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModParserExtensionV1 = require './mod_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'mod_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new ModParserExtensionV1 data
data.create {}, 'mod', 0
describe 'author', ->
it 'assigns to the current mod', ->
parser.execute name:'author', argText:'alpha'
data.getCurrent('mod').author.should.equal 'alpha'
data.errors.should.eql []
describe 'documentationUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'documentationUrl', argText:'alpha'
data.getCurrent('mod').documentationUrl.should.equal 'alpha'
data.errors.should.eql []
describe 'downloadUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'downloadUrl', argText:'alpha'
data.getCurrent('mod').downloadUrl.should.equal 'alpha'
data.errors.should.eql []
describe 'homePageUrl', ->
it 'assigns to the current mod', ->
parser.execute name:'homePageUrl', argText:'alpha'
data.getCurrent('mod').homePageUrl.should.equal 'alpha'
data.errors.should.eql []
describe 'mod', ->
it 'creates a new mod', ->
parser.execute name:'mod', argText:'alpha'
mod = data.getCurrent 'mod'
mod.id.should.equal 'alpha'
@@ -0,0 +1,33 @@
#
# Crafting Guide - mod_version_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class ModVersionParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'modVersion',
group: @_command_group
version: @_command_version
# Command Methods ##############################################################################
_command_group: (command)->
return if @missingArgText command
return if @missingCurrent command, 'modVersion'
group = @data.create command, 'itemGroup', command.argText
group.modVersion = @data.getCurrent 'modVersion'
_command_version: (command)->
return if @missingArgText command
return if @missingCurrent command, 'mod'
modVersion = @data.create command, 'modVersion', command.argText
modVersion.mod = @data.getCurrent 'mod'
@@ -0,0 +1,41 @@
#
# Crafting Guide - mod_version_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ModVersionParserExtensionV1 = require './mod_version_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'mod_version_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new ModVersionParserExtensionV1 data
data.create {}, 'mod', 0
describe 'group', ->
it 'creates a new item group', ->
data.create {}, 'modVersion', 1
parser.execute name:'group', argText:'alpha'
itemGroup = data.getCurrent('itemGroup')
itemGroup.id.should.equal 'alpha'
itemGroup.modVersion.id.should.equal 1
describe 'version', ->
it 'creates a new mod version', ->
parser.execute name:'version', argText:'alpha'
modVersion = data.getCurrent 'modVersion'
modVersion.id.should.equal 'alpha'
modVersion.mod.id.should.equal 0
@@ -0,0 +1,38 @@
#
# Crafting Guide - multiblock_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class MultiblockParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'multiblock',
layer: @_command_layer
multiblock: @_command_multiblock
# Command Methods ##############################################################################
_command_layer: (command)->
return if @missingCurrent command, 'multiblock'
return if @missingArgText command
multiblock = @data.getCurrent 'multiblock'
multiblock.layers ?= []
multiblock.layers.push command.argText
_command_multiblock: (command)->
return if @missingCurrent command, 'item'
item = @data.getCurrent 'item'
return if @duplicateField command, item, 'multiblock'
return if @incompatibleField command, item, 'recipe'
return if @incompatibleField command, item, 'gatherable'
multiblock = @data.create command, 'multiblock'
multiblock.item = item
@@ -0,0 +1,48 @@
#
# Crafting Guide - multiblock_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
MultiblockParserExtensionV1 = require './multiblock_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'multiblock_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new MultiblockParserExtensionV1 data
data.create {}, 'item', 1
describe 'layer', ->
it 'creates a new layer in the current multiblock', ->
data.create {}, 'multiblock', 2
parser.execute name:'layer', argText:'alpha'
multiblock = data.getCurrent 'multiblock'
multiblock.layers.length.should.equal 1
multiblock.layers[0].should.equal 'alpha'
it 'can create multiple layers', ->
data.create {}, 'multiblock', 2
parser.execute name:'layer', argText:'alpha'
parser.execute name:'layer', argText:'bravo'
multiblock = data.getCurrent 'multiblock'
multiblock.layers.should.eql ['alpha', 'bravo']
describe 'multiblock', ->
it 'creates a new multiblock', ->
parser.execute name:'multiblock'
multiblock = data.getCurrent 'multiblock'
multiblock.item.id.should.equal 1
@@ -0,0 +1,143 @@
#
# Crafting Guide - recipe_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class RecipeParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'recipe',
extras: @_command_extras
ignoreDuringCrafting: @_command_ignoreDuringCrafting
input: @_command_input
onlyIf: @_command_onlyIf
pattern: @_command_pattern
recipe: @_command_recipe
quantity: @_command_quantity
tools: @_command_tools
# Class Methods ################################################################################
@::PATTERN = /[0-9. ]+/
@::STACK = /^([0-9]+) +(.+)$/
# Error Checking Helpers #######################################################################
invalidPattern: (command)->
match = command.argText.match @PATTERN
if not match?
@data.addError command, "invalid pattern: \"#{command.argText}\""
return true
return false
# Command Methods ##############################################################################
_command_extras: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'extras', recipe
recipe.extras = command.args
_command_ignoreDuringCrafting: (command)->
return if @missingArgText command
return if @missingCurrent command, 'recipe'
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, recipe, 'ignoreDuringCrafting'
ignoreDuringCrafting = @parseBoolean command, command.argText
return unless ignoreDuringCrafting?
recipe.ignoreDuringCrafting = ignoreDuringCrafting
_command_input: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @data.getCurrent 'recipe'
multiblock = @data.getCurrent 'multiblock'
target = if multiblock? then multiblock else recipe
return if @duplicateField command, 'input', target
target.input = (@_parseStack(arg) for arg in command.args)
_command_onlyIf: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, recipe, 'condition'
words = command.argText.split ' '
inverted = false
if words[0] is 'not'
inverted = true
words.shift()
verb = words[0]
noun = words[1..].join ' '
return if @invalidVerb command, verb
recipe.condition = verb:verb, noun:noun, inverted:inverted
_command_pattern: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
return if @invalidPattern command
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'pattern', recipe
recipe.pattern = command.argText
_command_quantity: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgText command
value = @parseInt command, command.argText
return unless value?
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'quantity', recipe
recipe.quantity = value
_command_recipe: (command)->
return if @missingCurrent command, 'item'
recipe = @data.create command, 'recipe'
recipe.item = @data.getCurrent 'item'
_command_tools: (command)->
return if @missingCurrent command, 'recipe'
return if @missingArgs command
recipe = @data.getCurrent 'recipe'
return if @duplicateField command, 'tools', recipe
recipe.tools = (@_parseStack(arg) for arg in command.args)
# Custom Parsers ###############################################################################
_parseStack: (text)->
match = text.match @STACK
return name:text, quantity:1 unless match?
quantity = parseFloat match[1]
if isNaN quantity then quantity = 1
name = match[2]
return name:name, quantity:quantity
@@ -0,0 +1,125 @@
#
# Crafting Guide - recipe_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
RecipeParserExtensionV1 = require './recipe_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'recipe_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new RecipeParserExtensionV1 data
data.create {}, 'item', 0
describe 'extras', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'extras', args:['bravo']
recipe = data.getCurrent 'recipe'
recipe.extras.should.eql ['bravo']
data.errors.should.eql []
describe 'ignoreDuringCrafting', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'ignoreDuringCrafting', argText:'yes'
recipe = data.getCurrent 'recipe'
recipe.ignoreDuringCrafting.should.equal true
data.errors.should.eql []
describe 'input', ->
it 'correctly reads a single item', ->
data.create {}, 'recipe'
parser.execute name:'input', args:['bravo']
recipe = data.getCurrent 'recipe'
recipe.input.should.eql [name:'bravo', quantity:1]
data.errors.should.eql []
it 'correctly reads multiple items', ->
data.create {}, 'recipe'
parser.execute name:'input', args:['bravo', 'charlie']
recipe = data.getCurrent 'recipe'
recipe.input.should.eql [{name:'bravo', quantity:1}, {name:'charlie', quantity:1}]
data.errors.should.eql []
it 'correctly reads a stack', ->
data.create {}, 'recipe'
parser.execute name:'input', args:['10 bravo']
recipe = data.getCurrent 'recipe'
recipe.input.should.eql [name:'bravo', quantity:10]
data.errors.should.eql []
describe 'onlyIf', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'onlyIf', argText:'not mod Alpha'
recipe = data.getCurrent 'recipe'
recipe.condition.should.eql verb:'mod', noun:'Alpha', inverted:true
data.errors.should.eql []
describe 'pattern', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'pattern', argText:'00. 00. ...'
recipe = data.getCurrent 'recipe'
recipe.pattern.should.equal '00. 00. ...'
data.errors.should.eql []
it 'rejects an invalid pattern', ->
data.create {}, 'recipe'
parser.execute name:'pattern', argText:'alpha'
recipe = data.getCurrent 'recipe'
expect(recipe.pattern).to.be.undefined
(e.message for e in data.errors).should.eql ['invalid pattern: "alpha"']
describe 'quantity', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'quantity', argText:'42'
recipe = data.getCurrent 'recipe'
recipe.quantity.should.equal 42
data.errors.should.eql []
describe 'recipe', ->
it 'creates a new recipe', ->
parser.execute name:'recipe'
recipe = data.getCurrent 'recipe'
recipe.item.id.should.equal 0
data.errors.should.eql []
describe 'tools', ->
it 'assigns to the current recipe', ->
data.create {}, 'recipe'
parser.execute name:'tools', args:['bravo']
recipe = data.getCurrent 'recipe'
recipe.tools.should.eql [name:'bravo', quantity:1]
data.errors.should.eql []
@@ -0,0 +1,51 @@
#
# Crafting Guide - tutorial_pe_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
ParserExtension = require '../parser_extension'
########################################################################################################################
module.exports = class TutorialParserExtensionV1 extends ParserExtension
constructor: (data)->
super data, 'tutorial',
content: @_command_content
section: @_command_section
title: @_command_title
tutorial: @_command_tutorial
# Command Methods ##############################################################################
_command_content: (command)->
return if @missingCurrent command, 'tutorial'
return if @missingArgText command
section = @data.getCurrent 'section'
section.content = command.argText
_command_section: (command)->
return if @missingCurrent command, 'tutorial'
section = @data.create command, 'section'
section.tutorial = @data.getCurrent 'tutorial'
_command_title: (command)->
return if @missingCurrent command, 'section'
return if @missingArgText command
section = @data.getCurrent 'section'
return if @duplicateField command, section, 'title'
section.title = command.argText
_command_tutorial: (command)->
return if @missingCurrent command, 'modVersion'
return if @missingArgText command
tutorial = @data.create command, 'tutorial', command.argText
tutorial.name = command.argText
tutorial.modVersion = @data.getCurrent 'modVersion'
@@ -0,0 +1,63 @@
#
# Crafting Guide - tutorial_pe_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
TutorialParserExtensionV1 = require './tutorial_pe_v1'
ParserData = require '../parser_data'
########################################################################################################################
parser = data = null
########################################################################################################################
describe 'tutorial_pe_v1.coffee', ->
beforeEach ->
data = new ParserData
parser = new TutorialParserExtensionV1 data
data.create {}, 'modVersion', 1
describe 'content', ->
it 'assigns content to the current section', ->
data.create {}, 'tutorial', 2
data.create {}, 'section', 3
parser.execute name:'content', argText:'alpha'
data.errors.should.eql []
section = data.getCurrent 'section'
section.content.should.equal 'alpha'
describe 'section', ->
it 'creates a section in the current tutorial', ->
data.create {}, 'tutorial', 2
parser.execute name:'section'
data.errors.should.eql []
section = data.getCurrent 'section'
section.tutorial.id.should.equal 2
describe 'title', ->
it 'assigns a title to the current tutorial', ->
data.create {}, 'tutorial', 2
data.create {}, 'section', 3
parser.execute name:'title', argText:'alpha'
data.errors.should.eql []
section = data.getCurrent 'section'
section.title.should.equal 'alpha'
describe 'tutorial', ->
it 'creates a new tutorial', ->
parser.execute name:'tutorial', argText:'alpha'
data.errors.should.eql []
tutorial = data.getCurrent 'tutorial'
tutorial.name.should.equal 'alpha'
@@ -0,0 +1,138 @@
#
# Crafting Guide - command_parser_version_base.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
module.exports = class CommandParserVersionBase
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.showAllErrors ?= false
@_model = options.model
@_showAllErrors = options.showAllErrors
@errors = []
# Class Methods ################################################################################
@COMMAND = /\ *([^:]*):?(.*)/
@COMMENT = /([^\\]?)#.*/
@simplify: (text)->
text = text.trim()
text = text.replace /\ */g , ' '
text = text.replace /\n/g, ';'
text = text.replace /; */g, ';'
text = text.replace /;;*/g, ';'
text = text.replace /: /g, ':'
return text
# Public Methods ###############################################################################
parse: (text)->
@_rawData = {}
@_lineNumber = 1
@errors = []
@_lines = text.split '\n'
@_lineNumber = 0
while @_lineNumber < @_lines.length
@_lineNumber += 1
commands = @_parseLine @_lines[@_lineNumber - 1]
for command in commands
@_handleErrors @_execute, command
@_handleErrors @_buildModel, @_rawData, @_model
return @_model
unparse: ->
builder = new StringBuilder context:@_model
@_unparseModel builder, @_model
return builder.toString()
# Subclass Methods #############################################################################
_buildModel: (rawData, model)->
throw new Error 'Subclasses must override this method'
_unparseModel: (builder, model)->
throw new Error 'Subclasses must override this method'
_command_schema: -> # do nothing
# Private Methods ##############################################################################
_execute: (command)->
method = this["_command_#{command.name}"]
if not method? then throw new Error "Unknown command: #{command.name}"
@_handleErrors method, command.args
_parseLine: (line)->
line = line.replace CommandParserVersionBase.COMMENT, '$1'
line = line.trim()
return [] if line.length is 0
[line, hereDoc] = @_parseHereDoc line
lineParts = (part.trim() for part in line.split(';'))
commands = []
for linePart in lineParts
continue if linePart.length is 0
match = CommandParserVersionBase.COMMAND.exec linePart
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
args = []
args = (s for s in match[2].split(',') when s.length > 0) if match[2]?
args = (s.trim() for s in args)
args = (s for s in args when s.length > 0)
args.push hereDoc if hereDoc?
commands.push name:match[1], args:args
return commands
_handleErrors: (callback, args...)->
if args.length is 1 and _.isArray(args[0]) then args = args[0]
try
callback.apply this, args
catch e
e.message = "line #{@_lineNumber}: #{e.message}"
if not @_showAllErrors then throw e
@errors.push e
logger.error -> e.message
_parseHereDoc: (line)->
hereDocIndex = line.indexOf '<<-'
return [line, null] unless hereDocIndex isnt -1
hereDocStopText = line[hereDocIndex+3...line.length]
line = line[0...hereDocIndex]
hereDocLines = []
while true
@_lineNumber += 1
break if @_lineNumber >= @_lines.length
nextLine = @_lines[@_lineNumber-1]
break if nextLine.trim() is hereDocStopText
hereDocLines.push nextLine
shortestIndent = Number.MAX_VALUE
for hereDocLine in hereDocLines
continue if hereDocLine.trim().length is 0
shortestIndent = Math.min hereDocLine.match(/( *).*/)[1].length, shortestIndent
for i in [0...hereDocLines.length]
hereDocLines[i] = hereDocLines[i][shortestIndent..]
return [line, null] unless hereDocLines.length > 0
return [line, hereDocLines.join('\n')]
@@ -0,0 +1,48 @@
#
# Crafting Guide - command_parser_version_base.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
########################################################################################################################
parser = null
########################################################################################################################
describe 'command_parser_version_base.coffee', ->
beforeEach -> parser = new CommandParserVersionBase model:{}
describe '_parseHereDoc', ->
it 'returns null for non-heredoc lines', ->
result = parser._parseHereDoc 'foobar: baz'
expect(result[1]).to.be.null
it 'identifies the right text for a real heredoc', ->
parser._lines = ['command: <<-END', 'alpha', 'bravo', 'charlie', 'END', 'command1: arg2']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result[0].should.equal 'command: '
result[1].should.equal 'alpha\nbravo\ncharlie'
it 'identifies an empty heredoc', ->
parser._lines = ['command: <<-END', 'END']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result[0].should.equal 'command: '
expect(result[1]).to.be.null
it 'trims smallest leading whitespace', ->
parser._lines = ['command: <<-END', ' alpha', ' bravo', '', ' charlie', 'END', 'command1: arg2']
parser._lineNumber = 1
result = parser._parseHereDoc parser._lines[0]
result[0].should.equal 'command: '
result[1].should.equal 'alpha\n bravo\n\ncharlie'
@@ -0,0 +1,19 @@
#
# Crafting Guide - item_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
ItemParserV1 = require './item_parser_v1'
########################################################################################################################
module.exports = class ItemParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ItemParserV1 options
@@ -0,0 +1,72 @@
#
# Crafting Guide - item_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
########################################################################################################################
module.exports = class ItemParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildItem rawData, model
_unparseModel: (builder, model)->
builder.line 'schema: ', 1
builder.line()
@_unparseItem builder, model
# Command Methods ##############################################################################
_command_description: (textParts...)->
if not @_rawData.description?
@_rawData.description = ''
else
@_rawData.description += '\n'
@_rawData.description += textParts.join ', '
_command_officialUrl: (officialUrl)->
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
if not officialUrl? or (officialUrl.length is 0) then throw new Error 'officialUrl cannot be empty'
@_rawData.officialUrl = officialUrl
_command_video: (youTubeId, nameParts...)->
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
name = nameParts.join ', '
if not name?.length then throw new Error 'video declaration requires a name'
@_rawData.videos ?= []
@_rawData.videos.push youTubeId:youTubeId, name:name
# Object Building Methods ######################################################################
_buildItem: (rawData, model)->
model.description = rawData.description if rawData.description?
model.officialUrl = rawData.officialUrl if rawData.officialUrl?
model.videos = rawData.videos if rawData.videos?
# Un-parsing Methods ###########################################################################
_unparseItem: (builder, model)->
if model.officialUrl?
builder.line 'officialUrl: ', model.officialUrl
builder.line()
if model.description?
if model.description.indexOf('\n') isnt -1
builder.line 'description: <<-END'
builder.line model.description
builder.line 'END'
else
builder.line 'description: ', model.description
builder.line()
for video in model.videos
builder.line 'video: ', video.youTubeId, ', ', video.name
builder.line()
@@ -0,0 +1,99 @@
#
# Crafting Guide - item_parser_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
Item = require '../game/item'
ItemParserV1 = require './item_parser_v1'
########################################################################################################################
baseText = item = parser = null
########################################################################################################################
describe 'item_parser_v1.coffee', ->
beforeEach ->
item = new Item name:'alpha'
parser = new ItemParserV1 model:item
describe 'officialUrl', ->
it 'may be omitted', ->
parser.parse 'schema: 1\nvideo: youtubeid, Video Alpha\ndescription: Bravo, Charlie'
expect(item.officialUrl).to.be.null
it 'is assigned properly when given', ->
parser.parse 'schema: 1\nofficialUrl: http://testurl.com'
item.officialUrl.should.equal 'http://testurl.com'
it 'does not allow duplicate declarations', ->
func = -> parser.parse 'schema: 1\nofficialUrl: http://testurl.com\nofficialUrl: http://testurl2.com'
expect(func).to.throw Error, 'duplicate'
it 'does not allow an empty value if given', ->
func = -> parser.parse 'schema: 1\nofficialUrl:'
expect(func).to.throw Error, 'empty'
describe 'description', ->
it 'may be omitted', ->
parser.parse 'schema: 1\nvideo: youTubeId, Video Alpha\nofficialUrl: http://testurl.com'
expect(item.description).to.be.null
it 'is assigned properly when given', ->
parser.parse 'schema: 1\ndescription: Alpha Bravo Charlie'
item.description.should.equal 'Alpha Bravo Charlie'
it 'can be a heredoc', ->
parser.parse 'schema: 1\ndescription: <<-END\nAlpha\nBravo\nCharlie\nEND'
item.description.should.equal 'Alpha\nBravo\nCharlie'
it 'concatenates multiple declarations', ->
parser.parse 'schema: 1\ndescription: Alpha\ndescription: Bravo'
item.description.should.equal 'Alpha\nBravo'
describe 'video', ->
it 'may be omitted', ->
parser.parse 'schema: 1\nofficialUrl: http://testurl.com\ndescription: Alpha Bravo Charlie'
item.videos.should.eql []
it 'is assigned properly when given', ->
parser.parse 'schema: 1\nvideo: youtubeid, Alpha Bravo'
item.videos[0].should.eql youTubeId:'youtubeid', name:'Alpha Bravo'
item.videos.length.should.equal 1
it 'may be included multiple times', ->
parser.parse 'schema: 1\nvideo: youtubeid1, Alpha\nvideo: youtubeid2, Bravo'
item.videos[0].should.eql youTubeId:'youtubeid1', name:'Alpha'
item.videos[1].should.eql youTubeId:'youtubeid2', name:'Bravo'
item.videos.length.should.equal 2
it 'requires a YouTubeId and name', ->
func = -> parser.parse 'schema: 1\nvideo: alpha'
expect(func).to.throw Error, 'requires a name'
describe 'unparsing', ->
it 'can round-trip a fully described item', ->
text = """
schema: 1
officialUrl: http://testurl.com
description: <<-END
Alpha
Bravo
END
video: youtubeid1, Alpha Bravo
video: youtubeid2, Charlie Delta
"""
parser.parse text
parser.unparse().should.equal text
@@ -0,0 +1,19 @@
#
# Crafting Guide - mod_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
ModParserV1 = require './mod_parser_v1'
########################################################################################################################
module.exports = class ModParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ModParserV1 options
@@ -0,0 +1,94 @@
#
# Crafting Guide - mod_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
Mod = require '../game/mod'
ModVersion = require '../game/mod_version'
Tutorial = require '../site/tutorial'
########################################################################################################################
module.exports = class ModParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildMod rawData, model
_unparseModel: (builder, model)->
@_unparseMod builder, model
# Command Methods ##############################################################################
_command_author: (authorParts...)->
if @_rawData.author? then throw new Error 'duplicate declaration of "author"'
author = authorParts.join ', '
if author.length is 0 then throw new Error '"author" cannot be empty, but may be omitted'
@_rawData.author = author
_command_description: (descriptionParts...)->
if @_rawData.description? then throw new Error 'duplicate declaration of "description"'
description = descriptionParts.join ', '
if description.length is 0 then throw new Error '"description" cannot be empty, but may be omitted'
@_rawData.description = description
_command_documentationUrl: (documentationUrl)->
documentationUrl ?= ''
if @_rawData.documentationUrl? then throw new Error 'duplicate declaration of "documentationUrl"'
if documentationUrl.length is 0 then throw new Error 'documentationUrl cannot be empty (omit it instead)'
@_rawData.documentationUrl = documentationUrl
_command_downloadUrl: (downloadUrl)->
if @_rawData.downloadUrl? then throw new Error 'duplicate declaration of "downloadUrl"'
if downloadUrl.length is 0 then throw new Error 'downloadUrl cannot be empty (omit it instead)'
@_rawData.downloadUrl = downloadUrl
_command_homePageUrl: (homePageUrl='')->
if @_rawData.homePageUrl? then throw new Error 'duplicate declaration of "homePageUrl"'
if homePageUrl.length is 0 then throw new Error 'homePageUrl cannot be empty'
@_rawData.homePageUrl = homePageUrl
_command_name: (name)->
if @_rawData.name? then throw new Error 'duplicate declaration of "name"'
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.name = name
_command_tutorial: (nameParts...)->
name = nameParts.join(', ').trim()
if name.length is 0 then throw new Error '"name" cannot be empty'
@_rawData.tutorialNames ?= []
@_rawData.tutorialNames.push name
_command_version: (version='')->
if version.length is 0 then throw new Error 'version cannot be empty'
@_rawData.versions ?= []
@_rawData.versions.push version
# Object Building Methods ######################################################################
_buildMod: (rawData, model)->
if not rawData.name? then throw new Error 'the "name" declaration is required'
if not rawData.homePageUrl? then throw new Error 'the "homePageUrl" declaration is required'
if not rawData.versions? then throw new Error 'at least one "version" declaration is required'
model.author = rawData.author if rawData.author?
model.description = rawData.description if rawData.description?
model.documentationUrl = rawData.documentationUrl if rawData.documentationUrl?
model.downloadUrl = rawData.downloadUrl if rawData.downloadUrl?
model.name = rawData.name
model.homePageUrl = rawData.homePageUrl
if rawData.tutorialNames?
for tutorialName in rawData.tutorialNames
model.addTutorial new Tutorial name:tutorialName
for version in rawData.versions
model.addModVersion new ModVersion modSlug:model.slug, version:version
@@ -0,0 +1,19 @@
#
# Crafting Guide - mod_version_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
ModVersionParserV1 = require './mod_version_parser_v1'
########################################################################################################################
module.exports = class ModVersionParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new ModVersionParserV1 options
@@ -0,0 +1,380 @@
#
# Crafting Guide - mod_version_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
Item = require '../game/item'
ItemSlug = require '../game/item_slug'
ModVersion = require '../game/mod_version'
Multiblock = require '../game/multiblock'
Recipe = require '../game/recipe'
Stack = require '../game/simple_stack'
{StringBuilder} = require 'crafting-guide-common'
########################################################################################################################
module.exports = class ModVersionParserV1 extends CommandParserVersionBase
# Class Methods ################################################################################
@INTEGER = /[0-9]+/
@PATTERN = /^[0-9.]{3} ?[0-9.]{3} ?[0-9.]{3}$/
@STACK = /^([0-9]+) +(.*)$/
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildModVersion rawData, model
_unparseModel: (builder, model)->
@_unparseModVersion builder, model
# Command Methods ##############################################################################
_command_extras: (extraTerms...)->
if not @_recipeData? then throw new Error 'cannot declare "extras" before "recipe"'
if @_recipeData.output.length isnt 1 then throw new Error 'duplicate declaration of "extras"'
for term in extraTerms
@_recipeData.output.push @_parseStack term
_command_gatherable: (gatherable)->
if not @_itemData? then throw new Error 'cannot declare "gatherable" before "item"'
if @_itemData.gatherable? then throw new Error 'duplicate declaration of "gatherable"'
if not (gatherable in ['yes', 'no']) then throw new Error 'gatherable must be either "yes" or "no"'
@_itemData.gatherable = (gatherable is 'yes')
_command_group: (group)->
if group.length is 0 then throw new Error 'a group name cannot be empty'
@_rawData.group = group
_command_item: (name='')->
if not name.length > 0 then throw new Error 'the item name cannot be empty'
@_itemData = name:name, line:@_lineNumber, group:@_rawData.group, type:'new'
@_rawData.items ?= {}
@_rawData.items[name] = @_itemData
@_recipeData = null
_command_ignoreDuringCrafting: (value)->
if not @_recipeData? then throw new Error 'cannot declare "ignoreDuringCrafting" before "recipe"'
if @_recipeData.ignoreDuringCrafting? then throw new Error 'duplicate declaration of "ignoreDuringCrafting"'
if not (value in ['yes', 'no']) then throw new Error 'ignoreDuringCrafting must be either "yes" or "no"'
@_recipeData.ignoreDuringCrafting = (value is 'yes')
_command_input: (stackDescriptions...)->
activeData = if @_recipeData? then @_recipeData else @_multiblockData
if not activeData? then throw new Error 'cannot declare "input" before "recipe" or "multiblock"'
if activeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"'
for stackDescription in stackDescriptions
activeData.input.push @_parseStack stackDescription
_command_layer: (layerText)->
if not @_multiblockData? then throw new Error 'cannot declare "layer" before "multiblock"'
if not layerText? then throw new Error 'cannot have an empty layer'
if layerText.length is 0 then throw new Error 'cannot have an empty layer'
@_multiblockData.layers.push layerText
_command_multiblock: ->
if not @_itemData? then throw new Error 'cannot declare "multiblock" before "item"'
if @_itemData.multiblockData? then throw new Error 'duplicate declaration of "multiblock"'
@_recipeData = null
@_multiblockData = input:[], layers:[], line:@_lineNumber
@_itemData.multiblockData = @_multiblockData
_command_onlyIf: (condition)->
words = condition.split ' '
if words.length < 2 then throw new Error 'condition must include a verb followed by a noun'
inverted = false
if words[0] is 'not'
inverted = true
words.shift()
verb = words[0]
noun = words[1..].join ' '
if not (verb in ['item', 'mod']) then throw new Error "unknown verb: #{verb}"
@_recipeData.condition = verb:verb, noun:noun, inverted:inverted
_command_pattern: (pattern='')->
if not @_recipeData? then throw new Error 'cannot declare "pattern" before "recipe"'
if @_recipeData.pattern? then throw new Error 'duplicate declaration of "pattern"'
if not ModVersionParserV1.PATTERN.test pattern
throw new Error 'a pattern must have 9 digits using 0-9 for items and "." for an empty spot;
spaces are optional'
@_recipeData.pattern = pattern
_command_quantity: (quantity)->
if not @_recipeData? then throw new Error 'cannot declare "quantity" before "recipe"'
if @_recipeData.quantity? then throw new Error 'duplicate declaration of "quantity"'
if not ModVersionParserV1.INTEGER.test(quantity) then throw new Error 'quantity must be an integer'
@_recipeData.quantity = quantity
@_recipeData.output[0].quantity = parseInt quantity
_command_recipe: ->
if not @_itemData? then throw new Error 'cannot delcare "recipe" before "item"'
@_multiblockData = null
@_recipeData = line:@_lineNumber, input:[], output:[{quantity:1, name:@_itemData.name}], tools:[]
@_itemData.recipes ?= []
@_itemData.recipes.push @_recipeData
_command_tools: (toolNames...)->
if not @_recipeData? then throw new Error 'cannot declare "tools" before "recipe"'
if @_recipeData.tools.length isnt 0 then throw new Error 'duplicate declaration of "tools"'
for name in toolNames
if name.length is 0 then throw new Error 'tool names cannot be empty'
@_recipeData.tools.push name:name, quantity:1
_command_update: (name='')->
if not name.length > 0 then throw new 'the item name cannot be empty'
@_itemData = name:name, line:@_lineNumber, type:'update'
@_recipeData = null
@_rawData.items ?= {}
@_rawData.items[name] = @_itemData
# Parsing Helpers ##############################################################################
_parseStack: (stackText)->
match = ModVersionParserV1.STACK.exec stackText
if match?
return quantity:parseInt(match[1]), name:match[2]
else
return quantity:1, name:stackText
# Object Creation Methods ######################################################################
_buildModVersion: (modVersionData, modVersion)->
modVersionData.items ?= []
for itemName, itemData of modVersionData.items
@_handleErrors @_buildItem, modVersion, itemData
modVersion.sort()
return modVersion
_buildItem: (modVersion, itemData)->
@_lineNumber = itemData.line
itemData.gatherable ?= false
itemData.ignoreDuringCrafting ?= false
itemData.recipes ?= []
if itemData.type is 'new'
item = new Item
name: itemData.name,
ignoreDuringCrafting: itemData.ignoreDuringCrafting,
isGatherable: itemData.gatherable,
group: itemData.group
modVersion.addItem item
itemData.slug = item.slug
else
itemData.slug = ItemSlug.slugify itemData.name
modVersion.registerName itemData.slug, itemData.name
if itemData.multiblockData
item.multiblock = @_handleErrors @_buildMultiblock, modVersion, item, itemData.multiblockData
for recipeData in itemData.recipes
@_handleErrors @_buildRecipe, modVersion, itemData, recipeData
return item
_buildMultiblock: (modVersion, item, multiblockData)->
@_lineNumber = multiblockData.line
if multiblockData.layers.length is 0 then throw new Error '"multiblock" requires at least one "layer"'
if multiblockData.input.length is 0 then throw new Error '"multiblock" requires at least one "input"'
input = @_buildStackList modVersion, multiblockData.input
multiblock = new Multiblock input:input, layers:multiblockData.layers
return multiblock
_buildRecipe: (modVersion, itemData, recipeData)->
@_lineNumber = recipeData.line
if recipeData.input.length is 0 then throw new Error 'the "input" declaration is required'
if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required'
recipe = new Recipe
condition: recipeData.condition
ignoreDuringCrafting: recipeData.ignoreDuringCrafting
input: @_buildStackList modVersion, recipeData.input, recipeData.pattern
output: @_buildStackList modVersion, recipeData.output
pattern: recipeData.pattern
tools: @_buildStackList modVersion, recipeData.tools
modVersion.addRecipe recipe
return recipe
_buildStackList: (modVersion, data, pattern=null)->
createSlug = (name)=>
item = @_rawData.items[name]
if item? and item.type isnt 'update'
slug = new ItemSlug modVersion.modSlug, _.slugify name
else
slug = new ItemSlug name
modVersion.registerName slug, name
return slug
stacks = []
for stackData in data
itemSlug = createSlug stackData.name
stacks.push new Stack itemSlug:itemSlug, quantity:stackData.quantity
if pattern?
expectedIndexes = _.reduce [0...stacks.length], ((obj, i)-> obj[i] = true; return obj), {}
for c in pattern
continue if c is '.'
continue if c is ' '
delete expectedIndexes[c]
if not stacks[parseInt(c)]? then throw new Error "there is no item #{c} in this recipe"
unusedNames = _.map(_.keys(expectedIndexes), ((i)-> data[parseInt(i)].name))
if unusedNames.length > 1
throw new Error "#{unusedNames.join(', ')} are listed for this recipe, but do not appear in the pattern"
else if unusedNames.length is 1
throw new Error "#{unusedNames[0]} is listed for this recipe, but does not appear in the pattern"
return stacks
# Un-parsing Methods ###########################################################################
_unparseModVersion: (builder, modVersion)->
builder
.line 'schema: ', 1
.line()
modVersion.eachGroup (group)=>
@_unparseGroup builder, modVersion, group
externalRecipes = modVersion.findExternalRecipes()
keys = _.keys(externalRecipes).sort()
for itemSlugText in keys
recipeList = externalRecipes[itemSlugText]
builder
.line 'update: ', modVersion.findName ItemSlug.slugify(itemSlugText)
.indent()
.loop(recipeList, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r))
.outdent()
.line()
_unparseGroup: (builder, modVersion, group)->
if group isnt Item.Group.Other
builder
.line 'group: ', group
.line()
.indent()
modVersion.eachItemInGroup group, (item)=>
@_unparseItem builder, modVersion, item
builder.line()
if group isnt Item.Group.Other
builder.outdent()
_unparseItem: (builder, modVersion, item)->
recipes = modVersion.findRecipes item.slug, [], onlyPrimary:true
builder
.line 'item: ', item.name
.indent()
.onlyIf item.isGatherable, => builder.line 'gatherable: yes'
.onlyIf item.multiblock?, =>
builder
.indent()
.call => @_unparseMultiblock builder, item.multiblock
.outdent()
.onlyIf recipes.length > 0, =>
builder.loop recipes, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r)
.outdent()
_unparseMultiblock: (builder, multiblock)->
builder
.line 'multiblock:'
.indent()
.push "input: "
.call => @_unparseStackList builder, multiblock.input
.push ";\n"
.loop(multiblock.layers, delimiter:'', onEach:(builder, layer)=> builder.line "layer: #{layer}")
.outdent()
_unparseRecipe: (builder, recipe)->
inputStacks = recipe.input[..]
inputStacks.sort (a, b)-> Stack.compare a, b
inputNames = []
for stack in inputStacks
name = builder.context.findName stack.itemSlug
if stack.quantity > 1
inputNames.push "#{stack.quantity} #{name}"
else
inputNames.push name
patternMap = {'.', '.'}
for i in [0...recipe.input.length]
stack = recipe.input[i]
name = builder.context.findName(stack.itemSlug)
name = "#{stack.quantity} #{name}" if stack.quantity > 1
patternMap["#{i}"] = "#{inputNames.indexOf name}"
pattern = recipe.pattern or recipe.defaultPattern
newPattern = []
for c in pattern.split('')
newPattern.push patternMap[c]
newPattern = newPattern.join ''
newPattern = newPattern.replace /(...)(...)(...)/, '$1 $2 $3'
quantity = recipe.output[0].quantity
extraOutputs = recipe.output[0...recipe.output.length]
extraOutputs.shift()
builder
.line 'recipe:'
.indent()
.onlyIf recipe.condition?, =>
builder.push 'onlyIf: '
.onlyIf recipe.condition.inverted, -> builder.push 'not '
.line recipe.condition.verb, ' ', recipe.condition.noun
.onlyIf extraOutputs.length > 0, =>
builder
.push 'extras: '
.call => @_unparseStackList builder, extraOutputs
.line()
.onlyIf recipe.ignoreDuringCrafting, => builder.line 'ignoreDuringCrafting: yes'
.push 'input: '
.loop inputNames
.line()
.line 'pattern: ', newPattern
.onlyIf quantity > 1, => builder.line 'quantity: ', quantity
.onlyIf recipe.tools.length > 0, =>
builder
.push 'tools: '
.call => @_unparseStackList builder, recipe.tools
.line()
.outdent()
_unparseStackList: (builder, stackList)->
if stackList.length is 1 and stackList[0].quantity is 1
builder.push builder.context.findName(stackList[0].itemSlug)
else
builder.loop stackList, onEach:(b, stack)=>
builder
.onlyIf stack.quantity > 1, => builder.push stack.quantity, ' '
.push builder.context.findName stack.itemSlug
@@ -0,0 +1,344 @@
#
# Crafting Guide - mod_version_parser_v1.test.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
ItemSlug = require '../game/item_slug'
ModVersion = require '../game/mod_version'
ModVersionParserV1 = require './mod_version_parser_v1'
########################################################################################################################
baseText = modVersion = parser = null
########################################################################################################################
describe 'mod_version_parser_v1.coffee', ->
beforeEach ->
modVersion = new ModVersion modSlug:'test', version:'0.0'
parser = new ModVersionParserV1 model:modVersion
describe 'Item', ->
it 'allows multiple recipes', ->
recipes = "item: Charlie;
recipe:; input:Alpha; pattern:... .0. ...;
recipe:; input:Bravo; pattern:... 0.0 ...;"
modVersion = parser.parse recipes
recipes = modVersion.findRecipes ItemSlug.slugify 'test__charlie'
recipes[0].input[0].itemSlug.qualified.should.equal 'alpha'
recipes[1].input[0].itemSlug.qualified.should.equal 'bravo'
describe 'name', ->
it 'adds the name when present', ->
modVersion = parser.parse 'item: Charlie'
modVersion._items.charlie.name.should.equal 'Charlie'
it 'requires a non-empty name', ->
func = -> parser.parse 'item: \n'
expect(func).to.throw Error, 'cannot be empty'
describe 'gatherable', ->
it 'adds "gatherable" when present', ->
modVersion = parser.parse 'item: Alpha Bravo; gatherable: yes'
modVersion._items.alpha_bravo.isGatherable.should.be.true
it 'does not allow a duplicate "gatherable" declaration', ->
func = -> parser.parse 'item: Alpha Bravo; gatherable: yes; gatherable: yes'
expect(func).to.throw Error, 'duplicate'
it 'requires "gatherable" to be "yes" or "no"', ->
func = -> parser.parse 'item: Alpha Bravo; gatherable: true'
expect(func).to.throw Error, 'gatherable must be'
it 'does not allow "gatherable" before "item"', ->
func = -> parser.parse 'gatherable: yes; item: Alpha Bravo; gatherable: yes'
expect(func).to.throw Error, '"gatherable" before "item"'
describe 'Multiblock', ->
it 'adds a "multiblock" when present', ->
modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo; layer: 0'
item = modVersion.findItemByName 'Alpha'
item.multiblock.height.should.equal 1
it 'requires "item" be declared before "multiblock"', ->
func = -> parser.parse "multiblock:"
expect(func).to.throw Error, '"multiblock" before "item"'
it 'prohibits multiple "multiblock" commands per item"', ->
func = -> parser.parse "item: Alpha; multiblock:; multiblock:"
expect(func).to.throw Error, 'duplicate'
it 'prohibits multiblocks with no inputs', ->
func = -> parser.parse "item: Alpha; multiblock:; layer: 000"
expect(func).to.throw Error, 'at least one "input"'
describe 'layer', ->
it 'allows multiple "layer" commands', ->
modVersion = parser.parse 'item: Alpha; multiblock:; input:Bravo, Charlie; layer: 01 10; layer: 10 01'
item = modVersion.findItemByName 'Alpha'
item.multiblock.depth.should.equal 2
item.multiblock.height.should.equal 2
item.multiblock.width.should.equal 2
it 'prohibits empty layers', ->
func = -> parser.parse 'item: Alpha; multiblock:; input: Bravo; layer:; layer: 00 00'
expect(func).to.throw Error, 'empty layer'
it 'prohibits multiblocks with no layers', ->
func = -> parser.parse "item: Alpha; multiblock:; input: Bravo"
expect(func).to.throw Error, 'at least one "layer"'
describe 'Recipe', ->
beforeEach -> baseText = 'item: Charlie; '
describe 'input', ->
it 'adds "input" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: ... 010 ...'
charlieSlug = ItemSlug.slugify('test__charlie')
slugs = (s.itemSlug.item for s in modVersion.findRecipes(charlieSlug)[0].input)
slugs.should.eql ['alpha', 'bravo']
it 'requires an "input" declaration', ->
func = -> parser.parse baseText + 'recipe:; pattern: ... .0. ...'
expect(func).to.throw Error, 'the "input" declaration is required'
it 'does not allow a duplicate "input" declaration', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:....0....; input:Bravo'
expect(func).to.throw Error, 'duplicate declaration of "input"'
it 'does not allow "input" before "recipe"', ->
func = -> parser.parse baseText + 'input:Alpha, Bravo; recipe:; pattern:....0....'
expect(func).to.throw Error, 'cannot declare "input" before "recipe"'
it 'registers slugs for each input name', ->
modVersion = parser.parse baseText + 'recipe:; input:Delta, Echo, Foxtrot; pattern:...012...'
(s.item for s in modVersion._slugs).should.eql ['charlie', 'delta', 'echo', 'foxtrot']
it 'correctly handles recipes which use the same input multiple times', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha; pattern:.0..0....'
recipe = _.values(modVersion._recipes)[0]
recipe.getQuantityRequired(ItemSlug.slugify('alpha')).should.equal 2
it 'allows a quantity for each input', ->
modVersion = parser.parse baseText + 'recipe:; input: 12 Delta, 3 Echo; pattern:... 0.1 ...'
recipe = _.values(modVersion._recipes)[0]
recipe.input[0].quantity.should.equal 12
recipe.input[0].itemSlug.qualified.should.equal 'delta'
recipe.input[1].quantity.should.equal 3
recipe.input[1].itemSlug.qualified.should.equal 'echo'
describe 'pattern', ->
it 'adds "pattern" when present', ->
modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:... .0. .1.'
modVersion.findRecipes(ItemSlug.slugify('test__charlie'))[0].pattern.should.equal '... .0. .1.'
it 'requires a "pattern" declaration', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo'
expect(func).to.throw Error, 'the "pattern" declaration is required'
it 'does not allow a duplicate "pattern" declaration', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:....0..1.; pattern:01.......'
expect(func).to.throw Error, 'duplicate declaration of "pattern"'
it 'requires pattern to be the right length', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:000'
expect(func).to.throw Error, 'a pattern must have'
it 'requires pattern to only use proper characters', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:abc def ghi'
expect(func).to.throw Error, 'a pattern must have'
it 'requires pattern to only refer to existing items', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:... 010 ...'
expect(func).to.throw Error, 'there is no item 1 in this recipe'
it 'requires all items to appear in the pattern', ->
func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: 000 0.0 000'
expect(func).to.throw Error, 'Bravo is listed'
it 'does not allow "pattern" before "recipe"', ->
func = -> parser.parse baseText + 'pattern:... .0. ...; recipe:; inputs:Alpha'
expect(func).to.throw Error, 'cannot declare "pattern" before "recipe"'
describe 'quantity', ->
beforeEach ->
baseText = 'item: Charlie; recipe:; input:Alpha; pattern:...0.0...; '
it 'adds "quantity" when present', ->
modVersion = parser.parse baseText + 'quantity: 2'
modVersion.findRecipes(ItemSlug.slugify('test__charlie'))[0].output[0].quantity.should.equal 2
it 'does not allow a duplicate "quantity" declaration', ->
func = -> parser.parse baseText + 'quantity:1; quantity:2'
expect(func).to.throw Error, 'duplicate declaration of "quantity"'
it 'requires quantity to be an integer', ->
func = -> parser.parse baseText + 'quantity:ten'
expect(func).to.throw Error, 'quantity must be an integer'
it 'assumes a quantity of 1 by default', ->
modVersion = parser.parse baseText
modVersion.findRecipes(ItemSlug.slugify('test__charlie'))[0].output[0].quantity.should.equal 1
it 'does not allow "quantity" before recipe', ->
func = -> parser.parse 'item:Bravo; quantity:12; recipe:;'
expect(func).to.throw Error, 'cannot declare "quantity" before "recipe"'
describe 'onlyIf', ->
beforeEach ->
baseText = 'item: Charlie; recipe:; input:Alpha; pattern:...0.0...; '
it 'understands the "item" verb', ->
modVersion = parser.parse baseText + 'onlyIf: item Iron Ingot'
recipes = []
modVersion.eachRecipe (recipe)-> recipes.push recipe
recipe = recipes[0]
recipe.condition.should.eql verb:'item', noun:'Iron Ingot', inverted:false
it 'understands the "mod" verb', ->
modVersion = parser.parse baseText + 'onlyIf: mod BuildCraft'
recipes = []
modVersion.eachRecipe (recipe)-> recipes.push recipe
recipe = recipes[0]
recipe.condition.should.eql verb:'mod', noun:'BuildCraft', inverted:false
it 'understands inverting verbs', ->
modVersion = parser.parse baseText + 'onlyIf: not item Iron Ingot'
recipes = []
modVersion.eachRecipe (recipe)-> recipes.push recipe
recipe = recipes[0]
recipe.condition.should.eql verb:'item', noun:'Iron Ingot', inverted:true
it 'requires at least two words', ->
func = -> parser.parse baseText + 'onlyIf: item'
expect(func).to.throw Error, 'verb followed by a noun'
it 'only allows known verbs', ->
func = -> parser.parse baseText + 'onlyIf: foo Iron Ingot'
expect(func).to.throw Error, 'unknown verb'
describe 'output', ->
beforeEach ->
baseText = 'item: Delta; item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
it 'adds a single item as the default output', ->
modVersion = parser.parse baseText
stack = modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].output[0]
stack.itemSlug.qualified.should.equal 'test__bravo'
stack.quantity.should.equal 1
it 'can add multiple extras with quantities', ->
modVersion = parser.parse baseText + 'extras:2 Delta, 4 Echo'
output = modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].output
output[0].itemSlug.qualified.should.equal 'test__bravo'
output[0].quantity.should.equal 1
output[1].itemSlug.qualified.should.equal 'test__delta'
output[1].quantity.should.equal 2
output[2].itemSlug.qualified.should.equal 'echo'
output[2].quantity.should.equal 4
it 'does not allow "extras" before "recipe"', ->
func = -> parser.parse 'item:Bravo; extras:Charlie'
expect(func).to.throw Error, 'cannot declare "extras" before "recipe"'
it 'registers slugs for each output name', ->
modVersion = parser.parse baseText + 'extras:Delta, Echo'
(s.qualified for s in modVersion._slugs).should.eql [
'test__bravo', 'charlie', 'test__delta', 'echo'
]
it 'does not allow a duplicate "extras" declaration', ->
func = -> parser.parse baseText + 'extras:Echo; extras:Delta'
expect(func).to.throw Error, 'duplicate declaration of "extras"'
describe 'tools', ->
beforeEach ->
baseText = 'item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; '
it 'can add a single tool', ->
modVersion = parser.parse baseText + 'tools: Furnace'
modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].tools[0].itemSlug.item.should.equal 'furnace'
it 'can add multiple tools', ->
modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace'
tools = modVersion.findRecipes(ItemSlug.slugify('test__bravo'))[0].tools
tools[0].itemSlug.item.should.equal 'crafting_table'
tools[1].itemSlug.item.should.equal 'furnace'
it 'registers slugs for each tool name', ->
modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace'
(s.item for s in modVersion._slugs).should.eql ['bravo', 'charlie', 'crafting_table', 'furnace']
it 'does not allow a duplicate "tools" declaration', ->
func = -> parser.parse baseText + 'tools:Crafting Table; tools:Furnace'
expect(func).to.throw Error, 'duplicate declaration of "tools"'
describe "unparsing", ->
beforeEach ->
baseText = """
schema: 1
group: Agriculture
item: Apple
item: Baked Potato
recipe:
input: furnace fuel, Potato
pattern: .1. ... .0.
tools: Furnace
item: (filled) Canned Food
recipe:
input: 4 (Empty) Tin Can, Apple
pattern: .1. .0. ...
item: Pyramid
multiblock:
input: Cobblestone
layer: 000 000 000
layer: ... .0. ...
group: Functional Blocks
item: Furnace
recipe:
input: Cobblestone
pattern: 000 0.0 000
tools: Crafting Table
update: Iron Ingot
recipe:
input: furnace fuel, Iron Dust
pattern: .1. ... .0.
tools: Furnace
recipe:
onlyIf: item Redstone Furnace
input: Iron Ore
pattern: ... .0. ...
tools: Redstone Furnace
"""
it 'can round-trip a data file', ->
text = parser.unparse parser.parse baseText
actual = CommandParserVersionBase.simplify text
expected = CommandParserVersionBase.simplify baseText
actual.should.equal expected
@@ -0,0 +1,19 @@
#
# Crafting Guide - tutorial_parser.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
VersionedParserBase = require './versioned_parser_base'
TutorialParserV1 = require './tutorial_parser_v1'
########################################################################################################################
module.exports = class TutorialParser extends VersionedParserBase
# VersionedParserBase Overrides ################################################################
_createParsers: (options)->
return result =
'1': new TutorialParserV1 options
@@ -0,0 +1,65 @@
#
# Crafting Guide - tutorial_parser_v1.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
CommandParserVersionBase = require './command_parser_version_base'
Tutorial = require '../site/tutorial'
########################################################################################################################
module.exports = class TutorialParserV1 extends CommandParserVersionBase
# CommandParserVersionBase Overrides ###########################################################
_buildModel: (rawData, model)->
@_buildTutorial rawData, model
_unparseModel: (builder, model)->
@_unparseTutorial builder, model
# Command Methods ##############################################################################
_command_content: (contentParts...)->
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
if @_rawData.currentSection.content? then throw new Error 'duplicate declaration of content'
content = contentParts.join(', ').trim()
if not content.length > 0 then throw new Error 'content cannot be empty'
@_rawData.currentSection.content = content
_command_officialUrl: (officialUrl)->
if @_rawData.officialUrl? then throw new Error 'duplicate declaration of "officialUrl"'
if officialUrl.length is 0 then throw new Error 'officialUrl cannot be empty'
@_rawData.officialUrl = officialUrl
_command_section: (textParts...)->
@_rawData.sections ?= []
@_rawData.sections.push @_rawData.currentSection = {}
_command_title: (titleParts...)->
if not @_rawData.currentSection? then throw new Error 'cannot declare "title" before "section"'
if @_rawData.currentSection.title? then throw new Error 'duplicate declaration of title'
title = titleParts.join(', ').trim()
if not title.length > 0 then throw new Error 'title cannot be empty'
@_rawData.currentSection.title = title
_command_video: (youTubeId, nameParts...)->
if not youTubeId?.length then throw new Error 'video declaration requires a YouTubeID'
name = nameParts.join ', '
if not name?.length then throw new Error 'video declaration requires a name'
@_rawData.videos ?= []
@_rawData.videos.push youTubeId:youTubeId, name:name
# Object Building Methods ######################################################################
_buildTutorial: (rawData, model)->
if not rawData.sections? then throw new Error 'the "section" declaration is required'
model.officialUrl = rawData.officialUrl
model.videos = rawData.videos
model.sections = rawData.sections
@@ -0,0 +1,51 @@
#
# Crafting Guide - versioned_parser_base.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
########################################################################################################################
module.exports = class VersionedParserBase
constructor: (options={})->
@_parsers = @_createParsers options
@_currentSchema = _.chain(@_parsers).keys().last().value()
@errors = []
# Class Members ################################################################################
@SCHEMA = /schema: *([0-9]+)/
# Public Methods ###############################################################################
parse: (text)->
return unless text?
schema = @_identifySchema text
parser = @_parsers[schema]
if not parser? then throw new Error "schema version #{schema} is not supported"
parser.parse text
@errors = parser.errors
return @_model
unparse: (schema=null)->
schema ?= @_currentSchema
parser = @_parsers["#{schema}"]
if not parser? then throw new Error "version #{schema} is not supported"
return parser.unparse()
# Overridable Methods ##########################################################################
_createParsers: (options)->
throw new Error 'subclasses must override this method'
_identifySchema: (text)->
match = VersionedParserBase.SCHEMA.exec text
if not match? then throw new Error 'missing "schema" declaration'
return match[1]
+29 -77
View File
@@ -1,94 +1,46 @@
#
# Crafting Guide - craft_page.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{Inventory} = require("crafting-guide-common").models
{Observable} = require("crafting-guide-common").util
{PlanBuilder} = require("crafting-guide-common").crafting
{ResourcesEvaluator} = require("crafting-guide-common").crafting
{StepsEvaluator} = require("crafting-guide-common").crafting
BaseModel = require '../base_model'
Craftsman = require '../crafting/craftsman'
Inventory = require '../game/inventory'
ModPack = require '../game/mod_pack'
########################################################################################################################
module.exports = class CraftPage extends Observable
module.exports = class CraftPage extends BaseModel
constructor: (attributes={})->
super
constructor: (attributes={}, options={})->
if not attributes.modPack then throw new Error 'attributes.modPack is required'
attributes.params ?= null
attributes.craftsman ?= new Craftsman attributes.modPack
super attributes, options
@muted -> @modPack = attributes.modPack
@_currentPlan = null
@_have = new Inventory
@_planBuilder = new PlanBuilder new StepsEvaluator
@_want = new Inventory
@_consumeParams attributes.params
@_have.on Observable::ANY, this, "_onHaveChanged"
@_want.on Observable::ANY, this, "_onWantChanged"
# Properties ###################################################################################
Object.defineProperties @prototype,
currentPlan:
get: -> return @_currentPlan
set: -> throw new Error "currentPlan cannot be assigned"
have:
get: -> return @_have
set: -> throw new "have cannot be assigned"
isDirty:
get: -> return @_isDirty
set: -> throw new Error "isDirty cannot be assigned"
isOutdated:
get: -> return @currentPlan? and @isDirty
set: -> throw new Error "isOutdated cannot be assigned"
modPack:
get: -> return @_modPack
set: (modPack)->
if @_modPack? then throw new Error "modPack cannot be reassigned"
if not modPack? then throw new Error "modPack is required"
@_modPack = modPack
planBuilder:
get: -> return @_planBuilder
set: -> throw new Error "planBuilder cannot be assigned"
want:
get: -> return @_want
set: -> throw new Error "want cannot be assigned"
# Public Methods ###############################################################################
createPlan: ->
newPlan = @_planBuilder.createPlan @_want, @_have
@triggerPropertyChange "currentPlan", @_currentPlan, newPlan, ->
@_currentPlan = newPlan
@triggerPropertyChange "isDirty", @_isDirty, false
@modPack.on c.event.change, => @_consumeParams()
@on c.event.change + ':params', => @_consumeParams()
@craftsman.on c.event.change + ':stage', => @trigger c.event.change, this
@craftsman.on c.event.change + ':complete', => @trigger c.event.change, this
# Private Methods ##############################################################################
_consumeParams: (params)->
return unless params?.inventoryText?
_consumeParams: ->
return unless @params?
newWant = Inventory.fromUrlString params.inventoryText, @modPack
@_want.clear()
@craftsman.want.clear()
if not @params.inventoryText?
@params = null
else
inventory = new Inventory
inventory.parse @params.inventoryText
for itemId, stack of newWant.stacks
continue unless stack.item.isCraftable
@_want.add stack.item, stack.quantity
inventory.each (stack)=>
item = @modPack.findItem stack.itemSlug, enableAsNeeded:true
return unless item? and item.isCraftable
@craftsman.want.add stack.itemSlug, stack.quantity
inventory.remove stack.itemSlug
_onHaveChanged: ->
@triggerPropertyChange "isDirty", @_isDirty, true
_onWantChanged: ->
@triggerPropertyChange "isDirty", @_isDirty, true, ->
@_isDirty = true
if @want.isEmpty then @triggerPropertyChange "currentPlan", @_currentPlan, null
if inventory.isEmpty then @params = null
+2 -3
View File
@@ -1,12 +1,11 @@
#
# Crafting Guide - editable_file.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{BaseModel} = require("crafting-guide-common").deprecated
w = require "when"
BaseModel = require '../base_model'
########################################################################################################################
+1 -1
View File
@@ -1,7 +1,7 @@
#
# Crafting Guide - email_client.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
-88
View File
@@ -1,88 +0,0 @@
#
# Crafting Guide - file_cache.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# All rights reserved.
#
w = require "when"
########################################################################################################################
module.exports = class FileCache extends Backbone.Events
@::FILE_TOKEN = '#file='
constructor: (url)->
@_ajax = $.ajax
@_files = {}
@_loading = w.resolve(true)
if url? then @loadArchive(url).catch(->)
# Public Methods ###############################################################################
getFile: (file)->
return null unless @hasFile file
return @_files[file]
hasFile: (file)->
return @_files[file]?
loadArchive: (url)->
if not _.isFunction(@_ajax) then throw new Error '$.ajax is required'
newLoading = w.promise (resolve, reject)=>
@_ajax
url: url
dataType: 'text'
success: (text, status, xhr)=> resolve @_onLoadSuccess text, status, xhr
error: (xhr, status, error)=> resolve @_onLoadError error, status, xhr
@_loading = w.join @_loading, newLoading
# Property Methods #############################################################################
Object.defineProperties @prototype,
loading:
get: -> return @_loading
# Private Methods ##############################################################################
_onLoadError: (error, status, xhr)->
console.error "Failed to load archive: #{xhr.responseText}"
return false
_onLoadSuccess: (text, status, xhr)->
return @_parseArchive text
_parseArchive: (text)->
file = null
tokenIndex = nameStartIndex = nameEndIndex = fileStartIndex = fileEndIndex = 0
fileCount = 0
while true
tokenIndex = text.indexOf @FILE_TOKEN, fileStartIndex
if tokenIndex is -1
if file?
@_files[file] = text.substring fileStartIndex, text.length
fileCount += 1
break
if file?
fileEndIndex = tokenIndex
@_files[file] = text.substring fileStartIndex, fileEndIndex
fileCount += 1
nameStartIndex = tokenIndex + @FILE_TOKEN.length
nameEndIndex = text.indexOf '\n', nameStartIndex
if nameEndIndex is -1
file = text.substring nameStartIndex, text.length
@_files[file] = ''
break
file = text.substring nameStartIndex, nameEndIndex
fileStartIndex = nameEndIndex + '\n'.length
return fileCount > 0
@@ -1,78 +0,0 @@
#
# Crafting Guide - file_cache.test.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# All rights reserved.
#
FileCache = require './file_cache'
########################################################################################################################
describe 'file_cache.coffee', ->
beforeEach ->
@cache = new FileCache
makeTestAjax = (text)->
return (options)->
options.success text, 'success', {url:options.url}
it 'can load an archive with a single file', ->
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie delta'
@cache.loadArchive 'http://test'
.then =>
@cache.hasFile('alpha').should.equal true
@cache.getFile('alpha').should.equal 'bravo charlie delta'
it 'can load an archive with multiple files', ->
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\necho foxtrot'
@cache.loadArchive 'http://test'
.then =>
@cache.hasFile('alpha').should.equal true
@cache.getFile('alpha').should.equal 'bravo charlie\n'
@cache.hasFile('delta').should.equal true
@cache.getFile('delta').should.equal 'echo foxtrot'
it 'can load an archive without newlines', ->
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie#file=delta\necho foxtrot'
@cache.loadArchive 'http://test'
.then =>
@cache.hasFile('alpha').should.equal true
@cache.getFile('alpha').should.equal 'bravo charlie'
@cache.hasFile('delta').should.equal true
@cache.getFile('delta').should.equal 'echo foxtrot'
it 'can cope with an empty file in the middle of an archive', ->
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\n#file=echo\nfoxtrot golf'
@cache.loadArchive 'http://test'
.then =>
@cache.hasFile('alpha').should.equal true
@cache.getFile('alpha').should.equal 'bravo charlie\n'
@cache.hasFile('delta').should.equal true
@cache.getFile('delta').should.equal ''
@cache.hasFile('echo').should.equal true
@cache.getFile('echo').should.equal 'foxtrot golf'
it 'can cope with an empty file at the end of an archive', ->
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta'
@cache.loadArchive 'http://test'
.then =>
@cache.hasFile('alpha').should.equal true
@cache.getFile('alpha').should.equal 'bravo charlie\n'
@cache.hasFile('delta').should.equal true
@cache.getFile('delta').should.equal ''
it 'can load multiple, non-overlapping archives', ->
@cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\necho foxtrot'
@cache.loadArchive 'http://test'
@cache._ajax = makeTestAjax '#file=golf\nhotel india'
@cache.loadArchive 'http://test2'
.then =>
@cache.hasFile('alpha').should.equal true
@cache.getFile('alpha').should.equal 'bravo charlie\n'
@cache.hasFile('delta').should.equal true
@cache.getFile('delta').should.equal 'echo foxtrot'
@cache.hasFile('golf').should.equal true
@cache.getFile('golf').should.equal 'hotel india'
+2 -2
View File
@@ -1,11 +1,11 @@
#
# Crafting Guide - github_user.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{BaseModel} = require("crafting-guide-common").deprecated
BaseModel = require '../base_model'
########################################################################################################################
@@ -1,62 +0,0 @@
#
# Crafting Guide - item_display.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# All rights reserved.
#
_ = require "../../../common/underscore"
c = require "../../../common/constants"
{Inventory} = require("crafting-guide-common").models
########################################################################################################################
module.exports = class ItemDisplay
constructor: (item)->
@item = item
# Properties ###################################################################################
Object.defineProperties @prototype,
craftingUrl:
get: ->
inventory = new Inventory
inventory.add @item
c.url.crafting inventoryText:inventory.toUrlString()
iconUrl:
get: -> return c.url.itemIcon this
set: -> throw new Error "iconUrl cannot be assigned"
item:
get: -> return @_item
set: (item)->
if @_item? then throw new Error "item cannot be re-assigned"
if not item? then throw new Error "item is required"
@_item = item
itemSlug:
get: -> return _.slugify @name
set: -> throw new Error "slug cannot be assigned"
mod:
get: -> return @_item.mod
set: -> throw new Error "mod cannot be assigned"
modId:
get: -> return @_item.mod.id
set: -> throw new Error "modId cannot be assigned"
modUrl:
get: -> return c.url.mod modId:@item.mod.id
set: -> throw new Error "modUrl cannot be assigned"
name:
get: -> return @item.displayName
set: -> throw new Error "name cannot be assigned"
url:
get: -> return c.url.item this
set: -> throw new Error "url cannot be assigned"
+31 -70
View File
@@ -1,95 +1,56 @@
#
# Crafting Guide - item_page.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{Item} = require("crafting-guide-common").models
ItemDisplay = require "./item_display"
{Observable} = require("crafting-guide-common").util
BaseModel = require '../base_model'
########################################################################################################################
module.exports = class ItemPage extends Observable
module.exports = class ItemPage extends BaseModel
constructor: (item)->
if item.constructor isnt Item then throw new Error "item must be an Item instance"
@item = item
super
# Properties ###################################################################################
Object.defineProperties @prototype,
item:
get: -> return @_item
set: (item)->
if @_item? then throw new Error "item cannot be reassigned"
if not item? then throw new Error "item is required"
if @_item? then @_item.off Observable::ANY, this
@_item = item
@_item.on Observable::ANY, this, "_onItemChanged"
itemDisplay:
get: -> return @_itemDisplay ?= new ItemDisplay @item
mod:
get: -> return @_item.mod
set: -> throw new Error "mod cannot be assigned"
modPack:
get: -> return @mod.modPack
set: -> throw new Error "modPack cannot be assigned"
constructor: (attributes={}, options={})->
if not attributes.modPack? then throw new Error 'attributes.modPack is required'
attributes.item ?= null
super attributes, options
# Property Methods #############################################################################
findComponentInItems: ->
return @_findItemsWithMatchingRecipes (recipe)=>
recipe.computeQuantityRequired @item
findRecipes: ->
return (recipe for recipeId, recipe of @item.recipes)
return @_findRecipesMatching (recipe)=> recipe.requires @item.slug
findSimilarItems: ->
group = @mod.itemGroups[@item.groupName]
return unless group?
return null unless @item?.modVersion?
return null unless group.length > 0
return group
result = []
@item.modVersion.eachItemInGroup @item.group, (item)=>
result.push item
findToolForItems: ->
return @_findItemsWithMatchingRecipes (recipe)=>
return recipe.tools[@item.id]?
return null unless result.length > 0
return result
loadDetails: ->
stores.itemDetail.loadDetailFor @item
.catch (error)=>
if error.message.indexOf("404") is -1
logger.error "Could not load details for item #{@item.id}: #{error.message}"
else
logger.info "Item #{@item.id} has no details file."
findRecipes: ->
return @modPack.findRecipes @item?.slug, [], alwaysFromOwningMod:true
findToolForRecipes: ->
return @_findRecipesMatching (recipe)=> recipe.requiresTool @item.slug
# Private Methods ##############################################################################
_findItemsWithMatchingRecipes: (isMatching)->
_findRecipesMatching: (isAcceptable)->
return null unless @item?
result = {}
@modPack.eachMod (mod)=>
mod.eachRecipe (recipe)=>
if isAcceptable recipe
for outputStack in recipe.output
outputItem = @modPack.findItem outputStack.itemSlug, includeDisabled:true
result[outputItem.slug] = outputItem
for modId, mod of @modPack.mods
for itemId, item of mod.items
for recipeId, recipe of item.recipes
if isMatching(recipe)
result[item.id] = item
result = (item for itemId, item of result)
result.sort (a, b)->
if not a?.displayName? and not b?.displayName? then return 0
if not b?.displayName? then return +1
if not a?.displayName? then return +1
return a.displayName.localeCompare b.displayName
return result
_onItemChanged: ->
@trigger Observable::CHANGE
result = _.values result
return null unless result.length > 0
return result.sort (a, b)-> a.compareTo b
+44 -59
View File
@@ -1,104 +1,89 @@
#
# Crafting Guide - item_selector.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{Observable} = require("crafting-guide-common").util
BaseModel = require '../base_model'
ItemSlug = require '../game/item_slug'
########################################################################################################################
module.exports = class ItemSelector extends Observable
module.exports = class ItemSelector extends BaseModel
constructor: (modPack, options={})->
super
constructor: (attributes={}, options={})->
if not options.modPack? then throw new Error 'options.modPack is required'
options.isAcceptable ?= (item)-> return true # accept everything by default
super attributes, options
@_isAcceptable = options.isAcceptable or (item)-> return true # accept everything by default
@_isAcceptable = options.isAcceptable
@_maxResults = 100
@_minHintLength = 3
@_modPack = options.modPack
@_results = []
@modPack = modPack
# Property Methods #############################################################################
Object.defineProperties @prototype,
hint:
get: -> return @_hint
set: (hint)->
@triggerPropertyChange "hint", @_hint, hint, ->
@_hint = hint?.toLowerCase()
@_refreshResults()
get: ->
return @_hint
modPack:
get: -> return @_modPack
set: (modPack)->
if not modPack? then throw new Error "modPack is required"
if @_modPack? then throw new Error "modPack cannot be reassigned"
@_modPack = modPack
set: (newHint)->
oldHint = @_hint
return if newHint is oldHint
@_hint = newHint.toLowerCase()
@trigger c.event.change + ':hint', this, oldHint, newHint
@_refreshResults()
@trigger c.event.change, this
results:
get: -> return @_results
get: ->
return @_results
# Private Methods ##############################################################################
_computeScore: (item)->
_isMatch: (name, itemSlug)->
hintIndex = 0
hintLetter = @_hint[hintIndex]
name = item.displayName.toLowerCase()
nextScore = 1
totalScore = 0
name = name.toLowerCase()
for nameIndex in [0...name.length] by 1
if name.charAt(nameIndex) is hintLetter
totalScore += nextScore
nextScore += 1
hintIndex += 1
if hintIndex is @_hint.length
return 0 unless @_isAcceptable item
break
item = @_modPack.findItem itemSlug
return false unless item?
return @_isAcceptable item
hintLetter = @_hint[hintIndex]
else
nextScore = 1
return 0 if hintIndex < @_hint.length
return totalScore
return false
_refreshResults: ->
scoredItems = []
oldResults = @_results
newResults = {}
count = 0
logger.verbose => "Looking for items which match: #{@_hint}"
if @_hint.length >= @_minHintLength
for modId, mod of @_modPack.mods
@_modPack.eachMod (mod)=>
return if count >= @_maxResults
return unless mod.isEnabled
return unless mod.enabled
for itemId, item of mod.items
score = @_computeScore item
if score > 0
scoredItems.push score:score, item:item
mod.eachName (name, itemSlug)=>
return if count >= @_maxResults
scoredItems.sort (a, b)->
if a.score isnt b.score
return if a.score > b.score then -1 else +1
if @_isMatch name, itemSlug
if itemSlug.isQualified
newResults[itemSlug] = itemSlug
count += 1
nameA = a.item.displayName
nameB = b.item.displayName
if nameA.length isnt nameB.length
return if nameA.length < nameB.length then -1 else +1
if nameA isnt nameB
return if nameA < nameB then -1 else +1
return 0
@_results = []
for element in scoredItems
@_results.push element.item
break if @_results.length >= @_maxResults
@trigger c.event.change + ':results', this
newResults = _.values(newResults)
newResults.sort ItemSlug.compare
@_results = newResults
@trigger c.event.change + ':results', this, oldResults, newResults
+1 -1
View File
@@ -1,7 +1,7 @@
#
# Crafting Guide - markdown_image.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
@@ -1,26 +1,30 @@
#
# Crafting Guide - markdown_image_list.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
{Observable} = require("crafting-guide-common").util
BaseModel = require '../base_model'
MarkdownImage = require './markdown_image'
########################################################################################################################
module.exports = class MarkdownImageList extends Observable
module.exports = class MarkdownImageList extends BaseModel
constructor: (options={})->
super
constructor: (attributes={}, options={})->
attributes.imageBase ?= ''
attributes.markdownText ?= null
super attributes, options
@_images = []
@muted =>
@client = options.client
@imageBase = options.imageBase
@markdownText = options.markdownText
@client = options.client
@_images = {}
@on c.event.change + ':imageBase', =>
for fileName, image of @_images
image.path = @imageBase
@on c.event.change + ':markdownText', => @_analyzeMarkdownText()
@_analyzeMarkdownText()
# Public Methods ###############################################################################
@@ -42,43 +46,22 @@ module.exports = class MarkdownImageList extends Observable
# Property Methods #############################################################################
getAll: ->
fileNames = (fileName for fileName, image of @_images).sort()
result = []
for fileName in fileNames
result.push @_images[fileName]
return result
isValid: ->
for fileName, image of @_images
return false unless image.valid
return true
Object.defineProperties @prototype,
all:
get: ->
fileNames = (fileName for fileName, image of @_images).sort()
result = []
for fileName in fileNames
result.push @_images[fileName]
return result
client:
get: -> return @_client
set: (client)->
if not client? then throw new Error "client is required"
@_client = client
imageBase: ->
get: -> return @_imageBase
set: (imageBase)->
@triggerPropertyChange "imageBase", @_imageBase, imageBase, ->
@_imageBase = imageBase
for fileName, image of @_images
image.path = @imageBase
markdownText:
get: -> return @_markdownText
set: (markdownText)->
@triggerPropertyChange "markdownText", @_markdownText, markdownText, ->
@_markdownText = markdownText
@_analyzeMarkdownText()
valid:
get: ->
for fileName, image of @_images
return false unless image.valid
return true
all: {get:@prototype.getAll}
valid: {get:@prototype.isValid}
# Private Methods ##############################################################################
@@ -96,8 +79,8 @@ module.exports = class MarkdownImageList extends Observable
image = @_images[fileName]
if not image?
image = new MarkdownImage {fileName:fileName, path:@imageBase}, {client:@client}
image.on Observable::CHANGE, this, "_onImageChanged"
@trigger Observable::ADD, image
@listenTo image, c.event.change, => @trigger c.event.change, this
@trigger c.event.add, this, image
changed = true
newImages[fileName] = image
@@ -109,7 +92,4 @@ module.exports = class MarkdownImageList extends Observable
if changed
@_images = newImages
@trigger Observable::CHANGE
_onImageChanged: ->
@trigger Observable::CHANGE
@trigger c.event.change, this
@@ -1,7 +1,7 @@
#
# Crafting Guide - markdown_image_list.test.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
@@ -16,7 +16,7 @@ list = null
describe 'markdown_image_list.coffee', ->
beforeEach ->
list = new MarkdownImageList client:{fetchFile:sinon.stub().returns(w.promise(->))}
list = new MarkdownImageList {}, client:{fetchFile:sinon.stub().returns(w.promise(->))}
describe '_analyzeMarkdownText', ->
@@ -1,53 +0,0 @@
#
# Crafting Guide - multiblock_display.coffee
#
# Copyright © 2014-2017 by Redwood Labs
# All rights reserved.
#
{Inventory} = require("crafting-guide-common").models
{Observable} = require("crafting-guide-common").util
########################################################################################################################
module.exports = class MultiblockDisplay extends Observable
constructor: (recipe)->
@_layerInventories = []
@recipe = recipe
# Properties ###################################################################################
Object.defineProperties @prototype,
recipe:
get: -> return @_recipe
set: (recipe)->
if not recipe? then throw new Error "recipe is required"
if @_recipe? then throw new Error "recipe cannot be reassigned"
@_recipe = recipe
# Public Methods ###############################################################################
getInventory: (z)->
if not z?
return @_fullInventory ?= @_createFullInventory()
else
return @_layerInventories[z] ?= @_addLayerInventory z
# Private Methods ##############################################################################
_addLayerInventory: (z, inventory=null)->
inventory ?= new Inventory
for x in [0..@recipe.width]
for y in [0..@recipe.height]
stack = @recipe.getInputAt x, y, z
continue unless stack?
inventory.add stack.item, stack.quantity
return inventory
_createFullInventory: (inventory=null)->
inventory ?= new Inventory()
for z in [0..@recipe.depth]
@_addLayerInventory z, inventory
return inventory

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