diff --git a/.gitignore b/.gitignore index ba0802aa6..44a78773d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ Thumbs.db *.sublime-project *.sublime-workspace +# IntelliJ/WebStorm +*.iml + # NPM packages folder. node_modules/ bower_components/ @@ -77,4 +80,10 @@ bin/mongo/ # windows /SCOCODE.bat +# local settings +login.coffee + +# debugging +*.heapsnapshot + ### If you add something here, copy it to the end of .npmignore, too. ### diff --git a/.npmignore b/.npmignore index ae193b37d..5d0542980 100644 --- a/.npmignore +++ b/.npmignore @@ -53,6 +53,9 @@ Thumbs.db *.sublime-project *.sublime-workspace +# IntelliJ/WebStorm +*.iml + # NPM packages folder. node_modules/ @@ -89,6 +92,12 @@ mongo/ bin/node/ bin/mongo/ - # Karma coverage coverage/ + +# local settings +login.coffee + +# debugging +*.heapsnapshot + diff --git a/app/application.coffee b/app/application.coffee index f44287599..4252c87c6 100644 --- a/app/application.coffee +++ b/app/application.coffee @@ -1,10 +1,16 @@ FacebookHandler = require 'lib/FacebookHandler' GPlusHandler = require 'lib/GPlusHandler' +LinkedInHandler = require 'lib/LinkedInHandler' locale = require 'locale/locale' {me} = require 'lib/auth' Tracker = require 'lib/Tracker' CocoView = require 'views/kinds/CocoView' +marked.setOptions {gfm: true, sanitize: true, smartLists: true, breaks: false} + +# TODO, add C-style macro constants like this? +window.SPRITE_RESOLUTION_FACTOR = 4 + # Prevent Ctrl/Cmd + [ / ], P, S ctrlDefaultPrevented = [219, 221, 80, 83] preventBackspace = (event) -> @@ -22,7 +28,7 @@ elementAcceptsKeystrokes = (el) -> # not radio, checkbox, range, or color return (tag is 'textarea' or (tag is 'input' and type in textInputTypes) or el.contentEditable in ["", "true"]) and not (el.readOnly or el.disabled) -COMMON_FILES = ['/images/pages/base/modal_background.png', '/images/level/code_palette_background.png'] +COMMON_FILES = ['/images/pages/base/modal_background.png', '/images/level/code_palette_background.png', '/images/level/popover_background.png', '/images/level/code_editor_background.png'] preload = (arrayOfImages) -> $(arrayOfImages).each -> $('')[0].src = @ @@ -33,7 +39,7 @@ Application = initialize: -> @facebookHandler = new FacebookHandler() @gplusHandler = new GPlusHandler() $(document).bind 'keydown', preventBackspace - + @linkedinHandler = new LinkedInHandler() preload(COMMON_FILES) $.i18n.init { lng: me?.lang() ? 'en' diff --git a/app/assets/fonts/glyphicons-halflings-regular.eot b/app/assets/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 000000000..4a4ca865d Binary files /dev/null and b/app/assets/fonts/glyphicons-halflings-regular.eot differ diff --git a/app/assets/fonts/glyphicons-halflings-regular.svg b/app/assets/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..e3e2dc739 --- /dev/null +++ b/app/assets/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/fonts/glyphicons-halflings-regular.ttf b/app/assets/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..67fa00bf8 Binary files /dev/null and b/app/assets/fonts/glyphicons-halflings-regular.ttf differ diff --git a/app/assets/fonts/glyphicons-halflings-regular.woff b/app/assets/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..8c54182aa Binary files /dev/null and b/app/assets/fonts/glyphicons-halflings-regular.woff differ diff --git a/app/assets/images/level/gold_background.png b/app/assets/images/level/gold_background.png new file mode 100644 index 000000000..738b3592c Binary files /dev/null and b/app/assets/images/level/gold_background.png differ diff --git a/app/assets/images/level/gold_icon.png b/app/assets/images/level/gold_icon.png new file mode 100644 index 000000000..a8988a2ef Binary files /dev/null and b/app/assets/images/level/gold_icon.png differ diff --git a/app/assets/images/level/loading_left_wing.png b/app/assets/images/level/loading_left_wing.png index 5b7ba04a7..36d91671a 100644 Binary files a/app/assets/images/level/loading_left_wing.png and b/app/assets/images/level/loading_left_wing.png differ diff --git a/app/assets/images/level/loading_right_wing.png b/app/assets/images/level/loading_right_wing.png index 7f7ff29da..36dedada4 100644 Binary files a/app/assets/images/level/loading_right_wing.png and b/app/assets/images/level/loading_right_wing.png differ diff --git a/app/assets/images/level/prop_gold.png b/app/assets/images/level/prop_gold.png deleted file mode 100644 index 97e5afbfb..000000000 Binary files a/app/assets/images/level/prop_gold.png and /dev/null differ diff --git a/app/assets/images/pages/account/profile/education.png b/app/assets/images/pages/account/profile/education.png new file mode 100644 index 000000000..dad4914c6 Binary files /dev/null and b/app/assets/images/pages/account/profile/education.png differ diff --git a/app/assets/images/pages/account/profile/icon_facebook.png b/app/assets/images/pages/account/profile/icon_facebook.png new file mode 100644 index 000000000..b775c18fa Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_facebook.png differ diff --git a/app/assets/images/pages/account/profile/icon_github.png b/app/assets/images/pages/account/profile/icon_github.png new file mode 100644 index 000000000..fc1801abc Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_github.png differ diff --git a/app/assets/images/pages/account/profile/icon_gplus.png b/app/assets/images/pages/account/profile/icon_gplus.png new file mode 100644 index 000000000..c2343eb50 Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_gplus.png differ diff --git a/app/assets/images/pages/account/profile/icon_linkedin.png b/app/assets/images/pages/account/profile/icon_linkedin.png new file mode 100644 index 000000000..cdd0ff6c2 Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_linkedin.png differ diff --git a/app/assets/images/pages/account/profile/icon_twitter.png b/app/assets/images/pages/account/profile/icon_twitter.png new file mode 100644 index 000000000..1280ad6df Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_twitter.png differ diff --git a/app/assets/images/pages/account/profile/sample_profile.png b/app/assets/images/pages/account/profile/sample_profile.png new file mode 100644 index 000000000..b61294854 Binary files /dev/null and b/app/assets/images/pages/account/profile/sample_profile.png differ diff --git a/app/assets/images/pages/account/profile/sample_profile_thumb.png b/app/assets/images/pages/account/profile/sample_profile_thumb.png new file mode 100644 index 000000000..bc1e63e13 Binary files /dev/null and b/app/assets/images/pages/account/profile/sample_profile_thumb.png differ diff --git a/app/assets/images/pages/account/profile/work.png b/app/assets/images/pages/account/profile/work.png new file mode 100644 index 000000000..72e659071 Binary files /dev/null and b/app/assets/images/pages/account/profile/work.png differ diff --git a/app/assets/images/pages/play/easy_button.png b/app/assets/images/pages/play/easy_button.png new file mode 100644 index 000000000..c75dc83f9 Binary files /dev/null and b/app/assets/images/pages/play/easy_button.png differ diff --git a/app/assets/images/pages/play/hard_button.png b/app/assets/images/pages/play/hard_button.png new file mode 100644 index 000000000..880b42442 Binary files /dev/null and b/app/assets/images/pages/play/hard_button.png differ diff --git a/app/assets/images/pages/play/ladder/humans_ladder_easy.png b/app/assets/images/pages/play/ladder/humans_ladder_easy.png index ea34dcc5b..095b57688 100644 Binary files a/app/assets/images/pages/play/ladder/humans_ladder_easy.png and b/app/assets/images/pages/play/ladder/humans_ladder_easy.png differ diff --git a/app/assets/images/pages/play/ladder/humans_ladder_hard.png b/app/assets/images/pages/play/ladder/humans_ladder_hard.png index 8cd03225d..90afbd048 100644 Binary files a/app/assets/images/pages/play/ladder/humans_ladder_hard.png and b/app/assets/images/pages/play/ladder/humans_ladder_hard.png differ diff --git a/app/assets/images/pages/play/ladder/humans_ladder_medium.png b/app/assets/images/pages/play/ladder/humans_ladder_medium.png index f4b5fdf94..4d5570ebf 100644 Binary files a/app/assets/images/pages/play/ladder/humans_ladder_medium.png and b/app/assets/images/pages/play/ladder/humans_ladder_medium.png differ diff --git a/app/assets/images/pages/play/ladder/humans_ladder_tutorial.png b/app/assets/images/pages/play/ladder/humans_ladder_tutorial.png index 8e34fc924..d4f1ffffe 100644 Binary files a/app/assets/images/pages/play/ladder/humans_ladder_tutorial.png and b/app/assets/images/pages/play/ladder/humans_ladder_tutorial.png differ diff --git a/app/assets/images/pages/play/ladder/ogres_ladder_easy.png b/app/assets/images/pages/play/ladder/ogres_ladder_easy.png index d5e4695ff..ae82c36ca 100644 Binary files a/app/assets/images/pages/play/ladder/ogres_ladder_easy.png and b/app/assets/images/pages/play/ladder/ogres_ladder_easy.png differ diff --git a/app/assets/images/pages/play/ladder/ogres_ladder_medium.png b/app/assets/images/pages/play/ladder/ogres_ladder_medium.png index 5e327d74b..a86ac9585 100644 Binary files a/app/assets/images/pages/play/ladder/ogres_ladder_medium.png and b/app/assets/images/pages/play/ladder/ogres_ladder_medium.png differ diff --git a/app/assets/images/pages/play/ladder/ogres_ladder_tutorial.png b/app/assets/images/pages/play/ladder/ogres_ladder_tutorial.png index 16e952728..939a534c7 100644 Binary files a/app/assets/images/pages/play/ladder/ogres_ladder_tutorial.png and b/app/assets/images/pages/play/ladder/ogres_ladder_tutorial.png differ diff --git a/app/assets/images/pages/play/ladder/prize_aws.png b/app/assets/images/pages/play/ladder/prize_aws.png new file mode 100644 index 000000000..65facdf8f Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_aws.png differ diff --git a/app/assets/images/pages/play/ladder/prize_cash1.png b/app/assets/images/pages/play/ladder/prize_cash1.png new file mode 100644 index 000000000..2508a7e3c Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_cash1.png differ diff --git a/app/assets/images/pages/play/ladder/prize_cash2.png b/app/assets/images/pages/play/ladder/prize_cash2.png new file mode 100644 index 000000000..2b13bcc3d Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_cash2.png differ diff --git a/app/assets/images/pages/play/ladder/prize_cash3.png b/app/assets/images/pages/play/ladder/prize_cash3.png new file mode 100644 index 000000000..c70f428ab Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_cash3.png differ diff --git a/app/assets/images/pages/play/ladder/prize_custom_avatar.png b/app/assets/images/pages/play/ladder/prize_custom_avatar.png new file mode 100644 index 000000000..fae6cf950 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_custom_avatar.png differ diff --git a/app/assets/images/pages/play/ladder/prize_custom_wizard.png b/app/assets/images/pages/play/ladder/prize_custom_wizard.png new file mode 100644 index 000000000..108a31e69 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_custom_wizard.png differ diff --git a/app/assets/images/pages/play/ladder/prize_digital_ocean.png b/app/assets/images/pages/play/ladder/prize_digital_ocean.png new file mode 100644 index 000000000..e9c0406c1 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_digital_ocean.png differ diff --git a/app/assets/images/pages/play/ladder/prize_firebase.png b/app/assets/images/pages/play/ladder/prize_firebase.png new file mode 100644 index 000000000..3aec861c6 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_firebase.png differ diff --git a/app/assets/images/pages/play/ladder/prize_heap.png b/app/assets/images/pages/play/ladder/prize_heap.png new file mode 100644 index 000000000..2770d0456 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_heap.png differ diff --git a/app/assets/images/pages/play/ladder/prize_one_month.png b/app/assets/images/pages/play/ladder/prize_one_month.png new file mode 100644 index 000000000..be2942a09 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_one_month.png differ diff --git a/app/assets/images/pages/play/ladder/prize_oreilly.png b/app/assets/images/pages/play/ladder/prize_oreilly.png new file mode 100644 index 000000000..f0430a695 Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_oreilly.png differ diff --git a/app/assets/images/pages/play/ladder/prize_webstorm.png b/app/assets/images/pages/play/ladder/prize_webstorm.png new file mode 100644 index 000000000..dba0a30dc Binary files /dev/null and b/app/assets/images/pages/play/ladder/prize_webstorm.png differ diff --git a/app/assets/images/pages/play/medium_button.png b/app/assets/images/pages/play/medium_button.png new file mode 100644 index 000000000..41f87572e Binary files /dev/null and b/app/assets/images/pages/play/medium_button.png differ diff --git a/app/assets/images/pages/play/warmup_button.png b/app/assets/images/pages/play/warmup_button.png new file mode 100644 index 000000000..ccc6503b2 Binary files /dev/null and b/app/assets/images/pages/play/warmup_button.png differ diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js new file mode 100644 index 000000000..a75469a29 --- /dev/null +++ b/app/assets/javascripts/workers/aether_worker.js @@ -0,0 +1,99 @@ +var window = self; +var Global = self; + +importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); +console.log("Aether Tome worker has finished importing scripts."); +var aethers = {}; + +var createAether = function (spellKey, options) +{ + aethers[spellKey] = new Aether(options); + return JSON.stringify({ + "message": "Created aether for " + spellKey, + "function": "createAether" + }); +}; + +var hasChangedSignificantly = function(spellKey, a,b,careAboutLineNumbers,careAboutLint) { + + var hasChanged = aethers[spellKey].hasChangedSignificantly(a,b,careAboutLineNumbers,careAboutLint); + var functionName = "hasChangedSignificantly"; + var returnObject = { + "function":functionName, + "hasChanged": hasChanged, + "spellKey": spellKey + }; + return JSON.stringify(returnObject); +}; + +var updateLanguageAether = function(newLanguage) +{ + for (var spellKey in aethers) + { + if (aethers.hasOwnProperty(spellKey)) + { + aethers[spellKey].setLanguage(newLanguage); + } + + } +}; + +var lint = function(spellKey, source) +{ + var currentAether = aethers[spellKey]; + var lintMessages = currentAether.lint(source); + var functionName = "lint"; + var returnObject = { + "lintMessages": lintMessages, + "function": functionName + }; + return JSON.stringify(returnObject); +}; + +var transpile = function(spellKey, source) +{ + var currentAether = aethers[spellKey]; + currentAether.transpile(source); + var functionName = "transpile"; + var returnObject = { + "problems": currentAether.problems, + "function": functionName, + "spellKey": spellKey + }; + return JSON.stringify(returnObject); +}; +self.addEventListener('message', function(e) { + var data = JSON.parse(e.data); + if (data.function == "createAether") + { + self.postMessage(createAether(data.spellKey, data.options)); + } + else if (data.function == "updateLanguageAether") + { + updateLanguageAether(data.newLanguage) + } + else if (data.function == "hasChangedSignificantly") + { + self.postMessage(hasChangedSignificantly( + data.spellKey, + data.a, + data.b, + data.careAboutLineNumbers, + data.careAboutLint + )); + } + else if (data.function == "lint") + { + self.postMessage(lint(data.spellKey, data.source)); + } + else if (data.function == "transpile") + { + self.postMessage(transpile(data.spellKey, data.source)); + } + else + { + var message = "Didn't execute any function..."; + var returnObject = {"message":message, "function":"none"}; + self.postMessage(JSON.stringify(returnObject)); + } +}, false); diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js index 45c5e80d7..9b6264173 100644 --- a/app/assets/javascripts/workers/worker_world.js +++ b/app/assets/javascripts/workers/worker_world.js @@ -1,7 +1,5 @@ -// There's no reason that this file is in JavaScript instead of CoffeeScript. -// We should convert it and update the brunch config. +// This file is in JavaScript because we can't figure out how to get brunch to compile it bare. -// If we wanted to be more robust, we could use this: https://github.com/padolsey/operative/blob/master/src/operative.js if(typeof window !== 'undefined' || !self.importScripts) throw "Attempt to load worker_world into main window instead of web worker."; @@ -14,8 +12,8 @@ if (!Function.prototype.bind) { throw new TypeError("Function.prototype.bind (Shim) - target is not callable"); } - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof fNOP && oThis @@ -31,7 +29,7 @@ if (!Function.prototype.bind) { }; } -// assign global window so that Brunch's require (in world.js) can go into it +// Assign global window so that Brunch's require (in world.js) can go into it self.window = self; self.workerID = "Worker"; @@ -42,7 +40,7 @@ var console = { if(self.logsLogged++ == self.logLimit) self.postMessage({type: 'console-log', args: ["Log limit " + self.logLimit + " reached; shutting up."], id: self.workerID}); else if(self.logsLogged < self.logLimit) { - args = [].slice.call(arguments); + var args = [].slice.call(arguments); for(var i = 0; i < args.length; ++i) { if(args[i] && args[i].constructor) { if(args[i].constructor.className === "Thang" || args[i].isComponent) @@ -57,10 +55,10 @@ var console = { } } }}; // so that we don't crash when debugging statements happen -console.error = console.info = console.log; +console.error = console.warn = console.info = console.debug = console.log; self.console = console; -importScripts('/javascripts/world.js'); +self.importScripts('/javascripts/world.js', '/javascripts/lodash.js', '/javascripts/aether.js'); // We could do way more from this: http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment Object.defineProperty(self, "XMLHttpRequest", { @@ -69,31 +67,273 @@ Object.defineProperty(self, "XMLHttpRequest", { }); self.transferableSupported = function transferableSupported() { + if (typeof self._transferableSupported !== 'undefined') return self._transferableSupported; // Not in IE, even in IE 11 try { var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]); - return ab.byteLength == 0; + return self._transferableSupported = ab.byteLength == 0; } catch(error) { - return false; + return self._transferableSupported = false; } - return false; -} + return self._transferableSupported = false; +}; var World = self.require('lib/world/world'); var GoalManager = self.require('lib/world/GoalManager'); +Aether.addGlobal('Vector', require('lib/world/vector')); +Aether.addGlobal('_', _); + +var serializedClasses = { + "Thang": self.require('lib/world/thang'), + "Vector": self.require('lib/world/vector'), + "Rectangle": self.require('lib/world/rectangle') +}; +self.currentUserCodeMapCopy = ""; +self.currentDebugWorldFrame = 0; + +self.stringifyValue = function(value, depth) { + var brackets, i, isArray, isObject, key, prefix, s, sep, size, v, values, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3; + if (!value || _.isString(value)) { + return value; + } + if (_.isFunction(value)) { + if (depth === 2) { + return void 0; + } else { + return ""; + } + } + if (value === this.thang && depth) { + return ""; + } + if (depth === 2) { + if (((_ref = value.constructor) != null ? _ref.className : void 0) === "Thang") { + value = "<" + (value.type || value.spriteName) + " - " + value.id + ", " + (value.pos ? value.pos.toString() : 'non-physical') + ">"; + } else { + value = value.toString(); + } + return value; + } + isArray = _.isArray(value); + isObject = _.isObject(value); + if (!(isArray || isObject)) { + return value.toString(); + } + brackets = isArray ? ["[", "]"] : ["{", "}"]; + size = _.size(value); + if (!size) { + return brackets.join(""); + } + values = []; + if (isArray) { + for (_i = 0, _len = value.length; _i < _len; _i++) { + v = value[_i]; + s = this.stringifyValue(v, depth + 1); + if (s !== void 0) { + values.push("" + s); + } + } + } else { + _ref2 = (_ref1 = value.apiProperties) != null ? _ref1 : _.keys(value); + for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { + key = _ref2[_j]; + if (key[0] === "_") continue; + s = this.stringifyValue(value[key], depth + 1); + if (s !== void 0) { + values.push(key + ": " + s); + } + } + } + sep = '\n' + ((function() { + var _k, _results; + _results = []; + for (i = _k = 0; 0 <= depth ? _k < depth : _k > depth; i = 0 <= depth ? ++_k : --_k) { + _results.push(" "); + } + return _results; + })()).join(''); + prefix = (_ref3 = value.constructor) != null ? _ref3.className : void 0; + if (isArray) { + if (prefix == null) { + prefix = "Array"; + } + } + if (isObject) { + if (prefix == null) { + prefix = "Object"; + } + } + prefix = prefix ? prefix + " " : ""; + return "" + prefix + brackets[0] + sep + " " + (values.join(sep + ' ')) + sep + brackets[1]; +}; + + +self.retrieveValueFromFrame = function retrieveValueFromFrame(args) { + + var retrieveProperty = function retrieveProperty(currentThangID, currentSpellID, variableChain) + { + var prop; + var value; + var keys = []; + for (var i = 0, len = variableChain.length; i < len; i++) { + prop = variableChain[i]; + if (prop === "this") + { + value = self.debugWorld.thangMap[currentThangID]; + + } + else if (i === 0) + { + try + { + if (Aether.globals[prop]) + { + value = Aether.globals[prop]; + } + else + { + var flowStates = self.debugWorld.userCodeMap[currentThangID][currentSpellID].flow.states; + //we have to go to the second last flowState as we run the world for one additional frame + //to collect the flow + value = _.last(flowStates[flowStates.length - 1].statements).variables[prop]; + } + } + catch (e) + { + value = undefined; + } + + } + else + { + value = value[prop]; + } + keys.push(prop); + if (!value) break; + var classOfValue; + if (classOfValue = serializedClasses[value.CN]) + { + if (value.CN === "Thang") + { + var thang = self.debugWorld.thangMap[value.id]; + value = thang || ""; + } + else + { + value = classOfValue.deserializeFromAether(value); + } + } + } + var serializedProperty = { + "key": keys.join("."), + "value": self.stringifyValue(value,0) + }; + self.postMessage({type: 'debug-value-return', serialized: serializedProperty}); + }; + self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap); + self.setupDebugWorldToRunUntilFrame(args); + self.debugWorld.loadFrames( + retrieveProperty.bind({}, args.currentThangID, args.currentSpellID, args.variableChain), + self.onDebugWorldError, + self.onDebugWorldProgress, + false, + args.frame + ); +}; + +self.enableFlowOnThangSpell = function (thangID, spellID, userCodeMap) { + try { + var options = userCodeMap[thangID][spellID].originalOptions; + if (options.includeFlow === true && options.noSerializationInFlow === true) + return; + else + { + options.includeFlow = true; + options.noSerializationInFlow = true; + var temporaryAether = Aether.deserialize(userCodeMap[thangID][spellID]); + temporaryAether.transpile(temporaryAether.raw); + userCodeMap[thangID][spellID] = temporaryAether.serialize(); + } + + } + catch (error) { + console.log("Debug error enabling flow on", thangID, spellID + ":", error.toString() + "\n" + error.stack || error.stackTrace); + } +}; + +self.setupDebugWorldToRunUntilFrame = function (args) { + self.debugPostedErrors = {}; + self.debugt0 = new Date(); + self.debugPostedErrors = false; + self.logsLogged = 0; + + var stringifiedUserCodeMap = JSON.stringify(args.userCodeMap); + var userCodeMapHasChanged = ! _.isEqual(self.currentUserCodeMapCopy, stringifiedUserCodeMap); + self.currentUserCodeMapCopy = stringifiedUserCodeMap; + if (!self.debugWorld || userCodeMapHasChanged || args.frame < self.currentDebugWorldFrame) { + try { + self.debugWorld = new World(args.userCodeMap); + self.debugWorld.levelSessionIDs = args.levelSessionIDs; + if (args.level) + self.debugWorld.loadFromLevel(args.level, true); + self.debugWorld.debugging = true; + self.debugGoalManager = new GoalManager(self.debugWorld); + self.debugGoalManager.setGoals(args.goals); + self.debugGoalManager.setCode(args.userCodeMap); + self.debugGoalManager.worldGenerationWillBegin(); + self.debugWorld.setGoalManager(self.debugGoalManager); + } + catch (error) { + self.onDebugWorldError(error); + return; + } + Math.random = self.debugWorld.rand.randf; // so user code is predictable + } + self.debugWorld.totalFrames = args.frame; //hack to work around error checking + self.currentDebugWorldFrame = args.frame; +}; + + +self.onDebugWorldLoaded = function onDebugWorldLoaded() { + self.postMessage({type: 'debug-world-loaded'}); +}; + +self.onDebugWorldError = function onDebugWorldError(error) { + + if(!error.isUserCodeProblem) { + console.log("Debug Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace); + } + return true; +}; + +self.onDebugWorldProgress = function onDebugWorldProgress(progress) { + self.postMessage({type: 'debug-world-load-progress-changed', progress: progress}); +}; + +self.debugAbort = function () { + if(self.debugWorld) { + self.debugWorld.abort(); + self.debugWorld.destroy(); + self.debugWorld = null; + } + self.postMessage({type: 'debug-abort'}); +}; + self.runWorld = function runWorld(args) { self.postedErrors = {}; self.t0 = new Date(); - self.firstWorld = args.firstWorld; self.postedErrors = false; self.logsLogged = 0; - + try { - self.world = new World(args.worldName, args.userCodeMap); + self.world = new World(args.userCodeMap); + self.world.levelSessionIDs = args.levelSessionIDs; if(args.level) self.world.loadFromLevel(args.level, true); + self.world.preloading = args.preload; + self.world.headless = args.headless; self.goalManager = new GoalManager(self.world); self.goalManager.setGoals(args.goals); self.goalManager.setCode(args.userCodeMap); @@ -105,13 +345,19 @@ self.runWorld = function runWorld(args) { return; } Math.random = self.world.rand.randf; // so user code is predictable + self.postMessage({type: 'start-load-frames'}); self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress); }; self.onWorldLoaded = function onWorldLoaded() { self.goalManager.worldGenerationEnded(); + var goalStates = self.goalManager.getGoalStates(); + self.postMessage({type: 'end-load-frames', goalStates: goalStates}); var t1 = new Date(); var diff = t1 - self.t0; + if (self.world.headless) + return console.log('Headless simulation completed in ' + diff + 'ms.'); + var transferableSupported = self.transferableSupported(); try { var serialized = self.world.serialize(); @@ -122,32 +368,35 @@ self.onWorldLoaded = function onWorldLoaded() { var t2 = new Date(); //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); try { + var message = {type: 'new-world', serialized: serialized.serializedWorld, goalStates: goalStates}; if(transferableSupported) - self.postMessage({type: 'new-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()}, serialized.transferableObjects); + self.postMessage(message, serialized.transferableObjects); else - self.postMessage({type: 'new-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()}); + self.postMessage(message); } catch(error) { console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace); } var t3 = new Date(); console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms"); + self.world.goalManager.destroy(); + self.world.destroy(); self.world = null; }; self.onWorldError = function onWorldError(error) { - if(error instanceof Aether.problems.UserCodeProblem) { - if(!self.postedErrors[error.key]) { - var problem = error.serialize(); - self.postMessage({type: 'user-code-problem', problem: problem}); - self.postedErrors[error.key] = problem; + if(error.isUserCodeProblem) { + var errorKey = error.userInfo.key; + if(!errorKey || !self.postedErrors[errorKey]) { + self.postMessage({type: 'user-code-problem', problem: error}); + self.postedErrors[errorKey] = error; } } else { console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace); } /* We don't actually have the recoverable property any more; hmm - if(!self.firstWorld && !error.recoverable) { + if(!error.recoverable) { self.abort(); return false; } @@ -160,18 +409,22 @@ self.onWorldLoadProgress = function onWorldLoadProgress(progress) { }; self.abort = function abort() { - if(self.world && self.world.name) { - console.log("About to abort:", self.world.name, typeof self.world.abort); - if(typeof self.world !== "undefined") - self.world.abort(); + if(self.world) { + self.world.abort(); + self.world.goalManager.destroy(); + self.world.destroy(); self.world = null; } self.postMessage({type: 'abort'}); }; self.reportIn = function reportIn() { - self.postMessage({type: 'reportIn'}); -} + self.postMessage({type: 'report-in'}); +}; + +self.finalizePreload = function finalizePreload() { + self.world.finalizePreload(self.onWorldLoaded); +}; self.addEventListener('message', function(event) { self[event.data.func](event.data.args); diff --git a/app/assets/lib/ace/mode-clojure.js b/app/assets/lib/ace/mode-clojure.js new file mode 100644 index 000000000..0ce87413c --- /dev/null +++ b/app/assets/lib/ace/mode-clojure.js @@ -0,0 +1 @@ +ace.define("ace/mode/clojure",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/clojure_highlight_rules","ace/mode/matching_parens_outdent","ace/range"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("./clojure_highlight_rules").ClojureHighlightRules,o=e("./matching_parens_outdent").MatchingParensOutdent,u=e("../range").Range,a=function(){this.HighlightRules=s,this.$outdent=new o};r.inherits(a,i),function(){this.lineCommentStart=";",this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"){var o=t.match(/[\(\[]/);o&&(r+=" "),o=t.match(/[\)]/),o&&(r="")}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.$id="ace/mode/clojure"}.call(a.prototype),t.Mode=a}),ace.define("ace/mode/clojure_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){var e="* *1 *2 *3 *agent* *allow-unresolved-vars* *assert* *clojure-version* *command-line-args* *compile-files* *compile-path* *e *err* *file* *flush-on-newline* *in* *macro-meta* *math-context* *ns* *out* *print-dup* *print-length* *print-level* *print-meta* *print-readably* *read-eval* *source-path* *use-context-classloader* *warn-on-reflection* + - -> ->> .. / < <= = == > > >= >= accessor aclone add-classpath add-watch agent agent-errors aget alength alias all-ns alter alter-meta! alter-var-root amap ancestors and apply areduce array-map aset aset-boolean aset-byte aset-char aset-double aset-float aset-int aset-long aset-short assert assoc assoc! assoc-in associative? atom await await-for await1 bases bean bigdec bigint binding bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor boolean boolean-array booleans bound-fn bound-fn* butlast byte byte-array bytes cast char char-array char-escape-string char-name-string char? chars chunk chunk-append chunk-buffer chunk-cons chunk-first chunk-next chunk-rest chunked-seq? class class? clear-agent-errors clojure-version coll? comment commute comp comparator compare compare-and-set! compile complement concat cond condp conj conj! cons constantly construct-proxy contains? count counted? create-ns create-struct cycle dec decimal? declare definline defmacro defmethod defmulti defn defn- defonce defstruct delay delay? deliver deref derive descendants destructure disj disj! dissoc dissoc! distinct distinct? doall doc dorun doseq dosync dotimes doto double double-array doubles drop drop-last drop-while empty empty? ensure enumeration-seq eval even? every? false? ffirst file-seq filter find find-doc find-ns find-var first float float-array float? floats flush fn fn? fnext for force format future future-call future-cancel future-cancelled? future-done? future? gen-class gen-interface gensym get get-in get-method get-proxy-class get-thread-bindings get-validator hash hash-map hash-set identical? identity if-let if-not ifn? import in-ns inc init-proxy instance? int int-array integer? interleave intern interpose into into-array ints io! isa? iterate iterator-seq juxt key keys keyword keyword? last lazy-cat lazy-seq let letfn line-seq list list* list? load load-file load-reader load-string loaded-libs locking long long-array longs loop macroexpand macroexpand-1 make-array make-hierarchy map map? mapcat max max-key memfn memoize merge merge-with meta method-sig methods min min-key mod name namespace neg? newline next nfirst nil? nnext not not-any? not-empty not-every? not= ns ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers ns-resolve ns-unalias ns-unmap nth nthnext num number? odd? or parents partial partition pcalls peek persistent! pmap pop pop! pop-thread-bindings pos? pr pr-str prefer-method prefers primitives-classnames print print-ctor print-doc print-dup print-method print-namespace-doc print-simple print-special-doc print-str printf println println-str prn prn-str promise proxy proxy-call-with-super proxy-mappings proxy-name proxy-super push-thread-bindings pvalues quot rand rand-int range ratio? rational? rationalize re-find re-groups re-matcher re-matches re-pattern re-seq read read-line read-string reduce ref ref-history-count ref-max-history ref-min-history ref-set refer refer-clojure release-pending-sends rem remove remove-method remove-ns remove-watch repeat repeatedly replace replicate require reset! reset-meta! resolve rest resultset-seq reverse reversible? rseq rsubseq second select-keys send send-off seq seq? seque sequence sequential? set set-validator! set? short short-array shorts shutdown-agents slurp some sort sort-by sorted-map sorted-map-by sorted-set sorted-set-by sorted? special-form-anchor special-symbol? split-at split-with str stream? string? struct struct-map subs subseq subvec supers swap! symbol symbol? sync syntax-symbol-anchor take take-last take-nth take-while test the-ns time to-array to-array-2d trampoline transient tree-seq true? type unchecked-add unchecked-dec unchecked-divide unchecked-inc unchecked-multiply unchecked-negate unchecked-remainder unchecked-subtract underive unquote unquote-splicing update-in update-proxy use val vals var-get var-set var? vary-meta vec vector vector? when when-first when-let when-not while with-bindings with-bindings* with-in-str with-loading-context with-local-vars with-meta with-open with-out-str with-precision xml-seq zero? zipmap",t="throw try var def do fn if let loop monitor-enter monitor-exit new quote recur set!",n="true false nil",r=this.createKeywordMapper({keyword:t,"constant.language":n,"support.function":e},"identifier",!1," ");this.$rules={start:[{token:"comment",regex:";.*$"},{token:"keyword",regex:"[\\(|\\)]"},{token:"keyword",regex:"[\\'\\(]"},{token:"keyword",regex:"[\\[|\\]]"},{token:"keyword",regex:"[\\{|\\}|\\#\\{|\\#\\}]"},{token:"keyword",regex:"[\\&]"},{token:"keyword",regex:"[\\#\\^\\{]"},{token:"keyword",regex:"[\\%]"},{token:"keyword",regex:"[@]"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language",regex:"[!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+||=|!=|<=|>=|<>|<|>|!|&&]"},{token:r,regex:"[a-zA-Z_$][a-zA-Z0-9_$\\-]*\\b"},{token:"string",regex:'"',next:"string"},{token:"constant",regex:/:[^()\[\]{}'"\^%`,;\s]+/},{token:"string.regexp",regex:'/#"(?:\\.|(?:\\")|[^""\n])*"/g'}],string:[{token:"constant.language.escape",regex:"\\\\.|\\\\$"},{token:"string",regex:'[^"\\\\]+'},{token:"string",regex:'"',next:"start"}]}};r.inherits(s,i),t.ClojureHighlightRules=s}),ace.define("ace/mode/matching_parens_outdent",["require","exports","module","ace/range"],function(e,t,n){var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\)/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\))/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){var t=e.match(/^(\s+)/);return t?t[1]:""}}).call(i.prototype),t.MatchingParensOutdent=i}) diff --git a/app/assets/lib/ace/mode-lua.js b/app/assets/lib/ace/mode-lua.js new file mode 100644 index 000000000..c056df5e7 --- /dev/null +++ b/app/assets/lib/ace/mode-lua.js @@ -0,0 +1 @@ +ace.define("ace/mode/lua",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/lua_highlight_rules","ace/mode/folding/lua","ace/range","ace/worker/worker_client"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("./lua_highlight_rules").LuaHighlightRules,o=e("./folding/lua").FoldMode,u=e("../range").Range,a=e("../worker/worker_client").WorkerClient,f=function(){this.HighlightRules=s,this.foldingRules=new o};r.inherits(f,i),function(){function n(t){var n=0;for(var r=0;r0?1:0}this.lineCommentStart="--",this.blockComment={start:"--[",end:"]--"};var e={"function":1,then:1,"do":1,"else":1,elseif:1,repeat:1,end:-1,until:-1},t=["else","elseif","end","until"];this.getNextLineIndent=function(e,t,r){var i=this.$getIndent(t),s=0,o=this.getTokenizer().getLineTokens(t,e),u=o.tokens;return e=="start"&&(s=n(u)),s>0?i+r:s<0&&i.substr(i.length-r.length)==r&&!this.checkOutdent(e,t,"\n")?i.substr(0,i.length-r.length):i},this.checkOutdent=function(e,n,r){if(r!="\n"&&r!="\r"&&r!="\r\n")return!1;if(n.match(/^\s*[\)\}\]]$/))return!0;var i=this.getTokenizer().getLineTokens(n.trim(),e).tokens;return!i||!i.length?!1:i[0].type=="keyword"&&t.indexOf(i[0].value)!=-1},this.autoOutdent=function(e,t,r){var i=t.getLine(r-1),s=this.$getIndent(i).length,o=this.getTokenizer().getLineTokens(i,"start").tokens,a=t.getTabString().length,f=s+a*n(o),l=this.$getIndent(t.getLine(r)).length;if(l|<=|=>|==|~=|=|\\:|\\.\\.\\.|\\.\\."},{token:"paren.lparen",regex:"[\\[\\(\\{]"},{token:"paren.rparen",regex:"[\\]\\)\\}]"},{token:"text",regex:"\\s+|\\w+"}]},this.normalizeRules()};r.inherits(s,i),t.LuaHighlightRules=s}),ace.define("ace/mode/folding/lua",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode","ace/range","ace/token_iterator"],function(e,t,n){var r=e("../../lib/oop"),i=e("./fold_mode").FoldMode,s=e("../../range").Range,o=e("../../token_iterator").TokenIterator,u=t.FoldMode=function(){};r.inherits(u,i),function(){this.foldingStartMarker=/\b(function|then|do|repeat)\b|{\s*$|(\[=*\[)/,this.foldingStopMarker=/\bend\b|^\s*}|\]=*\]/,this.getFoldWidget=function(e,t,n){var r=e.getLine(n),i=this.foldingStartMarker.test(r),s=this.foldingStopMarker.test(r);if(i&&!s){var o=r.match(this.foldingStartMarker);if(o[1]=="then"&&/\belseif\b/.test(r))return;if(o[1]){if(e.getTokenAt(n,o.index+1).type==="keyword")return"start"}else{if(!o[2])return"start";var u=e.bgTokenizer.getState(n)||"";if(u[0]=="bracketedComment"||u[0]=="bracketedString")return"start"}}if(t!="markbeginend"||!s||i&&s)return"";var o=r.match(this.foldingStopMarker);if(o[0]==="end"){if(e.getTokenAt(n,o.index+1).type==="keyword")return"end"}else{if(o[0][0]!=="]")return"end";var u=e.bgTokenizer.getState(n-1)||"";if(u[0]=="bracketedComment"||u[0]=="bracketedString")return"end"}},this.getFoldWidgetRange=function(e,t,n){var r=e.doc.getLine(n),i=this.foldingStartMarker.exec(r);if(i)return i[1]?this.luaBlock(e,n,i.index+1):i[2]?e.getCommentFoldRange(n,i.index+1):this.openingBracketBlock(e,"{",n,i.index);var i=this.foldingStopMarker.exec(r);if(i)return i[0]==="end"&&e.getTokenAt(n,i.index+1).type==="keyword"?this.luaBlock(e,n,i.index+1):i[0][0]==="]"?e.getCommentFoldRange(n,i.index+1):this.closingBracketBlock(e,"}",n,i.index+i[0].length)},this.luaBlock=function(e,t,n){var r=new o(e,t,n),i={"function":1,"do":1,then:1,elseif:-1,end:-1,repeat:1,until:-1},u=r.getCurrentToken();if(!u||u.type!="keyword")return;var a=u.value,f=[a],l=i[a];if(!l)return;var c=l===-1?r.getCurrentTokenColumn():e.getLine(t).length,h=t;r.step=l===-1?r.stepBackward:r.stepForward;while(u=r.step()){if(u.type!=="keyword")continue;var p=l*i[u.value];if(p>0)f.unshift(u.value);else if(p<=0){f.shift();if(!f.length&&u.value!="elseif")break;p===0&&f.unshift(u.value)}}var t=r.getCurrentTokenRow();return l===-1?new s(t,e.getLine(t).length,h,c):new s(h,c,t,r.getCurrentTokenColumn())}}.call(u.prototype)}) diff --git a/app/assets/lib/ace/mode-python.js b/app/assets/lib/ace/mode-python.js new file mode 100644 index 000000000..69c918f04 --- /dev/null +++ b/app/assets/lib/ace/mode-python.js @@ -0,0 +1 @@ +ace.define("ace/mode/python",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/python_highlight_rules","ace/mode/folding/pythonic","ace/range"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("./python_highlight_rules").PythonHighlightRules,o=e("./folding/pythonic").FoldMode,u=e("../range").Range,a=function(){this.HighlightRules=s,this.foldingRules=new o("\\:")};r.inherits(a,i),function(){this.lineCommentStart="#",this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"){var o=t.match(/^.*[\{\(\[\:]\s*$/);o&&(r+=n)}return r};var e={pass:1,"return":1,raise:1,"break":1,"continue":1};this.checkOutdent=function(t,n,r){if(r!=="\r\n"&&r!=="\r"&&r!=="\n")return!1;var i=this.getTokenizer().getLineTokens(n.trim(),t).tokens;if(!i)return!1;do var s=i.pop();while(s&&(s.type=="comment"||s.type=="text"&&s.value.match(/^\s+$/)));return s?s.type=="keyword"&&e[s.value]:!1},this.autoOutdent=function(e,t,n){n+=1;var r=this.$getIndent(t.getLine(n)),i=t.getTabString();r.slice(-i.length)==i&&t.remove(new u(n,r.length-i.length,n,r.length))},this.$id="ace/mode/python"}.call(a.prototype),t.Mode=a}),ace.define("ace/mode/python_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){var e="and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield",t="True|False|None|NotImplemented|Ellipsis|__debug__",n="abs|divmod|input|open|staticmethod|all|enumerate|int|ord|str|any|eval|isinstance|pow|sum|basestring|execfile|issubclass|print|super|binfile|iter|property|tuple|bool|filter|len|range|type|bytearray|float|list|raw_input|unichr|callable|format|locals|reduce|unicode|chr|frozenset|long|reload|vars|classmethod|getattr|map|repr|xrange|cmp|globals|max|reversed|zip|compile|hasattr|memoryview|round|__import__|complex|hash|min|set|apply|delattr|help|next|setattr|buffer|dict|hex|object|slice|coerce|dir|id|oct|sorted|intern",r=this.createKeywordMapper({"invalid.deprecated":"debugger","support.function":n,"constant.language":t,keyword:e},"identifier"),i="(?:r|u|ur|R|U|UR|Ur|uR)?",s="(?:(?:[1-9]\\d*)|(?:0))",o="(?:0[oO]?[0-7]+)",u="(?:0[xX][\\dA-Fa-f]+)",a="(?:0[bB][01]+)",f="(?:"+s+"|"+o+"|"+u+"|"+a+")",l="(?:[eE][+-]?\\d+)",c="(?:\\.\\d+)",h="(?:\\d+)",p="(?:(?:"+h+"?"+c+")|(?:"+h+"\\.))",d="(?:(?:"+p+"|"+h+")"+l+")",v="(?:"+d+"|"+p+")",m="\\\\(x[0-9A-Fa-f]{2}|[0-7]{3}|[\\\\abfnrtv'\"]|U[0-9A-Fa-f]{8}|u[0-9A-Fa-f]{4})";this.$rules={start:[{token:"comment",regex:"#.*$"},{token:"string",regex:i+'"{3}',next:"qqstring3"},{token:"string",regex:i+'"(?=.)',next:"qqstring"},{token:"string",regex:i+"'{3}",next:"qstring3"},{token:"string",regex:i+"'(?=.)",next:"qstring"},{token:"constant.numeric",regex:"(?:"+v+"|\\d+)[jJ]\\b"},{token:"constant.numeric",regex:v},{token:"constant.numeric",regex:f+"[lL]\\b"},{token:"constant.numeric",regex:f+"\\b"},{token:r,regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|="},{token:"paren.lparen",regex:"[\\[\\(\\{]"},{token:"paren.rparen",regex:"[\\]\\)\\}]"},{token:"text",regex:"\\s+"}],qqstring3:[{token:"constant.language.escape",regex:m},{token:"string",regex:'"{3}',next:"start"},{defaultToken:"string"}],qstring3:[{token:"constant.language.escape",regex:m},{token:"string",regex:"'{3}",next:"start"},{defaultToken:"string"}],qqstring:[{token:"constant.language.escape",regex:m},{token:"string",regex:"\\\\$",next:"qqstring"},{token:"string",regex:'"|$',next:"start"},{defaultToken:"string"}],qstring:[{token:"constant.language.escape",regex:m},{token:"string",regex:"\\\\$",next:"qstring"},{token:"string",regex:"'|$",next:"start"},{defaultToken:"string"}]}};r.inherits(s,i),t.PythonHighlightRules=s}),ace.define("ace/mode/folding/pythonic",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode"],function(e,t,n){var r=e("../../lib/oop"),i=e("./fold_mode").FoldMode,s=t.FoldMode=function(e){this.foldingStartMarker=new RegExp("([\\[{])(?:\\s*)$|("+e+")(?:\\s*)(?:#.*)?$")};r.inherits(s,i),function(){this.getFoldWidgetRange=function(e,t,n){var r=e.getLine(n),i=r.match(this.foldingStartMarker);if(i)return i[1]?this.openingBracketBlock(e,i[1],n,i.index):i[2]?this.indentationBlock(e,n,i.index+i[2].length):this.indentationBlock(e,n)}}.call(s.prototype)}) diff --git a/app/assets/main.html b/app/assets/main.html index 96306649e..b0fac676c 100644 --- a/app/assets/main.html +++ b/app/assets/main.html @@ -34,6 +34,7 @@ + - + + + + + - +
@@ -117,16 +130,9 @@
-
- +
- + ") - $('head').append(script) - window[functionName] = (profile) => - @gravatarProfile = profile - @trigger('change', @) - - func = => @gravatarProfile = null unless @gravatarProfile - setTimeout(func, 1000) - displayName: -> - @get('name') or @gravatarName() or "Anoner" + @get('name') or "Anoner" lang: -> @get('preferredLanguage') or "en-US" - gravatarName: -> - @gravatarProfile?.entry[0]?.name?.formatted or '' - - gravatarPhotoURLs: -> - photos = @gravatarProfile?.entry[0]?.photos - return if not photos - (photo.value for photo in photos) - - getPhotoURL: -> - photoURL = @get('photoURL') - validURLs = @gravatarPhotoURLs() - return @gravatarAvatarURL() unless validURLs and validURLs.length - return validURLs[0] unless photoURL in validURLs - return photoURL + getPhotoURL: (size=80, useJobProfilePhoto=false) -> + photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null + photoURL ||= @get('photoURL') + if photoURL + prefix = if photoURL.search(/\?/) is -1 then "?" else "&" + return "#{photoURL}#{prefix}s=#{size}" if photoURL.search('http') isnt -1 # legacy + return "/file/#{photoURL}#{prefix}s=#{size}" + return "/db/user/#{@id}/avatar?s=#{size}" @getByID = (id, properties, force) -> {me} = require('lib/auth') @@ -66,7 +40,37 @@ module.exports = class User extends CocoModel success: -> user.loading = false Backbone.Mediator.publish('user:fetched') - user.loadGravatarProfile() + #user.trigger 'sync' # needed? ) cache[id] = user user + + getEnabledEmails: -> + @migrateEmails() + emails = _.clone(@get('emails')) or {} + emails = _.defaults emails, @schema().properties.emails.default + (emailName for emailName, emailDoc of emails when emailDoc.enabled) + + setEmailSubscription: (name, enabled) -> + newSubs = _.clone(@get('emails')) or {} + (newSubs[name] ?= {}).enabled = enabled + @set 'emails', newSubs + + emailMap: + announcement: 'generalNews' + developer: 'archmageNews' + tester: 'adventurerNews' + level_creator: 'artisanNews' + article_editor: 'scribeNews' + translator: 'diplomatNews' + support: 'ambassadorNews' + notification: 'anyNotes' + + migrateEmails: -> + return if @attributes.emails or not @attributes.emailSubscriptions + oldSubs = @get('emailSubscriptions') or [] + newSubs = {} + newSubs[newSubName] = { enabled: oldSubName in oldSubs } for oldSubName, newSubName of @emailMap + @set('emails', newSubs) + + isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled diff --git a/app/schemas/definitions/bus.coffee b/app/schemas/definitions/bus.coffee new file mode 100644 index 000000000..b5625025e --- /dev/null +++ b/app/schemas/definitions/bus.coffee @@ -0,0 +1,14 @@ +module.exports = + bus: + title: "Bus" + id: "bus" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Bus" # TODO + type: "object" + properties: # TODO + joined: + type: "boolean" + players: + type: "object" + required: ["joined", "players"] + additionalProperties: false \ No newline at end of file diff --git a/app/schemas/definitions/misc.coffee b/app/schemas/definitions/misc.coffee new file mode 100644 index 000000000..bbf9f5c02 --- /dev/null +++ b/app/schemas/definitions/misc.coffee @@ -0,0 +1,12 @@ +module.exports = + jQueryEvent: + title: "jQuery Event" + id: "jQueryEvent" + $schema: "http://json-schema.org/draft-04/schema#" + description: "A standard jQuery Event" + type: "object" + properties: # TODO schema complete + altKey: + type: "boolean" + required: [] + additionalProperties: true diff --git a/server/commons/i18n_schema.coffee b/app/schemas/i18n_schema.coffee similarity index 100% rename from server/commons/i18n_schema.coffee rename to app/schemas/i18n_schema.coffee diff --git a/app/schemas/languages.coffee b/app/schemas/languages.coffee new file mode 100644 index 000000000..053a89f2a --- /dev/null +++ b/app/schemas/languages.coffee @@ -0,0 +1,34 @@ +locale = require '../locale/locale' # requiring from app; will break if we stop serving from where app lives + +languages = [] +for code, localeInfo of locale + languages.push code: code, nativeDescription: localeInfo.nativeDescription, englishDescription: localeInfo.englishDescription + +module.exports.languages = languages +module.exports.languageCodes = languageCodes = (language.code for language in languages) +module.exports.languageCodesLower = languageCodesLower = (code.toLowerCase() for code in languageCodes) + +# Keep keys lower-case for matching and values with second subtag uppercase like i18next expects +languageAliases = + 'en': 'en-US' + + 'zh-cn': 'zh-HANS' + 'zh-hans-cn': 'zh-HANS' + 'zh-sg': 'zh-HANS' + 'zh-hans-sg': 'zh-HANS' + + 'zh-tw': 'zh-HANT' + 'zh-hant-tw': 'zh-HANT' + 'zh-hk': 'zh-HANT' + 'zh-hant-hk': 'zh-HANT' + 'zh-mo': 'zh-HANT' + 'zh-hant-mo': 'zh-HANT' + +module.exports.languageCodeFromAcceptedLanguages = languageCodeFromAcceptedLanguages = (acceptedLanguages) -> + for lang in acceptedLanguages ? [] + code = languageAliases[lang.toLowerCase()] + return code if code + codeIndex = _.indexOf languageCodesLower, lang + if codeIndex isnt -1 + return languageCodes[codeIndex] + return 'en-US' diff --git a/server/commons/metaschema.coffee b/app/schemas/metaschema.coffee similarity index 100% rename from server/commons/metaschema.coffee rename to app/schemas/metaschema.coffee diff --git a/server/articles/article_schema.coffee b/app/schemas/models/article.coffee similarity index 50% rename from server/articles/article_schema.coffee rename to app/schemas/models/article.coffee index 1fd4769f7..60f65640f 100644 --- a/server/articles/article_schema.coffee +++ b/app/schemas/models/article.coffee @@ -1,13 +1,14 @@ -c = require '../commons/schemas' +c = require './../schemas' ArticleSchema = c.object() c.extendNamedProperties ArticleSchema # name first ArticleSchema.properties.body = { type: 'string', title: 'Content', format: 'markdown' } -ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['body'] } +ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body'] } -c.extendBasicProperties(ArticleSchema, 'article') -c.extendSearchableProperties(ArticleSchema) -c.extendVersionedProperties(ArticleSchema, 'article') +c.extendBasicProperties ArticleSchema, 'article' +c.extendSearchableProperties ArticleSchema +c.extendVersionedProperties ArticleSchema, 'article' +c.extendPatchableProperties ArticleSchema module.exports = ArticleSchema diff --git a/server/levels/level_schema.coffee b/app/schemas/models/level.coffee similarity index 96% rename from server/levels/level_schema.coffee rename to app/schemas/models/level.coffee index 8d2d60cd3..444ac24c6 100644 --- a/server/levels/level_schema.coffee +++ b/app/schemas/models/level.coffee @@ -1,9 +1,10 @@ -c = require '../commons/schemas' -ThangComponentSchema = require './thangs/thang_component_schema' +c = require './../schemas' +ThangComponentSchema = require './thang_component' SpecificArticleSchema = c.object() c.extendNamedProperties SpecificArticleSchema # name first SpecificArticleSchema.properties.body = { type: 'string', title: 'Content', description: "The body content of the article, in Markdown.", format: 'markdown' } +SpecificArticleSchema.properties.i18n = {type: "object", format: 'i18n', props: ['name', 'body'], description: "Help translate this article"} SpecificArticleSchema.displayProperty = 'name' side = {title: "Side", description: "A side.", type: 'string', 'enum': ['left', 'right', 'top', 'bottom']} @@ -108,9 +109,9 @@ NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes lock: {title: "Lock", description: "Whether the interface should be locked so that the player's focus is on the script, or specific areas to lock.", type: ['boolean', 'array'], items: {type: 'string', enum: ['surface', 'editor', 'palette', 'hud', 'playback', 'playback-hover', 'level', ]}} letterbox: {type: 'boolean', title: 'Letterbox', description:'Turn letterbox mode on or off. Disables surface and playback controls.'} - goals: c.object {title: "Goals", description: "Add or remove goals for the player to complete in the level."}, - add: c.array {title: "Add", description: "Add these goals."}, GoalSchema - remove: c.array {title: "Remove", description: "Remove these goals."}, GoalSchema + goals: c.object {title: "Goals (Old)", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, + add: c.array {title: "Add", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, GoalSchema + remove: c.array {title: "Remove", description: "Deprecated. Goals removed here have no effect. Adjust goals in the level settings instead."}, GoalSchema playback: c.object {title: "Playback", description: "Control the playback of the level."}, playing: {type: 'boolean', title: "Set Playing", description: "Set whether playback is playing or paused."} @@ -130,7 +131,7 @@ NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes surface: c.object {title: "Surface", description: "Commands to issue to the Surface itself."}, focus: c.object {title: "Camera", description: "Focus the camera on a specific point on the Surface.", format:'viewport'}, - target: {anyOf: [PointSchema, thang, {type: 'null'}], title: "Target", description: "Where to center the camera view."} + target: {anyOf: [PointSchema, thang, {type: 'null'}], title: "Target", description: "Where to center the camera view.", default: {x:0, y:0}} zoom: {type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 64, title: "Zoom", description: "What zoom level to use."} duration: {type:'number', minimum: 0, title: "Duration", description: "in ms"} bounds: c.array {title:'Boundary', maxItems: 2, minItems: 2, default:[{x:0,y:0}, {x:46, y:39}], format: 'bounds'}, PointSchema @@ -243,6 +244,7 @@ c.extendBasicProperties LevelSchema, 'level' c.extendSearchableProperties LevelSchema c.extendVersionedProperties LevelSchema, 'level' c.extendPermissionsProperties LevelSchema, 'level' +c.extendPatchableProperties LevelSchema module.exports = LevelSchema diff --git a/server/levels/components/level_component_schema.coffee b/app/schemas/models/level_component.coffee similarity index 97% rename from server/levels/components/level_component_schema.coffee rename to app/schemas/models/level_component.coffee index ac399da2c..8552979ee 100644 --- a/server/levels/components/level_component_schema.coffee +++ b/app/schemas/models/level_component.coffee @@ -1,5 +1,5 @@ -c = require '../../commons/schemas' -metaschema = require '../../commons/metaschema' +c = require './../schemas' +metaschema = require './../metaschema' attackSelfCode = """ class AttacksSelf extends Component @@ -115,5 +115,6 @@ c.extendBasicProperties LevelComponentSchema, 'level.component' c.extendSearchableProperties LevelComponentSchema c.extendVersionedProperties LevelComponentSchema, 'level.component' c.extendPermissionsProperties LevelComponentSchema, 'level.component' +c.extendPatchableProperties LevelComponentSchema module.exports = LevelComponentSchema diff --git a/server/levels/feedbacks/level_feedback_schema.coffee b/app/schemas/models/level_feedback.coffee similarity index 95% rename from server/levels/feedbacks/level_feedback_schema.coffee rename to app/schemas/models/level_feedback.coffee index 54d9e84e1..f8bb6a73c 100644 --- a/server/levels/feedbacks/level_feedback_schema.coffee +++ b/app/schemas/models/level_feedback.coffee @@ -1,4 +1,4 @@ -c = require '../../commons/schemas' +c = require './../schemas' LevelFeedbackLevelSchema = c.object {required: ['original', 'majorVersion']}, { original: c.objectId({}) diff --git a/server/levels/sessions/level_session_schema.coffee b/app/schemas/models/level_session.coffee similarity index 77% rename from server/levels/sessions/level_session_schema.coffee rename to app/schemas/models/level_session.coffee index d798a9d88..a37c98aaf 100644 --- a/server/levels/sessions/level_session_schema.coffee +++ b/app/schemas/models/level_session.coffee @@ -1,4 +1,4 @@ -c = require '../../commons/schemas' +c = require './../schemas' LevelSessionPlayerSchema = c.object id: c.objectId @@ -101,10 +101,24 @@ _.extend LevelSessionSchema.properties, source: type: 'string' -# TODO: specify this more code: type: 'object' - + additionalProperties: + type: 'object' + additionalProperties: + type: 'string' + format: 'javascript' + + codeLanguage: + type: 'string' + default: 'javascript' + + playtime: + type: 'number' + title: 'Playtime' + default: 0 + description: 'The total playtime on this session' + teamSpells: type: 'object' additionalProperties: @@ -134,6 +148,21 @@ _.extend LevelSessionSchema.properties, submittedCode: type: 'object' + additionalProperties: + type: 'object' + additionalProperties: + type: 'string' + + submittedCodeLanguage: + type: 'string' + default: 'javascript' + + transpiledCode: + type: 'object' + additionalProperties: + type: 'object' + additionalProperties: + type: 'string' isRanking: type: 'boolean' @@ -170,6 +199,11 @@ _.extend LevelSessionSchema.properties, date: c.date title: 'Date computed' description: 'The date a match was computed.' + playtime: + title: 'Playtime so far' + description: 'The total seconds of playtime on this session when the match was computed.' + type: 'number' + metrics: type: 'object' title: 'Metrics' @@ -189,11 +223,19 @@ _.extend LevelSessionSchema.properties, sessionID: title: 'Opponent Session ID' description: 'The session ID of an opponent.' - type: ['object', 'string'] + type: ['object', 'string','null'] userID: title: 'Opponent User ID' description: 'The user ID of an opponent' - type: ['object','string'] + type: ['object','string','null'] + name: + title: 'Opponent name' + description: 'The name of the opponent' + type: ['string','null'] + totalScore: + title: 'Opponent total score' + description: 'The totalScore of a user when the match was computed' + type: ['number','string', 'null'] metrics: type: 'object' properties: diff --git a/server/levels/systems/level_system_schema.coffee b/app/schemas/models/level_system.coffee similarity index 96% rename from server/levels/systems/level_system_schema.coffee rename to app/schemas/models/level_system.coffee index cc4bc7891..1804de363 100644 --- a/server/levels/systems/level_system_schema.coffee +++ b/app/schemas/models/level_system.coffee @@ -1,5 +1,5 @@ -c = require '../../commons/schemas' -metaschema = require '../../commons/metaschema' +c = require './../schemas' +metaschema = require './../metaschema' jitterSystemCode = """ class Jitter extends System @@ -101,6 +101,7 @@ _.extend LevelSystemSchema.properties, c.extendBasicProperties LevelSystemSchema, 'level.system' c.extendSearchableProperties LevelSystemSchema c.extendVersionedProperties LevelSystemSchema, 'level.system' -c.extendPermissionsProperties LevelSystemSchema, 'level.system' +c.extendPermissionsProperties LevelSystemSchema +c.extendPatchableProperties LevelSystemSchema module.exports = LevelSystemSchema diff --git a/app/schemas/models/patch.coffee b/app/schemas/models/patch.coffee new file mode 100644 index 000000000..e14423371 --- /dev/null +++ b/app/schemas/models/patch.coffee @@ -0,0 +1,27 @@ +c = require './../schemas' + +patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article'] + +PatchSchema = c.object({title:'Patch', required:['target', 'delta', 'commitMessage']}, { + delta: { title: 'Delta', type:['array', 'object'] } + commitMessage: c.shortString({maxLength: 500, minLength: 1}) + creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) + created: c.date( { title: 'Created', readOnly: true }) + status: { enum: ['pending', 'accepted', 'rejected', 'withdrawn']} + + target: c.object({title: 'Target', required:['collection', 'id']}, { + collection: { enum: patchables } + id: c.objectId(title: 'Target ID') # search by this if not versioned + + # if target is versioned, want to know that info too + original: c.objectId(title: 'Target Original') # search by this if versioned + version: + properties: + major: { type: 'number', minimum: 0 } + minor: { type: 'number', minimum: 0 } + }) +}) + +c.extendBasicProperties(PatchSchema, 'patch') + +module.exports = PatchSchema diff --git a/server/levels/thangs/thang_component_schema.coffee b/app/schemas/models/thang_component.coffee similarity index 95% rename from server/levels/thangs/thang_component_schema.coffee rename to app/schemas/models/thang_component.coffee index 0118d3a4c..eebcf155b 100644 --- a/server/levels/thangs/thang_component_schema.coffee +++ b/app/schemas/models/thang_component.coffee @@ -1,4 +1,4 @@ -c = require '../../commons/schemas' +c = require './../schemas' module.exports = ThangComponentSchema = c.object { title: "Component" diff --git a/server/levels/thangs/thang_type_schema.coffee b/app/schemas/models/thang_type.coffee similarity index 95% rename from server/levels/thangs/thang_type_schema.coffee rename to app/schemas/models/thang_type.coffee index 8b70ccbaf..37624c180 100644 --- a/server/levels/thangs/thang_type_schema.coffee +++ b/app/schemas/models/thang_type.coffee @@ -1,5 +1,5 @@ -c = require '../../commons/schemas' -ThangComponentSchema = require './thang_component_schema' +c = require './../schemas' +ThangComponentSchema = require './thang_component' ThangTypeSchema = c.object() c.extendNamedProperties ThangTypeSchema # name first @@ -123,6 +123,7 @@ _.extend ThangTypeSchema.properties, title: 'Scale' type: 'number' positions: PositionsSchema + raster: { type: 'string', format: 'image-file', title: 'Raster Image' } colorGroups: c.object title: 'Color Groups' additionalProperties: @@ -146,8 +147,9 @@ ThangTypeSchema.definitions = action: ActionSchema sound: SoundSchema -c.extendBasicProperties(ThangTypeSchema, 'thang.type') -c.extendSearchableProperties(ThangTypeSchema) -c.extendVersionedProperties(ThangTypeSchema, 'thang.type') +c.extendBasicProperties ThangTypeSchema, 'thang.type' +c.extendSearchableProperties ThangTypeSchema +c.extendVersionedProperties ThangTypeSchema, 'thang.type' +c.extendPatchableProperties ThangTypeSchema module.exports = ThangTypeSchema diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee new file mode 100644 index 000000000..bf4072469 --- /dev/null +++ b/app/schemas/models/user.coffee @@ -0,0 +1,127 @@ +c = require './../schemas' +emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification'] + +UserSchema = c.object {}, + name: c.shortString({title: 'Display Name', default:''}) + email: c.shortString({title: 'Email', format: 'email'}) + firstName: c.shortString({title: 'First Name'}) + lastName: c.shortString({title: 'Last Name'}) + gender: {type: 'string', 'enum': ['male', 'female']} + password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'} + passwordReset: {type: 'string'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} + + facebookID: c.shortString({title: 'Facebook ID'}) + gplusID: c.shortString({title: 'G+ ID'}) + + wizardColor1: c.pct({title: 'Wizard Clothes Color'}) + volume: c.pct({title: 'Volume'}) + music: {type: 'boolean', default: true} + autocastDelay: {type: 'integer', 'default': 5000 } + lastLevel: { type: 'string' } + + emailSubscriptions: c.array {uniqueItems: true}, {'enum': emailSubscriptions} + emails: c.object {title: "Email Settings", default: {generalNews: {enabled:true}, anyNotes: {enabled:true}, recruitNotes: {enabled:true}}}, + # newsletters + generalNews: { $ref: '#/definitions/emailSubscription' } + adventurerNews: { $ref: '#/definitions/emailSubscription' } + ambassadorNews: { $ref: '#/definitions/emailSubscription' } + archmageNews: { $ref: '#/definitions/emailSubscription' } + artisanNews: { $ref: '#/definitions/emailSubscription' } + diplomatNews: { $ref: '#/definitions/emailSubscription' } + scribeNews: { $ref: '#/definitions/emailSubscription' } + + # notifications + anyNotes: { $ref: '#/definitions/emailSubscription' } # overrides any other notifications settings + recruitNotes: { $ref: '#/definitions/emailSubscription' } + employerNotes: { $ref: '#/definitions/emailSubscription' } + + # server controlled + permissions: c.array {'default': []}, c.shortString() + dateCreated: c.date({title: 'Date Joined'}) + anonymous: {type: 'boolean', 'default': true} + testGroupNumber: {type: 'integer', minimum: 0, maximum: 256, exclusiveMaximum: true} + mailChimp: {type: 'object'} + hourOfCode: {type: 'boolean'} + hourOfCodeComplete: {type: 'boolean'} + + emailLower: c.shortString() + nameLower: c.shortString() + passwordHash: {type: 'string', maxLength: 256} + + # client side + emailHash: {type: 'string'} + + #Internationalization stuff + preferredLanguage: {type: 'string', default: 'en', 'enum': c.getLanguageCodeArray()} + + signedCLA: c.date({title: 'Date Signed the CLA'}) + wizard: c.object {}, + colorConfig: c.object {additionalProperties: c.colorConfig()} + + aceConfig: c.object {}, + language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript', 'clojure', 'lua', 'python']} + keyBindings: {type: 'string', 'default': 'default', 'enum': ['default', 'vim', 'emacs']} + invisibles: {type: 'boolean', 'default': false} + indentGuides: {type: 'boolean', 'default': false} + behaviors: {type: 'boolean', 'default': false} + + simulatedBy: {type: 'integer', minimum: 0, default: 0} + simulatedFor: {type: 'integer', minimum: 0, default: 0} + + jobProfile: c.object {title: 'Job Profile', required: ['lookingFor', 'jobTitle', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', 'visa', 'work', 'education', 'projects', 'links']}, + lookingFor: {title: 'Looking For', type: 'string', enum: ['Full-time', 'Part-time', 'Remote', 'Contracting', 'Internship'], default: 'Full-time', description: 'What kind of developer position do you want?'} + jobTitle: {type: 'string', maxLength: 50, title: 'Desired Job Title', description: 'What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer"', default: 'Software Developer'} + active: {title: 'Open to Offers', type: 'boolean', description: 'Want interview offers right now?'} + updated: c.date {title: 'Last Updated', description: 'How fresh your profile appears to employers. Profiles go inactive after 4 weeks.'} + name: c.shortString {title: 'Name', description: 'Name you want employers to see, like "Nick Winter".'} + city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} + country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} + skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency. Employers will see the first five at a glance.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, + {type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} + experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} + shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} + longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} + visa: c.shortString {title: 'US Work Status', description: 'Are you authorized to work in the US, or do you need visa sponsorship?', enum: ['Authorized to work in the US', 'Need visa sponsorship'], default: 'Authorized to work in the US'} + work: c.array {title: 'Work Experience', description: 'List your relevant work experience, most recent first.'}, + c.object {title: 'Job', description: 'Some work experience you had.', required: ['employer', 'role', 'duration']}, + employer: c.shortString {title: 'Employer', description: 'Name of your employer.'} + role: c.shortString {title: 'Job Title', description: 'What was your job title or role?'} + duration: c.shortString {title: 'Duration', description: 'When did you hold this gig? Ex.: "Feb 2013 - present".'} + description: {type: 'string', title: 'Description', description: 'What did you do there? (140 chars; optional)', maxLength: 140} + education: c.array {title: 'Education', description: 'List your academic ordeals.'}, + c.object {title: 'Ordeal', description: 'Some education that befell you.', required: ['school', 'degree', 'duration']}, + school: c.shortString {title: 'School', description: 'Name of your school.'} + degree: c.shortString {title: 'Degree', description: 'What was your degree and field of study? Ex. Ph.D. Human-Computer Interaction (incomplete)'} + duration: c.shortString {title: 'Dates', description: 'When? Ex.: "Aug 2004 - May 2008".'} + description: {type: 'string', title: 'Description', description: 'Highlight anything about this educational experience. (140 chars; optional)', maxLength: 140} + projects: c.array {title: 'Projects (Top 3)', description: 'Highlight your projects to amaze employers.', maxItems: 3}, + c.object {title: 'Project', description: 'A project you created.', required: ['name', 'description', 'picture'], default: {name: 'My Project', description: 'A project I worked on.', link: 'http://example.com', picture: ''}}, + name: c.shortString {title: 'Project Name', description: 'What was the project called?', default: 'My Project'} + description: {type: 'string', title: 'Description', description: 'Briefly describe the project.', maxLength: 400, default: 'A project I worked on.', format: 'markdown'} + picture: {type: 'string', title: 'Picture', format: 'image-file', description: 'Upload a 230x115px or larger image showing off the project.'} + link: c.url {title: 'Link', description: 'Link to the project.', default: 'http://example.com'} + links: c.array {title: 'Personal and Social Links', description: 'Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog.'}, + c.object {title: 'Link', description: 'A link to another site you want to highlight, like your GitHub, your LinkedIn, or your blog.', required: ['name', 'link']}, + name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "Twitter"', format: 'link-name'} + link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'} + + jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'} + jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''} + employerAt: c.shortString {description: "If given employer permissions to view job candidates, for which employer?"} + signedEmployerAgreement: c.object {}, + linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."} + date: c.date {title: "Date signed employer agreement"} + data: c.object {description: "Cached LinkedIn data slurped from profile."} + + +c.extendBasicProperties UserSchema, 'user' + +c.definitions = + emailSubscription = + enabled: {type: 'boolean'} + lastSent: c.date() + count: {type: 'integer'} + +module.exports = UserSchema diff --git a/server/commons/schemas.coffee b/app/schemas/schemas.coffee similarity index 86% rename from server/commons/schemas.coffee rename to app/schemas/schemas.coffee index 060ff8348..9c20c9ba2 100644 --- a/server/commons/schemas.coffee +++ b/app/schemas/schemas.coffee @@ -1,5 +1,5 @@ #language imports -Language = require '../routes/languages' +Language = require './languages' # schema helper methods me = module.exports @@ -8,14 +8,17 @@ combine = (base, ext) -> return base unless ext? return _.extend(base, ext) +urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-‌​\.\?\,\'\/\\\+&%\$#_=]*)?$' + # Common schema properties me.object = (ext, props) -> combine {type: 'object', additionalProperties: false, properties: props or {}}, ext me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) -me.date = (ext) -> combine({type: 'string', format: 'date-time'}, ext) +me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) +me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} @@ -51,7 +54,21 @@ basicProps = (linkFragment) -> me.extendBasicProperties = (schema, linkFragment) -> schema.properties = {} unless schema.properties? _.extend(schema.properties, basicProps(linkFragment)) + +# PATCHABLE +patchableProps = -> + patches: me.array({title:'Patches'}, { + _id: me.objectId(links: [{rel: "db", href: "/db/patch/{($)}"}], title: "Patch ID", description: "A reference to the patch.") + status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']} + }) + allowPatches: { type: 'boolean' } + watchers: me.array({title:'Watchers'}, + me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])) + +me.extendPatchableProperties = (schema) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, patchableProps()) # NAMED diff --git a/app/schemas/subscriptions/app.coffee b/app/schemas/subscriptions/app.coffee new file mode 100644 index 000000000..29fe52087 --- /dev/null +++ b/app/schemas/subscriptions/app.coffee @@ -0,0 +1,48 @@ +module.exports = + "application:idle-changed": + {} # TODO schema + + "fbapi-loaded": + {} # TODO schema + + "logging-in-with-facebook": + {} # TODO schema + + "facebook-logged-in": + title: "Facebook logged in" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when you successfully logged in with facebook" + type: "object" + properties: + response: + type: "object" + properties: + status: { type: "string" } + authResponse: + type: "object" + properties: + accessToken: { type: "string" } + expiresIn: { type: "number" } + signedRequest: { type: "string" } + userID: { type: "string" } + required: ["response"] + + "facebook-logged-out": {} + + "linkedin-loaded": {} + + "gapi-loaded": + {} # TODO schema + + "logging-in-with-gplus": + {} # TODO schema + + "gplus-logged-in": + title: "G+ logged in" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when you successfully logged in with G+" + type: "object" + properties: + authResult: + type: "string" + required: ["authResult"] diff --git a/app/schemas/subscriptions/bus.coffee b/app/schemas/subscriptions/bus.coffee new file mode 100644 index 000000000..91569ae7f --- /dev/null +++ b/app/schemas/subscriptions/bus.coffee @@ -0,0 +1,71 @@ +module.exports = + "bus:connecting": + title: "Bus Connecting" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when a Bus starts connecting" + type: "object" + properties: + bus: + $ref: "bus" + + "bus:connected": + title: "Bus Connected" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when a Bus has connected" + type: "object" + properties: + bus: + $ref: "bus" + + "bus:disconnected": + title: "Bus Disconnected" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when a Bus has disconnected" + type: "object" + properties: + bus: + $ref: "bus" + + "bus:new-message": + title: "Message sent" + $schema: "http://json-schema.org/draft-04/schema#" + description: "A new message was sent" + type: "object" + properties: + message: + type: "string" + bus: + $ref: "bus" + + "bus:player-joined": + title: "Player joined" + $schema: "http://json-schema.org/draft-04/schema#" + description: "A new player has joined" + type: "object" + properties: + player: + type: "object" + bus: + $ref: "bus" + + "bus:player-left": + title: "Player left" + $schema: "http://json-schema.org/draft-04/schema#" + description: "A player has left" + type: "object" + properties: + player: + type: "object" + bus: + $ref: "bus" + + "bus:player-states-changed": + title: "Player state changes" + $schema: "http://json-schema.org/draft-04/schema#" + description: "State of the players has changed" + type: "object" + properties: + player: + type: "array" + bus: + $ref: "bus" diff --git a/app/schemas/subscriptions/editor.coffee b/app/schemas/subscriptions/editor.coffee new file mode 100644 index 000000000..eba61f772 --- /dev/null +++ b/app/schemas/subscriptions/editor.coffee @@ -0,0 +1,78 @@ +module.exports = + "save-new-version": + title: "Save New Version" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when a version gets saved" + type: "object" + properties: + major: + type: "boolean" + commitMessage: + type: "string" + required: ["major", "commitMessage"] + additionalProperties: false + + # TODO all these events starting with 'level:' should have 'editor' in their name + # to avoid confusion with level play events + + "level:view-switched": + title: "Level View Switched" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published whenever the view switches" + $ref: "jQueryEvent" + + "level-components-changed": + {} # TODO schema + + "edit-level-component": + {} # TODO schema + + "level-component-edited": + {} # TODO schema + + "level-component-editing-ended": + {} # TODO schema + + "level-systems-changed": + {} # TODO schema + + "edit-level-system": + {} # TODO schema + + "level-system-added": + {} # TODO schema + + "level-system-edited": + {} # TODO schema + + "level-system-editing-ended": + {} # TODO schema + + "level-thangs-changed": + title: "Level Thangs Changed" + $schema: "http://json-schema.org/draft-04/schema#" + description: "Published when a Thang changes" + type: "object" + properties: + thangsData: + type: "array" + required: ["thangsData"] + additionalProperties: false + + "edit-level-thang": + {} # TODO schema + + "level-thang-edited": + {} # TODO schema + + "level-thang-done-editing": + {} # TODO schema + + "level-loaded": + {} # TODO schema + + "level-reload-from-data": + {} # TODO schema + + "save-new-version": + {} # TODO schema diff --git a/app/schemas/subscriptions/errors.coffee b/app/schemas/subscriptions/errors.coffee new file mode 100644 index 000000000..4fa0e33ef --- /dev/null +++ b/app/schemas/subscriptions/errors.coffee @@ -0,0 +1,5 @@ +module.exports = + # app/lib/errors + "server-error": + {} # TODO schema + diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee new file mode 100644 index 000000000..5834aaff8 --- /dev/null +++ b/app/schemas/subscriptions/misc.coffee @@ -0,0 +1,20 @@ +module.exports = + "audio-played:loaded": + {} # TODO schema + + # TODO location is debatable + "note-group-started": + {} # TODO schema + + "note-group-ended": + {} # TODO schema + + "modal-closed": + {} # TODO schema + + # TODO I propose prepending 'modal:' + "save-new-version": + {} # TODO schema + + "router:navigate": + {} # TODO schema diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee new file mode 100644 index 000000000..04764da5f --- /dev/null +++ b/app/schemas/subscriptions/play.coffee @@ -0,0 +1,158 @@ +module.exports = + # TODO There should be a better way to divide these channels into smaller ones + + # TODO location is debatable + "echo-self-wizard-sprite": + {} # TODO schema + + "level:session-will-save": + {} # TODO schema + + "level-loader:progress-changed": + {} # TODO schema + + "level:shift-space-pressed": + {} # TODO schema + + "level:escape-pressed": + {} # TODO schema + + "level-enable-controls": + {} # TODO schema + + "level-set-letterbox": + {} # TODO schema + + "level:started": + {} # TODO schema + + "level-set-debug": + {} # TODO schema + + "level-set-grid": + {} # TODO schema + + "tome:cast-spell": + {} # TODO schema + + "level:restarted": + {} # TODO schema + + "level-set-volume": + {} # TODO schema + + "level-set-time": + {} # TODO schema + + "level-select-sprite": + {} # TODO schema + + "level-set-playing": + {} # TODO schema + + "level:team-set": + {} # TODO schema + + "level:docs-shown": {} + + "level:docs-hidden": {} + + "level:victory-hidden": + {} # TODO schema + + "next-game-pressed": + {} # TODO schema + + "focus-editor": + {} # TODO schema + + "end-current-script": + {} # TODO schema + + "script:reset": + {} # TODO schema + + "script:ended": + {} # TODO schema + + "end-all-scripts": {} + + "script:state-changed": + {} # TODO schema + + 'script-manager:tick': + type: 'object' + additionalProperties: false + properties: + scriptRunning: { type: 'string' } + noteGroupRunning: { type: 'string' } + timeSinceLastScriptEnded: { type: 'number' } + scriptStates: + type: 'object' + additionalProperties: + title: 'Script State' + type: 'object' + additionalProperties: false + properties: + timeSinceLastEnded: + type: 'number' + description: 'seconds since this script ended last' + timeSinceLastTriggered: + type: 'number' + description: 'seconds since this script was triggered last' + + "play-sound": + {} # TODO schema + + # TODO refactor name + "onLoadingViewUnveiled": + {} # TODO schema + + "playback:manually-scrubbed": + {} # TODO schema + + "change:editor-config": + {} # TODO schema + + "restart-level": + {} # TODO schema + + "play-next-level": + {} # TODO schema + + "level-select-sprite": + {} # TODO schema + + "level-toggle-grid": + {} # TODO schema + + "level-toggle-debug": + {} # TODO schema + + "level-toggle-pathfinding": + {} # TODO schema + + "level-scrub-forward": + {} # TODO schema + + "level-scrub-back": + {} # TODO schema + + "level-show-victory": + type: 'object' + additionalProperties: false + properties: + showModal: { type: 'boolean' } + + "level-highlight-dom": + type: 'object' + additionalProperties: false + properties: + selector: { type: 'string' } + delay: { type: 'number' } + sides: { type: 'array', items: { 'enum': ['left', 'right', 'top', 'bottom'] }} + offset: { type: 'object' } + rotation: { type: 'number' } + + "goal-manager:new-goal-states": + {} # TODO schema diff --git a/app/schemas/subscriptions/surface.coffee b/app/schemas/subscriptions/surface.coffee new file mode 100644 index 000000000..6fa5f2415 --- /dev/null +++ b/app/schemas/subscriptions/surface.coffee @@ -0,0 +1,96 @@ +module.exports = # /app/lib/surface + "camera-dragged": + {} # TODO schema + + "camera-zoom-in": + {} # TODO schema + + "camera-zoom-out": + {} # TODO schema + + "camera-zoom-to": + {} # TODO schema + + "camera:zoom-updated": + {} # TODO schema + + "sprite:speech-updated": + {} # TODO schema + + "dialogue-sound-completed": + {} # TODO schema + + "surface:gold-changed": + {} # TODO schema + + "surface:coordinate-selected": + {} # TODO schema + + "surface:coordinates-shown": + {} # TODO schema + + "level-sprite-clear-dialogue": + {} # TODO schema + + "sprite:loaded": + {} # TODO schema + + "choose-point": + {} # TODO schema + + "choose-region": + {} # TODO schema + + "surface:new-thang-added": + {} # TODO schema + + "surface:sprite-selected": + {} # TODO schema + + "thang-began-talking": + {} # TODO schema + + "thang-finished-talking": + {} # TODO schema + + "surface:world-set-up": + {} # TODO schema + + "surface:frame-changed": + {} # TODO schema + + "surface:playback-ended": + {} # TODO schema + + "surface:playback-restarted": + {} # TODO schema + + "level-set-playing": + {} # TODO schema + + "registrar-echo-states": + {} # TODO schema + + "surface:mouse-moved": + {} # TODO schema + + "surface:stage-mouse-down": + {} # TODO schema + + "surface:mouse-scrolled": + {} # TODO schema + + "surface:ticked": + {} # TODO schema + + "surface:mouse-over": + {} # TODO schema + + "surface:mouse-out": + {} # TODO schema + + "self-wizard:target-changed": + {} # TODO schema + + "echo-all-wizard-sprites": + {} # TODO schema diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee new file mode 100644 index 000000000..7c6a5a11f --- /dev/null +++ b/app/schemas/subscriptions/tome.coffee @@ -0,0 +1,73 @@ +module.exports = + "tome:cast-spell": + {} # TODO schema + + # TODO do we really need both 'cast-spell' and 'cast-spells'? + "tome:cast-spells": + {} # TODO schema + + "tome:manual-cast": + {} # TODO schema + + "tome:spell-created": + {} # TODO schema + + "tome:spell-debug-property-hovered": + {} # TODO schema + + "tome:toggle-spell-list": + {} # TODO schema + + "tome:reload-code": + {} # TODO schema + + "tome:palette-hovered": + {} # TODO schema + + "tome:palette-pin-toggled": + {} # TODO schema + + "tome:palette-clicked": + {} # TODO schema + + "tome:spell-statement-index-updated": + {} # TODO schema + + # TODO proposition: refactor 'tome' into spell events + "spell-beautify": + {} # TODO schema + + "spell-step-forward": + {} # TODO schema + + "spell-step-backward": + {} # TODO schema + + "tome:spell-loaded": + {} # TODO schema + + "tome:cast-spell": + {} # TODO schema + + "tome:spell-changed": + {} # TODO schema + + "tome:editing-ended": + {} # TODO schema + + "tome:editing-began": + {} # TODO schema + + "tome:problems-updated": + {} # TODO schema + + "tome:thang-list-entry-popover-shown": + {} # TODO schema + + "tome:spell-shown": + {} # TODO schema + + # TODO proposition: add tome to name + "focus-editor": + {} # TODO schema + diff --git a/app/schemas/subscriptions/user.coffee b/app/schemas/subscriptions/user.coffee new file mode 100644 index 000000000..44e713777 --- /dev/null +++ b/app/schemas/subscriptions/user.coffee @@ -0,0 +1,9 @@ +module.exports = + "me:synced": + {} # TODO schema + + "user-fetched": + {} # TODO schema + + "edit-wizard-settings": + {} # TODO schema diff --git a/app/schemas/subscriptions/world.coffee b/app/schemas/subscriptions/world.coffee new file mode 100644 index 000000000..d5e953de4 --- /dev/null +++ b/app/schemas/subscriptions/world.coffee @@ -0,0 +1,15 @@ +module.exports = + "god:user-code-problem": + {} # TODO schema + + "god:infinite-loop": + {} # TODO schema + + "god:user-code-problem": + {} # TODO schema + + "god:new-world-created": + {} # TODO schema + + "god:world-load-progress-changed": + {} # TODO schema \ No newline at end of file diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index 2edec8f24..59807aa1c 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -1,15 +1,220 @@ #profile-view - button - float: right - i - margin-right: 5px - - img.img-thumbnail - margin: 20px 0 + .profile-control-bar + background-color: rgb(78, 78, 78) + width: 100% + text-align: center + + button.edit-settings-button + margin: 2px + i + margin-right: 5px - li - list-style: none - - ul - margin: 0 + .approved, .not-approved + display: none + + .main-content-area padding: 0 + + .flat-button + width: 100% + margin-bottom: 10px + background: rgb(78, 78, 78) + border: 0 + border-radius: 0 + padding: 10px + + .public-profile-container + padding: 20px + + img.profile-photo + width: 256px + border-radius: 6px + + .job-profile-container + width: 100% + height: 100% + min-height: 600px + padding: 0 + display: table + + h1, h2, h3, h4, h5, h6 + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif + color: #555 + + ul.links, ul.projects + margin: 0 + padding: 0 + + li + list-style: none + + .job-profile-row + height: 100% + display: table-row + $side-width: 250px + $side-padding: 5px + $middle-width: 524px + $middle-padding: 20px + + .full-height-column + height: 100% + padding: $side-padding + display: table-cell + vertical-align: top + + h3:first-child + margin: 5px 0 5px 0 + + .left-column + width: $side-width - 2 * $side-padding + padding: $side-padding + background-color: rgb(220, 220, 220) + + .sub-column + width: $side-width - 2 * $side-padding + overflow-wrap: break-word + + .profile-photo-container + position: relative + margin-bottom: 10px + + img.profile-photo + width: $side-width - 2 * $side-padding + border-radius: 6px + + .profile-caption + background-color: rgba(0, 0, 0, 0.5) + color: white + border-bottom-right-radius: 6px + border-bottom-left-radius: 6px + position: absolute + width: 100% + bottom: 0px + text-align: center + + ul.links + li.has-icon + display: inline-block + img + margin: 0 0 10px 0 + li.has-icon:not(:nth-child(5)) + img + margin: 0 10px 10px 0 + + #contact-candidate + margin-top: 20px + background-color: rgb(177, 55, 25) + padding: 15px + font-size: 20px + + .middle-column + width: $middle-width - 2 * $middle-padding + padding-left: $middle-padding + padding-right: $middle-padding + background-color: white + + .sub-column + width: $middle-width - 2 * $middle-padding + overflow-wrap: break-word + + &.double-column + width: $middle-width + $side-width + 2 * $side-padding - 2 * $middle-padding + $middle-padding-double: 30px + padding-left: $middle-padding-double + padding-right: $middle-padding-double + + .sub-column + width: $middle-width + $side-width + 2 * $side-padding - 2 * $middle-padding - 2 * $middle-padding-double + overflow-wrap: break-word + + code + background-color: rgb(220, 220, 220) + color: #555 + margin: 2px 0 + display: inline-block + text-transform: lowercase + + .long-description + margin-top: 10px + img + max-width: 524px - 60px + max-height: 200px + + .experience-header + margin-top: 25px + + .header-icon + margin-right: 10px + width: 32px + height: 32px + + .experience-entry + margin-bottom: 15px + + .duration + margin-left: 10px + margin-bottom: 10px + + #job-profile-notes + width: 100% + height: 100px + + .right-column + width: $side-width + background-color: rgb(220, 220, 220) + + .sub-column + width: $side-width - 2 * $side-padding + overflow-wrap: break-word + + > h3:first-child + background-color: white + padding: 5px 5px + margin: 5px 2px 5px 2px + + ul.projects + li + margin-bottom: 10px + padding: 5px 3px + border: 2px solid rgb(220, 220, 220) + transition: .5s ease-in-out + position: relative + background-color: white + + &:hover + border-color: rgb(100, 130, 255) + + a + position: relative + z-index: 2 + + > a + position: absolute + width: 100% + height: 100% + top: 0 + left: 0 + z-index: 1 + + .project-image + width: 230px + height: 115px + background-size: cover + background-repeat: no-repeat + background-position: center + + -webkit-filter: grayscale(100%) + -webkit-transition: .5s ease-in-out + -moz-filter: grayscale(100%) + -moz-transition: .5s ease-in-out + -o-filter: grayscale(100%) + -o-transition: .5s ease-in-out + filter: grayscale(100%) + transition: .5s ease-in-out + + li:hover + .project-image + -webkit-filter: grayscale(0%) + -moz-filter: grayscale(0%) + -o-filter: grayscale(0%) + filter: grayscale(0%) diff --git a/app/styles/account/settings.sass b/app/styles/account/settings.sass index 8751e59ef..9780b6c50 100644 --- a/app/styles/account/settings.sass +++ b/app/styles/account/settings.sass @@ -8,15 +8,20 @@ background: #eee border-radius: 5px - #save-button - float: right + #save-button-container + position: fixed + top: 100px + width: 1000px + z-index: 10 - .thumbnails - text-align: center - .thumbnail - margin-bottom: 30px - margin-right: 20px - float: left + #save-button + float: right + + &.btn-info, &.btn-danger + opacity: 1.0 + + .gravatar-fallback + margin-top: 10px input.range position: relative @@ -37,4 +42,50 @@ font-size: 12px .form - max-width: 600px \ No newline at end of file + max-width: 600px + + #email-pane + #specific-notification-settings + padding-left: 20px + margin-left: 20px + border-left: 1px solid gray + +#job-profile-view + .profile-preview-button + &.bottom-preview + margin: 15px 0 0 0 + + .sample-profile-thumbnail + margin-top: -60px + + .profile-completion-progress + width: 100% + display: inline-block + height: 33px + + .progress-bar + line-height: 33px + + .progress-next-item + margin-top: -20px + margin-bottom: 15px + + #job-profile-treema + background-color: white + + input + width: 790px + + .treema-description + font-size: 14px + line-height: 22px + opacity: 1 + + .treema-row + padding-top: 6px + + .treema-image-file + img + display: block + clear: both + max-width: 300px diff --git a/app/styles/base.sass b/app/styles/base.sass index 30980c1fb..3fbc45e24 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -5,9 +5,8 @@ html background-color: #2f261d -html, body - // For level loading view wings - overflow-x: hidden +body + position: absolute !important // https://github.com/twbs/bootstrap/issues/9237 -- need a version that's not !important .secret @@ -49,7 +48,6 @@ h1 h2 h3 h4 margin: 0 auto .footer - height: 75px border-top: 1px solid black background-color: #2f261d p @@ -103,10 +101,19 @@ a[data-toggle="modal"] .modal-dialog padding: 5px - background: transparent url(/images/pages/base/modal_background.png) - background-size: 100% 100% - border: 0 - @include box-shadow(0 0 0 #000) + margin-top: 30px + margin-bottom: 0px + padding-top: 30px + .background-wrapper + background: url("/images/pages/base/modal_background.png") + background-size: 100% 100% + border: 0 + @include box-shadow(0 0 0 #000) + //position: absolute + width: 99% + + .background-wrapper.plain + background: white .modal-content @include box-shadow(none) @@ -126,6 +133,14 @@ a[data-toggle="modal"] background-color: transparent margin: 0 14px border-bottom-color: #ccc + .modal-footer.linkedin + text-align: center + .signin-text + font-size: 15px + padding-bottom: 10px + .login-link + cursor: pointer + // Bigger versions of some Bootstrap icons // TODO: make the non-white versions of these if we ever need them @@ -161,23 +176,28 @@ a[data-toggle="modal"] .icon-cog.big background-position: 0px 0px +// loading screens for everything but the play view .loading-screen - text-align: center .progress width: 50% margin: 0 25% + +// all loading screens +.loading-container + text-align: center .progress-bar width: 0% transition: width 0.1s ease - .errors .alert padding: 5px display: block margin: 10px auto .btn margin-left: 10px - + + .modal + overflow-y: auto !important .wait h3 text-align: center @@ -207,7 +227,7 @@ table.table .header-font font-family: $headings-font-family -body[lang='ru'], body[lang|='zh'], body[lang='ja'], body[lang='pl'], body[lang='tr'], body[lang='cs'], body[lang='el'], body[lang='ro'], body[lang='vi'], body[lang='th'], body[lang='ko'], body[lang='sk'], body[lang='sl'], body[lang='bg'], body[lang='he'], body[lang='lt'], body[lang='sr'], body[lang='uk'], body[lang='hi'], body[lang='ur'], +body[lang='ru'], body[lang|='zh'], body[lang='pl'], body[lang='tr'], body[lang='cs'], body[lang='el'], body[lang='ro'], body[lang='vi'], body[lang='th'], body[lang='ko'], body[lang='sk'], body[lang='sl'], body[lang='bg'], body[lang='he'], body[lang='lt'], body[lang='sr'], body[lang='uk'], body[lang='hi'], body[lang='ur'], body[lang='hu'] h1, h2, h3, h4, h5, h6 font-family: 'Open Sans Condensed', Impact, "Arial Narrow", "Arial", sans-serif text-transform: uppercase @@ -217,6 +237,23 @@ body[lang='ru'], body[lang|='zh'], body[lang='ja'], body[lang='pl'], body[lang=' font-family: 'Open Sans Condensed', Impact, "Arial Narrow", "Arial", sans-serif !important text-transform: uppercase letter-spacing: -1px !important + +body[lang='ja'] + h1, h2, h3, h4, h5, h6 + font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, "ï¼­ï¼³ Pゴシック", "MS PGothic", 'Open Sans Condensed', sans-serif + text-transform: uppercase + letter-spacing: -1px !important + + .header-font + font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, "ï¼­ï¼³ Pゴシック", "MS PGothic", 'Open Sans Condensed', sans-serif + text-transform: uppercase + letter-spacing: -1px !important + + #top-nav + .navbar-nav + li + a.header-font + font-size: 16px @media only screen and (max-width: 800px) .main-content-area @@ -233,3 +270,18 @@ body[lang='ru'], body[lang|='zh'], body[lang='ja'], body[lang='pl'], body[lang=' margin-bottom: 20px .partner-badges display: none + +// point the new glyphicons to the fonts in public + +@font-face + font-family: 'Glyphicons Halflings' + src: url("/fonts/glyphicons-halflings-regular.eot") + src: url("/fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("/fonts/glyphicons-halflings-regular.woff") format("woff"), url("/fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("/fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular") format("svg") + +.spr:after + content: " " +.spl:before + content: " " + +a[data-toggle="coco-modal"] + cursor: pointer diff --git a/app/styles/bootstrap/_variables.scss b/app/styles/bootstrap/_variables.scss index 814f79351..e35c82986 100644 --- a/app/styles/bootstrap/_variables.scss +++ b/app/styles/bootstrap/_variables.scss @@ -39,7 +39,7 @@ $brand-primary: #948754 !default; $brand-success: $green !default; $brand-warning: $orange !default; $brand-danger: $red !default; -$brand-info: #f9e811 !default; +$brand-info: $blueDark !default; // Scaffolding // ------------------------- diff --git a/app/styles/common/top_nav.sass b/app/styles/common/top_nav.sass index 9a41771fe..3619a3f9b 100644 --- a/app/styles/common/top_nav.sass +++ b/app/styles/common/top_nav.sass @@ -10,16 +10,19 @@ font-weight: 400 letter-spacing: 1px - .navbuttontext-user-name - max-width: 125px - overflow: hidden - text-overflow: ellipsis - white-space: nowrap + .navbuttontext-account display: inline-block padding: 0 5px 0 0 - margin: 0 + margin: 0 5px 0 0 height: 18px + .account-settings-image + width: 18px + height: 18px + + .glyphicon-user + font-size: 16px + .nav.navbar-link-text, .nav.navbar-link-text > li > a font-weight: normal font-size: 25px diff --git a/app/styles/community.sass b/app/styles/community.sass new file mode 100644 index 000000000..5156d3919 --- /dev/null +++ b/app/styles/community.sass @@ -0,0 +1,7 @@ +#community-view + + .community_columns + width: 330px + float: left + padding-left: 10px + padding-right: 10px \ No newline at end of file diff --git a/app/styles/contribute_classes.sass b/app/styles/contribute_classes.sass index 9244aca55..e4b8ff7fb 100644 --- a/app/styles/contribute_classes.sass +++ b/app/styles/contribute_classes.sass @@ -49,9 +49,15 @@ &:hover background-color: rgba(200, 244, 255, 0.2) - h4 - text-align: center + a:not(.has-github) + cursor: default + text-decoration: none + img max-width: 100px max-height: 100px + .caption + background-color: transparent + h4 + text-align: center diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass new file mode 100644 index 000000000..013478efb --- /dev/null +++ b/app/styles/editor/delta.sass @@ -0,0 +1,43 @@ +.delta-view + .panel-heading + font-size: 13px + padding: 4px + .row + padding: 5px 10px + + .delta-added + border-color: green + > .panel-heading + background-color: lighten(green, 70%) + strong + color: green + + .delta-modified + border-color: darkgoldenrod + > .panel-heading + background-color: lighten(darkgoldenrod, 40%) + strong + color: darkgoldenrod + + .delta-text-diff + border-color: blue + > .panel-heading + background-color: lighten(blue, 45%) + strong + color: blue + table + width: 100% + + .delta-deleted + border-color: red + > .panel-heading + background-color: lighten(red, 42%) + strong + color: red + + .delta-moved-index + border-color: darkslategray + > .panel-heading + background-color: lighten(darkslategray, 60%) + strong + color: darkslategray diff --git a/app/styles/editor/level/component/edit.sass b/app/styles/editor/level/component/edit.sass index 5c9deda4c..e4301dbde 100644 --- a/app/styles/editor/level/component/edit.sass +++ b/app/styles/editor/level/component/edit.sass @@ -1,4 +1,14 @@ #editor-level-component-edit-view + nav + margin-bottom: 0 + + #component-patches + padding: 0 10px 10px + background: white + + .patches-view + padding: 10px 20px 10px 0px + .navbar-text float: left @@ -7,9 +17,13 @@ left: 0 right: 0 bottom: 0 - top: 50px + top: 35px border: 2px solid black border-top: none - .active > a, .active > a:hover, .active > a:focus - background-color: white !important \ No newline at end of file + .inner-editor + position: absolute + left: 0 + right: 0 + bottom: 0 + top: 0px \ No newline at end of file diff --git a/app/styles/editor/level/components_tab.sass b/app/styles/editor/level/components_tab.sass index b8a029a5b..2903a25e3 100644 --- a/app/styles/editor/level/components_tab.sass +++ b/app/styles/editor/level/components_tab.sass @@ -1,4 +1,14 @@ #editor-level-components-tab-view + h3 + margin-top: 0 + @media screen and (max-width: 800px) + display: none + + .toggle + padding: 6px 8px + z-index: 11 + margin-top: 1px + margin-left: 2px .components-container position: absolute @@ -7,13 +17,16 @@ .treema-root position: absolute - top: 50px + top: 35px bottom: 0 width: 250px overflow: scroll .treema-children .treema-row * cursor: pointer !important + + #components-treema + z-index: 11 .edit-component-container margin-left: 290px @@ -22,16 +35,39 @@ left: 0px top: 0 bottom: 0 - + @media screen and (max-width: 800px) + margin-left: 0px + + .nav-tabs + margin-left: 80px + + li + z-index: 11 + .treema-root position: absolute - top: 50px + top: 35px right: 0 left: 0px bottom: 0 overflow: scroll - #create-new-component-button + #create-new-component-button-no-select position: absolute top: 0 right: 0 + left: auto + @media screen and (max-width: 800px) + left: 40px + top: 1px + bottom: auto + padding: 8px 10px + .text + display: block + @media screen and (max-width: 800px) + display: none + [class^='icon-'] + display: none + @media screen and (max-width: 800px) + display: block + diff --git a/app/styles/editor/level/edit.sass b/app/styles/editor/level/edit.sass index f1cfc2303..09bb03fe0 100644 --- a/app/styles/editor/level/edit.sass +++ b/app/styles/editor/level/edit.sass @@ -1,10 +1,10 @@ #editor-level-view + &, #level-editor-top-nav + // min-width: 1024px + a font-family: helvetica, arial, sans serif - #top-nav - display: none - position: absolute top: 0px left: 0px @@ -12,22 +12,111 @@ bottom: 0px $BG: rgba(228, 207, 140, 1.0) - + $NAVBG: #2f261d + + .dropdown-menu + position: absolute + background-color: #FFF + border: 1px solid rgba(0, 0, 0, 0.15) + border-radius: 4px + box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.176) + left: 0px + right: auto + + li a + color: #555 + padding: 3px 20px + + .navbar-nav + float: left + margin: 0 + + > li + float: left + li.navbar-btn margin-right: 5px - #level-editor-top-nav - .nav-tabs - border-bottom: 0 !important - .active > a, .active > a:hover, .active > a:focus - background-color: $BG !important - border-color: darken($BG, 50%) - border-bottom: 0 + // custom navbar height rules + .navbar-nav > li > a + padding: 7px 8px 8px + cursor: pointer + &:hover + background-color: lighten($NAVBG, 10%) + .navbar + min-height: 0px + border-radius: 0 + .navbar-right + // not sure why bootstrap puts a big negative margin in, but this overrides it + margin-right: 10px + float: right + + .dropdown-menu + right: 0px + left: auto + + // custom navbar styling + .navbar-brand + padding-top: 7px + padding-bottom: 7px + color: lighten(gold, 30%) + .navbar-header + border-left: 2px solid lighten($NAVBG, 20%) + border-right: 2px solid lighten($NAVBG, 20%) + background: lighten($NAVBG, 10%) + margin-left: 20px + float: left + .nav-tabs + margin-left: 5px + border-bottom: 0 !important + + li + float: left + display: block + + @media only screen and (max-width: 800px) + + li + float: none + display: none + z-index: 12 + + a + background-color: $BG + border-color: darken($BG, 50%) + border-width: 0px 1px + border-radius: 0px + + li:first-child > a + border-radius: 5px 5px 0px 0px + border-top-width: 1px + + li:last-child > a + border-radius: 0px 0px 5px 5px + border-bottom-width: 1px + + li.active + display: block + + .active > a, .active > a:hover, .active > a:focus + background-color: $BG !important + border-color: darken($BG, 50%) + border-bottom: 0 + a + padding: 7px 5px !important + + .dropdown-menu a + cursor: pointer + &:hover + background-color: #d3d3d3 + + .badge + background-color: green .outer-content background-color: $BG position: absolute - top: 0 + top: 35px bottom: 0 left: 0 right: 0 @@ -45,12 +134,11 @@ #level-editor-tabs position: absolute - left: 20px - right: 20px - top: 66px - bottom: 20px + left: 15px + right: 15px + top: 15px + bottom: 15px .treema-root background-color: white border-radius: 4px - diff --git a/app/styles/editor/level/scripts_tab.sass b/app/styles/editor/level/scripts_tab.sass index b0a308e59..a74bacdfa 100644 --- a/app/styles/editor/level/scripts_tab.sass +++ b/app/styles/editor/level/scripts_tab.sass @@ -1,5 +1,11 @@ #editor-level-scripts-tab-view + .toggle + z-index: 11 + margin-top: -10px + margin-left: -10px + float: left + .treema-script cursor: pointer @@ -9,9 +15,15 @@ bottom: 0 width: 250px overflow: scroll + @media screen and (max-width: 800px) + top: 40px + z-index: 11 #script-treema margin-left: 290px max-height: 100% overflow: scroll box-sizing: border-box + @media screen and (max-width: 800px) + margin-left: 30px + top: -50px diff --git a/app/styles/editor/level/settings_tab.sass b/app/styles/editor/level/settings_tab.sass index 81b0f8a97..58bae43d3 100644 --- a/app/styles/editor/level/settings_tab.sass +++ b/app/styles/editor/level/settings_tab.sass @@ -1,2 +1,5 @@ #editor-level-settings-tab-view color: black + + .treema-value img + max-width: 100% diff --git a/app/styles/editor/level/system/edit.sass b/app/styles/editor/level/system/edit.sass index e86dc5a46..f8ca710d1 100644 --- a/app/styles/editor/level/system/edit.sass +++ b/app/styles/editor/level/system/edit.sass @@ -1,4 +1,14 @@ #editor-level-system-edit-view + nav + margin-bottom: 0 + + #system-patches + padding: 0 10px 10px + background: white + + .patches-view + padding: 10px 20px 10px 0px + .navbar-text float: left @@ -7,9 +17,13 @@ left: 0 right: 0 bottom: 0 - top: 50px + top: 35px border: 2px solid black border-top: none - - .active > a, .active > a:hover, .active > a:focus - background-color: white !important \ No newline at end of file + + .inner-editor + position: absolute + left: 0 + right: 0 + bottom: 0 + top: 0px \ No newline at end of file diff --git a/app/styles/editor/level/systems_tab.sass b/app/styles/editor/level/systems_tab.sass index 88585504d..c74f79be9 100644 --- a/app/styles/editor/level/systems_tab.sass +++ b/app/styles/editor/level/systems_tab.sass @@ -1,4 +1,14 @@ #editor-level-systems-tab-view + h3 + margin-top: 0 + @media screen and (max-width: 800px) + display: none + + .toggle + padding: 6px 8px + z-index: 11 + margin-top: 0px + margin-left: 2px .systems-container position: absolute @@ -7,10 +17,13 @@ .treema-root position: absolute - top: 50px + top: 35px bottom: 0 width: 250px overflow: scroll + @media screen and (max-width: 800px) + z-index: 10 + bottom: -35px .treema-children .treema-row * cursor: pointer !important @@ -19,6 +32,21 @@ position: absolute bottom: 0 left: 170px + top: auto + @media screen and (max-width: 800px) + left: 40px + top: 1px + bottom: auto + padding: 8px 10px + + .text + display: block + @media screen and (max-width: 800px) + display: none + [class^='icon-'] + display: none + @media screen and (max-width: 800px) + display: block .edit-system-container margin-left: 290px @@ -27,10 +55,18 @@ left: 0px top: 0 bottom: 0 + @media screen and (max-width: 800px) + margin-left: 0px + + .nav-tabs + margin-left: 120px + + li + z-index: 11 .treema-root position: absolute - top: 50px + top: 35px right: 0 left: 0px bottom: 0 @@ -40,3 +76,17 @@ position: absolute top: 0 right: 0 + left: auto + @media screen and (max-width: 800px) + left: 80px + top: 1px + bottom: auto + padding: 8px 10px + .text + display: block + @media screen and (max-width: 800px) + display: none + [class^='icon-'] + display: none + @media screen and (max-width: 800px) + display: block diff --git a/app/styles/editor/level/thangs_tab.sass b/app/styles/editor/level/thangs_tab.sass index 9edfe6958..e37db3036 100644 --- a/app/styles/editor/level/thangs_tab.sass +++ b/app/styles/editor/level/thangs_tab.sass @@ -1,5 +1,7 @@ @import "../../bootstrap/mixins" - + +$mobile: 1050px + #editor-level-thangs-tab-view $addPaletteIconColumns: 3 $extantThangsWidth: 300px @@ -8,12 +10,60 @@ $addPaletteIconMargin: 2px $addPaletteWidth: ($addPaletteIconWidth + 2 * $addPaletteIconPadding + 2 * $addPaletteIconMargin) * $addPaletteIconColumns + 20 + #toggle + display: none + position: absolute + z-index: 11 + left: -14px + @media screen and (max-width: $mobile) + display: block + + .toggle + left: 0 + + .toggle + display: none + float: none + z-index: 11 + position: absolute + right: -14px + z-index: 11 + margin: 0 + padding: 8px + @media screen and (max-width: $mobile) + display: block + + .thangs-column + background-color: #E4CF8C + + @media screen and (max-width: $mobile) + display: block + + h3 + @media screen and (max-width: $mobile) + display: none + + #all-thangs + display: block + @media screen and (max-width: $mobile) + display: none + .thangs-container width: $extantThangsWidth position: absolute left: 0 top: 0 bottom: 0 + z-index: 11 + @media screen and (max-width: $mobile) + width: auto + left: 18px + bottom: -18px + + .btn-group + margin: 0 + @media screen and (max-width: $mobile) + margin: 5px h3 margin: 0 -20px 0 0 @@ -25,6 +75,10 @@ right: 0 bottom: 0 overflow: scroll + margin: 0 + @media screen and (max-width: $mobile) + margin: 5px + top: 40px &.hide-except-Unit .treema-node @@ -62,11 +116,15 @@ .world-container margin-left: $extantThangsWidth margin-right: $addPaletteWidth + @media screen and (max-width: $mobile) + margin-left: 0 + margin-right: 0 padding: 0 20px box-sizing: border-box h3 margin: 0 -10px 0 0 + text-align: center .add-thangs-palette width: $addPaletteWidth @@ -75,6 +133,20 @@ right: 0 top: 0 bottom: 0 + @media screen and (max-width: $mobile) + display: none + right: 18px + z-index: 11 + width: $addPaletteWidth + 10 + bottom: -15px + //height: auto + //padding-bottom: 10px + + input + width: $addPaletteWidth + margin: 0 + @media screen and (max-width: $mobile) + margin: 0 5px #thangs-list position: relative @@ -83,6 +155,9 @@ bottom: 10px overflow: scroll height: 100% + margin: 0 + @media screen and (max-width: $mobile) + margin: 0 5px h3 margin: 0 -20px 0 0 diff --git a/app/styles/editor/patch.sass b/app/styles/editor/patch.sass new file mode 100644 index 000000000..3296d946c --- /dev/null +++ b/app/styles/editor/patch.sass @@ -0,0 +1,3 @@ +#patch-modal + .modal-body + padding: 10px \ No newline at end of file diff --git a/app/styles/editor/patches.sass b/app/styles/editor/patches.sass new file mode 100644 index 000000000..f4130ec5a --- /dev/null +++ b/app/styles/editor/patches.sass @@ -0,0 +1,6 @@ +.patches-view + .status-buttons + margin-bottom: 10px + + .patch-icon + cursor: pointer diff --git a/app/styles/editor/thang/edit.sass b/app/styles/editor/thang/edit.sass index 871cab229..565b7e88a 100644 --- a/app/styles/editor/thang/edit.sass +++ b/app/styles/editor/thang/edit.sass @@ -50,6 +50,9 @@ #settings-col float: left width: 550px + + .treema-row img + max-width: 100% #thang-type-treema height: 400px @@ -66,6 +69,8 @@ background-color: white border-radius: 4px + + #spritesheets border: 1px solid green max-width: 100% diff --git a/app/styles/employers.sass b/app/styles/employers.sass new file mode 100644 index 000000000..1c7538cf2 --- /dev/null +++ b/app/styles/employers.sass @@ -0,0 +1,38 @@ +#employers-view + #see-candidates + cursor: pointer + .tablesorter + //img + // display: none + + .tablesorter-header + cursor: pointer + &:hover + color: black + + &:first-child + // Make sure that "Developer #56" doesn't wrap onto second row + min-width: 110px + + .tablesorter-headerAsc + background-color: #cfc + + .tablesorter-headerDesc + background-color: #ccf + + tr + cursor: pointer + + code + background-color: rgb(220, 220, 220) + color: #555 + margin: 2px 0 + display: inline-block + text-transform: lowercase + + td:nth-child(3) select + min-width: 100px + td:nth-child(6) select + min-width: 50px + td:nth-child(7) select + min-width: 100px diff --git a/app/styles/home.sass b/app/styles/home.sass index e2ba1fdb0..2c49541c7 100644 --- a/app/styles/home.sass +++ b/app/styles/home.sass @@ -7,6 +7,9 @@ text-align: center margin-top: 0 + #front-screenshot + margin: 15px 0 40px 150px + #trailer-wrapper position: relative margin: 0 auto 40px @@ -101,6 +104,8 @@ font-size: 30px #trailer-wrapper display: none + #front-screenshot + display: none #mobile-trailer-wrapper display: inline-block diff --git a/app/styles/modal/auth.sass b/app/styles/modal/auth.sass new file mode 100644 index 000000000..3a1ae75c0 --- /dev/null +++ b/app/styles/modal/auth.sass @@ -0,0 +1,20 @@ +#auth-modal + .network-login + float: left + width: 100px + text-align: left + + #gplus-login-button + position: relative + top: 1px + + #recover-account-wrapper + float: right + + .modal-footer + height: 70px + padding: 20px 10px + border-top: 1px solid darkgray + + .btn + margin-right: 10px \ No newline at end of file diff --git a/app/styles/modal/login.sass b/app/styles/modal/login.sass deleted file mode 100644 index e89a92118..000000000 --- a/app/styles/modal/login.sass +++ /dev/null @@ -1,12 +0,0 @@ -#login-modal, #signup-modal - .network-logins div - float: left - margin-right: 20px - - .wait - margin-bottom: 20px - h3 - text-align: center - - a[data-toggle="coco-modal"] - cursor: pointer diff --git a/app/styles/modal/model.sass b/app/styles/modal/model.sass new file mode 100644 index 000000000..8ecf502dd --- /dev/null +++ b/app/styles/modal/model.sass @@ -0,0 +1,9 @@ +#model-modal + .treema-root + background-color: white + + .modal-dialog + width: 1000px + + .treema-ace .ace_editor + height: 600px diff --git a/app/styles/modal/save_version.sass b/app/styles/modal/save_version.sass index 9af4225dc..66de28a29 100644 --- a/app/styles/modal/save_version.sass +++ b/app/styles/modal/save_version.sass @@ -1,4 +1,12 @@ #save-version-modal + .modal-body + padding: 10px 50px 30px 20px + + .modal-footer + text-align: left + .buttons + text-align: right + #cla-link cursor: pointer text-decoration: underline @@ -25,3 +33,23 @@ font-size: 0.9em font-style: italic + .delta-view + overflow-y: auto + padding: 10px + border: 1px solid black + background: lighten(#add8e6, 17%) + margin-bottom: 10px + ul + padding-left: 20px + + form + width: 100% + + .commit-message + display: block + width: 100% + + .checkbox + margin: 10px 10px 0 + input + margin-right: 5px \ No newline at end of file diff --git a/app/styles/play.sass b/app/styles/play.sass index e3e31d8fa..900207ca9 100644 --- a/app/styles/play.sass +++ b/app/styles/play.sass @@ -46,6 +46,3 @@ color: black text-shadow: 0 1px 0 white - .alert-warning h2 - color: black - text-align: center diff --git a/app/styles/play/common/ladder_submission.sass b/app/styles/play/common/ladder_submission.sass new file mode 100644 index 000000000..4de48fcee --- /dev/null +++ b/app/styles/play/common/ladder_submission.sass @@ -0,0 +1,14 @@ +.ladder-submission-view + button + text-shadow: 0px -1px 0px black + + .last-submitted, .help-simulate + font-size: 14px + font-weight: normal + + .last-submitted + float: left + + .help-simulate + float: right + diff --git a/app/styles/play/ladder.sass b/app/styles/play/ladder.sass deleted file mode 100644 index 149caeac1..000000000 --- a/app/styles/play/ladder.sass +++ /dev/null @@ -1,77 +0,0 @@ -#ladder-view - h1 - text-align: center - - .tab-pane - margin-top: 10px - - .rank-cell, .fight-cell - text-align: center - - .score-cell - width: 50px - text-align: center - - .play-button - margin-bottom: 10px - background-image: none - - .spectate-button-container - margin-top: 10px - text-align: center - - .name-col-cell - max-width: 300px - text-overflow: ellipsis - white-space: nowrap - overflow: hidden - - .ellipsis-row - text-align: center - - // friend column - - .friends-header - margin-top: 0 - margin-bottom: 5px - - .connect-buttons - margin-bottom: 15px - .btn - margin-right: 5px - - .friend-entry img - float: left - margin-right: 10px - - .friend-entry - margin-bottom: 15px - - .connect-facebook - background-color: #4c66a4 !important - background-image: none - color: white - - .connect-google-plus - background-color: #CC3234 !important - background-image: none - color: white - - td - padding: 1px 2px - - tr.stale - opacity: 0.5 - - tr.win .state-cell - color: #172 - tr.loss .state-cell - color: #712 - - #must-log-in button - margin-right: 10px - -@media only screen and (max-width: 800px) - #ladder-view - #level-column img - width: 100% \ No newline at end of file diff --git a/app/styles/play/ladder/ladder.sass b/app/styles/play/ladder/ladder.sass new file mode 100644 index 000000000..51ecbd02e --- /dev/null +++ b/app/styles/play/ladder/ladder.sass @@ -0,0 +1,141 @@ +#ladder-view + .main-content-area + background-color: whitesmoke + + #level-column img + margin: -14px -12px 0px -12px + width: 100% + width: -webkit-calc(100% + 24px) + width: calc(100% + 24px) + + h1 + text-align: center + + .tournament-blurb + margin: -20px -12px 20px -12px + padding: 10px + background-color: white + + h2 + text-align: center + + a + font-weight: bold + + .sponsor-logos + padding: 10px 15px 10px 15px + + -webkit-filter: grayscale(100%) + -webkit-transition: .5s ease-in-out + -moz-filter: grayscale(100%) + -moz-transition: .5s ease-in-out + -o-filter: grayscale(100%) + -o-transition: .5s ease-in-out + filter: grayscale(100%) + transition: .5s ease-in-out + + &:hover + -webkit-filter: grayscale(0%) + -moz-filter: grayscale(0%) + -o-filter: grayscale(0%) + filter: grayscale(0%) + + img + margin: 0px 15px + + .tab-pane + margin-top: 10px + + .rank-cell, .fight-cell + text-align: center + + .score-cell + width: 50px + text-align: center + + .play-button + margin-bottom: 10px + background-image: none + + .spectate-button-container + margin-top: 10px + text-align: center + + .name-col-cell + max-width: 300px + text-overflow: ellipsis + white-space: nowrap + overflow: hidden + + .ellipsis-row + text-align: center + + .simulator-leaderboard-cell + text-align: center + + // friend column + + .friends-header + margin-top: 0 + margin-bottom: 5px + + .connect-buttons + margin-bottom: 15px + .btn + margin-right: 5px + + .friend-entry img + float: left + margin-right: 10px + + .friend-entry + margin-bottom: 15px + + .connect-facebook + background-color: #4c66a4 !important + background-image: none + color: white + + .connect-google-plus + background-color: #CC3234 !important + background-image: none + color: white + + td + padding: 1px 2px + + #must-log-in button + margin-right: 10px + + #prize_table + width: 960px + font-weight: bold + + thead + font-size: 24px + + tbody + tr:not(:first-child) + border-top: 10px solid #ddd + + td + vertical-align: middle + + &:nth-child(1), &:nth-child(3) + text-align: center + font-size: 24px + + li + list-style: none + + &:not(:last-child) + margin-bottom: 10px + border-bottom: 1px solid #ddd + + img + margin-right: 10px + +@media only screen and (max-width: 800px) + #ladder-view + #level-column img + width: 100% diff --git a/app/styles/play/ladder/ladder_tab.sass b/app/styles/play/ladder/ladder_tab.sass index 6d65cc5a6..f722faf18 100644 --- a/app/styles/play/ladder/ladder_tab.sass +++ b/app/styles/play/ladder/ladder_tab.sass @@ -4,6 +4,9 @@ white-space: nowrap overflow: hidden text-overflow: ellipsis + + .histogram-display + height: 130px .bar rect fill: steelblue @@ -39,4 +42,8 @@ .ogres-rank-text fill: #3f44bf - \ No newline at end of file + + .load-more-ladder-entries + position: absolute + right: 15px + bottom: -5px diff --git a/app/styles/play/ladder/my_matches_tab.sass b/app/styles/play/ladder/my_matches_tab.sass index a68f12225..66b03d72b 100644 --- a/app/styles/play/ladder/my_matches_tab.sass +++ b/app/styles/play/ladder/my_matches_tab.sass @@ -26,4 +26,13 @@ fill: #555555 shape-rendering: crispEdges - \ No newline at end of file + tr.fresh + background-color: #39F + tr.stale + opacity: 0.5 + tr.win .state-cell + color: #172 + tr.loss .state-cell + color: #712 + + diff --git a/app/styles/play/ladder_home.sass b/app/styles/play/ladder_home.sass new file mode 100644 index 000000000..805c0376e --- /dev/null +++ b/app/styles/play/ladder_home.sass @@ -0,0 +1,54 @@ +@import "app/styles/bootstrap/mixins" +@import "app/styles/bootstrap/variables" + +#ladder-home-view + .level + width: 100% + position: relative + margin-bottom: 20px + text-shadow: 2px 2px 5px black + + &:hover div + color: lighten($yellow, 20%) + + &:hover img + filter: brightness(1.2) + -webkit-filter: brightness(1.2) + box-shadow: 0 0 5px black + + .level-image + width: 100% + + .overlay-text + color: $yellow + font-family: Bangers + @include transition(color .10s linear) + + .level-difficulty + position: absolute + left: 0px + bottom: 0px + font-size: 25px + padding-right: 10px + background-color: rgba(255, 255, 255, 0.75) + border-radius: 6px + + .play-text-container + position: absolute + left: 50% + bottom: -10px + + .play-text + margin-left: -50% + font-size: 50px + + a[disabled] .level + opacity: 0.7 + + a.complete .level-difficulty:after + content: " - Complete!" + color: $yellow + + a.started .level-difficulty:after + content: " - Started" + color: desaturate($yellow, 50%) diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass index d051e9f43..64ea6e43a 100644 --- a/app/styles/play/level.sass +++ b/app/styles/play/level.sass @@ -1,6 +1,11 @@ @import "app/styles/bootstrap/mixins" @import "app/styles/mixins" +body.is-playing + background-color: black + .footer + background-color: black + #level-view margin: 0 auto @include user-select(none) @@ -11,9 +16,10 @@ #canvas-wrapper width: 55% position: relative + overflow: hidden canvas#surface - background-color: #ddd + background-color: #333 width: 100% display: block z-index: 1 @@ -55,12 +61,6 @@ #multiplayer-join-link font-size: 12px - #level-done-button - position: absolute - right: 46% - top: 43px - @include box-shadow(4px 4px 15px black) - // Custom Buttons .btn.banner @include banner-button(#FFF, #333) diff --git a/app/styles/play/level/control_bar.sass b/app/styles/play/level/control_bar.sass index 2e0b65365..ad5c89a36 100644 --- a/app/styles/play/level/control_bar.sass +++ b/app/styles/play/level/control_bar.sass @@ -39,3 +39,7 @@ top: -7px font-size: 13px height: 24px + + + #level-done-button + display: none diff --git a/app/styles/play/level/goals.sass b/app/styles/play/level/goals.sass index 6ad657253..e07eb4ef3 100644 --- a/app/styles/play/level/goals.sass +++ b/app/styles/play/level/goals.sass @@ -1,29 +1,30 @@ +@import "../../bootstrap/mixins" + #goals-view position: absolute left: 10px - top: 42px - background-color: rgba(200,200,200,0.8) - - &.brighter - background-color: rgba(200,200,200,1.0) + top: -100px + @include transition(top 0.5s ease-in-out) + background-color: rgba(200,200,200,1.0) border: black - padding: 5px 7px 5px 5px + padding: 15px 7px 2px 5px box-sizing: border-box border: 1px solid #333 border-radius: 5px - cursor: pointer - user-select: none - -webkit-user-select: none - h3 - font-size: 16px + .goals-status + font-size: 14px margin: 0 - line-height: 20px color: black - - i - margin-right: 5px + .success + color: darkgreen + .timed-out + color: darkslategray + .failure + color: darkred + .incomplete + color: darkgoldenrod ul padding-left: 0 diff --git a/app/styles/play/level/gold.sass b/app/styles/play/level/gold.sass index 706aa8a36..c1efa04bb 100644 --- a/app/styles/play/level/gold.sass +++ b/app/styles/play/level/gold.sass @@ -1,44 +1,46 @@ @import "app/styles/mixins" +@import "app/styles/bootstrap/mixins" #gold-view + display: none position: absolute right: 46% top: 42px user-select: none -webkit-user-select: none + @include transition(box-shadow .2s linear) + padding: 4px + background: transparent url(/images/level/gold_background.png) no-repeat + background-size: 100% 100% + border-radius: 4px - h3 + &:hover + box-shadow: 2px 2px 2px black + + .team-gold font-size: 16px margin: 0 line-height: 20px - color: hsla(205,0%,31%,1) - text-shadow: 0px 1px 1px white, 0px -1px 1px white, 1px 0px 1px white, -1px 0px 1px white + color: hsla(205,0%,51%,1) + display: inline-block + padding: 0px 4px &.team-humans color: hsla(4,80%,51%,1) &.team-ogres - color: hsla(205,100%,31%,1) + color: hsla(205,100%,51%,1) &.team-allies, &.team-minions - color: hsla(116,80%,31%,1) + color: hsla(116,80%,51%,1) img width: 16px height: 16px border-radius: 2px - padding: 2px - @include gradient-radial-custom-stops(hsla(205,0%,74%,1), 20%, hsla(205,0%,31%,1), 70%) + padding: 1px + margin-top: -1px - &.team-humans img - @include gradient-radial-custom-stops(hsla(4,80%,74%,1), 20%, hsla(4,80%,51%,1), 70%) - - &.team-ogres img - @include gradient-radial-custom-stops(hsla(205,100%,74%,1), 20%, hsla(205,100%,31%,1), 70%) - - &.team-allies img, &.team-minions img - @include gradient-radial-custom-stops(hsla(116,80%,74%,1), 20%, hsla(116,80%,31%,1), 70%) - .gold-amount display: inline-block - width: 20px + min-width: 20px diff --git a/app/styles/play/level/hud.sass b/app/styles/play/level/hud.sass index b508a3158..bfcbccba7 100644 --- a/app/styles/play/level/hud.sass +++ b/app/styles/play/level/hud.sass @@ -125,7 +125,7 @@ background-position-x: -6 * $iconSize &.prop-label-icon-maxSpeed background-position-x: -7 * $iconSize - &.prop-label-icon-gold + &.prop-label-icon-gold, &.prop-label-icon-bountyGold background-position-x: -8 * $iconSize .prop-value.bar-prop diff --git a/app/styles/play/level/loading.sass b/app/styles/play/level/loading.sass index 177334f72..3dac90ec8 100644 --- a/app/styles/play/level/loading.sass +++ b/app/styles/play/level/loading.sass @@ -1,16 +1,9 @@ @import "app/styles/bootstrap/mixins" @import "app/styles/mixins" -@mixin sky-background($url: '', $backgroundPosition: left) - $top: #95D9EF - $mid: #FFFFFF - $bot: #8EC643 - $stop: 99.6% - background: $mid - background-image: url($url) // fallback - background-image: url($url), -webkit-linear-gradient(top, $top, $mid $stop, $bot) - background-image: url($url), -ms-linear-gradient(top, $top, $mid $stop, $bot) - background-image: url($url), linear-gradient(to bottom, $top, $mid $stop, $bot) +@mixin wing-background($url: '', $backgroundPosition: left) + background: black + background-image: url($url) background-repeat: no-repeat background-position: top $backgroundPosition background-size: contain @@ -22,7 +15,9 @@ position: absolute z-index: 20 $UNVEIL_TIME: 1.2s - pointer-events: none + + &.unveiled + pointer-events: none .loading-details position: absolute @@ -67,11 +62,11 @@ position: absolute .left-wing - @include sky-background('/images/level/loading_left_wing.png', right) + @include wing-background('/images/level/loading_left_wing.png', right) left: -50% transition: all $UNVEIL_TIME ease .right-wing - @include sky-background('/images/level/loading_right_wing.png', left) + @include wing-background('/images/level/loading_right_wing.png', left) right: -50% transition: all $UNVEIL_TIME ease diff --git a/app/styles/play/level/modal/docs.sass b/app/styles/play/level/modal/docs.sass index 3c25f0389..2f833d8ef 100644 --- a/app/styles/play/level/modal/docs.sass +++ b/app/styles/play/level/modal/docs.sass @@ -1,2 +1,5 @@ #docs-modal .modal-dialog - width: 800px \ No newline at end of file + width: 800px + + li:not(.active) a[data-toggle="tab"] + cursor: pointer diff --git a/app/styles/play/level/modal/keyboard_shortcuts.sass b/app/styles/play/level/modal/keyboard_shortcuts.sass new file mode 100644 index 000000000..f0b13b5f3 --- /dev/null +++ b/app/styles/play/level/modal/keyboard_shortcuts.sass @@ -0,0 +1,12 @@ +#keyboard-shortcuts-modal + dl.dl-horizontal + dt + width: 120px + + code + color: #333 + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif + font-size: 100% + + dd + margin-left: 140px diff --git a/app/styles/play/level/tome/spell.sass b/app/styles/play/level/tome/spell.sass index aca71b8c8..73d727457 100644 --- a/app/styles/play/level/tome/spell.sass +++ b/app/styles/play/level/tome/spell.sass @@ -1,4 +1,4 @@ -@import "../../../bootstrap/mixins" +@import "app/styles/bootstrap/mixins" @mixin editor-height($extraHeight) @include box-sizing(border-box) @@ -42,6 +42,9 @@ width: 100% height: 100% + .powered-by-firepad + display: none + .ace_editor // When Firepad isn't active, .ace_editor needs the width/height set itself. @include editor-height(0px) @@ -109,8 +112,9 @@ // Override faint gray border-color: #BFF - .ace_identifier - border-bottom: 1px dotted rgba(255, 128, 128, 0.45) + // Decided it wasn't useful to show what can be hovered, since almost anything can, so we have to make it too faint to be useful if we don't want it to be really distracting. + //.ace_identifier + // border-bottom: 1px dotted rgba(0, 51, 255, 0.25) .ace_text-layer .ace_comment color: darken(rgb(103, 164, 200), 5%) @@ -119,3 +123,13 @@ // https://github.com/codecombat/codecombat/issues/6 color: rgb(145, 48, 50) + .ace_search + background-color: rgba(216, 187, 165, 1) + border: 0 + @include box-shadow(1px 2px 1px #444) + + .ace_search_field + width: 190px + + .ace_searchbtn, .ace_replacebtn + padding: 0px 4px diff --git a/app/styles/play/level/tome/spell_palette.sass b/app/styles/play/level/tome/spell_palette.sass index e941f8def..52ce37658 100644 --- a/app/styles/play/level/tome/spell_palette.sass +++ b/app/styles/play/level/tome/spell_palette.sass @@ -15,7 +15,7 @@ background-color: transparent background-size: 100% 100% z-index: 0 - overflow-y: auto + //overflow-y: auto img position: absolute @@ -47,6 +47,12 @@ &.multiple-tabs li:not(.active) a cursor: pointer + .tab-content + height: 80px + .nano-pane + width: 7px + right: 5px + //.nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus // background-color: lighten(rgb(230, 212, 146), 10%) diff --git a/app/styles/play/level/tome/spell_toolbar.sass b/app/styles/play/level/tome/spell_toolbar.sass index 082e1d2b2..9d7413705 100644 --- a/app/styles/play/level/tome/spell_toolbar.sass +++ b/app/styles/play/level/tome/spell_toolbar.sass @@ -6,6 +6,7 @@ margin: 4px 1% height: 45px width: 97% + z-index: 4 //background-color: rgba(100, 45, 210, 0.15) .flow diff --git a/app/styles/play/spectate.sass b/app/styles/play/spectate.sass index f04075b79..43d44e320 100644 --- a/app/styles/play/spectate.sass +++ b/app/styles/play/spectate.sass @@ -10,6 +10,8 @@ display: none #docs-button display: none + #gold-view + right: 1% #control-bar-view width: 100% @@ -31,6 +33,7 @@ max-height: 1284px .level-content + //max-width: 1920px position: relative margin: 0px auto @@ -40,7 +43,7 @@ margin: 0 auto canvas#surface - background-color: #ddd + background-color: #333 max-height: 93% max-height: -webkit-calc(100% - 60px) max-height: calc(100% - 60px) diff --git a/app/styles/treema-ext.sass b/app/styles/treema-ext.sass index 9e59b7773..be549724c 100644 --- a/app/styles/treema-ext.sass +++ b/app/styles/treema-ext.sass @@ -18,20 +18,6 @@ border: 3px inset rgba(0, 100, 100, 0.2) box-sizing: border-box padding: 5px - -.treema-node a.btn - height: 17px - display: inline-block - position: relative - width: 20px - padding: 0 - float: left - margin-right: 2px - - i - position: absolute - top: 1px - left: 3px .treema-selection-map position: fixed diff --git a/app/templates/about.jade b/app/templates/about.jade index 869d5331e..ab7e09a41 100644 --- a/app/templates/about.jade +++ b/app/templates/about.jade @@ -157,7 +157,8 @@ block content .col-sm-8 - h3 Glen De Cauwsemaecker + h3 + a(href="http://www.glendc.com/") Glen De Cauwsemaecker p(data-i18n="about.glen_description") | Programmer and passionate game developer, diff --git a/app/templates/account/job_profile.jade b/app/templates/account/job_profile.jade new file mode 100644 index 000000000..6ec836343 --- /dev/null +++ b/app/templates/account/job_profile.jade @@ -0,0 +1,28 @@ +h3(data-i18n="account_settings.job_profile") Job Profile + +.row + .col-md-9 + if me.get('jobProfileApproved') + p.lead(data-i18n="account_settings.job_profile_approved") Your job profile has been approved by CodeCombat. Employers will be able to see it until you either mark it inactive or it has not been changed for four weeks. + else + p.lead(data-i18n="account_settings.job_profile_explanation") Hi! Fill this out, and we will get in touch about finding you a software developer job. + + .row + .col-md-9 + .progress.profile-completion-progress + .progress-bar.progress-bar-success + .progress-next-item + + .col-md-3 + a.btn.btn-large.btn-primary.profile-preview-button.top-preview(href="/account/profile/#{me.id}", target="job_profile", data-i18n="account_settings.view_profile") View Your Profile + + .col-md-3 + .thumbnail.sample-profile-thumbnail + a(href="http://codecombat.com/images/pages/account/profile/sample_profile.png", target="_blank") + img(src="/images/pages/account/profile/sample_profile_thumb.png" alt="Sample Profile Thumbnail") + .caption + span(data-i18n="account_settings.sample_profile") See a sample profile + +#job-profile-treema + +a.btn.btn-large.btn-primary.profile-preview-button.bottom-preview(href="/account/profile/#{me.id}", target="job_profile", data-i18n="account_settings.view_profile") View Your Profile diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index 65dc9786b..aedd0d623 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -1,72 +1,130 @@ extends /templates/base block content + + if myProfile || (me.isAdmin() && user.get('jobProfile')) + .profile-control-bar + if myProfile + a(href=user.get('jobProfile') ? "/account/settings#job-profile" : "/account/settings") + button.btn.edit-settings-button + i.icon-cog + span(data-i18n="account_profile.edit_settings") Edit Settings + if me.isAdmin() && user.get('jobProfile') + button.btn.edit-settings-button#toggle-job-profile-approved + i.icon-cog + span(data-i18n='account_profile.approved').approved Approved + span(data-i18n='account_profile.not_approved').not-approved Not Approved + if user.id != me.id + button.btn.edit-settings-button#enter-espionage-mode 007 - if myProfile - a(href="/account/settings") - button.btn - i.icon-cog - span(data-i18n="account_profile.edit_settings") Edit Settings + if user.get('jobProfile') && allowedToViewJobProfile + - var profile = user.get('jobProfile'); + .job-profile-container + .job-profile-row + .left-column.full-height-column + .sub-column + .profile-photo-container + img.profile-photo(src=user.getPhotoURL(240, true)) + .profile-caption= profile.jobTitle || 'Software Developer' - h2 - if grav && grav.name && grav.name.formatted - span(data-i18n="account_profile.profile_for_prefix") Profile for - span= grav.name.formatted - span(data-i18n="account_profile.profile_for_suffix") - else - span(data-i18n="account_profile.profile") Profile + if profileLinks.length + ul.links + each link in profileLinks + if link.link && link.name + li(title=profile.name + " on " + link.name, class=link.icon ? "has-icon" : "") + a(href=link.link) + if link.icon + img(src=link.icon.url, alt=link.icon.name) + else + button.btn.btn-large.btn-inverse.flat-button= link.name + + div= profile.city + ', ' + profile.country + div= profile.visa + div + span(data-i18n="account_profile.looking_for") Looking for: + | #{profile.lookingFor} + div + span(data-i18n="account_profile.last_updated") Last updated: + | #{moment(profile.updated).fromNow()} + + button#contact-candidate.btn.btn-large.btn-inverse.flat-button + span(data-i18n="account_profile.contact") Contact + | #{profile.name.split(' ')[0]} - if loadingProfile - p(data-i18n="common.loading") Loading... - - else if !user.get('emailHash') - p(data-i18n="account_profile.user_not_found") No user found. Check the URL? - - else if !user.gravatarProfile - if myProfile - p - span(data-i18n="account_profile.gravatar_not_found_mine") We couldn't find your profile associated with: - strong "#{me.get('email')}" - span(data-i18n="account_profile.gravatar_not_found_email_suffix") . - span - span(data-i18n="account_profile.gravatar_signup_prefix") Sign up at - a(href="http://en.gravatar.com/") Gravatar - span(data-i18n="account_profile.gravatar_signup_suffix") to get set up! - else - p(data-i18n="account_profile.gravatar_not_found_other") - | Alas, there's no profile associated with this person's email address. + .middle-column.full-height-column + .sub-column + h3= profile.name || "Anonymous Developer" + if profile.shortDescription + p= profile.shortDescription + + each skill in profile.skills + code= skill + span + if profile.longDescription + div.long-description!= marked(profile.longDescription) + + if profile.work.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/work.png", alt="") + span(data-i18n="account_profile.work_experience") Work Experience + each job in profile.work + if job.role && job.employer + div.experience-entry + div.duration.pull-right= job.duration + | #{job.role} at #{job.employer} + .clearfix + if job.description + div!= marked(job.description) + + if profile.education.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/education.png", alt="") + span(data-i18n="account_profile.education") Education + each school in profile.education + if school.degree && school.school + div.experience-entry + div.duration.pull-right= school.duration + | #{school.degree} at #{school.school} + .clearfix + if school.description + div!= marked(school.description) + + if user.get('jobProfileNotes') || me.isAdmin() + h3.experience-header(data-i18n="account_profile.our_notes") Our Notes + - var notes = user.get('jobProfileNotes') || ''; + if me.isAdmin() + textarea#job-profile-notes!= notes + button.btn.btn-primary#save-notes-button Save Notes + else + div!= marked(notes) + .right-column.full-height-column + .sub-column + if profile.projects.length + h3(data-i18n="account_profile.projects") Projects + ul.projects + each project in profile.projects + if project.name + li + if project.link && project.link.length && project.link != 'http://example.com' + a(href=project.link) + if project.picture + .project-image(style="background-image: url('/file/" + project.picture + "')") + p= project.name + div!= marked(project.description) + else if allowedToViewJobProfile + .public-profile-container + h2 Loading... + + else - .container - div.row - div.col-xs-3 - img(src=photoURL).img-thumbnail - - p.about-me #{grav.aboutMe} + .public-profile-container + h2 + span(data-i18n="account_profile.profile_for_prefix") Profile for + span= user.get('name') + span(data-i18n="account_profile.profile_for_suffix") - if grav.emails - div.col-xs-3 - h3(data-i18n="account_profile.gravatar_contact") Contact - ul - each email in grav.emails - li #{email.value} - - if grav.urls && grav.urls.length - div.col-xs-3 - h3(data-i18n="account_profile.gravatar_websites") Websites - ul - each url in grav.urls - li - a(href="#{url.value}") #{url.title} - - if grav.accounts - div.col-xs-3 - h3(data-i18n="account_profile.gravatar_accounts") As Seen On - ul - each account in grav.accounts - li - a(href="#{account.url}") #{account.domain} - - hr - p - a(href="#{grav.profileUrl}", data-i18n="account_profile.gravatar_profile_link") Full Gravatar Profile + img.profile-photo(src=user.getPhotoURL(256)) + + h2 TODO + p Public user profiles are not ready yet. If you are seeing this, we probably have a bug leading to a broken link. \ No newline at end of file diff --git a/app/templates/account/settings.jade b/app/templates/account/settings.jade index 91b533b1b..b609768d1 100644 --- a/app/templates/account/settings.jade +++ b/app/templates/account/settings.jade @@ -8,7 +8,8 @@ block content p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings. else - button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically + #save-button-container + button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically ul.nav.nav-pills#settings-tabs li @@ -21,6 +22,9 @@ block content a(href="#password-pane", data-toggle="tab", data-i18n="account_settings.password_tab") Password li a(href="#email-pane", data-toggle="tab", data-i18n="account_settings.emails_tab") Emails + if showsJobProfileTab + li + a(href="#job-profile-pane", data-toggle="tab", data-i18n="account_settings.job_profile_tab") Job Profile .tab-content#settings-panes #general-pane.tab-pane @@ -28,34 +32,22 @@ block content .form .form-group label.control-label(for="name", data-i18n="general.name") Name - input#name.form-control(name="name", type="text", value="#{me.get('name')||''}", placeholder="#{gravatarName}") + input#name.form-control(name="name", type="text", value="#{me.get('name') || ''}") .form-group label.control-label(for="email", data-i18n="general.email") Email input#email.form-control(name="email", type="text", value="#{me.get('email')}") if !isProduction .form-group.checkbox - label(for="email", data-i18n="account_settings.admin") Admin - input#admin(name="admin", type="checkbox", checked=me.get('permissions').indexOf('admin')>-1)) + label(for="admin", data-i18n="account_settings.admin") Admin + input#admin(name="admin", type="checkbox", checked=me.get('permissions').indexOf('admin') != -1) #picture-pane.tab-pane - h3(data-i18n="account_settings.gravatar_select") Select which Gravatar photo to use - p - if !photos - span(data-i18n="account_settings.gravatar_add_photos") Add thumbnails and photos to a Gravatar account for your email to choose an image. - - else - .thumbnails - each photo, i in photos - .thumbnail - label(for="photo-#{i}") - img(src=photo) - br - input(type="radio", name="photoURL", value="#{photo}", id="photo-#{i}", checked=photo==chosenPhoto) - .clearfix - p - a(href="http://en.gravatar.com/profiles/edit/?noclose#your-images", target="_blank", data-i18n="account_settings.gravatar_add_more_photos") Add more photos to your Gravatar account to access them here. - + h3(data-i18n="account_settings.upload_picture") Upload a picture + #picture-treema + .gravatar-fallback + img(src=me.getPhotoURL(256), alt="Gravatar", title="Gravatar fallback image") + #wizard-pane.tab-pane #wizard-settings-view @@ -75,16 +67,28 @@ block content p .form .form-group.checkbox - label.control-label(for="email_announcement", data-i18n="account_settings.email_announcements") Announcements - input#email_announcement(name="email_announcement", type="checkbox", checked=subs.announcement) + label.control-label(for="email_generalNews", data-i18n="account_settings.email_announcements") Announcements + input#email_generalNews(name="email_generalNews", type="checkbox", checked=subs.generalNews) span.help-block(data-i18n="account_settings.email_announcements_description") Get emails on the latest news and developments at CodeCombat. - + + hr + h4(data-i18n="account_settings.email_notifications") Notifications + span(data-i18n="account_settings.email_notifications_summary") Controls for personalized, automatic email notifications related to your CodeCombat activity. + .form .form-group.checkbox - label.control-label(for="email_notification", data-i18n="account_settings.email_notifications") Notifications - input#email_notification(name="email_notification", type="checkbox", checked=subs.notification) - span.help-block(data-i18n="account_settings.email_notifications_description") Get periodic notifications for your account. - hr + label.control-label(for="email_anyNotes", data-i18n="account_settings.email_any_notes") Any Notifications + input#email_anyNotes(name="email_anyNotes", type="checkbox", checked=subs.anyNotes) + span.help-block(data-i18n="account_settings.email_any_notes_description") Disable to stop all activity notification emails. + + fieldset#specific-notification-settings + + .form-group.checkbox + label.control-label(for="email_recruitNotes", data-i18n="account_settings.email_recruit_notes") Job Opportunities + input#email_recruitNotes(name="email_recruitNotes", type="checkbox", checked=subs.recruitNotes) + span.help-block(data-i18n="account_settings.email_recruit_notes_description") If you play really well, we may contact you about getting you a (better) job. + + hr h4(data-i18n="account_settings.contributor_emails") Contributor Class Emails span(data-i18n="account_settings.contribute_prefix") We're looking for people to join our party! Check out the @@ -93,63 +97,66 @@ block content .form .form-group.checkbox - label.control-label(for="email_developer") + label.control-label(for="email_archmageNews") span(data-i18n="classes.archmage_title") | Archmage | span(data-i18n="classes.archmage_title_description") | (Coder) - input#email_developer(name="email_developer", type="checkbox", checked=subs.developer) + input#email_archmageNews(name="email_archmageNews", type="checkbox", checked=subs.archmageNews) span(data-i18n="contribute.archmage_subscribe_desc").help-block Get emails about general news and announcements about CodeCombat. .form-group.checkbox - label.control-label(for="email_level_creator") + label.control-label(for="email_artisanNews") span(data-i18n="classes.artisan_title") | Artisan | span(data-i18n="classes.artisan_title_description") | (Level Builder) - input#email_level_creator(name="email_level_creator", type="checkbox", checked=subs.level_creator) + input#email_artisanNews(name="email_artisanNews", type="checkbox", checked=subs.artisanNews) span(data-i18n="contribute.artisan_subscribe_desc").help-block Get emails on level editor updates and announcements. .form-group.checkbox - label.control-label(for="email_tester") + label.control-label(for="email_adventurerNews") span(data-i18n="classes.adventurer_title") | Adventurer | span(data-i18n="classes.adventurer_title_description") | (Level Playtester) - input#email_tester(name="email_tester", type="checkbox", checked=subs.tester) + input#email_adventurerNews(name="email_adventurerNews", type="checkbox", checked=subs.adventurerNews) span(data-i18n="contribute.adventurer_subscribe_desc").help-block Get emails when there are new levels to test. .form-group.checkbox - label.control-label(for="email_article_editor") + label.control-label(for="email_scribeNews") span(data-i18n="classes.scribe_title") | Scribe | span(data-i18n="classes.scribe_title_description") | (Article Editor) - input#email_article_editor(name="email_article_editor", type="checkbox", checked=subs.article_editor) + input#email_scribeNews(name="email_scribeNews", type="checkbox", checked=subs.scribeNews) span(data-i18n="contribute.scribe_subscribe_desc").help-block Get emails about article writing announcements. .form-group.checkbox - label.control-label(for="email_translator") + label.control-label(for="email_diplomatNews") span(data-i18n="classes.diplomat_title") | Diplomat | span(data-i18n="classes.diplomat_title_description") | (Translator) - input#email_translator(name="email_translator", type="checkbox", checked=subs.translator) + input#email_diplomatNews(name="email_diplomatNews", type="checkbox", checked=subs.diplomatNews) span(data-i18n="contribute.diplomat_subscribe_desc").help-block Get emails about i18n developments and, eventually, levels to translate. .form-group.checkbox - label.control-label(for="email_support") + label.control-label(for="email_ambassadorNews") span(data-i18n="classes.ambassador_title") | Ambassador | span(data-i18n="classes.ambassador_title_description") | (Support) - input#email_support(name="email_support", type="checkbox", checked=subs.support) + input#email_ambassadorNews(name="email_ambassadorNews", type="checkbox", checked=subs.ambassadorNews) span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments. button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All + + #job-profile-pane.tab-pane + #job-profile-view diff --git a/app/templates/account/wizard_settings.jade b/app/templates/account/wizard_settings.jade index 95684394c..f0c95410e 100644 --- a/app/templates/account/wizard_settings.jade +++ b/app/templates/account/wizard_settings.jade @@ -1,9 +1,9 @@ #color-settings table.table.table-bordered tr - th - th Color - th Group + th(data-i18n="wizard_settings.active") Active + th(data-i18n="wizard_settings.color") Color + th(data-i18n="wizard_settings.group") Group for group in colorGroups tr.color-group(data-name=group.name) td.enabled-cell diff --git a/app/templates/base.jade b/app/templates/base.jade index 3aec9624e..dc5d79286 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -16,12 +16,8 @@ body ul.nav.navbar-nav li.play a.header-font(href='/play', data-i18n="nav.play") Levels - li.editor - a.header-font(href='/editor', data-i18n="nav.editor") Editor - li.blog - a.header-font(href='http://blog.codecombat.com/', data-i18n="nav.blog") Blog - li.forum - a.header-font(href='http://discourse.codecombat.com/', data-i18n="nav.forum") Forum + li + a.header-font(href='/community', data-i18n="nav.community") Community .nav.navbar.navbar-fixed-top#top-nav .content.clearfix @@ -33,31 +29,24 @@ body if me.get('anonymous') === false button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out - a.btn.btn-primary.navbuttontext.header-font(href="/account/profile/#{me.id}") - div.navbuttontext-user-name - | #{me.displayName()} - i.icon-cog.icon-white.big + a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings") + div.navbuttontext-account(data-i18n="nav.account") Account + if me.get('photoURL') + img.account-settings-image(src=me.getPhotoURL(18), alt="") + else + span.glyphicon.glyphicon-user else - button.btn.btn-primary.navbuttontext.header-font(data-toggle="coco-modal", data-target="modal/signup", data-i18n="login.sign_up") Create Account - button.btn.btn-primary.navbuttontext.header-font(data-toggle="coco-modal", data-target="modal/login", data-i18n="login.log_in") Log In + button.btn.btn-primary.navbuttontext.header-font.auth-button + span(data-i18n="login.log_in") Log In + span.spr.spl / + span(data-i18n="login.sign_up") Create Account ul(class='navbar-link-text').nav.navbar-nav.pull-right li.play a.header-font(href='/play', data-i18n="nav.play") Levels - li.editor - a.header-font(href='/editor', data-i18n="nav.editor") Editor - li.blog - a.header-font(href='http://blog.codecombat.com/', data-i18n="nav.blog") Blog - li.forum - a.header-font(href='http://discourse.codecombat.com/', data-i18n="nav.forum") Forum - if me.isAdmin() - li.admin - a.header-font(href='/admin', data-i18n="nav.admin") Admin - - - - + li + a.header-font(href='/community', data-i18n="nav.community") Community block outer_content #outer-content-wrapper @@ -68,10 +57,10 @@ body p If this is showing, you dun goofed block footer - .footer + .footer.clearfix .content p.footer-link-text - if pathname == "/" + if pathname == "/" || (me.get('permissions') || []).indexOf('employer') != -1 a(href='/employers', title='Home', tabindex=-1, data-i18n="nav.employers") Employers else a(href='/', title='Home', tabindex=-1, data-i18n="nav.home") Home @@ -79,6 +68,11 @@ body a(href='/legal', title='Legal', tabindex=-1, data-i18n="nav.legal") Legal a(href='/about', title='About', tabindex=-1, data-i18n="nav.about") About a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Contact + a(href='/editor', data-i18n="nav.editor") Editor + a(href='http://blog.codecombat.com/', data-i18n="nav.blog") Blog + a(href='http://discourse.codecombat.com/', data-i18n="nav.forum") Forum + if me.isAdmin() + a(href='/admin', data-i18n="nav.admin") Admin .share-buttons .g-plusone(data-href="http://codecombat.com", data-size="medium") diff --git a/app/templates/cla.jade b/app/templates/cla.jade index b07ca988c..cdf375968 100644 --- a/app/templates/cla.jade +++ b/app/templates/cla.jade @@ -80,12 +80,13 @@ hr if me.get('anonymous') - h1 - | Either - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="login.sign_up") create an account - | or - a(data-toggle="coco-modal", data-target="modal/login", data-i18n="login.log_in") log In - | to sign this agreement. + p + strong You must be signed in to sign this agreement. + + button.btn.btn-primary.auth-button + span(data-i18n="login.log_in") Log In + span.spr.spl / + span(data-i18n="login.sign_up") Create Account else h3 SIGN HERE diff --git a/app/templates/community.jade b/app/templates/community.jade new file mode 100644 index 000000000..3ff8d3444 --- /dev/null +++ b/app/templates/community.jade @@ -0,0 +1,88 @@ +extends /templates/base + +block content + + h1(data-i18n="community.main_title") CodeCombat Community + + p There are dozens of ways you can get involved with CodeCombat. Check out the resources we've built, decide what sounds the most fun, and we look forward to working with you! + + div + + .community_columns + + h2 Levels and Art + + p We have built several tools that enable users to get not only edit, but also build new game content! + + ul + li + a(href="/editor/level", data-i18n="community.level_editor") + | : fork, edit, or build your own CodeCombat levels. New levels can be kept private or published to the community. + li + a(href="/editor/thang", data-i18n="editor.thang_title") + | : modify or import new art assets for the game using our powerful editor. + li + a(href="/editor/article", data-i18n="editor.article_title") + | : edit or create documentation used in CodeCombat levels. + + p Right now most of our editing tools are very rough, but we are improving them constantly and welcome your feedback. + + .community_columns + + h2 Connect + + p There are a bunch of ways you can connect with us and get involved in the ongoing development of CodeCombat: + + ul + + li We write about our progress and current projects on our + a(href="http://blog.codecombat.com", data-i18n="nav.blog") + | . + li Participate in our active user community by checking out our + a(href="http://discourse.codecombat.com", data-i18n="nav.forum") + | . + li For regular news about learning to code, games, and education, check out our + a(href="https://www.facebook.com/codecombat", data-i18n="community.facebook") + | . + li For realtime status or to have a quick chat, follow us on + a(href="https://twitter.com/CodeCombat", data-i18n="community.twitter") + | . + li Don't like Facebook? We're on + a(href="https://plus.google.com/115285980638641924488/posts", data-i18n="community.gplus") + | . + li You can also find us in our + a(href="http://www.hipchat.com/g3plnOKqa", data-i18n="editor.hipchat_url") + + .community_columns + + h2 Contribute + + p Put your skills to use helping us teach the world to code. We have a lot of roles you can consider, and if we don't have a role for you, let us know: + + ul + + li + a(href="/contribute#archmage", data-i18n="classes.archmage_title") + | : contribute by writing code. + li + a(href="/contribute#artisan", data-i18n="classes.artisan_title") + | : build new game levels. + li + a(href="/contribute#adventurer", data-i18n="classes.adventurer_title") + | : test new game levels. + li + a(href="/contribute#scribe", data-i18n="classes.scribe_title") + | : write educational documentation. + li + a(href="/contribute#diplomat", data-i18n="classes.diplomat_title") + | : translate site content. + li + a(href="/contribute#ambassador", data-i18n="classes.ambassador_title") + | : support our community of educators and coders. + li + a(href="/contribute#counselor", data-i18n="classes.counselor_title") + | : offer your advice and business acumen to the founders. + + | Check out the + a(href="/contribute", data-i18n="nav.contribute") + | page to find out more about the roles and how you can get started. diff --git a/app/templates/contribute/adventurer.jade b/app/templates/contribute/adventurer.jade index 0cbe1e866..3b9367620 100644 --- a/app/templates/contribute/adventurer.jade +++ b/app/templates/contribute/adventurer.jade @@ -53,30 +53,12 @@ block content span(data-i18n="contribute.adventurer_join_suf") | so if you prefer to be notified those ways, sign up there! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="tester", data-contributor-class-name="adventurer") - label.checkbox(for="tester").well - input(type='checkbox', name="tester", id="tester") - span(data-i18n="contribute.adventurer_subscribe_desc") - | Get emails when there are new levels to test. - .saved-notification ✓ Saved - - //#Contributors - // h3(data-i18n="contribute.brave_adventurers") - // | Our Brave Adventurers: - // ul.adventurers - // li Kieizroe - // li ... many, many more + //h3(data-i18n="contribute.brave_adventurers") + // | Our Brave Adventurers: + // + //#contributor-list div.clearfix diff --git a/app/templates/contribute/ambassador.jade b/app/templates/contribute/ambassador.jade index dc1048ac6..2d1f65564 100644 --- a/app/templates/contribute/ambassador.jade +++ b/app/templates/contribute/ambassador.jade @@ -47,29 +47,12 @@ block content | solving levels can summon higher level wizards to help them. | This will be a great way for ambassadors to do their thing. We'll keep you posted! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="support", data-contributor-class-name="ambassador") - label.checkbox(for="support").well - input(type='checkbox', name="support", id="support") - span(data-i18n="contribute.ambassador_subscribe_desc") - | Get emails on support updates and multiplayer developments. - .saved-notification ✓ Saved - - //#Contributors - // h3(data-i18n="contribute.helpful_ambassadors") - // | Our Helpful Ambassadorsd: - // ul.ambassadors - // li + //h3(data-i18n="contribute.helpful_ambassadors") + // | Our Helpful Ambassadorsd: + // + //#contributor-list div.clearfix diff --git a/app/templates/contribute/archmage.jade b/app/templates/contribute/archmage.jade index db7dad7db..ae982472e 100644 --- a/app/templates/contribute/archmage.jade +++ b/app/templates/contribute/archmage.jade @@ -57,37 +57,12 @@ block content span(data-i18n="contribute.join_desc_4") | and we'll go from there! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="developer", data-contributor-class-name="archmage") - label.checkbox(for="developer").well - input(type='checkbox', name="developer", id="developer") - span(data-i18n="contribute.archmage_subscribe_desc") - | Get emails on new coding opportunities and announcements. - .saved-notification ✓ Saved + h3(data-i18n="contribute.powerful_archmages") + | Our Powerful Archmages: - #Contributors - h3(data-i18n="contribute.powerful_archmages") - | Our Powerful Archmages: - .row - for contributor in contributors - .col-xs-6.col-md-3 - .thumbnail - if contributor.avatar - img.img-responsive(src="/images/pages/contribute/archmage/" + contributor.avatar + "_small.png", alt="") - else - img.img-responsive(src="/images/pages/contribute/archmage.png", alt="") - .caption - h4= contributor.name + #contributor-list div.clearfix diff --git a/app/templates/contribute/artisan.jade b/app/templates/contribute/artisan.jade index 9e6d1320d..54a27327b 100644 --- a/app/templates/contribute/artisan.jade +++ b/app/templates/contribute/artisan.jade @@ -54,38 +54,13 @@ block content li a(href="http://discourse.codecombat.com", data-i18n="contribute.artisan_join_step4") Post your levels on the forum for feedback. - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="level_creator", data-contributor-class-name="artisan") - label.checkbox(for="level_creator").well - input(type='checkbox', name="level_creator", id="level_creator") - span(data-i18n="contribute.artisan_subscribe_desc") - | Get emails on level editor updates and announcements. - .saved-notification ✓ Saved + h3(data-i18n="contribute.creative_artisans") + | Our Creative Artisans: - #Contributors - h3(data-i18n="contribute.creative_artisans") - | Our Creative Artisans: - .row - for contributor in contributors - .col-xs-6.col-md-3 - .thumbnail - if contributor.avatar - img.img-responsive(src="/images/pages/contribute/artisan/" + contributor.avatar + "_small.png", alt="") - else - img.img-responsive(src="/images/pages/contribute/artisan.png", alt="") - .caption - h4= contributor.name + #contributor-list div.clearfix diff --git a/app/templates/contribute/contribute.jade b/app/templates/contribute/contribute.jade index ffb7045a6..d9457e6d8 100644 --- a/app/templates/contribute/contribute.jade +++ b/app/templates/contribute/contribute.jade @@ -37,18 +37,7 @@ block content | - Nick, George, Scott, Michael, Jeremy and Glen hr - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous #archmage.header-scrolling-fix .class_image @@ -69,13 +58,7 @@ block content p.lead(data-i18n="contribute.more_about_archmage") | Learn More About Becoming an Archmage - label.checkbox(for="developer").well - input(type='checkbox', name="developer", id="developer") - span(data-i18n="contribute.archmage_subscribe_desc") - | Get emails on new coding opportunities and announcements. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="developer", data-contributor-class-name="archmage") #artisan.header-scrolling-fix @@ -102,13 +85,7 @@ block content p.lead(data-i18n="contribute.more_about_artisan") | Learn More About Becoming An Artisan - label.checkbox(for="level_creator").well - input(type='checkbox', name="level_creator", id="level_creator") - span(data-i18n="contribute.artisan_subscribe_desc") - | Get emails on level editor updates and announcements. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="level_creator", data-contributor-class-name="artisan") #adventurer.header-scrolling-fix @@ -130,13 +107,7 @@ block content p.lead(data-i18n="contribute.more_about_adventurer") | Learn More About Becoming an Adventurer - label.checkbox(for="tester").well - input(type='checkbox', name="tester", id="tester") - span(data-i18n="contribute.adventurer_subscribe_desc") - | Get emails when there are new levels to test. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="tester", data-contributor-class-name="adventurer") #scribe.header-scrolling-fix @@ -162,13 +133,7 @@ block content p.lead(data-i18n="contribute.more_about_scribe") | Learn More About Becoming a Scribe - label.checkbox(for="article_editor").well - input(type='checkbox', name="article_editor", id="article_editor") - span(data-i18n="contribute.scribe_subscribe_desc") - | Get emails about article writing announcements. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="article_editor", data-contributor-class-name="scribe") #diplomat.header-scrolling-fix @@ -191,14 +156,8 @@ block content p.lead(data-i18n="contribute.more_about_diplomat") | Learn More About Becoming a Diplomat - label.checkbox(for="translator").well - input(type='checkbox', name="translator", id="translator") - span(data-i18n="contribute.diplomat_subscribe_desc") - | Get emails about i18n developments and levels to translate. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved - + .contributor-signup(data-contributor-class-id="translator", data-contributor-class-name="diplomat") + #ambassador.header-scrolling-fix .class_image @@ -212,19 +171,13 @@ block content | We are trying to build a community, and every community needs a support team when | there are troubles. We have got chats, emails, and social networks so that our users | can get acquainted with the game. If you want to help people get involved, have fun, - | and learn some programming, then this class is for you. + | and learn some programming, then this c lass is for you. a(href="/contribute/ambassador") p.lead(data-i18n="contribute.more_about_ambassador") | Learn More About Becoming an Ambassador - label.checkbox(for="support").well - input(type='checkbox', name="support", id="support") - span(data-i18n="contribute.ambassador_subscribe_desc") - | Get emails on support updates and multiplayer developments. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="support", data-contributor-class-name="ambassador") #counselor.header-scrolling-fix diff --git a/app/templates/contribute/contributor_list.jade b/app/templates/contribute/contributor_list.jade new file mode 100644 index 000000000..32e5ac9e3 --- /dev/null +++ b/app/templates/contribute/contributor_list.jade @@ -0,0 +1,13 @@ +.row + for contributor in contributors + .col-xs-6.col-md-3 + .thumbnail + - var src = "/images/pages/contribute/" + contributorClassName + ".png"; + - if(contributor.avatar) + - src = src.replace(contributorClassName, contributorClassName + "/" + contributor.avatar + "_small"); + - if(contributor.id) + - src = "/db/user/" + contributor.id + "/avatar?s=100&fallback=" + src; + a(href=contributor.github ? "https://github.com/codecombat/codecombat/commits?author=" + contributor.github : null, class=contributor.github ? 'has-github' : '') + img.img-responsive(src=src, alt=contributor.name) + .caption + h4= contributor.name diff --git a/app/templates/contribute/contributor_signup.jade b/app/templates/contribute/contributor_signup.jade new file mode 100644 index 000000000..cd47b26f4 --- /dev/null +++ b/app/templates/contribute/contributor_signup.jade @@ -0,0 +1,5 @@ +label.checkbox(for=contributorClassName).well + input(type='checkbox', name=contributorClassName, id=contributorClassName) + span(data-i18n="contribute.#{contributorClassName}_subscribe_desc") + .saved-notification ✓ Saved + diff --git a/app/templates/contribute/contributor_signup_anonymous.jade b/app/templates/contribute/contributor_signup_anonymous.jade new file mode 100644 index 000000000..4fc05a4bb --- /dev/null +++ b/app/templates/contribute/contributor_signup_anonymous.jade @@ -0,0 +1,13 @@ +if me.attributes.anonymous + div#sign-up.alert.alert-info + strong(data-i18n="contribute.alert_account_message_intro") + | Hey there! + span + span(data-i18n="contribute.alert_account_message") + | To subscribe for class emails, you'll need to be logged in first. + + strong.spl + a.auth-button + span(data-i18n="login.log_in") Log In + span.spr.spl / + span(data-i18n="login.sign_up") Create Account \ No newline at end of file diff --git a/app/templates/contribute/diplomat.jade b/app/templates/contribute/diplomat.jade index c662dce6a..845da167d 100644 --- a/app/templates/contribute/diplomat.jade +++ b/app/templates/contribute/diplomat.jade @@ -44,51 +44,37 @@ block content | , edit it online, and submit a pull request. Also, check this box below to | keep up-to-date on new internationalization developments! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="translator", data-contributor-class-name="diplomat") - label.checkbox(for="translator").well - input(type='checkbox', name="translator", id="translator") - span(data-i18n="contribute.diplomat_subscribe_desc") - | Get emails about i18n developments and levels to translate. - .saved-notification ✓ Saved + h3(data-i18n="contribute.translating_diplomats") + | Our Translating Diplomats: - #Contributors - h3(data-i18n="contribute.translating_diplomats") - | Our Translating Diplomats: - ul.diplomats - li Turkish - Nazım Gediz AydındoÄŸmuÅŸ, cobaimelan, wakeup - li Brazilian Portuguese - Gutenberg Barros, Kieizroe, Matthew Burt, brunoporto, cassiocardoso - li Portugal Portuguese - Matthew Burt, ReiDuKuduro - li German - Dirk, faabsen, HiroP0, Anon, bkimminich - li Thai - Kamolchanok Jittrepit - li Vietnamese - An Nguyen Hoang Thien - li Dutch - Glen De Cauwsemaecker, Guido Zuidhof, Ruben Vereecken, Jasper D'haene - li Greek - Stergios - li Latin American Spanish - Jesús Ruppel, Matthew Burt, Mariano Luzza - li Spain Spanish - Matthew Burt, DanielRodriguezRivero, Anon, Pouyio - li French - Xeonarno, Elfisen, Armaldio, MartinDelille, pstweb, veritable, jaybi, xavismeh, Anon, Feugy - li Hungarian - ferpeter, csuvsaregal, atlantisguru, Anon - li Japanese - g1itch, kengos - li Chinese - Adam23, spacepope, yangxuan8282 - li Polish - Anon, Kacper Ciepielewski - li Danish - Einar Rasmussen, sorsjen, Randi Hillerøe, Anon - li Slovak - Anon - li Persian - Reza Habibi (Rehb) - li Czech - vanous - li Russian - fess89, ser-storchak, Mr A - li Ukrainian - fess89 - li Italian - flauta - li Norwegian - bardeh + //#contributor-list + // TODO: collect CodeCombat userids for these guys so we can include a tiled list + ul.diplomats + li Turkish - Nazım Gediz AydındoÄŸmuÅŸ, cobaimelan, wakeup + li Brazilian Portuguese - Gutenberg Barros, Kieizroe, Matthew Burt, brunoporto, cassiocardoso + li Portugal Portuguese - Matthew Burt, ReiDuKuduro + li German - Dirk, faabsen, HiroP0, Anon, bkimminich + li Thai - Kamolchanok Jittrepit + li Vietnamese - An Nguyen Hoang Thien + li Dutch - Glen De Cauwsemaecker, Guido Zuidhof, Ruben Vereecken, Jasper D'haene + li Greek - Stergios + li Latin American Spanish - Jesús Ruppel, Matthew Burt, Mariano Luzza + li Spain Spanish - Matthew Burt, DanielRodriguezRivero, Anon, Pouyio + li French - Xeonarno, Elfisen, Armaldio, MartinDelille, pstweb, veritable, jaybi, xavismeh, Anon, Feugy + li Hungarian - ferpeter, csuvsaregal, atlantisguru, Anon + li Japanese - g1itch, kengos, treby + li Chinese - Adam23, spacepope, yangxuan8282, Cheng Zheng + li Polish - Anon, Kacper Ciepielewski + li Danish - Einar Rasmussen, sorsjen, Randi Hillerøe, Anon + li Slovak - Anon + li Persian - Reza Habibi (Rehb) + li Czech - vanous + li Russian - fess89, ser-storchak, Mr A + li Ukrainian - fess89 + li Italian - flauta + li Norwegian - bardeh div.clearfix diff --git a/app/templates/contribute/scribe.jade b/app/templates/contribute/scribe.jade index eafab49b6..92155143c 100644 --- a/app/templates/contribute/scribe.jade +++ b/app/templates/contribute/scribe.jade @@ -44,37 +44,12 @@ block content | tell us a little about yourself, your experience with programming and | what sort of things you'd like to write about. We'll go from there! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="article_editor", data-contributor-class-name="scribe") - label.checkbox(for="article_editor").well - input(type='checkbox', name="article_editor", id="article_editor") - span(data-i18n="contribute.scribe_subscribe_desc") - | Get emails about article writing announcements. - .saved-notification ✓ Saved - - #Contributors - h3(data-i18n="contribute.diligent_scribes") - | Our Diligent Scribes: - ul.scribes - li Ryan Faidley - li Glen De Cauwsemaecker - li Mischa Lewis-Norelle - li Tavio - li Ronnie Cheng - li engstrom - li Dman19993 - li mattinsler - + h3(data-i18n="contribute.diligent_scribes") + | Our Diligent Scribes: + + #contributor-list(data-contributor-class-name="scribe") div.clearfix diff --git a/app/templates/editor.jade b/app/templates/editor.jade index c92140c29..82101e03f 100644 --- a/app/templates/editor.jade +++ b/app/templates/editor.jade @@ -34,11 +34,9 @@ block content hr p - span(data-i18n="editor.security_notice") - | Many major features in these editors are not currently enabled by default. - | As we improve the security of these systems, they will be made generally available. - | If you'd like to use these features sooner, - a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/contact", data-i18n="editor.contact_us") email us! + span(data-i18n="editor.got_questions") Questions about using the CodeCombat editors? + | + a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/contact", data-i18n="editor.contact_us") Contact us! | span(data-i18n="editor.hipchat_prefix") You can also find us in our | diff --git a/app/templates/editor/article/edit.jade b/app/templates/editor/article/edit.jade index 4969e30e9..d61460e60 100644 --- a/app/templates/editor/article/edit.jade +++ b/app/templates/editor/article/edit.jade @@ -10,10 +10,10 @@ block content li.active | #{article.attributes.name} - button(data-i18n="general.history").btn.btn-primary#history-button History + button(data-i18n="general.version_history").btn.btn-primary#history-button Version History button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true").btn.btn-primary#revert-button Revert button(data-i18n="article.edit_btn_preview", disabled=authorized === true ? undefined : "true").btn.btn-primary#preview-button Preview - button(data-toggle="coco-modal", data-target="modal/save_version", data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save + button(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save h3(data-i18n="article.edit_article_title") Edit Article span diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade new file mode 100644 index 000000000..60b95cc79 --- /dev/null +++ b/app/templates/editor/delta.jade @@ -0,0 +1,48 @@ +- var i = 0 + +mixin deltaPanel(delta, conflict) + - delta.index = i++ + .delta.panel.panel-default(class='delta-'+delta.action, data-index=i) + .panel-heading + if delta.action === 'added' + strong(data-i18n="delta.added") Added + if delta.action === 'modified' + strong(data-i18n="delta.modified") Modified + if delta.action === 'deleted' + strong(data-i18n="delta.deleted") Deleted + if delta.action === 'moved-index' + strong(data-i18n="delta.modified_array") Moved Index + if delta.action === 'text-diff' + strong(data-i18n="delta.text_diff") Text Diff + span + a(data-toggle="collapse" data-parent="#delta-accordion"+(counter) href="#collapse-"+(i+counter)) + span= delta.humanPath + + .panel-collapse.collapse(id="collapse-"+(i+counter)) + .panel-body.row(class=conflict ? "conflict-details" : "details") + if delta.action === 'added' + .new-value.col-md-12= delta.right + if delta.action === 'modified' + .old-value.col-md-6= delta.left + .new-value.col-md-6= delta.right + if delta.action === 'deleted' + .col-md-12 + div.old-value= delta.left + if delta.action === 'text-diff' + .col-md-12 + div.text-diff + if delta.action === 'moved-index' + .col-md-12 + span Moved array value #{JSON.stringify(delta.left)} to index #{delta.destinationIndex} + + if delta.conflict && !conflict + .panel-body + strong(data-i18n="delta.merge_conflict_with") MERGE CONFLICT WITH + +deltaPanel(delta.conflict, true) + +.panel-group(id='delta-accordion-'+(counter)) + for delta in deltas + +deltaPanel(delta) + if !deltas.length + alert.alert-warning(data-i18n="delta.no_changes") No changes + \ No newline at end of file diff --git a/app/templates/editor/level/component/edit.jade b/app/templates/editor/level/component/edit.jade index 44d368080..fda8ef9be 100644 --- a/app/templates/editor/level/component/edit.jade +++ b/app/templates/editor/level/component/edit.jade @@ -1,23 +1,44 @@ nav.navbar.navbar-default(role='navigation') - .container-fluid - .navbar-header - span.navbar-brand - span(data-i18n="editor.level_component_edit_title") - | Edit Component - span : - | "#{editTitle}" - .collapse.navbar-collapse - ul.nav.navbar-nav.nav-tabs - li.active - a(href="#component-code" data-toggle="tab" data-i18n="general.code") Code + ul.nav.navbar-nav.nav-tabs + li.active + a(href="#component-code" data-toggle="tab" data-i18n="general.code")#component-code-tab Code + li + a(href="#component-config-schema" data-toggle="tab" data-i18n="editor.level_component_config_schema")#component-config-schema-tab Config Schema + li + a(href="#component-settings" data-toggle="tab" data-i18n="editor.level_component_settings")#component-settings-tab Settings + li + a(href="#component-patches" data-toggle="tab" data-i18n="resources.patches")#component-patches-tab Patches + + .navbar-header + span.navbar-brand= editTitle + + ul.nav.navbar-nav.navbar-right + li.dropdown + a(data-toggle='dropdown') + span.glyphicon-chevron-down.glyphicon + + ul.dropdown-menu + li.dropdown-header Actions li - a(href="#component-config-schema" data-toggle="tab" data-i18n="editor.level_component_config_schema") Config Schema - li - a(href="#component-settings" data-toggle="tab" data-i18n="editor.level_component_settings") Settings - ul.nav.navbar-nav.navbar-left - li(data-i18n="general.history").btn.btn-primary.navbar-btn#history-button History - ul.nav.navbar-nav.navbar-right - li(data-i18n="editor.level_component_btn_new").btn.btn-primary.navbar-btn#create-new-component-button Create New Component + a#component-watch-button + span.watch + span.glyphicon.glyphicon-eye-open + span.spl Watch + span.unwatch.secret + span.glyphicon.glyphicon-eye-close + span.spl Unwatch + + li#patch-component-button + a(data-i18n="common.submit_patch") Submit Patch + if me.isAdmin() + li#create-new-component-button + a(data-i18n="editor.level_component_b_new") Create New Component + + li.divider + li.dropdown-header Info + + li#component-history-button + a(data-i18n="general.version_history") Version History .tab-content .tab-pane.active#component-code @@ -26,3 +47,5 @@ nav.navbar.navbar-default(role='navigation') #config-schema-treema .tab-pane#component-settings #edit-component-treema + .tab-pane#component-patches + .patches-view \ No newline at end of file diff --git a/app/templates/editor/level/components_tab.jade b/app/templates/editor/level/components_tab.jade index e97f47aa4..3d62ca0ce 100644 --- a/app/templates/editor/level/components_tab.jade +++ b/app/templates/editor/level/components_tab.jade @@ -1,9 +1,13 @@ .components-container h3(data-i18n="editor.level_component_tab_title") Current Components + button.navbar-toggle.toggle.btn-primary(type="button" data-toggle="collapse" data-target="#components-treema") + span.icon-list #components-treema .edit-component-container if me.isAdmin() - button(data-i18n="editor.level_component_btn_new").btn.btn-primary#create-new-component-button Create New Component + button.btn.btn-primary#create-new-component-button-no-select + span.icon-plus + span.text(data-i18n="editor.level_component_btn_new") Create New Component #editor-level-component-edit-view diff --git a/app/templates/editor/level/edit.jade b/app/templates/editor/level/edit.jade index f90485b49..c2dfbac6f 100644 --- a/app/templates/editor/level/edit.jade +++ b/app/templates/editor/level/edit.jade @@ -1,69 +1,92 @@ extends /templates/base -block outer_content - .outer-content - +block header + if level.loading nav.navbar.navbar-default(role='navigation')#level-editor-top-nav .container-fluid ul.nav.navbar-nav li - a(href="/editor/level") Back - .navbar-header - span.navbar-brand - span(data-i18n="editor.level_title") Level Editor - span : - span.level-title #{level.attributes.name} - .collapse.navbar-collapse - ul.nav.navbar-nav.nav-tabs - - li.active - a(href="#editor-level-thangs-tab-view", data-toggle="tab", data-i18n="editor.level_tab_thangs") Thangs - li - a(href="#editor-level-scripts-tab-view", data-toggle="tab", data-i18n="editor.level_tab_scripts") Scripts - li - a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings - li - a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components") Components - li - a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems - - - ul.nav.navbar-nav.navbar-right - li(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true").btn.btn-primary.navbar-btn#revert-button Revert - li(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary.navbar-btn#commit-level-start-button Save - li(data-i18n="common.fork", disabled=anonymous ? "true": undefined).btn.btn-primary.navbar-btn#fork-level-start-button Fork - li(title="⌃↩ or ⌘↩: Play preview of current level", data-i18n="common.play")#play-button.btn.btn-inverse.banner.navbar-btn Play! - - li.divider - - li.dropdown - a.dropdown-toggle(href='#', data-toggle='dropdown') - | More - b.caret - ul.dropdown-menu - li#history-button - a(href='#', data-i18n="general.version_history") Version History - li - a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home') Wiki - li - a(href='http://www.hipchat.com/g3plnOKqa') Live Chat - li - a(href='http://discourse.codecombat.com/category/artisan') Forum - li - a(data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Email - - - - ul.dropdown-menu - li - span(data-i18n="editor.level_some_options").dropdown-menu-header Some Options? - li.divider - li - a(data-delay="1000", href="#", data-i18n="common.delay_1_sec") 1 second - a(data-delay="3000", href="#", data-i18n="common.delay_3_sec") 3 seconds - a(data-delay="5000", href="#", data-i18n="common.delay_5_sec") 5 seconds - a(data-delay="90019001", href="#", data-i18n="common.manual") Manual + a(href="/editor/level") + span.glyphicon-home.glyphicon + else + nav.navbar.navbar-default(role='navigation')#level-editor-top-nav + ul.nav.navbar-nav + li + a(href="/editor/level") + span.glyphicon-home.glyphicon + + ul.nav.navbar-nav.nav-tabs + li.active + a(href="#editor-level-thangs-tab-view", data-toggle="tab", data-i18n="editor.level_tab_thangs") Thangs + li + a(href="#editor-level-scripts-tab-view", data-toggle="tab", data-i18n="editor.level_tab_scripts") Scripts + li + a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings + li + a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components")#components-tab Components + li + a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems + li + a(href="#editor-level-patches", data-toggle="tab")#patches-tab + span(data-i18n="resources.patches").spr Patches + - var patches = level.get('patches') + if patches && patches.length + span.badge= patches.length + + .navbar-header + span.navbar-brand #{level.attributes.name} + + ul.nav.navbar-nav.navbar-right + if authorized + li#commit-level-start-button + a + span.glyphicon-floppy-disk.glyphicon + else + li#level-patch-button + a + span.glyphicon-floppy-disk.glyphicon + + li(title="⌃↩ or ⌘↩: Play preview of current level")#play-button + a + span.glyphicon-play.glyphicon + li.dropdown + a(data-toggle='dropdown') + span.glyphicon-chevron-down.glyphicon + ul.dropdown-menu + li.dropdown-header Actions + li + a#level-watch-button + span.watch + span.glyphicon.glyphicon-eye-open + span.spl Watch + span.unwatch.secret + span.glyphicon.glyphicon-eye-close + span.spl Unwatch + + li(class=anonymous ? "disabled": "") + a(data-i18n="common.fork")#fork-level-start-button Fork + li(class=anonymous ? "disabled": "") + a(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert")#revert-button Revert + li(class=anonymous ? "disabled": "") + a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n + li.divider + li.dropdown-header Info + li#level-history-button + a(href='#', data-i18n="general.version_history") Version History + li.divider + li.dropdown-header Help + li + a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki", target="_blank") Wiki + li + a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat", target="_blank") Live Chat + li + a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum", target="_blank") Forum + li + a(data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Email + +block outer_content + .outer-content div.tab-content#level-editor-tabs div.tab-pane.active#editor-level-thangs-tab-view @@ -74,6 +97,9 @@ block outer_content div.tab-pane#editor-level-components-tab-view div.tab-pane#editor-level-systems-tab-view + + div.tab-pane#editor-level-patches + .patches-view div#error-view diff --git a/app/templates/editor/level/fork.jade b/app/templates/editor/level/fork.jade index 255fc8d80..6c4f43553 100644 --- a/app/templates/editor/level/fork.jade +++ b/app/templates/editor/level/fork.jade @@ -6,12 +6,12 @@ block modal-header-content block modal-body-content form#save-level-form.form .form-group - label(for="level-name") Name + label(for="level-name", data-i18n="general.name") Name input#level-name(name="name", type="text").form-control block modal-footer-content - button.btn(data-dismiss="modal") Cancel - button.btn.btn-primary#fork-level-confirm-button Save + button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel + button.btn.btn-primary#fork-level-confirm-button(data-i18n="common.save") Save block modal-body-wait-content - h3 Creating Fork... + h3(data-i18n="editor.fork_creating") Creating Fork... diff --git a/app/templates/editor/level/modal/world_select.jade b/app/templates/editor/level/modal/world_select.jade index deabb6f62..fbc77f1df 100644 --- a/app/templates/editor/level/modal/world_select.jade +++ b/app/templates/editor/level/modal/world_select.jade @@ -27,7 +27,7 @@ block modal-body-content div.alert.alert-info strong Enter | to confirm - canvas(width=1848, height=1178) + canvas(width=924, height=589) block modal-footer-content a.btn.btn-primary#done-button Done diff --git a/app/templates/editor/level/save.jade b/app/templates/editor/level/save.jade index 8ada52b23..eccef370a 100644 --- a/app/templates/editor/level/save.jade +++ b/app/templates/editor/level/save.jade @@ -3,19 +3,20 @@ extends /templates/modal/save_version block modal-body-content h3= "Level: " + level.get('name') + " - " + (levelNeedsSave ? "Modified" : "Not Modified") if levelNeedsSave - form#save-level-form.form - .form-group - label.control-label(for="level-commit-message") Commit Message - textarea.form-control#level-commit-message(name="commit-message", type="text") + .changes-stub + form#save-level-form.form-inline + .form-group.commit-message + input.form-control#level-commit-message(name="commit-message", type="text") if level.isPublished() - .form-group.checkbox - label.control-label(for="level-version-is-major") Major Changes? - input#level-version-is-major(name="version-is-major", type="checkbox") - span.help-block (Could this update break old solutions of the level?) + .checkbox + label + input#level-version-is-major(name="version-is-major", type="checkbox") + span(data-i18n="versions.new_major_version") New Major Version if !level.isPublished() - .form-group.checkbox - label.control-label(for="level-publish") Publish This Level (irreversible)? - input#level-publish(name="publish", type="checkbox") + .checkbox + label + input#level-publish(name="publish", type="checkbox") + span(data-i18n="common.publish") Publish if modifiedComponents.length hr @@ -23,17 +24,17 @@ block modal-body-content each component in modifiedComponents - var id = component.get('_id') h4= "Component: " + component.get('system') + '.' + component.get('name') - form.component-form(id="save-component-" + id + "-form") + .changes-stub + form.form-inline.component-form(id="save-component-" + id + "-form") input(name="component-original", type="hidden", value=component.get('original')) input(name="component-parent-major-version", type="hidden", value=component.get('version').major) - .form-group - label.control-label(for=id + "-commit-message") Commit Message - textarea.form-control(id=id + "-commit-message", name="commit-message", type="text") + .form-group.commit-message + input.form-control(id=id + "-commit-message", name="commit-message", type="text") if component.isPublished() - .form-group.checkbox - label.control-label(for=id + "-version-is-major") Major Changes? - input(id=id + "-version-is-major", name="version-is-major", type="checkbox") - span.help-block (Could this update break anything depending on this Component?) + .checkbox + label + input(id=id + "-version-is-major", name="version-is-major", type="checkbox") + span(data-i18n="versions.new_major_version") New Major Version if modifiedSystems.length hr @@ -41,14 +42,14 @@ block modal-body-content each system in modifiedSystems - var id = system.get('_id') h4= "System: " + system.get('name') - form.system-form(id="save-system-" + id + "-form") + .changes-stub + form.form-inline.system-form(id="save-system-" + id + "-form") input(name="system-original", type="hidden", value=system.get('original')) input(name="system-parent-major-version", type="hidden", value=system.get('version').major) - .form-group - label.control-label(for=id + "-commit-message") Commit Message - textarea.form-control(id=id + "-commit-message", name="commit-message", type="text") + .form-group.commit-message + input.form-control(id=id + "-commit-message", name="commit-message", type="text", placeholder="Commit Message") if system.isPublished() - .form-group.checkbox - label.control-label(for=id + "-version-is-major") Major Changes? - input(id=id + "-version-is-major", name="version-is-major", type="checkbox") - span.help-block (Could this update break anything depending on this System?) + .checkbox + label + input(id=id + "-version-is-major", name="version-is-major", type="checkbox") + span(data-i18n="versions.new_major_version") New Major Version diff --git a/app/templates/editor/level/scripts_tab.jade b/app/templates/editor/level/scripts_tab.jade index 85618f619..b3b074f17 100644 --- a/app/templates/editor/level/scripts_tab.jade +++ b/app/templates/editor/level/scripts_tab.jade @@ -1,3 +1,6 @@ +button.navbar-toggle.toggle.btn-primary(type="button", data-toggle="collapse", data-target="#scripts-treema") + span.icon-list + #scripts-treema #script-treema diff --git a/app/templates/editor/level/system/edit.jade b/app/templates/editor/level/system/edit.jade index db20287b6..555709fca 100644 --- a/app/templates/editor/level/system/edit.jade +++ b/app/templates/editor/level/system/edit.jade @@ -1,21 +1,41 @@ nav.navbar.navbar-default(role='navigation') - .container-fluid - .navbar-header - span.navbar-brand - span(data-i18n="editor.level_system_edit_title") - | Edit System - span : - | "#{editTitle}" - .collapse.navbar-collapse - ul.nav.navbar-nav.nav-tabs - li.active - a(href="#system-code" data-toggle="tab") Code + + ul.nav.navbar-nav.nav-tabs + li.active + a(href="#system-code" data-toggle="tab")#system-code-tab Code + li + a(href="#system-config-schema" data-toggle="tab")#system-config-schema-tab Config Schema + li + a(href="#system-settings" data-toggle="tab")#system-settings-tab Settings + li + a(href="#system-patches" data-toggle="tab" data-i18n="resources.patches")#system-patches-tab Patches + + ul.nav.navbar-nav.navbar-right + li.dropdown + a(data-toggle='dropdown') + span.glyphicon-chevron-down.glyphicon + ul.dropdown-menu + li.dropdown-header Actions li - a(href="#system-config-schema" data-toggle="tab") Config Schema - li - a(href="#system-settings" data-toggle="tab") Settings - ul.nav.navbar-nav.navbar-right - li(data-i18n="editor.level_system_btn_new").btn.btn-primary.navbar-btn#create-new-system-button Create New System + a#system-watch-button + span.watch + span.glyphicon.glyphicon-eye-open + span.spl Watch + span.unwatch.secret + span.glyphicon.glyphicon-eye-close + span.spl Unwatch + li#patch-system-button + a(data-i18n="common.submit_patch") Submit Patch + if me.isAdmin() + li#create-new-system + a(data-i18n="editor.level_system_btn_new") Create New System + li.divider + li.dropdown-header Info + li#system-history-button + a(data-i18n="general.version_history") Version History + + .navbar-header + span.navbar-brand= editTitle .tab-content .tab-pane.active#system-code @@ -24,3 +44,5 @@ nav.navbar.navbar-default(role='navigation') #config-schema-treema .tab-pane#system-settings #edit-system-treema + .tab-pane#system-patches + .patches-view \ No newline at end of file diff --git a/app/templates/editor/level/systems_tab.jade b/app/templates/editor/level/systems_tab.jade index f09edb2c6..8cb062bac 100644 --- a/app/templates/editor/level/systems_tab.jade +++ b/app/templates/editor/level/systems_tab.jade @@ -1,11 +1,17 @@ .systems-container + button.navbar-toggle.toggle.btn-primary(type="button" data-toggle="collapse" data-target="#systems-treema") + span.icon-list h3(data-i18n="editor.level_systems_tab_title") Current Systems #systems-treema .edit-system-container if me.isAdmin() - button(data-i18n="editor.level_systems_btn_new").btn.btn-primary#create-new-system-button Create New System + button.btn.btn-primary#create-new-system-button + span.icon-file + span.text(data-i18n="editor.level_systems_btn_new") Create New System #editor-level-system-edit-view -button(data-i18n="editor.level_systems_btn_add").btn.btn-primary#add-system-button Add System +button.btn.btn-primary#add-system-button + span.icon-plus + span.text(data-i18n="editor.level_systems_btn_add") Add System \ No newline at end of file diff --git a/app/templates/editor/level/thangs_tab.jade b/app/templates/editor/level/thangs_tab.jade index b0b86868c..1ff12bc12 100644 --- a/app/templates/editor/level/thangs_tab.jade +++ b/app/templates/editor/level/thangs_tab.jade @@ -1,7 +1,12 @@ -.thangs-container.thangs-column +div#toggle + button.navbar-toggle.toggle.btn-primary#thangs-container-toggle(type="button", data-toggle="collapse", data-target="#all-thangs") + span.icon-list +button.navbar-toggle.toggle.btn-primary#thangs-palette-toggle(type="button", data-toggle="collapse", data-target="#add-thangs-column") + span.icon-plus +.thangs-container.thangs-column#all-thangs h3(data-i18n="editor.level_tab_thangs_title") Current Thangs .btn-group(data-toggle="buttons-radio")#extant-thangs-filter - button.btn.btn-primary All + button.btn.btn-primary(data-i18n="editor.level_tab_thangs_all") All button.btn.btn-primary(value="Unit", title="Unit") i.icon-user button.btn.btn-primary(value="Wall", title="Wall") @@ -19,10 +24,10 @@ #canvas-wrapper ul.dropdown-menu#contextmenu li#delete - a Delete + a(data-i18n="editor.delete") Delete li#duplicate - a Duplicate - canvas(width=1848, height=1178)#surface + a(data-i18n="editor.duplicate") Duplicate + canvas(width=924, height=589)#surface #canvas-left-gradient.gradient #canvas-top-gradient.gradient diff --git a/app/templates/editor/patch_modal.jade b/app/templates/editor/patch_modal.jade new file mode 100644 index 000000000..4d46d8cb5 --- /dev/null +++ b/app/templates/editor/patch_modal.jade @@ -0,0 +1,24 @@ +extends /templates/modal/modal_base + +block modal-header-content + .modal-header-content + h3 Patch + +block modal-body-content + .modal-body + p= patch.get('commitMessage') + .changes-stub + + +block modal-footer + .modal-footer + button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel + if isPatchCreator + if status != 'withdrawn' + button.btn.btn-danger#withdraw-button Withdraw + if isPatchRecipient + if status != 'accepted' + button.btn.btn-primary#accept-button Accept + if status != 'rejected' + button.btn.btn-danger#reject-button Reject + \ No newline at end of file diff --git a/app/templates/editor/patches.jade b/app/templates/editor/patches.jade new file mode 100644 index 000000000..872788e7d --- /dev/null +++ b/app/templates/editor/patches.jade @@ -0,0 +1,30 @@ +.btn-group(data-toggle="buttons").status-buttons + label.btn.btn-default.pending + input(type="radio", name="status", value="pending") + | Pending + label.btn.btn-default.accepted + input(type="radio", name="status", value="accepted") + | Accepted + label.btn.btn-default.rejected + input(type="radio", name="status", value="rejected") + | Rejected + label.btn.btn-default.withdrawn + input(type="radio", name="status", value="withdrawn") + | Withdrawn + +if patches.loading + p Loading +else + table.table.table-condensed.table-bordered + tr + th Submitter + th Submitted + th Commit Message + th Review + for patch in patches + tr + td= patch.userName + td= moment(patch.get('created')).format('llll') + td= patch.get('commitMessage') + td + span.glyphicon.glyphicon-wrench(data-patch-id=patch.id).patch-icon diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index 1e8ce462d..b1924f439 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -12,8 +12,8 @@ block content img#portrait.img-thumbnail - button.btn.btn-secondary#history-button(data-i18n="general.history") History - button.btn.btn-primary#save-button(data-toggle="coco-modal", data-target="modal/save_version", data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save + button.btn.btn-secondary#history-button(data-i18n="general.version_history") Version History + button.btn.btn-primary#save-button(data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save button.btn.btn-primary#revert-button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true") Revert h3 Edit Thang Type: "#{thangType.attributes.name}" @@ -27,11 +27,13 @@ block content a(href="#editor-thang-spritesheets-view", data-toggle="tab") Spritesheets li a(href="#editor-thang-colors-tab-view", data-toggle="tab")#color-tab Colors + li + a(href="#editor-thang-patches-view", data-toggle="tab")#patches-tab Patches div.tab-content div.tab-pane#editor-thang-colors-tab-view - div.tab-pane.active#editor-thang-main-tab-view + div.tab-pane#editor-thang-main-tab-view.active div.main-area.well div.file-controls @@ -83,6 +85,10 @@ block content div.tab-pane#editor-thang-spritesheets-view div#spritesheets + + div.tab-pane#editor-thang-patches-view + + div.patches-view div#error-view diff --git a/app/templates/employers.jade b/app/templates/employers.jade index 485d21622..dcb6d2cac 100644 --- a/app/templates/employers.jade +++ b/app/templates/employers.jade @@ -2,33 +2,66 @@ extends /templates/base block content - .row + h1(data-i18n="employers.want_to_hire_our_players") Want to hire expert CodeCombat players? - .col-md-6 - - h2 CodeCombat for Employers - - p.lead Want to hire expert CodeCombat players? - - p - | CodeCombat doesn't just have beginners. We also have expert software developers who play our - a(href="http://blog.codecombat.com/beat-this-level-get-a-programming-job") developer challenge levels - | . If your company is seeking technical talent, then we'd be happy to help place candidates with you. - - p We were actually overwhelmed by how many talented developers rushed to site, crushed our version of the algorithm in the Gridmancer challenge, and were looking for job opportunities, especially in the SF Bay Area where CodeCombat is located. So if you're an employer, now's a great time to get in touch and meet some amazing programmers. - - p If this sounds interesting, then let's get in touch, find out what you're looking for, talk about recruitment terms, and see what we can do for you. Don't worry–we are not your traditional recruiter. We're a tech company like you who happens to have a ton of great programmers looking to us for help with the job search. - - h3 - a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/contact") Contact Us + p + span(data-i18n="employers.candidates_count_prefix") We currently have + if candidates.length + | #{candidates.length} + else + span(data-i18n="employers.candidates_count_many") many + | + span(data-i18n="employers.candidates_count_suffix") highly skilled and vetted developers looking for work. + if !isEmployer - .span5 - - h2 Candidate Statistics - - h4 Resumes: 46 - h4 Ages: 16 - 45 - h4 Experience: 0 - 30 years - h4 Skill: from interns and entry level to senior developers and management - h4 Technologies: just about everything - h4 Countries: USA, Canada, Australia, and many more + h3 + a#see-candidates(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/employer_signup", data-i18n="employers.see_candidates") Click here to see our candidates + + if candidates.length + table.table.table-condensed.table-hover.table-responsive.tablesorter + thead + tr + th(data-i18n="general.name") Name + th(data-i18n="employers.candidate_location") Location + th(data-i18n="employers.candidate_looking_for") Looking For + th(data-i18n="employers.candidate_role") Role + th(data-i18n="employers.candidate_top_skills") Top Skills + th(data-i18n="employers.candidate_years_experience") Yrs Exp + th(data-i18n="employers.candidate_last_updated") Last Updated + if me.isAdmin() + th(data-i18n="employers.candidate_approved") Us? + th(data-i18n="employers.candidate_active") Them? + + tbody + for candidate, index in candidates + - var profile = candidate.get('jobProfile'); + - var authorized = candidate.id; // If we have the id, then we are authorized. + tr(data-candidate-id=candidate.id, id=candidate.id) + td + if authorized + img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50) + p= profile.name + else + img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50) + p Developer ##{index + 1} + if profile.country == 'USA' + td= profile.city + else + td= profile.country + td= profile.lookingFor + td= profile.jobTitle + td + each skill in profile.skills.slice(0, 10) + code= skill + span + td= profile.experience + td(data-profile-age=(new Date() - new Date(profile.updated)) / 86400 / 1000)= moment(profile.updated).fromNow() + if me.isAdmin() + if candidate.get('jobProfileApproved') + td ✓ + else + td ✗ + if profile.active + td ✓ + else + td ✗ \ No newline at end of file diff --git a/app/templates/home.jade b/app/templates/home.jade index 376ea27fb..e790d40bf 100644 --- a/app/templates/home.jade +++ b/app/templates/home.jade @@ -4,12 +4,29 @@ block content h1#site-slogan(data-i18n="home.slogan") Learn to Code JavaScript by Playing a Game - #trailer-wrapper - - img(src="/images/pages/home/video_border.png") - #mobile-trailer-wrapper - - hr + if frontPageContent == 'video' + //- if language is Chinese, we use youku, because China can't visit youtube. + //- otherwise, we use youtube. + if languageName == "zh-HANS" + #trailer-wrapper + + img(src="/images/pages/home/video_border.png") + #mobile-trailer-wrapper + + else + #trailer-wrapper + + img(src="/images/pages/home/video_border.png") + #mobile-trailer-wrapper + + hr + + else if frontPageContent == 'screenshot' + #front-screenshot + img(src="/images/pages/home/front_screenshot_01.png", alt="") + + else if frontPageContent == 'nothing' + p   .alert.alert-danger.lt-ie10 strong(data-i18n="home.no_ie") CodeCombat does not run in Internet Explorer 9 or older. Sorry! @@ -33,7 +50,7 @@ block content h4(data-i18n="home.for_beginners") For Beginners .play-text(data-i18n="home.play") Play - a#multiplayer(href="/play/ladder/dungeon-arena") + a#multiplayer(href="/play/ladder") div.game-mode-wrapper if isEnglish img(src="/images/pages/home/multiplayer.jpg").img-rounded diff --git a/app/templates/kinds/search.jade b/app/templates/kinds/search.jade index b7babdc95..9660027b3 100644 --- a/app/templates/kinds/search.jade +++ b/app/templates/kinds/search.jade @@ -9,9 +9,9 @@ block content | #{currentEditor} if me.get('anonymous') - a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/signup", role="button") Sign Up to Create a New #{modelLabel} + a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/auth", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Content else - a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal" data-i18n="#{currentNew}") Create a New Something + a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal", data-i18n="#{currentNew}") Create a New Something input#search(data-i18n="[placeholder]#{currentSearch}") hr div.results @@ -20,18 +20,19 @@ block content // TODO: make this into a ModalView subview div.modal.fade#new-model-modal .modal-dialog - .modal-content - .modal-header - h3 Create New #{modelLabel} - .modal-body - form.form - .form-group - label.control-label(for="name") Name - input#name.form-control(name="name", type="text") - .modal-footer - button.btn(data-dismiss="modal") Cancel - button.btn.btn-primary.new-model-submit Create - .modal-body.wait.secret - h3 Reticulating Splines... - .progress.progress-striped.active - .progress-bar + .background-wrapper + .modal-content + .modal-header + h3(data-i18n="#{currentNew}") Create New #{modelLabel} + .modal-body + form.form + .form-group + label.control-label(for="name", data-i18n="general.name") Name + input#name.form-control(name="name", type="text") + .modal-footer + button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel + button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create + .modal-body.wait.secret + h3(data-i18n="play_level.tip_reticulating") Reticulating Splines... + .progress.progress-striped.active + .progress-bar diff --git a/app/templates/loading.jade b/app/templates/loading.jade index df2fc0eb5..ea7c1fce4 100644 --- a/app/templates/loading.jade +++ b/app/templates/loading.jade @@ -1,4 +1,4 @@ -.loading-screen +.loading-screen.loading-container h1(data-i18n="common.loading") Loading... .progress .progress-bar diff --git a/app/templates/loading_error.jade b/app/templates/loading_error.jade index b56ede4ac..2e9a8edfe 100644 --- a/app/templates/loading_error.jade +++ b/app/templates/loading_error.jade @@ -25,7 +25,7 @@ strong(data-i18n="loading_error.unknown") Unknown error. if resourceIndex !== undefined - button.btn.btn-sm.retry-loading-resource(data-i18n="common.retry", data-resource-index=resourceIndex) Retry + button.btn.btn-xs.retry-loading-resource(data-i18n="common.retry", data-resource-index=resourceIndex) Retry if requestIndex !== undefined - button.btn.btn-sm.retry-loading-request(data-i18n="common.retry", data-request-index=requestIndex) Retry + button.btn.btn-xs.retry-loading-request(data-i18n="common.retry", data-request-index=requestIndex) Retry \ No newline at end of file diff --git a/app/templates/modal/auth.jade b/app/templates/modal/auth.jade new file mode 100644 index 000000000..626ca4827 --- /dev/null +++ b/app/templates/modal/auth.jade @@ -0,0 +1,61 @@ +extends /templates/modal/modal_base + +block modal-header-content + if mode === 'login' + h3(data-i18n="login.log_in") Log In + if mode === 'signup' + if title === 'short' + h3(data-i18n="login.sign_up") Create Account + else + h3(data-i18n="signup.create_account_title") Create Account to Save Progress + +block modal-body-content + + if showRequiredError + .alert.alert-success + span(data-i18n="signup.required") You need to log in before you can that way. + + else if mode === 'signup' && descriptionOn === "yes" + p(data-i18n="signup.description") It's free. Just need a couple things and you'll be good to go: + + form.form + .form-group + label.control-label(for="email", data-i18n="general.email") Email + input#email.input-large.form-control(name="email", type="email", value=formValues.email) + .form-group + label.control-label(for="password", data-i18n="general.password") Password + input#password.input-large.form-control(name="password", type="password", value=formValues.password) + + if mode === 'signup' + .form-group.checkbox + label.control-label(for="subscribe") + input#subscribe(name="subscribe", type="checkbox", checked='checked') + span(data-i18n="signup.email_announcements") Receive announcements by email + .form-group.checkbox + label.control-label(for="confirm-age") + input#confirm-age(name="confirm-age", type="checkbox", checked='checked') + span(data-i18n="signup.coppa") 13+ or non-USA + a(href="https://en.wikipedia.org/wiki/Children's_Online_Privacy_Protection_Act", data-i18n="signup.coppa_why", target="_blank") (Why?) + + if mode === 'login' + input.btn.btn-info.btn-large#login-button(value=translate("login.log_in"), type="submit") + .btn.btn-default.btn-large#switch-to-signup-button(data-i18n="login.sign_up") Create Account + if mode === 'signup' + input.btn.btn-info.btn-large#signup-button(value=translate("signup.sign_up"), type="submit") + + +block modal-body-wait-content + + if mode === 'login' + h3(data-i18n="login.logging_in") Logging In + if mode === 'signup' + h3(data-i18n="signup.creating") Creating Account... + +block modal-footer + .modal-footer + div.network-login + .fb-login-button(data-show-faces="false", data-width="200", data-max-rows="1", data-scope="email") + div.network-login + .gplus-login-button#gplus-login-button + div#recover-account-wrapper + a(data-toggle="coco-modal", data-target="modal/recover", data-i18n="login.recover")#link-to-recover recover account diff --git a/app/templates/modal/diplomat_suggestion.jade b/app/templates/modal/diplomat_suggestion.jade index b3d29cb6b..56d98f33a 100644 --- a/app/templates/modal/diplomat_suggestion.jade +++ b/app/templates/modal/diplomat_suggestion.jade @@ -1,7 +1,7 @@ extends /templates/modal/modal_base block modal-header-content - h3(data-i18n="diplomat_suggestion.title") + h3(data-i18n="diplomat_suggestion.title") Help translate CodeCombat! block modal-body-content h4(data-i18n="diplomat_suggestion.sub_heading") We need your language skills. diff --git a/app/templates/modal/employer_signup_modal.jade b/app/templates/modal/employer_signup_modal.jade new file mode 100644 index 000000000..0274d0eeb --- /dev/null +++ b/app/templates/modal/employer_signup_modal.jade @@ -0,0 +1,64 @@ +extends /templates/modal/modal_base + +block modal-header-content + if userIsAnonymous || !userIsAuthorized + h3(data-i18n="employer_signup.title") Sign up to hire CodeCombat players! + else + h3 CodeCombat Placement Agreement + +block modal-body-content + if userIsAnonymous + if userIsAuthorized + | You appear to be authorized on CodeCombat with LinkedIn. + else + h4(data-i18n="employer_signup.sub_heading") Let us find your next brilliant developers. + p Create an account to get started! + .form + .form-group + label.control-label(for="signup-email", data-i18n="general.email") Email + input#signup-email.form-control.input-large(name="email",type="email") + .form-group + label.control-label(for="signup-password", data-i18n="general.password") Password + input#signup-password.input-large.form-control(name="password", type="password") + else if !userIsAuthorized + .modal-footer.linkedin + p Please sign into your LinkedIn account to verify your identity. + script(type="in/Login" id="linkedInAuthButton" data-onAuth="contractCallback") + + else + | Please agree to our terms before accessing our candidates. + br + br + b Who we are: + | CodeCombat is a programming game that both teaches and vets programmers. If you accept this agreement, we will let you hire the most talented developers on our platform. + br + br + b Placement fee: + | If you hire our any of our players, you agree to pay us 15% of the candidate's first year annualized starting base salary. The fee is due on the first day that the candidate is employed and is 100% refundable for up to 90 day if the candidate doesn't remain employed at the company for any reason. + br + br + b Interns are free: + | We will not bill you for interns and part time hires (remote or onsite) hired through this site, provided they do not become full time hires within 1 year of their start date. If they do become full time hires within 1 year of their start date, we will invoice you 15% of their first year's annualized starting base salary on their first day of full time employment. For these hires, the 90 day guarantee does not apply. + br + br + | By clicking Agree, you are agreeing to CodeCombat's Placement Agreement on behalf of your company. You also consent to CodeCombat storing basic LinkedIn profile data for verification purposes, including your name, email, public profile URL, and work history. +block modal-footer + if userIsAnonymous + if !userIsAuthorized + .modal-footer.linkedin + button.btn.btn-primary(id="create-account-button") Create Account + br + br + | Already have a CodeCombat account? + a.login-link(data-toggle="coco-modal", data-target="modal/auth") Log in to continue! + else + .modal-footer.linkedin + a.login-link(data-toggle="coco-modal", data-target="modal/auth") Please log in to continue. + else if !userIsAnonymous && !userIsAuthorized + .modal-footer.linkedin + else if userIsAuthorized && !userHasSignedContract + .modal-footer.linkedin + button.btn.btn-primary(id="contract-agreement-button") I agree + else + .modal-footer.linkedin + | Thanks! You've already agreed to the contract. \ No newline at end of file diff --git a/app/templates/modal/job_profile_contact.jade b/app/templates/modal/job_profile_contact.jade new file mode 100644 index 000000000..412d3fca8 --- /dev/null +++ b/app/templates/modal/job_profile_contact.jade @@ -0,0 +1,22 @@ +extends /templates/modal/contact + +block modal-header-content + h3(data-i18n="contact.contact_candidate") Contact Candidate + +block modal-body-content + p(data-i18n="contact.recruitment_reminder") Use this form to reach out to candidates you are interested in interviewing. Remember that CodeCombat charges 15% of first-year salary. The fee is due upon hiring the employee and is refundable for 90 days if the employee does not remain employed. Part time, remote, and contract employees are free, as are interns. + .form + .form-group + label.control-label(for="contact-email", data-i18n="general.email") Email + input#contact-email.form-control(name="email", type="email", value=me.get('email'), placeholder="Where should the candidate reply?") + .form-group + label.control-label(for="contact-subject", data-i18n="general.subject") Subject + input#contact-subject.form-control(name="subject", type="text", value="Job interest", placeholder="Subject of the email the candidate will receive.") + .form-group + label.control-label(for="contact-message", data-i18n="general.message") Message + textarea#contact-message.form-control(name="message", rows=8) + +block modal-footer-content + span.sending-indicator.pull-left.secret(data-i18n="common.sending") Sending... + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="common.cancel").btn Cancel + button.btn.btn-primary#contact-submit-button(data-i18n="common.send") Send diff --git a/app/templates/modal/login.jade b/app/templates/modal/login.jade deleted file mode 100644 index bd0307824..000000000 --- a/app/templates/modal/login.jade +++ /dev/null @@ -1,32 +0,0 @@ -extends /templates/modal/modal_base - -block modal-header-content - h3(data-i18n="login.log_in") Log In - -block modal-body-content - .form - .form-group - label.control-label(for="login-email", data-i18n="general.email") Email - input#login-email.input-large.form-control(name="email", type="email") - .form-group - label.control-label(for="login-password", data-i18n="general.password") Password - input#login-password.input-large.form-control(name="password", type="password") - -block modal-body-wait-content - h3(data-i18n="login.logging_in") Logging In - -block modal-footer - .modal-footer - button.btn.btn-primary.btn-large#login-button(data-i18n="login.log_in") Log In - - .modal-footer.network-logins - div - .fb-login-button(data-show-faces="false", data-width="200", data-max-rows="1", data-scope="email") - div - .gplus-login-button#gplus-login-button - div - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="login.sign_up")#link-to-signup create new account - span , - span(data-i18n="general.or") or - span - a(data-toggle="coco-modal", data-target="modal/recover", data-i18n="login.recover")#link-to-recover recover account diff --git a/app/templates/modal/modal_base.jade b/app/templates/modal/modal_base.jade index caebe847c..e2c2d527f 100644 --- a/app/templates/modal/modal_base.jade +++ b/app/templates/modal/modal_base.jade @@ -1,26 +1,27 @@ .modal-dialog - .modal-content - block modal-header - .modal-header - if closeButton - .button.close(type="button", data-dismiss="modal", aria-hidden="true") × - block modal-header-content - h3 man bites God - - block modal-body - .modal-body - block modal-body-content - p Man Bites God are the bad boys of the Melbourne live music and comedy scene. It is like being drowned in a bathtub of harmony. - img(src="http://www.manbitesgod.com/images/picturecoupleb.jpg") - img(src="http://www.manbitesgod.com/images/manrantb.jpg") + .background-wrapper + .modal-content + block modal-header + .modal-header + if closeButton + .button.close(type="button", data-dismiss="modal", aria-hidden="true") × + block modal-header-content + h3 man bites God + + block modal-body + .modal-body + block modal-body-content + p Man Bites God are the bad boys of the Melbourne live music and comedy scene. It is like being drowned in a bathtub of harmony. + img(src="http://www.manbitesgod.com/images/picturecoupleb.jpg") + img(src="http://www.manbitesgod.com/images/manrantb.jpg") - .modal-body.wait.secret - block modal-body-wait-content - h3 Reticulating Splines... - .progress.progress-striped.active - .progress-bar + .modal-body.wait.secret + block modal-body-wait-content + h3 Reticulating Splines... + .progress.progress-striped.active + .progress-bar - block modal-footer - .modal-footer - block modal-footer-content - button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay \ No newline at end of file + block modal-footer + .modal-footer + block modal-footer-content + button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true", data-i18n="modal.okay") Okay \ No newline at end of file diff --git a/app/templates/modal/model.jade b/app/templates/modal/model.jade new file mode 100644 index 000000000..4fae4c294 --- /dev/null +++ b/app/templates/modal/model.jade @@ -0,0 +1,9 @@ +extends /templates/modal/modal_base + +block modal-header + +block modal-body-content + for model in models + h3= model.type() + ': ' + model.id + .model-treema(data-model-id=model.id) + hr diff --git a/app/templates/modal/revert.jade b/app/templates/modal/revert.jade index f20edd7d2..28b111337 100644 --- a/app/templates/modal/revert.jade +++ b/app/templates/modal/revert.jade @@ -10,4 +10,4 @@ block modal-body-content td | #{model.type()}: #{model.get('name')} td - button(value=model.id) Revert \ No newline at end of file + button(value=model.id, data-i18n="editor.revert") Revert diff --git a/app/templates/modal/save_version.jade b/app/templates/modal/save_version.jade index d1f8fc219..748c541a9 100644 --- a/app/templates/modal/save_version.jade +++ b/app/templates/modal/save_version.jade @@ -1,30 +1,46 @@ extends /templates/modal/modal_base block modal-header-content - h3(data-i18n="versions.save_version_title") Save New Version + if isPatch + h3(data-i18n="versions.submit_patch_title") Submit Patch + else + h3(data-i18n="versions.save_version_title") Save New Version block modal-body-content - form.form - .form-group - label.control-label(for="commitMessage", data-i18n="general.commit_msg") Commit Message - textarea#commit-message.input-large.form-control(name="commitMessage", type="text") - .form-group - label.control-label(for="level-version-is-major", data-i18n="versions.new_major_version") New Major Version - input#major-version.input-large.form-control(name="version-is-major", type="checkbox") - span.help-block + if hasChanges + .changes-stub + form.form-inline + .form-group.commit-message + input.form-control#commit-message(name="commitMessage", type="text") + if !isPatch + .checkbox + label + input#major-version(name="version-is-major", type="checkbox") + span(data-i18n="versions.new_major_version") New Major Version + else + .alert.alert-danger No changes block modal-body-wait-content - h3(data-i18n="common.saving") Saving... + if hasChanges + if isPatch + h3(data-i18n="versions.submitting_patch") Submitting Patch... + else + h3(data-i18n="common.saving") Saving... block modal-footer-content - if !noSaveButton + if hasChanges #accept-cla-wrapper.alert.alert-info span(data-i18n="versions.cla_prefix") To save changes, first you must agree to our | strong#cla-link(data-i18n="versions.cla_url") CLA span(data-i18n="versions.cla_suffix") . - button.btn#agreement-button(data-i18n="versions.cla_agree") I AGREE + button.btn.btn-sm#agreement-button(data-i18n="versions.cla_agree") I AGREE + if isPatch + .alert.alert-info An owner will need to approve it before your changes will become visible. - button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel - if !noSaveButton - button.btn.btn-primary#save-version-button(data-i18n="common.save") Save + .buttons + button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel + if hasChanges && !isPatch + button.btn.btn-primary#save-version-button(data-i18n="common.save") Save + if hasChanges && isPatch + button.btn.btn-primary#submit-patch-button(data-i18n="versions.submit_patch") Submit Patch \ No newline at end of file diff --git a/app/templates/modal/signup.jade b/app/templates/modal/signup.jade deleted file mode 100644 index c603d019b..000000000 --- a/app/templates/modal/signup.jade +++ /dev/null @@ -1,34 +0,0 @@ -extends /templates/modal/modal_base - -block modal-header-content - h3(data-i18n="signup.create_account_title") Create Account to Save Progress - -block modal-body-content - if showRequiredError - .alert.alert-success - span(data-i18n="signup.required") You need to sign up first before you can go over there. Luckily, it's really easy. - else - p(data-i18n="signup.description") It's free. Just need a couple things and you'll be good to go: - .form - .form-group - label.control-label(for="signup-email", data-i18n="general.email") Email - input#signup-email.form-control.input-large(name="email", type="email") - .form-group - label.control-label(for="signup-password", data-i18n="general.password") Password - input#signup-password.input-large.form-control(name="password", type="password") - hr - .form-group.checkbox - label.control-label(for="signup-subscribe") - input#signup-subscribe(name="subscribe", type="checkbox", checked='checked') - span(data-i18n="signup.email_announcements") Receive announcements by email - .form-group.checkbox - label.control-label(for="signup-confirm-age") - input#signup-confirm-age(name="confirm-age", type="checkbox", checked='checked') - span(data-i18n="signup.coppa") 13+ or non-USA - a(href="https://en.wikipedia.org/wiki/Children's_Online_Privacy_Protection_Act", data-i18n="signup.coppa_why", target="_blank") (Why?) - -block modal-body-wait-content - h3(data-i18n="signup.creating") Creating Account... - -block modal-footer-content - button.btn.btn-primary.btn-large#signup-button(data-i18n="signup.sign_up") Sign Up diff --git a/app/templates/modal/versions.jade b/app/templates/modal/versions.jade index bd5b099eb..c3818573b 100755 --- a/app/templates/modal/versions.jade +++ b/app/templates/modal/versions.jade @@ -8,17 +8,23 @@ block modal-header-content block modal-body-content if dataList - table.table + table.table.table-condensed tr + th th(data-i18n="general.name") Name th(data-i18n="general.version") Version th(data-i18n="general.commit_msg") Commit Message for data in dataList tr + td + input(type="checkbox", value=data._id).select td a(href="/editor/#{page}/#{data.slug || data._id}") | #{data.name} td #{data.version.major}.#{data.version.minor} td #{data.commitMessage} + div.delta-container + div.delta-view + block modal-footer-content \ No newline at end of file diff --git a/app/templates/play.jade b/app/templates/play.jade index 911f14c92..f8d07e62f 100644 --- a/app/templates/play.jade +++ b/app/templates/play.jade @@ -2,12 +2,6 @@ extends /templates/base block content - if notFound - div(class="alert alert-warning") - h2 - span(data-i18n="play_level.level_load_error") Level could not be loaded: - | #{notFound} - h1(data-i18n="play.choose_your_level") Choose Your Level p span(data-i18n="play.adventurer_prefix") You can jump to any level below, or discuss the levels on diff --git a/app/templates/play/common/ladder_submission.jade b/app/templates/play/common/ladder_submission.jade new file mode 100644 index 000000000..dc387bc62 --- /dev/null +++ b/app/templates/play/common/ladder_submission.jade @@ -0,0 +1,16 @@ +button.btn.btn-lg.btn-block.btn-success.rank-button + span(data-i18n="ladder.rank_no_code").unavailable.secret No New Code to Rank + span(data-i18n="ladder.rank_my_game").rank.secret Rank My Game! + span(data-i18n="ladder.rank_submitting").submitting.secret Submitting... + span(data-i18n="ladder.rank_submitted").submitted.secret Submitted for Ranking + span(data-i18n="ladder.rank_failed").failed.secret Failed to Rank + span(data-i18n="ladder.rank_being_ranked").ranking.secret Game Being Ranked + +if lastSubmitted + .last-submitted.secret + span(data-i18n="ladder.rank_last_submitted") submitted + | #{lastSubmitted} + +a(href=simulateURL, data-i18n="ladder.help_simulate").help-simulate.secret Help simulate games? + +.clearfix \ No newline at end of file diff --git a/app/templates/play/ladder.jade b/app/templates/play/ladder.jade deleted file mode 100644 index 1fe679657..000000000 --- a/app/templates/play/ladder.jade +++ /dev/null @@ -1,60 +0,0 @@ -extends /templates/base -block content - - div#level-column - if levelDescription - div!= levelDescription - else - h1= level.get('name') - - div#columns.row - div.column.col-md-2 - for team in teams - div.column.col-md-4 - a(style="background-color: #{team.primaryColor}", data-team=team.id).play-button.btn.btn-danger.btn-block.btn-lg - span(data-i18n="play.play_as") Play As - | - span= team.name - div.column.col-md-2 - - .spectate-button-container - a(href="/play/spectate/#{level.get('slug')}").spectate-button.btn.btn-primary.center - span(data-i18n="play.spectate") Spectate - - hr - - ul.nav.nav-pills - li.active - a(href="#ladder", data-toggle="tab", data-i18n="general.ladder") Ladder - if !me.get('anonymous') - li - a(href="#my-matches", data-toggle="tab", data-i18n="ladder.my_matches") My Matches - li - a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate - - div.tab-content - .tab-pane.active.well#ladder - #ladder-tab-view - .tab-pane.well#my-matches - #my-matches-tab-view - .tab-pane.well#simulate - p(id="simulation-status-text") - if simulationStatus - | #{simulationStatus} - else - span(data-i18n="ladder.simulation_explanation") By simulating games you can get your game ranked faster! - p - button(data-i18n="ladder.simulate_games").btn.btn-warning.btn-lg.highlight#simulate-button Simulate Games! - if false && me.isAdmin() - p - button(data-i18n="ladder.simulate_all").btn.btn-danger.btn-lg.highlight#simulate-all-button RESET AND SIMULATE GAMES - - p.simulation-count - span(data-i18n="ladder.games_simulated_by") Games simulated by you: - | - span#simulated-by-you= me.get('simulatedBy') || 0 - - p.simulation-count - span(data-i18n="ladder.games_simulated_for") Games simulated for you: - | - span#simulated-for-you= me.get('simulatedFor') || 0 \ No newline at end of file diff --git a/app/templates/play/ladder/ladder.jade b/app/templates/play/ladder/ladder.jade new file mode 100644 index 000000000..c786ed655 --- /dev/null +++ b/app/templates/play/ladder/ladder.jade @@ -0,0 +1,697 @@ +extends /templates/base +block content + - var base = "/images/pages/play/ladder/prize_"; + + div#level-column + if levelDescription + div!= levelDescription + else + h1= level.get('name') + + if level.get('name') == 'Greed' + .tournament-blurb + h2 + span(data-i18n="ladder.tournament_ends") Tournament ends + | #{tournamentTimeLeft} + p + span(data-i18n="ladder.tournament_blurb") Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details + | + a(href="http://blog.codecombat.com/multiplayer-programming-tournament", data-i18n="ladder.tournament_blurb_blog") on our blog + | . + + .sponsor-logos + a(href="https://heapanalytics.com/") + img(src=base + "heap.png") + a(href="https://www.firebase.com/") + img(src=base + "firebase.png") + a(href="https://onemonthrails.com/") + img(src=base + "one_month.png") + a(href="http://www.jetbrains.com/webstorm/") + img(src=base + "webstorm.png") + a(href="http://shop.oreilly.com/category/ebooks.do") + img(src=base + "oreilly.png") + a(href="http://aws.amazon.com/") + img(src=base + "aws.png") + + div#columns.row + div.column.col-md-2 + for team in teams + div.column.col-md-4 + a(style="background-color: #{team.primaryColor}", data-team=team.id).play-button.btn.btn-danger.btn-block.btn-lg + span(data-i18n="play.play_as") Play As + | + span= team.name + div.column.col-md-2 + + .spectate-button-container + a(href="/play/spectate/#{level.get('slug')}").spectate-button.btn.btn-primary.center + span(data-i18n="play.spectate") Spectate + + hr + + ul.nav.nav-pills + li.active + a(href="#ladder", data-toggle="tab", data-i18n="general.ladder") Ladder + if !me.get('anonymous') + li + a(href="#my-matches", data-toggle="tab", data-i18n="ladder.my_matches") My Matches + li + a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate + if level.get('name') == 'Greed' + li + a(href="#prizes", data-toggle="tab", data-i18n="ladder.prizes") Prizes + li + a(href="#rules", data-toggle="tab", data-i18n="ladder.rules") Rules + + div.tab-content + .tab-pane.active.well#ladder + #ladder-tab-view + .tab-pane.well#my-matches + #my-matches-tab-view + .tab-pane.well#simulate + #simulate-tab-view + .tab-pane.well#prizes + h1(data-i18n="ladder_prizes.title") Tournament Prizes + p + span(data-i18n="ladder_prizes.blurb_1") These prizes will be awarded according to + | + a(href="#rules", data-i18n="ladder_prizes.blurb_2") the tournament rules + | + span(data-i18n="ladder_prizes.blurb_3") to the top human and ogre players. + | + strong(data-i18n="ladder_prizes.blurb_4") Two teams means double the prizes! + | + span(data-i18n="ladder_prizes.blurb_5") (There will be two first place winners, two second-place winners, etc.) + + table#prize_table.table + thead + tr + td(data-i18n="ladder_prizes.rank") Rank + td(data-i18n="ladder_prizes.prizes") Prizes + td(data-i18n="ladder_prizes.total_value") Total Value + tbody + tr + td 1st + td + ul.list-unstyled + li + img(src=base + "cash1.png") + span $512 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "custom_wizard.png") + span(data-i18n="ladder_prizes.custom_wizard") Custom CodeCombat Wizard + li + img(src=base + "custom_avatar.png") + span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_coupon") coupon: choose either Rails or HTML + | - $99 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $2054 + tr + td 2nd + td + ul.list-unstyled + li + img(src=base + "cash2.png") + span $256 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "custom_avatar.png") + span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $1229 + tr + td 3rd + td + ul.list-unstyled + li + img(src=base + "cash2.png") + span $128 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "custom_avatar.png") + span(data-i18n="ladder_prizes.custom_avatar") Custom CodeCombat avatar + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $1101 + tr + td 4th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $64 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $887 + tr + td 5th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $32 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "heap.png") + span + a(href="https://heapanalytics.com/") Heap Analytics + | + span(data-i18n="ladder_prizes.heap") for six months of "Startup" access + | - $354 + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $855 + tr + td 6th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $16 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $485 + tr + td 7th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $8 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $477 + tr + td 8th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $4 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $473 + tr + td 9th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $2 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $471 + tr + td 10th + td + ul.list-unstyled + li + img(src=base + "cash3.png") + span $1 + span(data-i18n="ladder_prizes.in_cash") in cash + li + img(src=base + "firebase.png") + span + a(href="https://www.firebase.com/") Firebase + | + span(data-i18n="ladder_prizes.credits") credits + | - $300 + li + img(src=base + "one_month.png") + span + a(href="https://onemonthrails.com/") One Month Rails + | + span(data-i18n="ladder_prizes.one_month_discount") discount, 30% off: choose either Rails or HTML + | - $30 + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $470 + tr + td 11 - 40 + td + ul.list-unstyled + li + img(src=base + "webstorm.png") + span + a(href="http://www.jetbrains.com/webstorm/") Webstorm + | + span(data-i18n="ladder_prizes.license") license + | - $49 + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $139 + tr + td 41 - 100 + td + ul.list-unstyled + li + img(src=base + "oreilly.png") + span + a(href="http://shop.oreilly.com/category/ebooks.do") O'Reilly + | + span(data-i18n="ladder_prizes.oreilly") ebook of your choice + | - $40 + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $90 + tr + td 101+ + td + ul.list-unstyled + li + img(src=base + "aws.png") + span + a(href="http://aws.amazon.com/") Amazon Web Services + | + span(data-i18n="ladder_prizes.credits") credits + | - $50 + td $50 + + .tab-pane.well#rules + h1(data-i18n="ladder.tournament_rules") Tournament Rules + h2 General + p You don't have to buy anything to participate in the tournament, and trying to pay us won't increase your odds of winning. Although we don't anticipate the rules changing, they are subject to change. + + h2 Dates and Times + p The tournament starts on Tuesday, May 20 at 8:30AM and ends on Tuesday, June 10 at 5:00PM PDT. After the tournament finishes, we will check the games manually to prevent duplicate entries and cheating. We will email all the winners within two weeks of the end date. + + h2 Eligilibity + p The tournament is open to anyone over the age of 13. Players are allowed to form teams to compete, but we will only be rewarding submissions, so if a team of 10 wins, they will need to split the prize. + + p The tournament is NOT open to people who live in countries or states that prohibit participating or receiving a prize in a challenge (these include, but are not limited to Brazil, Quebec, Italy, Cuba, Sudan, Iran, North Korea, and Syria). To clarify, people from the aforementioned places are welcome to play the Greed level, but cannot receive prizes. Organizations involved in putting the tournament together (namely CodeCombat and all of our employees) are excluded from participating/winning prizes. + + h2 Submission Requirements + p + | To be eligible to win prizes, players must submit their code to the Greed ladder for ranking AND defeat our default AI. Every player that submits their code to the ladder and beats our default AI will receive $50 in AWS credits as described on the + a(href="#prizes", data-i18n="ladder_prizes.tournament_prizes") Tournament Prizes + | page. + + p + | There are some restrictions regarding who can use the AWS credits. Please see the additional rules of use on + a(href="https://aws.amazon.com/awscredits") Amazon's AWS credits page. + + h2 Submission Rights + p We reserve the right to use your submission and site identity (including username, avatar, and any information you mark as public) to promote the tournament. This is in keeping with our overall site terms of service. + + h2 Judging Criteria + p + | We will calculate final rankings by running the top games from the public leaderboard from both teams against each other and sorting solutions by wins and losses. The number of games from each side to be used in the final ranking is yet to be determined, but is probably around 150. The final ranking will be performed with a snapshot of solutions taken the end of the contest. The final ranking methedology is subject to change. We will not be evaluating code in any manual way for common traits like adequate documentation, cleanliness, etc. We reserve the right to disqualify any player for any reason. The public leaderboards are a good proxy for your final rank, but are not guaranteed to be accurate. To repeat, + strong the leaderboards are only a preliminary proxy for your final rank + | . + + p + | Your rank will change as players submit more solutions and more matches are played according to + a(href="https://github.com/codecombat/bayesian-battle") our open-source ranking library, Bayesian Battle + | , but our final ranking will use an exhaustive pairwise matching round amongst the top players as described above. + + h2 Prizes + p + | Prizes will be awarded to everyone that achieves a rank covered on the + a(href="#prizes", data-i18n="ladder.prizes") Tournament Prizes + | page. + + p Please remember that the player ranks listed on the prize page refer to ranks WITHIN a leaderboard. So if you are the #2 Ogre player, you will win the #2 prize. Similarly, if you are the #3 Human player, you will receive the #3 prize. If you have submissions on both leaderboards, we will only count your highest submission for the purposes of distributing prizes. As a result, your final ranking may be higher than your preliminary ranking due to removing duplicate submissions above you. + + h2 Verifying Potential Winners + p We may ask players to identify themselves so that we can detect duplicate entries. This may be done in the form of a Facebook, Google+, or LinkedIn profile, but we may need more information. All players eligible for prizes agree that refusing to provide us with identifying information may lead to ineligibility for prizes. + + p On a related note, if we have reason to believe that a player has intentionally submitted duplicate entries for the purpose of receiving more prizes or manipulating the leaderboards in any way, we will remove that player and all submissions we believe to be associated with them. We want this to be fair for everyone. + + h2 Prize Distribution + p Different sponsors require different ways of claiming their prizes, and we will work with winners to ensure they are able to redeem their prizes in a timely fashion. For cash prizes, we will deliver the money via PayPal. We will not ship checks, money orders, or cash through the mail. We will assume reasonable international money transfer costs to deliver cash prizes through Paypal. + + p Winners are responsible for any taxes associated with claiming their prizes. CodeCombat is not responsible for filing paperwork on behalf of winners for tax claims. + + h2 Contact + p + | If you have any questions or would like to get in touch with us for any other reason, we can be reached at team@codecombat.com. You can also post to our public + a(href="http://discourse.codecombat.com/") Discourse forum + | . diff --git a/app/templates/play/ladder/ladder_tab.jade b/app/templates/play/ladder/ladder_tab.jade index 1b7d7351e..9a5cf746a 100644 --- a/app/templates/play/ladder/ladder_tab.jade +++ b/app/templates/play/ladder/ladder_tab.jade @@ -1,5 +1,5 @@ div#columns.row - for team in teams + for team, teamIndex in teams div.column.col-md-4 div(id="histogram-display-#{team.name}", class="histogram-display",data-team-name=team.name) table.table.table-bordered.table-condensed.table-hover @@ -17,10 +17,10 @@ div#columns.row - var topSessions = team.leaderboard.topPlayers.models; - var showJustTop = team.leaderboard.inTopSessions() || me.get('anonymous'); - - if(!showJustTop) topSessions = topSessions.slice(0, 10); + - if(!showJustTop && topSessions.length == 20) topSessions = topSessions.slice(0, 10); for session, rank in topSessions - var myRow = session.get('creator') == me.id - tr(class=myRow ? "success" : "") + tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id) td.rank-cell= rank + 1 td.score-cell= Math.round(session.get('totalScore') * 100) td.name-col-cell= session.get('creatorName') || "Anonymous" @@ -33,29 +33,31 @@ div#columns.row td(colspan=4).ellipsis-row ... for session in team.leaderboard.nearbySessions() - var myRow = session.get('creator') == me.id - tr(class=myRow ? "success" : "") + tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id) td.rank-cell= session.rank td.score-cell= Math.round(session.get('totalScore') * 100) td.name-col-cell= session.get('creatorName') || "Anonymous" td.fight-cell a(href="/play/level/#{level.get('slug') || level.id}/?team=#{team.otherTeam}&opponent=#{session.id}") span(data-i18n="ladder.fight") Fight! - + if teamIndex == 1 + .btn.btn-sm.load-more-ladder-entries More + div.column.col-md-4 - h4.friends-header Friends Playing + h4.friends-header(data-i18n="ladder.friends_playing") Friends Playing if me.get('anonymous') div.alert.alert-info - a(data-toggle="coco-modal", data-target="modal/signup") Sign up to play with your friends! + a(data-toggle="coco-modal", data-target="modal/auth", data-i18n="ladder.log_in_for_friends") Log in to play with your friends! else if !onFacebook || !onGPlus div.connect-buttons - | Connect and play against your friends! + span(data-i18n="ladder.social_connect_blurb") Connect and play against your friends! br if !onFacebook - button.btn.btn-sm.connect-facebook Facebook + button.btn.btn-sm.connect-facebook(data-i18n="community.facebook") Facebook if !onGPlus - button.btn.btn-sm.connect-google-plus Google+ + button.btn.btn-sm.connect-google-plus(data-i18n="community.gplus") Google+ if friends.length for friend in friends @@ -70,5 +72,5 @@ div#columns.row a(href="/play/level/#{level.get('slug') || level.id}/?team=#{friend.otherTeam}&opponent=#{friend._id}") span(data-i18n="ladder.fight") Fight! - else - p Invite your friends to join you in battle! + else if onFacebook || onGPlus + p(data-i18n="ladder.invite_friends_to_battle") Invite your friends to join you in battle! diff --git a/app/templates/play/ladder/my_matches_tab.jade b/app/templates/play/ladder/my_matches_tab.jade index 4975fa963..4d91d0757 100644 --- a/app/templates/play/ladder/my_matches_tab.jade +++ b/app/templates/play/ladder/my_matches_tab.jade @@ -1,9 +1,3 @@ -//if matches.length -// p#your-score -// span Your Current Score: -// span -// strong= score - div#columns.row for team in teams div.matches-column.col-md-6 @@ -23,28 +17,20 @@ div#columns.row if team.session tr th(colspan=4) - button.btn.btn-warning.btn-block.rank-button(data-session-id=team.session.id) - span(data-i18n="ladder.rank_no_code").unavailable.hidden No New Code to Rank - span(data-i18n="ladder.rank_my_game").rank.hidden Rank My Game! - span(data-i18n="ladder.rank_submitting").submitting.hidden Submitting... - span(data-i18n="ladder.rank_submitted").submitted.hidden Submitted for Ranking - span(data-i18n="ladder.rank_failed").failed.hidden Failed to Rank - span(data-i18n="ladder.rank_being_ranked").ranking.hidden Game Being Ranked + .ladder-submission-view(data-session-id=team.session.id) - if team.chartData + if team.scoreHistory tr th(colspan=4, style="color: #{team.primaryColor}") div(class="score-chart-wrapper", data-team-name=team.name, id="score-chart-#{team.name}") - - tr th(data-i18n="general.result") Result th(data-i18n="general.opponent") Opponent th(data-i18n="general.when") When th for match in team.matches - tr(class=(match.stale ? "stale " : "") + match.state) + tr(class=(match.stale ? "stale " : "") + (match.fresh ? "fresh " : "") + match.state) td.state-cell if match.state === 'win' span(data-i18n="general.win").win Win diff --git a/app/templates/play/ladder/play_modal.jade b/app/templates/play/ladder/play_modal.jade index 72cc45267..c3a0eb074 100644 --- a/app/templates/play/ladder/play_modal.jade +++ b/app/templates/play/ladder/play_modal.jade @@ -14,10 +14,11 @@ block modal-body-content div#normal-view - p.tutorial-suggestion - strong(data-i18n="ladder.tutorial_not_sure") Not sure what's going on? - | - a(href="/play/level/#{levelID}-tutorial", data-i18n="ladder.tutorial_play_first") Play the tutorial first. + if tutorialLevelExists + p.tutorial-suggestion + strong(data-i18n="ladder.tutorial_not_sure") Not sure what's going on? + | + a(href="/play/level/#{levelID}-tutorial", data-i18n="ladder.tutorial_play_first") Play the tutorial first. a(href="/play/level/#{levelID}?team=#{teamID}") div.play-option diff --git a/app/templates/play/ladder/simulate_tab.jade b/app/templates/play/ladder/simulate_tab.jade new file mode 100644 index 000000000..fe266c6d9 --- /dev/null +++ b/app/templates/play/ladder/simulate_tab.jade @@ -0,0 +1,52 @@ +p(id="simulation-status-text") + if simulationStatus + | #{simulationStatus} + else + span(data-i18n="ladder.simulation_explanation") By simulating games you can get your game ranked faster! +p + button(data-i18n="ladder.simulate_games").btn.btn-warning.btn-lg.highlight#simulate-button Simulate Games! + +p.simulation-count + span(data-i18n="ladder.games_simulated_by") Games simulated by you: + | + span#simulated-by-you= me.get('simulatedBy') || 0 + +p.simulation-count + span(data-i18n="ladder.games_simulated_for") Games simulated for you: + | + span#simulated-for-you= me.get('simulatedFor') || 0 +p.simulation-count + span(data-i18n="ladder.games_in_queue") Games currently in the queue: + | + span#games-in-queue= numberOfGamesInQueue || 0 +table.table.table-bordered.table-condensed.table-hover + tr + th + th(data-i18n="general.player").name-col-cell Player + th(data-i18n="ladder.games_simulated") Games simulated + th(data-i18n="ladder.games_played") Games played + th(data-i18n="ladder.ratio") Ratio + - var topSimulators = simulatorsLeaderboardData.topSimulators.models; + - var showJustTop = simulatorsLeaderboardData.inTopSimulators() || me.get('anonymous'); + - if(!showJustTop) topSimulators = topSimulators.slice(0, 10); + for user, rank in topSimulators + - var myRow = user.id == me.id + tr(class=myRow ? "success" : "") + td.simulator-leaderboard-cell= rank + 1 + td.name-col-cell= user.get('name') || "Anonymous" + td.simulator-leaderboard-cell= user.get('simulatedBy') + td.simulator-leaderboard-cell= user.get('simulatedFor') + td.simulator-leaderboard-cell= Math.round((user.get('simulatedBy') / user.get('simulatedFor')) * 10) / 10 + + if !showJustTop && simulatorsLeaderboardData.nearbySimulators().length + tr(class="active") + td(colspan=5).ellipsis-row ... + for user in simulatorsLeaderboardData.nearbySimulators() + - var myRow = user.id == me.id + - var ratio = user.get('simulatedBy') / user.get('simulatedFor'); + tr(class=myRow ? "success" : "") + td.simulator-leaderboard-cell= user.rank + td.name-col-cell= user.get('name') || "Anonymous" + td.simulator-leaderboard-cell= user.get('simulatedBy') + td.simulator-leaderboard-cell= user.get('simulatedFor') + td.simulator-leaderboard-cell= _.isNaN(ratio) || ratio == Infinity ? '' : ratio.toFixed(1) diff --git a/app/templates/play/ladder_home.jade b/app/templates/play/ladder_home.jade new file mode 100644 index 000000000..e3b976ae6 --- /dev/null +++ b/app/templates/play/ladder_home.jade @@ -0,0 +1,23 @@ +extends /templates/base + +block content + + each campaign in campaigns + .campaign-container + h1 + a(href="/play/#{campaign.levels[0].levelPath || 'level'}/#{campaign.levels[0].id}", data-i18n="play.campaign_#{campaign.id}")= campaign.name + p.campaign-description(data-i18n="[html]play.campaign_#{campaign.id}_description")!= campaign.description + each level in campaign.levels + a(href=level.disabled ? "/play/ladder" : "/play/ladder/#{level.id}", disabled=level.disabled, class=levelStatusMap[level.id] || '', title=level.description) + .level + if level.image + img.level-image(src="#{level.image}", alt="#{level.name}").img-rounded + else + img.level-image(src="/images/pages/home/multiplayer_notext.jpg", alt="#{level.name}").img-rounded + //h3= level.name + (level.disabled ? " (Coming soon!)" : "") + .overlay-text.level-difficulty + span(data-i18n="play.level_difficulty") Difficulty: + each i in Array(level.difficulty) + | ★ + .play-text-container + .overlay-text.play-text(data-i18n="home.play") Play diff --git a/app/templates/play/level.jade b/app/templates/play/level.jade index fe4170d1c..89418e365 100644 --- a/app/templates/play/level.jade +++ b/app/templates/play/level.jade @@ -1,7 +1,6 @@ #level-loading-view .level-content - #control-bar-view #code-area @@ -9,13 +8,10 @@ #tome-view #canvas-wrapper - canvas(width=1848, height=1178)#surface + canvas(width=924, height=589)#surface #canvas-left-gradient.gradient #canvas-top-gradient.gradient - - a.btn.btn-primary.banner.secret#level-done-button(data-i18n="play_level.done") Done - - #goals-view.secret + #goals-view.secret #gold-view.secret.expanded diff --git a/app/templates/play/level/control_bar.jade b/app/templates/play/level/control_bar.jade index 13670857b..d85a5c700 100644 --- a/app/templates/play/level/control_bar.jade +++ b/app/templates/play/level/control_bar.jade @@ -15,4 +15,6 @@ else if spectateGame button.btn.btn-xs.btn-inverse.banner#next-game-button(title="Next Game", data-i18n="play_level.next-game") Next game! -button.btn.btn-xs.btn-inverse.banner#restart-button(title="Reload all custom code to reset level", data-i18n="play_level.restart") Restart \ No newline at end of file +button.btn.btn-xs.btn-inverse.banner#restart-button(title="Reload all custom code to reset level", data-i18n="play_level.restart") Restart + +button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done diff --git a/app/templates/play/level/goals.jade b/app/templates/play/level/goals.jade index 150edf34c..51d64f23b 100644 --- a/app/templates/play/level/goals.jade +++ b/app/templates/play/level/goals.jade @@ -1,5 +1,8 @@ -h3 - i.icon-plus-sign.expanded - i.icon-minus-sign.collapsed - span(data-i18n="play_level.goals") Goals -ul#primary-goals-list \ No newline at end of file +ul#primary-goals-list +div.goals-status + strong(data-i18n="play_level.goals") Goals + span.spl.spr : + span(data-i18n="play_level.success").secret.goal-status.success Success! + span(data-i18n="play_level.incomplete").secret.goal-status.incomplete Incomplete + span(data-i18n="play_level.timed_out").secret.goal-status.timed-out Ran out of time + span(data-i18n="play_level.failing").secret.goal-status.failure Failing diff --git a/app/templates/play/level/hud.jade b/app/templates/play/level/hud.jade index f3a67ff9b..5153f1379 100644 --- a/app/templates/play/level/hud.jade +++ b/app/templates/play/level/hud.jade @@ -9,12 +9,14 @@ .thang-name .thang-actions.thang-elem - .action-header(data-i18n="play_level.action_timeline") Action Timeline - .table-container - .progress-arrow.progress-indicator - .progress-line.progress-indicator - table - tbody + .nano + .nano-content + .action-header(data-i18n="play_level.action_timeline") Action Timeline + .table-container + .progress-arrow.progress-indicator + .progress-line.progress-indicator + table + tbody .dialogue-area p.bubble.dialogue-bubble diff --git a/app/templates/play/level/level_loading.jade b/app/templates/play/level/level_loading.jade index cfd949909..9cfc3622d 100644 --- a/app/templates/play/level/level_loading.jade +++ b/app/templates/play/level/level_loading.jade @@ -2,7 +2,7 @@ .right-wing -.loading-details +.loading-details.loading-container .load-progress .progress.progress-striped.active @@ -40,3 +40,6 @@ strong.tip.rare span(data-i18n='play_level.tip_harry') Yer a Wizard, span= me.get('name') || 'Anoner' + + .errors + \ No newline at end of file diff --git a/app/templates/play/level/modal/editor_config.jade b/app/templates/play/level/modal/editor_config.jade index 8353d82b6..89ec7f585 100644 --- a/app/templates/play/level/modal/editor_config.jade +++ b/app/templates/play/level/modal/editor_config.jade @@ -6,11 +6,19 @@ block modal-header-content block modal-body-content .form .form-group.select-group - label.control-label(for="tome-language" data-i18n="play_level.editor_config_language_label") Programming Language + label.control-label(for="tome-session-language" data-i18n="play_level.editor_config_level_language_label") Language for This Level + select#tome-session-language(name="language") + for option in languages + option(value=option.id selected=(sessionLanguage === option.id))= option.name + span.help-block(data-i18n="play_level.editor_config_level_language_description") Define the programming language for this particular level. + + .form-group.select-group + label.control-label(for="tome-language" data-i18n="play_level.editor_config_default_language_label") Default Programming Language select#tome-language(name="language") - option(value="javascript" selected=(language === "javascript")) JavaScript - option(value="coffeescript" selected=(language === "coffeescript")) CoffeeScript - span.help-block(data-i18n="play_level.editor_config_language_description") Define the programming language you want to code in. + for option in languages + option(value=option.id selected=(language === option.id))= option.name + span.help-block(data-i18n="play_level.editor_config_default_language_description") Define the programming language you want to code in when starting new levels. + .form-group.select-group label.control-label(for="tome-key-bindings" data-i18n="play_level.editor_config_keybindings_label") Key Bindings select#tome-key-bindings(name="keyBindings") @@ -18,16 +26,19 @@ block modal-body-content option(value="vim" selected=(keyBindings === "vim")) Vim option(value="emacs" selected=(keyBindings === "emacs")) Emacs span.help-block(data-i18n="play_level.editor_config_keybindings_description") Adds additional shortcuts known from the common editors. + .form-group.checkbox label(for="tome-invisibles") input#tome-invisibles(name="invisibles", type="checkbox", checked=invisibles) span(data-i18n="play_level.editor_config_invisibles_label") Show Invisibles span.help-block(data-i18n="play_level.editor_config_invisibles_description") Displays invisibles such as spaces or tabs. + .form-group.checkbox label(for="tome-indent-guides") input#tome-indent-guides(name="indentGuides", type="checkbox", checked=indentGuides) span(data-i18n="play_level.editor_config_indentguides_label") Show Indent Guides span.help-block(data-i18n="play_level.editor_config_indentguides_description") Displays vertical lines to see indentation better. + .form-group.checkbox label(for="tome-behaviors") input#tome-behaviors(name="behaviors", type="checkbox", checked=behaviors) diff --git a/app/templates/play/level/modal/infinite_loop.jade b/app/templates/play/level/modal/infinite_loop.jade index bb563ba2e..b2128482b 100644 --- a/app/templates/play/level/modal/infinite_loop.jade +++ b/app/templates/play/level/modal/infinite_loop.jade @@ -8,5 +8,6 @@ block modal-body-content p(data-i18n="play_level.infinite_loop_explanation") The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know. block modal-footer-content - a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.infinite_loop_wait").btn#restart-level-infinite-loop-retry-button Try Again - a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.infinite_loop_reload").btn.btn-primary#restart-level-infinite-loop-confirm-button Reset Level + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.infinite_loop_try_again").btn#restart-level-infinite-loop-retry-button Try Again + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.infinite_loop_reset_level").btn.btn-danger#restart-level-infinite-loop-confirm-button Reset Level + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.infinite_loop_comment_out").btn.btn-primary#restart-level-infinite-loop-comment-button Comment Out My Code diff --git a/app/templates/play/level/modal/keyboard_shortcuts.jade b/app/templates/play/level/modal/keyboard_shortcuts.jade new file mode 100644 index 000000000..195558eed --- /dev/null +++ b/app/templates/play/level/modal/keyboard_shortcuts.jade @@ -0,0 +1,63 @@ +extends /templates/modal/modal_base + +block modal-header-content + h3(data-i18n="keyboard_shortcuts.keyboard_shortcuts") Keyboard Shortcuts + +block modal-body-content + dl.dl-horizontal + dt(title="Shift+" + enter) + code ⇧+#{enter} + dd(data-i18n="keyboard_shortcuts.cast_spell") Cast current spell. + dl.dl-horizontal + dt(title="Shift+" + space) + code ⇧+#{space} + dd(data-i18n="keyboard_shortcuts.continue_script") Continue past current script. + dl.dl-horizontal + dt(title=escapeKey) + code Esc + dd(data-i18n="keyboard_shortcuts.skip_scripts") Skip past all skippable scripts. + dl.dl-horizontal + dt(title=ctrlName + "+P") + code #{ctrl}+P + dd(data-i18n="keyboard_shortcuts.toggle_playback") Toggle play/pause. + dl.dl-horizontal + dt(title=ctrlName + "+[, " + ctrlName + "+]") + code #{ctrl}+[ + | , + code #{ctrl}+] + dd(data-i18n="keyboard_shortcuts.scrub_playback") Scrub back and forward through time. + dl.dl-horizontal + dt(title=ctrlName + "+" + altName + "+[, " + ctrlName + "+" + altName + "+]") + code #{ctrl}+#{alt}+[ + | , + code #{ctrl}+#{alt}+] + dd(data-i18n="keyboard_shortcuts.scrub_execution") Scrub through current spell execution. + dl.dl-horizontal + dt(title=ctrlName + "+\\") + code #{ctrl}+\ + dd(data-i18n="keyboard_shortcuts.toggle_debug") Toggle debug display. + dl.dl-horizontal + dt(title=ctrlName + "+G") + code #{ctrl}+G + dd(data-i18n="keyboard_shortcuts.toggle_grid") Toggle grid overlay. + dl.dl-horizontal + dt(title=ctrlName + "+O") + code #{ctrl}+O + dd(data-i18n="keyboard_shortcuts.toggle_pathfinding") Toggle pathfinding overlay. + dl.dl-horizontal + dt(title=ctrlName + "+Shift+B") + code #{ctrl}+⇧+B + dd(data-i18n="keyboard_shortcuts.beautify") Beautify your code by standardizing its formatting. + dl.dl-horizontal + dt(title="Arrow keys") + code ↠+ | , + code → + | , + code ↑ + | , + code ↓ + dd(data-i18n="keyboard_shortcuts.move_wizard") Move your Wizard around the level. + +block modal-footer-content + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="modal.close").btn.btn-primary Close diff --git a/app/templates/play/level/modal/multiplayer.jade b/app/templates/play/level/modal/multiplayer.jade index 2f19a008b..cc2712f38 100644 --- a/app/templates/play/level/modal/multiplayer.jade +++ b/app/templates/play/level/modal/multiplayer.jade @@ -27,10 +27,11 @@ block modal-body-content if ladderGame if me.get('anonymous') - p Sign in or create an account and get your solution on the leaderboard! + p(data-i18n="play_level.multiplayer_sign_in_leaderboard") Sign in or create an account and get your solution on the leaderboard. + else if readyToRank + .ladder-submission-view else - a#go-to-leaderboard-button.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches") Go to the leaderboard! - p You can submit your game to be ranked from the leaderboard page. + a.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches", data-i18n="play_level.victory_go_ladder") Return to Ladder block modal-footer-content a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="modal.close").btn.btn-primary Close diff --git a/app/templates/play/level/modal/victory.jade b/app/templates/play/level/modal/victory.jade index 92c253955..48ccd6af3 100644 --- a/app/templates/play/level/modal/victory.jade +++ b/app/templates/play/level/modal/victory.jade @@ -12,7 +12,7 @@ block modal-body-content block modal-footer-content if readyToRank - button.btn.btn-success.rank-game-button(data-i18n="play_level.victory_rank_my_game") Rank My Game + .ladder-submission-view else if level.get('type') === 'ladder' a.btn.btn-primary(href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_go_ladder") Return to Ladder else if hasNextLevel @@ -22,7 +22,7 @@ block modal-footer-content if me.get('anonymous') p.sign-up-poke button.btn.btn-success.sign-up-button.btn-large(data-toggle="coco-modal", data-target="modal/signup", data-i18n="play_level.victory_sign_up") Sign Up to Save Progress - span(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account! + span(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account! p.clearfix else div.rating.secret diff --git a/app/templates/play/level/playback.jade b/app/templates/play/level/playback.jade index 6406aa71b..afcbd8276 100644 --- a/app/templates/play/level/playback.jade +++ b/app/templates/play/level/playback.jade @@ -34,6 +34,9 @@ button.btn.btn-xs.btn-inverse#music-button(title="Toggle Music") i.icon-globe | Debug Mode i.icon-ok.secret + li.selectable#view-keyboard-shortcuts + i.icon-info-sign + span(data-i18n="play_level.keyboard_shortcuts") Key Shortcuts li(title="Ctrl/Cmd + G: Toggle grid display").selectable#grid-toggle i.icon-th span(data-i18n="play_level.grid") Grid diff --git a/app/templates/play/level/team_gold.jade b/app/templates/play/level/team_gold.jade new file mode 100644 index 000000000..41d773717 --- /dev/null +++ b/app/templates/play/level/team_gold.jade @@ -0,0 +1,3 @@ +div(class="team-gold team-" + team) + img(src="/images/level/gold_icon.png", alt="") + div(class="gold-amount team-" + team) diff --git a/app/templates/play/level/tome/spell_palette.jade b/app/templates/play/level/tome/spell_palette.jade index d587da8c5..f87259d77 100644 --- a/app/templates/play/level/tome/spell_palette.jade +++ b/app/templates/play/level/tome/spell_palette.jade @@ -6,5 +6,5 @@ ul(class="nav nav-pills" + (tabbed ? ' multiple-tabs' : '')) h4= group .tab-content each slug, group in entryGroupSlugs - div(id="palette-tab-" + slug, class="tab-pane" + (group == "this" || slug == defaultGroupSlug ? " active" : "")) - div(class="properties properties-" + slug) + div(id="palette-tab-" + slug, class="tab-pane nano" + (group == "this" || slug == defaultGroupSlug ? " active" : "")) + div(class="properties properties-" + slug + " nano-content") diff --git a/app/templates/play/spectate.jade b/app/templates/play/spectate.jade index ee9408d4c..e6a9650f9 100644 --- a/app/templates/play/spectate.jade +++ b/app/templates/play/spectate.jade @@ -3,7 +3,7 @@ .level-content #control-bar-view #canvas-wrapper - canvas(width=1848, height=1178)#surface + canvas(width=924, height=589)#surface #canvas-left-gradient.gradient #canvas-top-gradient.gradient #gold-view.secret.expanded diff --git a/app/treema-ext.coffee b/app/treema-ext.coffee index 6d2b0c3ed..044c7e6d4 100644 --- a/app/treema-ext.coffee +++ b/app/treema-ext.coffee @@ -1,5 +1,5 @@ CocoModel = require 'models/CocoModel' -CocoCollection = require 'models/CocoCollection' +CocoCollection = require 'collections/CocoCollection' {me} = require('lib/auth') locale = require 'locale/locale' @@ -28,7 +28,7 @@ class LiveEditingMarkup extends TreemaNode.nodeMap.ace valEl.append( $('
').append( $('') - .addClass('btn') + .addClass('btn btn-sm btn-primary') .click(=> filepicker.pick @onFileChosen) ) ) @@ -55,38 +55,38 @@ class LiveEditingMarkup extends TreemaNode.nodeMap.ace buildValueForDisplay: (valEl) -> @editor?.destroy() valEl.html(marked(@data)) - + class SoundFileTreema extends TreemaNode.nodeMap.string valueClass: 'treema-sound-file' editable: false soundCollection: 'files' - + onClick: (e) -> return if $(e.target).closest('.btn').length super(arguments...) - + getFiles: -> @settings[@soundCollection]?.models or [] buildValueForDisplay: (valEl) -> mimetype = "audio/#{@keyForParent}" - pickButton = $('') + pickButton = $('') .click(=> filepicker.pick {mimetypes:[mimetype]}, @onFileChosen) - playButton = $('') + playButton = $('') .click(@playFile) - stopButton = $('') + stopButton = $('') .click(@stopFile) - + dropdown = $('') dropdownButton = $('') - .addClass('btn dropdown-toggle') + .addClass('btn btn-primary btn-xs dropdown-toggle') .attr('href', '#') - .append($('')) + .append($('')) .dropdown() - + dropdown.append dropdownButton - + menu = $('') files = @getFiles() for file in files @@ -102,22 +102,22 @@ class SoundFileTreema extends TreemaNode.nodeMap.string @data = $(e.target).data('fullPath') or @data @reset() dropdown.append(menu) - + valEl.append(pickButton) if @data valEl.append(playButton) valEl.append(stopButton) - valEl.append(dropdown) if files.length and @canEdit() + valEl.append(dropdown) # if files.length and @canEdit() if @data path = @data.split('/') name = path[path.length-1] valEl.append($('').text(name)) - + reset: -> @instance = null @flushChanges() @refreshDisplay() - + playFile: => @src = "/file/#{@data}" @@ -129,27 +129,27 @@ class SoundFileTreema extends TreemaNode.nodeMap.string registered = createjs.Sound.registerSound(@src) if registered is true @instance = createjs.Sound.play(@src) - + else f = (event) => @instance = createjs.Sound.play(event.src) if event.src is @src createjs.Sound.removeEventListener('fileload', f) createjs.Sound.addEventListener('fileload', f) - + stopFile: => @instance?.stop() - + onFileChosen: (InkBlob) => if not @settings.filePath console.error('Need to specify a filePath for this treema', @getRoot()) throw Error('cannot upload file') - + body = url: InkBlob.url filename: InkBlob.filename mimetype: InkBlob.mimetype path: @settings.filePath force: true - + @uploadingPath = [@settings.filePath, InkBlob.filename].join('/') $.ajax('/file', { type: 'POST', data: body, success: @onFileUploaded }) @@ -168,7 +168,7 @@ class ImageFileTreema extends TreemaNode.nodeMap.string buildValueForDisplay: (valEl) -> mimetype = 'image/*' - pickButton = $('') + pickButton = $(' Upload Picture') .click(=> filepicker.pick {mimetypes:[mimetype]}, @onFileChosen) valEl.append(pickButton) @@ -233,6 +233,7 @@ class InternationalizationNode extends TreemaNode.nodeMap.object type: "object" properties: {} } + return i18nChildSchema unless @parent unless @schema.props? console.warn "i18n props array is empty! Filling with all parent properties by default" @schema.props = (prop for prop,_ of @parent.schema.properties when prop isnt "i18n") @@ -279,13 +280,13 @@ class LatestVersionReferenceNode extends TreemaNode search: => term = @getValEl().find('input').val() return if term is @lastTerm - + # HACK while search is broken if @collection @lastTerm = term @searchCallback() return - + @getSearchResultsEl().empty() if @lastTerm and not term return unless term @lastTerm = term @@ -295,9 +296,9 @@ class LatestVersionReferenceNode extends TreemaNode # HACK while search is broken # @collection.url = "#{@url}?term=#{term}&project=true" @collection.url = "#{@url}?term=#{''}&project=true" - + @collection.fetch() - @listenTo(@collection, 'sync', @searchCallback) + @collection.once 'sync', @searchCallback, @ searchCallback: -> container = @getSearchResultsEl().detach().empty() @@ -306,10 +307,10 @@ class LatestVersionReferenceNode extends TreemaNode row = $('
').addClass('treema-search-result-row') text = @formatDocument(model) continue unless text? - + # HACK while search is broken continue unless text.toLowerCase().indexOf(@lastTerm.toLowerCase()) >= 0 - + row.addClass('treema-search-selected') if first first = false row.text(text) @@ -332,7 +333,7 @@ class LatestVersionReferenceNode extends TreemaNode if @instance and not m m = @instance m.url = -> urlGoingFor - @settings.supermodel.addModel(m) + @settings.supermodel.registerModel(m) return 'Unknown' unless m return m.get('name') diff --git a/app/views/account/job_profile_view.coffee b/app/views/account/job_profile_view.coffee new file mode 100644 index 000000000..8c11aa2c8 --- /dev/null +++ b/app/views/account/job_profile_view.coffee @@ -0,0 +1,132 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/account/job_profile' +{me} = require('lib/auth') + +module.exports = class JobProfileView extends CocoView + id: 'job-profile-view' + template: template + + editableSettings: [ + 'lookingFor', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', + 'work', 'education', 'visa', 'projects', 'links', 'jobTitle', 'photoURL' + ] + readOnlySettings: [] #['updated'] + + afterRender: -> + super() + return unless @supermodel.finished() + _.defer => @buildJobProfileTreema() # Not sure why, but the Treemas don't fully build without this if you reload the page. + + buildJobProfileTreema: -> + visibleSettings = @editableSettings.concat @readOnlySettings + data = _.pick (me.get('jobProfile') ? {}), (value, key) => key in visibleSettings + data.name ?= (me.get('firstName') + ' ' + me.get('lastName')).trim() if me.get('firstName') + schema = _.cloneDeep me.schema().properties.jobProfile + schema.properties = _.pick schema.properties, (value, key) => key in visibleSettings + schema.required = _.intersection schema.required, visibleSettings + for prop in @readOnlySettings + schema.properties[prop].readOnly = true + treemaOptions = + filePath: "db/user/#{me.id}" + schema: schema + data: data + aceUseWrapMode: true + callbacks: {change: @onJobProfileChanged} + nodeClasses: + 'skill': SkillTagNode + 'link-name': LinkNameNode + 'city': CityNode + 'country': CountryNode + + @jobProfileTreema = @$el.find('#job-profile-treema').treema treemaOptions + @jobProfileTreema.build() + @jobProfileTreema.open() + @updateProgress() + + onJobProfileChanged: (e) => + @hasEditedProfile = true + @trigger 'change' + @updateProgress() + + updateProgress: -> + completed = 0 + totalWeight = 0 + next = null + for metric in metrics = @getProgressMetrics() + done = metric.fn() + completed += metric.weight ? 1 if done + totalWeight += metric.weight + next = metric.name unless next or done + progress = Math.round 100 * completed / totalWeight + bar = @$el.find('.profile-completion-progress .progress-bar') + bar.css 'width', "#{progress}%" + text = "" + if progress > 19 + text = "#{progress}% complete" + else if progress > 5 + text = "#{progress}%" + bar.text text + bar.parent().toggle Boolean progress + @$el.find('.progress-next-item').text(next).toggle Boolean next + + getProgressMetrics: -> + return @progressMetrics if @progressMetrics + schema = me.schema().properties.jobProfile + jobProfile = @jobProfileTreema.data + exists = (field) -> -> jobProfile[field] + modified = (field) -> -> jobProfile[field] and jobProfile[field] isnt schema.properties[field].default + listStarted = (field, subfields) -> -> jobProfile[field]?.length and _.every subfields, (subfield) -> jobProfile[field][0][subfield] + @progressMetrics = [ + {name: "Mark yourself open to offers to show up in searches.", weight: 1, fn: modified 'active'} + {name: "Specify your desired job title.", weight: 0, fn: exists 'jobTitle'} + {name: "Provide your name.", weight: 1, fn: modified 'name'} + {name: "Choose your city.", weight: 1, fn: modified 'city'} + {name: "Pick your country.", weight: 0, fn: exists 'country'} + {name: "List at least five skills.", weight: 2, fn: -> jobProfile.skills.length >= 5} + {name: "Write a short description to summarize yourself at a glance.", weight: 2, fn: modified 'shortDescription'} + {name: "Fill in your main description to sell yourself and describe the work you're looking for.", weight: 3, fn: modified 'longDescription'} + {name: "List your work experience.", weight: 3, fn: listStarted 'work', ['role', 'employer']} + {name: "Recount your educational ordeals.", weight: 3, fn: listStarted 'education', ['degree', 'school']} + {name: "Show off up to three projects you've worked on.", weight: 3, fn: listStarted 'projects', ['name']} + {name: "Add any personal or social links.", weight: 2, fn: listStarted 'links', ['link', 'name']} + {name: "Add an optional professional photo.", weight: 2, fn: modified 'photoURL'} + ] + + getData: -> + return {} unless me.get('jobProfile') or @hasEditedProfile + _.pick @jobProfileTreema.data, (value, key) => key in @editableSettings + + +commonSkills = ['c#', 'java', 'javascript', 'php', 'android', 'jquery', 'python', 'c++', 'html', 'mysql', 'ios', 'asp.net', 'css', 'sql', 'iphone', '.net', 'objective-c', 'ruby-on-rails', 'c', 'ruby', 'sql-server', 'ajax', 'wpf', 'linux', 'database', 'django', 'vb.net', 'windows', 'facebook', 'r', 'html5', 'multithreading', 'ruby-on-rails-3', 'wordpress', 'winforms', 'node.js', 'spring', 'osx', 'performance', 'visual-studio-2010', 'oracle', 'swing', 'algorithm', 'git', 'linq', 'apache', 'web-services', 'perl', 'wcf', 'entity-framework', 'bash', 'visual-studio', 'sql-server-2008', 'hibernate', 'actionscript-3', 'angularjs', 'matlab', 'qt', 'ipad', 'sqlite', 'cocoa-touch', 'cocoa', 'flash', 'mongodb', 'codeigniter', 'jquery-ui', 'css3', 'tsql', 'google-maps', 'silverlight', 'security', 'delphi', 'vba', 'postgresql', 'jsp', 'shell', 'internet-explorer', 'google-app-engine', 'sockets', 'validation', 'scala', 'oop', 'unit-testing', 'xaml', 'parsing', 'twitter-bootstrap', 'google-chrome', 'http', 'magento', 'email', 'android-layout', 'flex', 'rest', 'maven', 'jsf', 'listview', 'date', 'winapi', 'windows-phone-7', 'facebook-graph-api', 'unix', 'url', 'c#-4.0', 'jquery-ajax', 'svn', 'symfony2', 'table', 'cakephp', 'firefox', 'ms-access', 'java-ee', 'jquery-mobile', 'python-2.7', 'tomcat', 'zend-framework', 'opencv', 'visual-c++', 'opengl', 'spring-mvc', 'sql-server-2005', 'authentication', 'search', 'xslt', 'servlets', 'pdf', 'animation', 'math', 'batch-file', 'excel-vba', 'iis', 'mod-rewrite', 'sharepoint', 'gwt', 'powershell', 'visual-studio-2012', 'haskell', 'grails', 'ubuntu', 'networking', 'nhibernate', 'design-patterns', 'testing', 'jpa', 'visual-studio-2008', 'core-data', 'user-interface', 'audio', 'backbone.js', 'gcc', 'mobile', 'design', 'activerecord', 'extjs', 'video', 'stored-procedures', 'optimization', 'drupal', 'image-processing', 'android-intent', 'logging', 'web-applications', 'razor', 'database-design', 'azure', 'vim', 'memory-management', 'model-view-controller', 'cordova', 'c++11', 'selenium', 'ssl', 'assembly', 'soap', 'boost', 'canvas', 'google-maps-api-3', 'netbeans', 'heroku', 'jsf-2', 'encryption', 'hadoop', 'linq-to-sql', 'dll', 'xpath', 'data-binding', 'windows-phone-8', 'phonegap', 'jdbc', 'python-3.x', 'twitter', 'mvvm', 'gui', 'web', 'jquery-plugins', 'numpy', 'deployment', 'ios7', 'emacs', 'knockout.js', 'graphics', 'joomla', 'unicode', 'windows-8', 'android-fragments', 'ant', 'command-line', 'version-control', 'yii', 'github', 'amazon-web-services', 'macros', 'ember.js', 'svg', 'opengl-es', 'django-models', 'solr', 'orm', 'blackberry', 'windows-7', 'ruby-on-rails-4', 'compiler', 'tcp', 'pdo', 'architecture', 'groovy', 'nginx', 'concurrency', 'paypal', 'iis-7', 'express', 'vbscript', 'google-chrome-extension', 'memory-leaks', 'rspec', 'actionscript', 'interface', 'fonts', 'oauth', 'ssh', 'tfs', 'junit', 'struts2', 'd3.js', 'coldfusion', '.net-4.0', 'jqgrid', 'asp-classic', 'https', 'plsql', 'stl', 'sharepoint-2010', 'asp.net-web-api', 'mysqli', 'sed', 'awk', 'internet-explorer-8', 'jboss', 'charts', 'scripting', 'matplotlib', 'laravel', 'clojure', 'entity-framework-4', 'intellij-idea', 'xml-parsing', 'sqlite3', '3d', 'io', 'mfc', 'devise', 'playframework', 'youtube', 'amazon-ec2', 'localization', 'cuda', 'jenkins', 'ssis', 'safari', 'doctrine2', 'vb6', 'amazon-s3', 'dojo', 'air', 'eclipse-plugin', 'android-asynctask', 'crystal-reports', 'cocos2d-iphone', 'dns', 'highcharts', 'ruby-on-rails-3.2', 'ado.net', 'sql-server-2008-r2', 'android-emulator', 'spring-security', 'cross-browser', 'oracle11g', 'bluetooth', 'f#', 'msbuild', 'drupal-7', 'google-apps-script', 'mercurial', 'xna', 'google-analytics', 'lua', 'parallel-processing', 'internationalization', 'java-me', 'mono', 'monotouch', 'android-ndk', 'lucene', 'kendo-ui', 'linux-kernel', 'terminal', 'phpmyadmin', 'makefile', 'ffmpeg', 'applet', 'active-directory', 'coffeescript', 'pandas', 'responsive-design', 'xhtml', 'silverlight-4.0', '.net-3.5', 'jaxb', 'ruby-on-rails-3.1', 'gps', 'geolocation', 'network-programming', 'windows-services', 'laravel-4', 'ggplot2', 'rss', 'webkit', 'functional-programming', 'wsdl', 'telerik', 'maven-2', 'cron', 'mapreduce', 'websocket', 'automation', 'windows-runtime', 'django-forms', 'tkinter', 'android-widget', 'android-activity', 'rubygems', 'content-management-system', 'doctrine', 'django-templates', 'gem', 'fluent-nhibernate', 'seo', 'meteor', 'serial-port', 'glassfish', 'documentation', 'cryptography', 'ef-code-first', 'extjs4', 'x86', 'wordpress-plugin', 'go', 'wix', 'linq-to-entities', 'oracle10g', 'cocos2d', 'selenium-webdriver', 'open-source', 'jtable', 'qt4', 'smtp', 'redis', 'jvm', 'openssl', 'timezone', 'nosql', 'erlang', 'playframework-2.0', 'machine-learning', 'mocking', 'unity3d', 'thread-safety', 'android-actionbar', 'jni', 'udp', 'jasper-reports', 'zend-framework2', 'apache2', 'internet-explorer-7', 'sqlalchemy', 'neo4j', 'ldap', 'jframe', 'youtube-api', 'filesystems', 'make', 'flask', 'gdb', 'cassandra', 'sms', 'g++', 'django-admin', 'push-notification', 'statistics', 'tinymce', 'locking', 'javafx', 'firefox-addon', 'fancybox', 'windows-phone', 'log4j', 'uikit', 'prolog', 'socket.io', 'icons', 'oauth-2.0', 'refactoring', 'sencha-touch', 'elasticsearch', 'symfony1', 'google-api', 'webserver', 'wpf-controls', 'microsoft-metro', 'gtk', 'flex4', 'three.js', 'gradle', 'centos', 'angularjs-directive', 'internet-explorer-9', 'sass', 'html5-canvas', 'interface-builder', 'programming-languages', 'gmail', 'jersey', 'twitter-bootstrap-3', 'arduino', 'requirejs', 'cmake', 'web-development', 'software-engineering', 'startups', 'entrepreneurship', 'social-media-marketing', 'writing', 'marketing', 'web-design', 'graphic-design', 'game-development', 'game-design', 'photoshop', 'illustrator', 'robotics', 'aws', 'devops', 'mathematica', 'bioinformatics', 'data-vis', 'ui', 'embedded-systems', 'codecombat'] + +commonLinkNames = ['GitHub', 'Facebook', 'Twitter', 'G+', 'LinkedIn', 'Personal Website', 'Blog'] + +countries = ['Afghanistan', 'Albania', 'Algeria', 'American Samoa', 'Andorra', 'Angola', 'Anguilla', 'Antarctica', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Aruba', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bermuda', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei Darussalam', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Cayman Islands', 'Central African Republic', 'Chad', 'Chile', 'China', 'Christmas Island', 'Cocos (Keeling) Islands', 'Colombia', 'Comoros', 'Democratic Republic of the Congo (Kinshasa)', 'Congo, Republic of (Brazzaville)', 'Cook Islands', 'Costa Rica', 'Ivory Coast', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Ethiopia', 'Falkland Islands', 'Faroe Islands', 'Fiji', 'Finland', 'France', 'French Guiana', 'French Polynesia', 'French Southern Territories', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Gibraltar', 'Great Britain', 'Greece', 'Greenland', 'Grenada', 'Guadeloupe', 'Guam', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hong Kong', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'North Korea', 'South Korea', 'Kosovo', 'Kuwait', 'Kyrgyzstan', 'Lao, People\'s Democratic Republic', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macau', 'Macedonia, Rep. of', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Martinique', 'Mauritania', 'Mauritius', 'Mayotte', 'Mexico', 'Micronesia, Federal States of', 'Moldova, Republic of', 'Monaco', 'Mongolia', 'Montenegro', 'Montserrat', 'Morocco', 'Mozambique', 'Myanmar, Burma', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'Netherlands Antilles', 'New Caledonia', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'Niue', 'Northern Mariana Islands', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestinian territories', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Pitcairn Island', 'Poland', 'Portugal', 'Puerto Rico', 'Qatar', 'Reunion Island', 'Romania', 'Russian Federation', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Swaziland', 'Sweden', 'Switzerland', 'Syria, Syrian Arab Republic', 'Taiwan', 'Tajikistan', 'Tanzania; officially the United Republic of Tanzania', 'Thailand', 'Tibet', 'Timor-Leste', 'Togo', 'Tokelau', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Turks and Caicos Islands', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'USA', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Vatican City State', 'Venezuela', 'Vietnam', 'Virgin Islands (British)', 'Virgin Islands (U.S.)', 'Wallis and Futuna Islands', 'Western Sahara', 'Yemen', 'Zambia', 'Zimbabwe'] + +commonCities = ['Tokyo', 'Jakarta', 'Seoul', 'Delhi', 'Shanghai', 'Manila', 'Karachi', 'New York', 'Sao Paulo', 'Mexico City', 'Cairo', 'Beijing', 'Osaka', 'Mumbai (Bombay)', 'Guangzhou', 'Moscow', 'Los Angeles', 'Calcutta', 'Dhaka', 'Buenos Aires', 'Istanbul', 'Rio de Janeiro', 'Shenzhen', 'Lagos', 'Paris', 'Nagoya', 'Lima', 'Chicago', 'Kinshasa', 'Tianjin', 'Chennai', 'Bogota', 'Bengaluru', 'London', 'Taipei', 'Ho Chi Minh City (Saigon)', 'Dongguan', 'Hyderabad', 'Chengdu', 'Lahore', 'Johannesburg', 'Tehran', 'Essen', 'Bangkok', 'Hong Kong', 'Wuhan', 'Ahmedabad', 'Chongqung', 'Baghdad', 'Hangzhou', 'Toronto', 'Kuala Lumpur', 'Santiago', 'Dallas-Fort Worth', 'Quanzhou', 'Miami', 'Shenyang', 'Belo Horizonte', 'Philadelphia', 'Nanjing', 'Madrid', 'Houston', 'Xi\'an-Xianyang', 'Milan', 'Luanda', 'Pune', 'Singapore', 'Riyadh', 'Khartoum', 'Saint Petersburg', 'Atlanta', 'Surat', 'Washington', 'Bandung', 'Surabaya', 'Yangoon', 'Alexandria', 'Guadalajara', 'Harbin', 'Boston', 'Zhengzhou', 'Qingdao', 'Abidjan', 'Barcelona', 'Monterrey', 'Ankara', 'Suzhou', 'Phoenix-Mesa', 'Salvador', 'Porto Alegre', 'Rome', 'Accra', 'Sydney', 'Recife', 'Naples', 'Detroit', 'Dalian', 'Fuzhou', 'Medellin', 'San Francisco', 'Silicon Valley', 'Portland', 'Seattle', 'Austin', 'Denver', 'Boulder'] + +autoFocus = true # Not working right now, possibly a Treema bower thing. + +class SkillTagNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonSkills, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl + +class LinkNameNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonLinkNames, minLength: 0, delay: 0, autoFocus: autoFocus) + valEl + +class CityNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonCities, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl + +class CountryNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: countries, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index f61f7f1e2..49249f317 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -1,36 +1,90 @@ View = require 'views/kinds/RootView' template = require 'templates/account/profile' User = require 'models/User' +JobProfileContactView = require 'views/modal/job_profile_contact_modal' module.exports = class ProfileView extends View id: "profile-view" template: template - loadingProfile: true + + events: + 'click #toggle-job-profile-approved': 'toggleJobProfileApproved' + 'click save-notes-button': 'onJobProfileNotesChanged' + 'click #contact-candidate': 'onContactCandidate' + 'click #enter-espionage-mode': 'enterEspionageMode' constructor: (options, @userID) -> + @onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000 super options - @user = User.getByID(@userID) - @loadingProfile = false if 'gravatarProfile' of @user - @listenTo(@user, 'change', @userChanged) - @listenTo(@user, 'error', @userError) - - userChanged: (user) -> - @loadingProfile = false if 'gravatarProfile' of user - @render() - - userError: (user) -> - @loadingProfile = false - @render() + if @userID is me.id + @user = me + else if me.isAdmin() or "employer" in me.get('permissions') + @user = User.getByID(@userID) + @user.fetch() + @listenTo @user, "sync", => + @render() getRenderData: -> context = super() - grav = @user.gravatarProfile - grav = grav.entry[0] if grav - addedContext = - user: @user - loadingProfile: @loadingProfile - myProfile: @user.id is context.me.id - grav: grav - photoURL: @user.getPhotoURL() - context[key] = addedContext[key] for key of addedContext + context.user = @user + context.allowedToViewJobProfile = me.isAdmin() or "employer" in me.get('permissions') + context.myProfile = @user.id is context.me.id + context.marked = marked + context.moment = moment + context.iconForLink = @iconForLink + if links = @user.get('jobProfile')?.links + links = ($.extend(true, {}, link) for link in links) + link.icon = @iconForLink link for link in links + context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first context + + afterRender: -> + super() + @updateProfileApproval() if me.isAdmin() + unless @user.get('jobProfile')?.projects?.length + @$el.find('.right-column').hide() + @$el.find('.middle-column').addClass('double-column') + + updateProfileApproval: -> + approved = @user.get 'jobProfileApproved' + @$el.find('.approved').toggle Boolean(approved) + @$el.find('.not-approved').toggle not approved + + toggleJobProfileApproved: -> + approved = not @user.get 'jobProfileApproved' + @user.set 'jobProfileApproved', approved + @user.save() + @updateProfileApproval() + + enterEspionageMode: -> + postData = emailLower: @user.get('email').toLowerCase(), usernameLower: @user.get('name').toLowerCase() + $.ajax + type: "POST", + url: "/auth/spy" + data: postData + success: @espionageSuccess + + espionageSuccess: (model) -> + window.location.reload() + + onJobProfileNotesChanged: (e) => + notes = @$el.find("#job-profile-notes").val() + @user.set 'jobProfileNotes', notes + @user.save() + + iconForLink: (link) -> + icons = [ + {icon: 'facebook', name: 'Facebook', domain: /facebook\.com/, match: /facebook/i} + {icon: 'twitter', name: 'Twitter', domain: /twitter\.com/, match: /twitter/i} + {icon: 'github', name: 'GitHub', domain: /github\.(com|io)/, match: /github/i} + {icon: 'gplus', name: 'Google Plus', domain: /plus\.google\.com/, match: /(google|^g).?(\+|plus)/i} + {icon: 'linkedin', name: 'LinkedIn', domain: /linkedin\.com/, match: /(google|^g).?(\+|plus)/i} + ] + for icon in icons + if (link.name.search(icon.match) isnt -1) or (link.link.search(icon.domain) isnt -1) + icon.url = "/images/pages/account/profile/icon_#{icon.icon}.png" + return icon + null + + onContactCandidate: (e) -> + @openModalView new JobProfileContactView recipientID: @user.id diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index df815b3cb..1aae7bc6a 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -3,8 +3,10 @@ template = require 'templates/account/settings' {me} = require('lib/auth') forms = require('lib/forms') User = require('models/User') +AuthModalView = require 'views/modal/auth_modal' WizardSettingsView = require './wizard_settings_view' +JobProfileView = require './job_profile_view' module.exports = class SettingsView extends View id: 'account-settings-view' @@ -19,18 +21,7 @@ module.exports = class SettingsView extends View @save = _.debounce(@save, 200) super options return unless me - @listenTo(me, 'change', @refreshPicturePane) # depends on gravatar load @listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError)) - window.f = @getSubscriptions - - refreshPicturePane: -> - h = $(@template(@getRenderData())) - newPane = $('#picture-pane', h) - oldPane = $('#picture-pane') - active = oldPane.hasClass('active') - oldPane.replaceWith(newPane) - newPane.i18n() - newPane.addClass('active') if active afterRender: -> super() @@ -45,9 +36,20 @@ module.exports = class SettingsView extends View ) @chooseTab(location.hash.replace('#','')) - WizardSettingsView = new WizardSettingsView() - @listenTo(WizardSettingsView, 'change', @save) - @insertSubView WizardSettingsView + + wizardSettingsView = new WizardSettingsView() + @listenTo wizardSettingsView, 'change', @save + @insertSubView wizardSettingsView + + @jobProfileView = new JobProfileView() + @listenTo @jobProfileView, 'change', @save + @insertSubView @jobProfileView + _.defer => @buildPictureTreema() # Not sure why, but the Treemas don't fully build without this if you reload the page. + + afterInsert: -> + super() + if me.get('anonymous') + @openModalView new AuthModalView() chooseTab: (category) -> id = "##{category}-pane" @@ -62,26 +64,45 @@ module.exports = class SettingsView extends View getRenderData: -> c = super() return c unless me - c.gravatarName = c.me?.gravatarName() - c.photos = me.gravatarPhotoURLs() - c.chosenPhoto = me.getPhotoURL() c.subs = {} - c.subs[sub] = 1 for sub in c.me.get('emailSubscriptions') or ['announcement', 'notification', 'tester', 'level_creator', 'developer'] + c.subs[sub] = 1 for sub in c.me.getEnabledEmails() + c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1 c getSubscriptions: -> - inputs = $('#email-pane input[type="checkbox"]', @$el) - inputs = ($(i) for i in inputs) - subs = (i.attr('name') for i in inputs when i.prop('checked')) - subs = (s.replace('email_', '') for s in subs) - subs + inputs = ($(i) for i in $('#email-pane input[type="checkbox"].changed', @$el)) + emailNames = (i.attr('name').replace('email_', '') for i in inputs) + enableds = (i.prop('checked') for i in inputs) + _.zipObject emailNames, enableds toggleEmailSubscriptions: => subs = @getSubscriptions() - $('#email-pane input[type="checkbox"]', @$el).prop('checked', not Boolean(subs.length)) + $('#email-pane input[type="checkbox"]', @$el).prop('checked', not _.any(_.values(subs))).addClass('changed') @save() - save: -> + buildPictureTreema: -> + data = photoURL: me.get('photoURL') + data.photoURL = null if data.photoURL?.search('gravatar') isnt -1 # Old style + schema = $.extend true, {}, me.schema() + schema.properties = _.pick me.schema().properties, 'photoURL' + schema.required = ['photoURL'] + treemaOptions = + filePath: "db/user/#{me.id}" + schema: schema + data: data + callbacks: {change: @onPictureChanged} + + @pictureTreema = @$el.find('#picture-treema').treema treemaOptions + @pictureTreema?.build() + @pictureTreema?.open() + @$el.find('.gravatar-fallback').toggle not me.get 'photoURL' + + onPictureChanged: (e) => + @trigger 'change' + @$el.find('.gravatar-fallback').toggle not me.get 'photoURL' + + save: (e) -> + $(e.target).addClass('changed') if e forms.clearFormAlerts(@$el) @grabData() res = me.validate() @@ -94,14 +115,14 @@ module.exports = class SettingsView extends View res = me.save() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) - .addClass('btn-info').show().removeClass('btn-danger') + .removeClass('btn-danger').addClass('btn-success').show() res.error -> errors = JSON.parse(res.responseText) forms.applyErrorsToForm(@$el, errors) - save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-info').addClass('btn-danger') + save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-success').addClass('btn-danger', 500) res.success (model, response, options) -> - save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-info') + save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-success', 500) grabData: -> @grabPasswordData() @@ -120,12 +141,23 @@ module.exports = class SettingsView extends View me.set('password', password1) grabOtherData: -> - me.set('name', $('#name', @$el).val()) - me.set('email', $('#email', @$el).val()) - me.set('emailSubscriptions', @getSubscriptions()) + me.set 'name', $('#name', @$el).val() + me.set 'email', $('#email', @$el).val() + for emailName, enabled of @getSubscriptions() + me.setEmailSubscription emailName, enabled + me.set 'photoURL', @pictureTreema.get('/photoURL') adminCheckbox = @$el.find('#admin') if adminCheckbox.length permissions = [] permissions.push 'admin' if adminCheckbox.prop('checked') me.set('permissions', permissions) + + jobProfile = me.get('jobProfile') ? {} + updated = false + for key, val of @jobProfileView.getData() + updated = updated or jobProfile[key] isnt val + jobProfile[key] = val + if updated + jobProfile.updated = (new Date()).toISOString() + me.set 'jobProfile', jobProfile diff --git a/app/views/admin/base_view.coffee b/app/views/admin/base_view.coffee index 7fe8c09c1..5db653086 100644 --- a/app/views/admin/base_view.coffee +++ b/app/views/admin/base_view.coffee @@ -1,6 +1,6 @@ -View = require 'views/kinds/RootView' +RootView = require 'views/kinds/RootView' template = require 'templates/base' -module.exports = class BaseView extends View +module.exports = class BaseView extends RootView id: "base-view" template: template diff --git a/app/views/admin/level_sessions_view.coffee b/app/views/admin/level_sessions_view.coffee index e66fefc2f..c00fcc2fe 100644 --- a/app/views/admin/level_sessions_view.coffee +++ b/app/views/admin/level_sessions_view.coffee @@ -16,12 +16,12 @@ module.exports = class LevelSessionsView extends View @getLevelSessions() getLevelSessions: -> - @sessions = new LevelSessionCollection + @sessions = new LevelSessionCollection() @sessions.fetch() - @listenTo(@sessions, 'all', @render) + @listenToOnce @sessions, 'all', @render getRenderData: => c = super() c.sessions = @sessions.models c.moment = moment - c \ No newline at end of file + c diff --git a/app/views/admin/users_view.coffee b/app/views/admin/users_view.coffee index c19c7bd37..acc9a8152 100644 --- a/app/views/admin/users_view.coffee +++ b/app/views/admin/users_view.coffee @@ -38,8 +38,7 @@ module.exports = class UsersView extends View @users.fetch() @listenTo(@users, 'all', @render) - getRenderData: => + getRenderData: -> c = super() c.users = (user.attributes for user in @users.models) - console.log('our render data', c) c \ No newline at end of file diff --git a/app/views/admin_view.coffee b/app/views/admin_view.coffee index cd06c0999..8c93ff616 100644 --- a/app/views/admin_view.coffee +++ b/app/views/admin_view.coffee @@ -1,36 +1,31 @@ {backboneFailure, genericFailure} = require 'lib/errors' View = require 'views/kinds/RootView' template = require 'templates/admin' -storage = require 'lib/storage' module.exports = class AdminView extends View id: "admin-view" template: template - + events: 'click #enter-espionage-mode': 'enterEspionageMode' - + enterEspionageMode: -> userEmail = $("#user-email").val().toLowerCase() username = $("#user-username").val().toLowerCase() - - userIdentifier = userEmail || username + postData = usernameLower: username emailLower: userEmail - + $.ajax type: "POST", url: "/auth/spy" data: postData success: @espionageSuccess error: @espionageFailure - + espionageSuccess: (model) -> - storage.save('whoami',model) window.location.reload() - + espionageFailure: (jqxhr, status,error)-> console.log "There was an error entering espionage mode: #{error}" - - diff --git a/app/views/community_view.coffee b/app/views/community_view.coffee new file mode 100644 index 000000000..e31b09f33 --- /dev/null +++ b/app/views/community_view.coffee @@ -0,0 +1,6 @@ +View = require 'views/kinds/RootView' +template = require 'templates/community' + +module.exports = class CommunityView extends View + id: "community-view" + template: template diff --git a/app/views/contribute/adventurer_view.coffee b/app/views/contribute/adventurer_view.coffee index cdb711e98..9428a8624 100644 --- a/app/views/contribute/adventurer_view.coffee +++ b/app/views/contribute/adventurer_view.coffee @@ -4,4 +4,5 @@ template = require 'templates/contribute/adventurer' module.exports = class AdventurerView extends ContributeClassView id: "adventurer-view" - template: template \ No newline at end of file + template: template + contributorClassName: 'adventurer' diff --git a/app/views/contribute/ambassador_view.coffee b/app/views/contribute/ambassador_view.coffee index 669951176..6ea7a68cd 100644 --- a/app/views/contribute/ambassador_view.coffee +++ b/app/views/contribute/ambassador_view.coffee @@ -5,3 +5,4 @@ template = require 'templates/contribute/ambassador' module.exports = class AmbassadorView extends ContributeClassView id: "ambassador-view" template: template + contributorClassName: 'ambassador' diff --git a/app/views/contribute/archmage_view.coffee b/app/views/contribute/archmage_view.coffee index ec18303f8..7ac7508ca 100644 --- a/app/views/contribute/archmage_view.coffee +++ b/app/views/contribute/archmage_view.coffee @@ -4,28 +4,30 @@ template = require 'templates/contribute/archmage' module.exports = class ArchmageView extends ContributeClassView id: "archmage-view" template: template + contributorClassName: 'archmage' contributors: [ - {name: "Tom Steinbrecher", avatar: "tom"} - {name: "Sébastien Moratinos", avatar: "sebastien"} - {name: "deepak1556", avatar: "deepak"} - {name: "Ronnie Cheng", avatar: "ronald"} - {name: "Chloe Fan", avatar: "chloe"} - {name: "Rachel Xiang", avatar: "rachel"} - {name: "Dan Ristic", avatar: "dan"} - {name: "Brad Dickason", avatar: "brad"} + {id: "52bfc3ecb7ec628868001297", name: "Tom Steinbrecher", github: "TomSteinbrecher"} + {id: "5272806093680c5817033f73", name: "Sébastien Moratinos", github: "smoratinos"} + {name: "deepak1556", avatar: "deepak", github: "deepak1556"} + {name: "Ronnie Cheng", avatar: "ronald", github: "rhc2104"} + {name: "Chloe Fan", avatar: "chloe", github: "chloester"} + {name: "Rachel Xiang", avatar: "rachel", github: "rdxiang"} + {name: "Dan Ristic", avatar: "dan", github: "dristic"} + {name: "Brad Dickason", avatar: "brad", github: "bdickason"} {name: "Rebecca Saines", avatar: "becca"} - {name: "Laura Watiker", avatar: "laura"} - {name: "Shiying Zheng", avatar: "shiying"} - {name: "Mischa Lewis-Norelle", avatar: "mischa"} + {name: "Laura Watiker", avatar: "laura", github: "lwatiker"} + {name: "Shiying Zheng", avatar: "shiying", github: "shiyingzheng"} + {name: "Mischa Lewis-Norelle", avatar: "mischa", github: "mlewisno"} {name: "Paul Buser", avatar: "paul"} {name: "Benjamin Stern", avatar: "ben"} {name: "Alex Cotsarelis", avatar: "alex"} {name: "Ken Stanley", avatar: "ken"} - {name: "devast8a", avatar: ""} - {name: "phansch", avatar: ""} - {name: "Zach Martin", avatar: ""} + {name: "devast8a", avatar: "", github: "devast8a"} + {name: "phansch", avatar: "", github: "phansch"} + {name: "Zach Martin", avatar: "", github: "zachster01"} {name: "David Golds", avatar: ""} - {name: "gabceb", avatar: ""} - {name: "MDP66", avatar: ""} + {name: "gabceb", avatar: "", github: "gabceb"} + {name: "MDP66", avatar: "", github: "MDP66"} + {name: "Alexandru Caciulescu", avatar: "", github: "Darredevil"} ] diff --git a/app/views/contribute/artisan_view.coffee b/app/views/contribute/artisan_view.coffee index b7d184d57..dbad64902 100644 --- a/app/views/contribute/artisan_view.coffee +++ b/app/views/contribute/artisan_view.coffee @@ -5,10 +5,11 @@ template = require 'templates/contribute/artisan' module.exports = class ArtisanView extends ContributeClassView id: "artisan-view" template: template + contributorClassName: 'artisan' contributors: [ {name: "Sootn", avatar: ""} - {name: "Zach Martin", avatar: ""} + {name: "Zach Martin", avatar: "", github: "zachster01"} {name: "Aftermath", avatar: ""} {name: "mcdavid1991", avatar: ""} {name: "dwhittaker", avatar: ""} @@ -19,6 +20,6 @@ module.exports = class ArtisanView extends ContributeClassView {name: "Axandre Oge", avatar: "axandre"} {name: "Katharine Chan", avatar: "katharine"} {name: "Derek Wong", avatar: "derek"} - {name: "Alexandru Caciulescu", avatar: ""} - {name: "Prabh Simran Singh Baweja", avatar: ""} + {name: "Alexandru Caciulescu", avatar: "", github: "Darredevil"} + {name: "Prabh Simran Singh Baweja", avatar: "", github: "prabh27"} ] diff --git a/app/views/contribute/contribute_class_view.coffee b/app/views/contribute/contribute_class_view.coffee index 5442ff719..d9110c3fd 100644 --- a/app/views/contribute/contribute_class_view.coffee +++ b/app/views/contribute/contribute_class_view.coffee @@ -1,6 +1,9 @@ SignupModalView = require 'views/modal/signup_modal' View = require 'views/kinds/RootView' {me} = require('lib/auth') +contributorSignupAnonymousTemplate = require 'templates/contribute/contributor_signup_anonymous' +contributorSignupTemplate = require 'templates/contribute/contributor_signup' +contributorListTemplate = require 'templates/contribute/contributor_list' module.exports = class ContributeClassView extends View navPrefix: '/contribute' @@ -16,26 +19,25 @@ module.exports = class ContributeClassView extends View afterRender: -> super() + @$el.find('.contributor-signup-anonymous').replaceWith(contributorSignupAnonymousTemplate(me: me)) + @$el.find('.contributor-signup').each -> + context = me: me, contributorClassName: $(@).data('contributor-class-name') + $(@).replaceWith(contributorSignupTemplate(context)) + @$el.find('#contributor-list').replaceWith(contributorListTemplate(contributors: @contributors, contributorClassName: @contributorClassName)) + checkboxes = @$el.find('input[type="checkbox"]').toArray() _.forEach checkboxes, (el) -> el = $(el) - if el.attr('name') in me.get('emailSubscriptions') - el.prop('checked', true) + el.prop('checked', true) if me.isEmailSubscriptionEnabled(el.attr('name')+'News') onCheckboxChanged: (e) -> el = $(e.target) checked = el.prop('checked') subscription = el.attr('name') - subscriptions = me.get('emailSubscriptions') ? [] - if checked and not (subscription in subscriptions) - subscriptions.push(subscription) - if me.get 'anonymous' - @openModalView new SignupModalView() - if not checked - subscriptions = _.without subscriptions, subscription + + me.setEmailSubscription subscription+'News', checked + me.save() + @openModalView new SignupModalView() if me.get 'anonymous' el.parent().find('.saved-notification').finish().show('fast').delay(3000).fadeOut(2000) - me.set('emailSubscriptions', subscriptions) - me.save() - contributors: [] diff --git a/app/views/contribute/counselor_view.coffee b/app/views/contribute/counselor_view.coffee index b589c4ed9..6e5a6be2c 100644 --- a/app/views/contribute/counselor_view.coffee +++ b/app/views/contribute/counselor_view.coffee @@ -5,3 +5,4 @@ template = require 'templates/contribute/counselor' module.exports = class CounselorView extends ContributeClassView id: "counselor-view" template: template + contributorClassName: 'counselor' diff --git a/app/views/contribute/diplomat_view.coffee b/app/views/contribute/diplomat_view.coffee index 6b159bfed..0769af517 100644 --- a/app/views/contribute/diplomat_view.coffee +++ b/app/views/contribute/diplomat_view.coffee @@ -5,3 +5,4 @@ template = require 'templates/contribute/diplomat' module.exports = class DiplomatView extends ContributeClassView id: "diplomat-view" template: template + contributorClassName: 'diplomat' diff --git a/app/views/contribute/scribe_view.coffee b/app/views/contribute/scribe_view.coffee index 2aedbbb98..7f87a3275 100644 --- a/app/views/contribute/scribe_view.coffee +++ b/app/views/contribute/scribe_view.coffee @@ -5,3 +5,14 @@ template = require 'templates/contribute/scribe' module.exports = class ScribeView extends ContributeClassView id: "scribe-view" template: template + contributorClassName: 'scribe' + + contributors: [ + {name: "Ryan Faidley"} + {name: "Mischa Lewis-Norelle", github: "mlewisno"} + {name: "Tavio"} + {name: "Ronnie Cheng", github: "rhc2104"} + {name: "engstrom"} + {name: "Dman19993"} + {name: "mattinsler"} + ] diff --git a/app/views/contribute_view.coffee b/app/views/contribute_view.coffee index aefef4aca..12a35c71f 100644 --- a/app/views/contribute_view.coffee +++ b/app/views/contribute_view.coffee @@ -1,6 +1,5 @@ ContributeClassView = require 'views/contribute/contribute_class_view' template = require 'templates/contribute/contribute' -SignupModalView = require 'views/modal/signup_modal' module.exports = class ContributeView extends ContributeClassView id: "contribute-view" diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index 875dc6113..c165c9701 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -3,6 +3,7 @@ VersionHistoryView = require './versions_view' ErrorView = require '../../error_view' template = require 'templates/editor/article/edit' Article = require 'models/Article' +SaveVersionModal = require 'views/modal/save_version_modal' module.exports = class ArticleEditView extends View id: "editor-article-edit-view" @@ -12,6 +13,7 @@ module.exports = class ArticleEditView extends View events: 'click #preview-button': 'openPreview' 'click #history-button': 'showVersionHistory' + 'click #save-button': 'openSaveModal' subscriptions: 'save-new-version': 'saveNewArticle' @@ -33,17 +35,11 @@ module.exports = class ArticleEditView extends View ) @article.fetch() - @article.loadSchema() - @listenToOnce(@article, 'sync', @onArticleSync) - @listenToOnce(@article, 'schema-loaded', @buildTreema) + @listenToOnce(@article, 'sync', @buildTreema) @pushChangesToPreview = _.throttle(@pushChangesToPreview, 500) - onArticleSync: -> - @article.loaded = true - @buildTreema() - buildTreema: -> - return if @treema? or (not @article.loaded) or (not Article.hasSchema()) + return if @treema? or (not @article.loaded) unless @article.attributes.body @article.set('body', '') @startsLoading = false @@ -52,8 +48,8 @@ module.exports = class ArticleEditView extends View options = data: data filePath: "db/thang.type/#{@article.get('original')}" - schema: Article.schema.attributes - readOnly: true unless me.isAdmin() or @article.hasWriteAccess(me) + schema: Article.schema + readOnly: me.get('anonymous') callbacks: change: @pushChangesToPreview @treema = @$el.find('#article-treema').treema(options) @@ -78,13 +74,16 @@ module.exports = class ArticleEditView extends View afterRender: -> super() return if @startsLoading - @showReadOnly() unless me.isAdmin() or @article.hasWriteAccess(me) + @showReadOnly() if me.get('anonymous') - openPreview: => + openPreview: -> @preview = window.open('/editor/article/x/preview', 'preview', 'height=800,width=600') @preview.focus() if window.focus @preview.onload = => @pushChangesToPreview() return false + + openSaveModal: -> + @openModalView(new SaveVersionModal({model: @article})) saveNewArticle: (e) -> @treema.endExistingEdits() diff --git a/app/views/editor/components/config.coffee b/app/views/editor/components/config.coffee index 016cb41a0..7b605a5a1 100644 --- a/app/views/editor/components/config.coffee +++ b/app/views/editor/components/config.coffee @@ -63,6 +63,7 @@ module.exports = class ComponentConfigView extends CocoView @editThangTreema = @$el.find('.treema').treema treemaOptions @editThangTreema.build() @editThangTreema.open(2) + @hideLoading() onConfigEdited: => @changed = true diff --git a/app/views/editor/components/main.coffee b/app/views/editor/components/main.coffee index 7b813595b..e8040f231 100644 --- a/app/views/editor/components/main.coffee +++ b/app/views/editor/components/main.coffee @@ -17,35 +17,19 @@ module.exports = class ThangComponentEditView extends CocoView @world = options.world @level = options.level @callback = options.callback - - render: => - return if @destroyed - for model in [Level, LevelComponent] - temp = new model() - @listenToOnce temp, 'schema-loaded', @render unless model.schema?.loaded - if not @componentCollection - @componentCollection = @supermodel.getCollection new ComponentsCollection() - unless @componentCollection.loaded - @listenToOnce(@componentCollection, 'sync', @onComponentsSync) - @componentCollection.fetch() - super() # do afterRender at the end - + @componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').model + afterRender: -> super() - return @showLoading() unless @componentCollection?.loaded and Level.schema.loaded and LevelComponent.schema.loaded - @hideLoading() + return unless @supermodel.finished() @buildExtantComponentTreema() @buildAddComponentTreema() - onComponentsSync: -> - return if @destroyed - @supermodel.addCollection @componentCollection - @render() - buildExtantComponentTreema: -> + level = new Level() treemaOptions = supermodel: @supermodel - schema: Level.schema.get('properties').thangs.items.properties.components + schema: level.schema().properties.thangs.items.properties.components data: _.cloneDeep @components callbacks: {select: @onSelectExtantComponent, change:@onChangeExtantComponents} noSortable: true @@ -69,7 +53,7 @@ module.exports = class ThangComponentEditView extends CocoView treemaOptions = supermodel: @supermodel - schema: { type: 'array', items: LevelComponent.schema.attributes } + schema: { type: 'array', items: LevelComponent.schema } data: ($.extend(true, {}, c) for c in components) callbacks: {select: @onSelectAddableComponent, enter: @onAddComponentEnterPressed } readOnly: true @@ -81,7 +65,8 @@ module.exports = class ThangComponentEditView extends CocoView _.defer (=> @addComponentsTreema = @$el.find('#add-component-column .treema').treema treemaOptions @addComponentsTreema.build() - ), 100 + @hideLoading() + ), 500 onSelectAddableComponent: (e, selected) => @extantComponentsTreema.deselectAll() @@ -159,17 +144,18 @@ module.exports = class ThangComponentEditView extends CocoView @reportChanges() onAddComponentEnterPressed: (node) => - extantSystems = - (@supermodel.getModelByOriginalAndMajorVersion LevelSystem, sn.original, sn.majorVersion).attributes.name.toLowerCase() for idx, sn of @level.get('systems') - requireSystem = node.data.system.toLowerCase() + if extantSystems + extantSystems = + (@supermodel.getModelByOriginalAndMajorVersion LevelSystem, sn.original, sn.majorVersion).attributes.name.toLowerCase() for idx, sn of @level.get('systems') + requireSystem = node.data.system.toLowerCase() - if requireSystem not in extantSystems - warn_element = 'Component ' + node.data.name + ' requires system ' + requireSystem + ' which is currently not specified in this level.' - noty({ - text: warn_element, - layout: 'bottomLeft', - type: 'warning' - }) + if requireSystem not in extantSystems + warn_element = 'Component ' + node.data.name + ' requires system ' + requireSystem + ' which is currently not specified in this level.' + noty({ + text: warn_element, + layout: 'bottomLeft', + type: 'warning' + }) currentSelection = @addComponentsTreema?.getLastSelectedTreema()?.data._id diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee new file mode 100644 index 000000000..9036f5557 --- /dev/null +++ b/app/views/editor/delta.coffee @@ -0,0 +1,113 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/delta' +deltasLib = require 'lib/deltas' + +TEXTDIFF_OPTIONS = + baseTextName: "Old" + newTextName: "New" + contextSize: 5 + viewType: 1 + +module.exports = class DeltaView extends CocoView + + ### + Takes a CocoModel instance (model) and displays changes since the + last save (attributes vs _revertAttributes). + + * If headModel is included, will look for and display conflicts with the changes in model. + * If comparisonModel is included, will show deltas between model and comparisonModel instead + of changes within model itself. + + ### + + @deltaCounter: 0 + className: "delta-view" + template: template + + constructor: (options) -> + super(options) + @expandedDeltas = [] + @skipPaths = options.skipPaths + + for modelName in ['model', 'headModel', 'comparisonModel'] + continue unless m = options[modelName] + @[modelName] = @supermodel.loadModel(m, 'document').model + + @buildDeltas() if @supermodel.finished() + + onLoaded: -> + @buildDeltas() + super() + + buildDeltas: -> + if @comparisonModel + @expandedDeltas = @model.getExpandedDeltaWith(@comparisonModel) + else + @expandedDeltas = @model.getExpandedDelta() + + @filterExpandedDeltas() + + if @headModel + @headDeltas = @headModel.getExpandedDelta() + @conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas) + + filterExpandedDeltas: -> + return unless @skipPaths + for path, i in @skipPaths + @skipPaths[i] = [path] if _.isString(path) + newDeltas = [] + for delta in @expandedDeltas + skip = false + for skipPath in @skipPaths + if _.isEqual _.first(delta.dataPath, skipPath.length), skipPath + skip = true + break + newDeltas.push delta unless skip + @expandedDeltas = newDeltas + + getRenderData: -> + c = super() + c.deltas = @expandedDeltas + c.counter = DeltaView.deltaCounter + DeltaView.deltaCounter += @expandedDeltas.length + c + + afterRender: -> + deltas = @$el.find('.details') + for delta, i in deltas + deltaEl = $(delta) + deltaData = @expandedDeltas[i] + @expandDetails(deltaEl, deltaData) + + conflictDeltas = @$el.find('.conflict-details') + conflicts = (delta.conflict for delta in @expandedDeltas when delta.conflict) + for delta, i in conflictDeltas + deltaEl = $(delta) + deltaData = conflicts[i] + @expandDetails(deltaEl, deltaData) + + expandDetails: (deltaEl, deltaData) -> + treemaOptions = { schema: deltaData.schema, readOnly: true } + + if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') + options = _.defaults {data: deltaData.left}, treemaOptions + TreemaNode.make(leftEl, options).build() + + if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value') + options = _.defaults {data: deltaData.right}, treemaOptions + TreemaNode.make(rightEl, options).build() + + if deltaData.action is 'text-diff' + left = difflib.stringAsLines deltaData.left + right = difflib.stringAsLines deltaData.right + sm = new difflib.SequenceMatcher(left, right) + opcodes = sm.get_opcodes() + el = deltaEl.find('.text-diff') + options = {baseTextLines: left, newTextLines: right, opcodes: opcodes} + args = _.defaults options, TEXTDIFF_OPTIONS + el.append(diffview.buildView(args)) + + getApplicableDelta: -> + delta = @model.getDelta() + delta = deltasLib.pruneConflictsFromDelta delta, @conflicts if @conflicts + delta \ No newline at end of file diff --git a/app/views/editor/level/add_thangs_view.coffee b/app/views/editor/level/add_thangs_view.coffee index 35e8917c9..cf1824201 100644 --- a/app/views/editor/level/add_thangs_view.coffee +++ b/app/views/editor/level/add_thangs_view.coffee @@ -1,7 +1,7 @@ View = require 'views/kinds/CocoView' add_thangs_template = require 'templates/editor/level/add_thangs' ThangType = require 'models/ThangType' -CocoCollection = require 'models/CocoCollection' +CocoCollection = require 'collections/CocoCollection' class ThangTypeSearchCollection extends CocoCollection url: '/db/thang.type/search?project=true' @@ -22,13 +22,9 @@ module.exports = class AddThangsView extends View constructor: (options) -> super options @world = options.world - @thangTypes = @supermodel.getCollection new ThangTypeSearchCollection() # should load depended-on Components, too - @listenToOnce(@thangTypes, 'sync', @onThangTypesLoaded) - @thangTypes.fetch() - onThangTypesLoaded: -> - return if @destroyed - @render() # do it again but without the loading screen + # should load depended-on Components, too + @thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model getRenderData: (context={}) -> context = super(context) @@ -59,7 +55,6 @@ module.exports = class AddThangsView extends View context afterRender: -> - return if @startsLoading super() runSearch: (e) => diff --git a/app/views/editor/level/component/edit.coffee b/app/views/editor/level/component/edit.coffee index 7571e9a80..6d416e011 100644 --- a/app/views/editor/level/component/edit.coffee +++ b/app/views/editor/level/component/edit.coffee @@ -1,7 +1,9 @@ View = require 'views/kinds/CocoView' -VersionHistoryView = require 'views/editor/component/versions_view' template = require 'templates/editor/level/component/edit' LevelComponent = require 'models/LevelComponent' +VersionHistoryView = require 'views/editor/component/versions_view' +PatchesView = require 'views/editor/patches_view' +SaveVersionModal = require 'views/modal/save_version_modal' module.exports = class LevelComponentEditView extends View id: "editor-level-component-edit-view" @@ -10,8 +12,14 @@ module.exports = class LevelComponentEditView extends View events: 'click #done-editing-component-button': 'endEditing' - 'click #history-button': 'showVersionHistory' 'click .nav a': (e) -> $(e.target).tab('show') + 'click #component-patches-tab': -> @patchesView.load() + 'click #component-code-tab': 'buildCodeEditor' + 'click #component-config-schema-tab': 'buildConfigSchemaTreema' + 'click #component-settings-tab': 'buildSettingsTreema' + 'click #component-history-button': 'showVersionHistory' + 'click #patch-component-button': 'startPatchingComponent' + 'click #component-watch-button': 'toggleWatchComponent' constructor: (options) -> super options @@ -21,26 +29,30 @@ module.exports = class LevelComponentEditView extends View getRenderData: (context={}) -> context = super(context) context.editTitle = "#{@levelComponent.get('system')}.#{@levelComponent.get('name')}" + context.component = @levelComponent context + onLoaded: -> @render() afterRender: -> super() @buildSettingsTreema() @buildConfigSchemaTreema() @buildCodeEditor() + @patchesView = @insertSubView(new PatchesView(@levelComponent), @$el.find('.patches-view')) + @$el.find('#component-watch-button').find('> span').toggleClass('secret') if @levelComponent.watching() buildSettingsTreema: -> data = _.pick @levelComponent.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep LevelComponent.schema.attributes + schema = _.cloneDeep LevelComponent.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings - + treemaOptions = supermodel: @supermodel schema: schema data: data + readonly: me.get('anonymous') callbacks: {change: @onComponentSettingsEdited} - treemaOptions.readOnly = true unless me.isAdmin() @componentSettingsTreema = @$el.find('#edit-component-treema').treema treemaOptions @componentSettingsTreema.build() @componentSettingsTreema.open() @@ -55,32 +67,33 @@ module.exports = class LevelComponentEditView extends View buildConfigSchemaTreema: -> treemaOptions = supermodel: @supermodel - schema: LevelComponent.schema.get('properties').configSchema + schema: LevelComponent.schema.properties.configSchema data: @levelComponent.get 'configSchema' + readOnly: me.get('anonymous') callbacks: {change: @onConfigSchemaEdited} - treemaOptions.readOnly = true unless me.isAdmin() @configSchemaTreema = @$el.find('#config-schema-treema').treema treemaOptions @configSchemaTreema.build() @configSchemaTreema.open() # TODO: schema is not loaded for the first one here? - @configSchemaTreema.tv4.addSchema('metaschema', LevelComponent.schema.get('properties').configSchema) + @configSchemaTreema.tv4.addSchema('metaschema', LevelComponent.schema.properties.configSchema) onConfigSchemaEdited: => @levelComponent.set 'configSchema', @configSchemaTreema.data Backbone.Mediator.publish 'level-component-edited', levelComponent: @levelComponent buildCodeEditor: -> - editorEl = @$el.find '#component-code-editor' - editorEl.text @levelComponent.get('code') + @editor?.destroy() + editorEl = $('
').text(@levelComponent.get('code')).addClass('inner-editor') + @$el.find('#component-code-editor').empty().append(editorEl) @editor = ace.edit(editorEl[0]) - @editor.setReadOnly(not me.isAdmin()) + @editor.setReadOnly(me.get('anonymous')) session = @editor.getSession() session.setMode 'ace/mode/coffee' session.setTabSize 2 session.setNewLineMode = 'unix' session.setUseSoftTabs true @editor.on('change', @onEditorChange) - + onEditorChange: => @levelComponent.set 'code', @editor.getValue() Backbone.Mediator.publish 'level-component-edited', levelComponent: @levelComponent @@ -90,11 +103,20 @@ module.exports = class LevelComponentEditView extends View Backbone.Mediator.publish 'level-component-editing-ended', levelComponent: @levelComponent null + showVersionHistory: (e) -> + versionHistoryView = new VersionHistoryView {}, @levelComponent.id + @openModalView versionHistoryView + Backbone.Mediator.publish 'level:view-switched', e + + startPatchingComponent: (e) -> + @openModalView new SaveVersionModal({model:@levelComponent}) + Backbone.Mediator.publish 'level:view-switched', e + + toggleWatchComponent: -> + button = @$el.find('#component-watch-button') + @levelComponent.watch(button.find('.watch').is(':visible')) + button.find('> span').toggleClass('secret') + destroy: -> @editor?.destroy() super() - - showVersionHistory: (e) -> - versionHistoryView = new VersionHistoryView component:@levelComponent, @levelComponent.id - @openModalView versionHistoryView - Backbone.Mediator.publish 'level:view-switched', e \ No newline at end of file diff --git a/app/views/editor/level/component/new.coffee b/app/views/editor/level/component/new.coffee index ec83ec757..88b7761b8 100644 --- a/app/views/editor/level/component/new.coffee +++ b/app/views/editor/level/component/new.coffee @@ -33,6 +33,6 @@ module.exports = class LevelComponentNewView extends View console.log "Got errors:", JSON.parse(res.responseText) forms.applyErrorsToForm(@$el, JSON.parse(res.responseText)) res.success => - @supermodel.addModel component + @supermodel.registerModel component Backbone.Mediator.publish 'edit-level-component', original: component.get('_id'), majorVersion: 0 @hide() diff --git a/app/views/editor/level/components_tab_view.coffee b/app/views/editor/level/components_tab_view.coffee index 823bebed1..a0fe32f94 100644 --- a/app/views/editor/level/components_tab_view.coffee +++ b/app/views/editor/level/components_tab_view.coffee @@ -14,16 +14,17 @@ module.exports = class ComponentsTabView extends View className: 'tab-pane' subscriptions: - 'level-thangs-changed': 'onLevelThangsChanged' 'edit-level-component': 'editLevelComponent' 'level-component-edited': 'onLevelComponentEdited' 'level-component-editing-ended': 'onLevelComponentEditingEnded' events: 'click #create-new-component-button': 'createNewLevelComponent' + 'click #create-new-component-button-no-select': 'createNewLevelComponent' - onLevelThangsChanged: (e) -> - thangsData = e.thangsData + onLoaded: -> + + refreshLevelThangsTreema: (thangsData) -> presentComponents = {} for thang in thangsData for component in thang.components diff --git a/app/views/editor/level/edit.coffee b/app/views/editor/level/edit.coffee index b685d457f..c97be9459 100644 --- a/app/views/editor/level/edit.coffee +++ b/app/views/editor/level/edit.coffee @@ -4,6 +4,7 @@ Level = require 'models/Level' LevelSystem = require 'models/LevelSystem' World = require 'lib/world/world' DocumentFiles = require 'collections/DocumentFiles' +LevelLoader = require 'lib/LevelLoader' ThangsTabView = require './thangs_tab_view' SettingsTabView = require './settings_tab_view' @@ -12,62 +13,45 @@ ComponentsTabView = require './components_tab_view' SystemsTabView = require './systems_tab_view' LevelSaveView = require './save_view' LevelForkView = require './fork_view' +SaveVersionModal = require 'views/modal/save_version_modal' +PatchesView = require 'views/editor/patches_view' VersionHistoryView = require './versions_view' ErrorView = require '../../error_view' module.exports = class EditorLevelView extends View id: "editor-level-view" template: template - startsLoading: true cache: false events: 'click #play-button': 'onPlayLevel' 'click #commit-level-start-button': 'startCommittingLevel' 'click #fork-level-start-button': 'startForkingLevel' - 'click #history-button': 'showVersionHistory' - + 'click #level-history-button': 'showVersionHistory' + 'click #patches-tab': -> @patchesView.load() + 'click #components-tab': -> @componentsTab.refreshLevelThangsTreema @level.get('thangs') + 'click #level-patch-button': 'startPatchingLevel' + 'click #level-watch-button': 'toggleWatchLevel' + 'click #pop-level-i18n-button': -> @level.populateI18N() + 'mouseup .nav-tabs > li a': 'toggleTab' + constructor: (options, @levelID) -> super options - @listenToOnce(@supermodel, 'loaded-all', @onAllLoaded) - - # load only the level itself and the one it points to, but no others - # TODO: this is duplicated in views/play/level_view.coffee; need cleaner method - @supermodel.shouldPopulate = (model) -> - @levelsLoaded ?= 0 - @levelsLoaded += 1 if model.constructor.className is "Level" - return false if @levelsLoaded > 1 - return true - @supermodel.shouldSaveBackups = (model) -> model.constructor.className in ['Level', 'LevelComponent', 'LevelSystem'] - - @level = new Level _id: @levelID - @listenToOnce(@level, 'sync', @onLevelLoaded) - - @listenToOnce(@supermodel, 'error', - () => - @hideLoading() - @insertSubView(new ErrorView()) - ) - @supermodel.populateModel @level + @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, headless: true, editorMode: true + @level = @levelLoader.level + @files = new DocumentFiles(@levelLoader.level) + @supermodel.loadCollection(@files, 'file_names') showLoading: ($el) -> - $el ?= @$el.find('.tab-content') + $el ?= @$el.find('.outer-content') super($el) - onLevelLoaded: -> - @files = new DocumentFiles(@level) - @files.fetch() - - onAllLoaded: -> - @level.unset('nextLevel') if _.isString(@level.get('nextLevel')) - @initWorld() - @startsLoading = false - @render() # do it again but without the loading screen - - initWorld: -> - @world = new World @level.name + onLoaded: -> + _.defer => + @world = @levelLoader.world + @render() getRenderData: (context={}) -> context = super(context) @@ -77,18 +61,20 @@ module.exports = class EditorLevelView extends View context afterRender: -> - return if @startsLoading super() - new LevelSystem # temp; trigger the LevelSystem schema to be loaded, if it isn't already + return unless @supermodel.finished() @$el.find('a[data-toggle="tab"]').on 'shown.bs.tab', (e) => Backbone.Mediator.publish 'level:view-switched', e - @thangsTab = @insertSubView new ThangsTabView world: @world, supermodel: @supermodel - @settingsTab = @insertSubView new SettingsTabView world: @world, supermodel: @supermodel + @thangsTab = @insertSubView new ThangsTabView world: @world, supermodel: @supermodel, level: @level + @settingsTab = @insertSubView new SettingsTabView supermodel: @supermodel @scriptsTab = @insertSubView new ScriptsTabView world: @world, supermodel: @supermodel, files: @files @componentsTab = @insertSubView new ComponentsTabView supermodel: @supermodel @systemsTab = @insertSubView new SystemsTabView supermodel: @supermodel Backbone.Mediator.publish 'level-loaded', level: @level - @showReadOnly() unless me.isAdmin() or @level.hasWriteAccess(me) + @showReadOnly() if me.get('anonymous') + @patchesView = @insertSubView(new PatchesView(@level), @$el.find('.patches-view')) + @listenTo @patchesView, 'accepted-patch', -> setTimeout "location.reload()", 400 + @$el.find('#level-watch-button').find('> span').toggleClass('secret') if @level.watching() onPlayLevel: (e) -> sendLevel = => @@ -103,9 +89,12 @@ module.exports = class EditorLevelView extends View @childWindow.onPlayLevelViewLoaded = (e) => sendLevel() # still a hack @childWindow.focus() + startPatchingLevel: (e) -> + @openModalView new SaveVersionModal({model:@level}) + Backbone.Mediator.publish 'level:view-switched', e + startCommittingLevel: (e) -> - levelSaveView = new LevelSaveView level: @level, supermodel: @supermodel - @openModalView levelSaveView + @openModalView new LevelSaveView level: @level, supermodel: @supermodel Backbone.Mediator.publish 'level:view-switched', e startForkingLevel: (e) -> @@ -117,3 +106,18 @@ module.exports = class EditorLevelView extends View versionHistoryView = new VersionHistoryView level:@level, @levelID @openModalView versionHistoryView Backbone.Mediator.publish 'level:view-switched', e + + toggleWatchLevel: -> + button = @$el.find('#level-watch-button') + @level.watch(button.find('.watch').is(':visible')) + button.find('> span').toggleClass('secret') + + toggleTab: (e) -> + return unless $(document).width() <= 800 + li = $(e.target).closest('li') + if li.hasClass('active') + li.parent().find('li').show() + else + li.parent().find('li').hide() + li.show() + console.log li.hasClass('active') diff --git a/app/views/editor/level/home.coffee b/app/views/editor/level/home.coffee index ffb1a5ac9..247c9d3ff 100644 --- a/app/views/editor/level/home.coffee +++ b/app/views/editor/level/home.coffee @@ -1,8 +1,8 @@ SearchView = require 'views/kinds/SearchView' -module.exports = class ThangTypeHomeView extends SearchView +module.exports = class EditorSearchView extends SearchView id: "editor-level-home-view" modelLabel: 'Level' model: require 'models/Level' modelURL: '/db/level' - tableTemplate: require 'templates/editor/level/table' \ No newline at end of file + tableTemplate: require 'templates/editor/level/table' diff --git a/app/views/editor/level/modal/world_select.coffee b/app/views/editor/level/modal/world_select.coffee index da3c6696b..05cef5e38 100644 --- a/app/views/editor/level/modal/world_select.coffee +++ b/app/views/editor/level/modal/world_select.coffee @@ -78,8 +78,8 @@ module.exports = class WorldSelectModal extends View showZoomRegion: -> d = @defaultFromZoom - canvasWidth = 1848 # Dimensions for canvas player. Need these somewhere. - canvasHeight = 1178 + canvasWidth = 924 # Dimensions for canvas player. Need these somewhere. + canvasHeight = 589 dimensions = {x: canvasWidth/d.zoom, y: canvasHeight/d.zoom} dimensions = @surface.camera.surfaceToWorld(dimensions) width = dimensions.x diff --git a/app/views/editor/level/save_view.coffee b/app/views/editor/level/save_view.coffee index e3e5ad25c..75226dd54 100644 --- a/app/views/editor/level/save_view.coffee +++ b/app/views/editor/level/save_view.coffee @@ -3,11 +3,13 @@ template = require 'templates/editor/level/save' forms = require 'lib/forms' LevelComponent = require 'models/LevelComponent' LevelSystem = require 'models/LevelSystem' +DeltaView = require 'views/editor/delta' module.exports = class LevelSaveView extends SaveVersionModal template: template instant: false modalWidthPercent: 60 + plain: true events: 'click #save-version-button': 'commitLevel' @@ -23,10 +25,27 @@ module.exports = class LevelSaveView extends SaveVersionModal context.levelNeedsSave = @level.hasLocalChanges() context.modifiedComponents = _.filter @supermodel.getModels(LevelComponent), @shouldSaveEntity context.modifiedSystems = _.filter @supermodel.getModels(LevelSystem), @shouldSaveEntity - context.noSaveButton = not (context.levelNeedsSave or context.modifiedComponents.length or context.modifiedSystems.length) + context.hasChanges = (context.levelNeedsSave or context.modifiedComponents.length or context.modifiedSystems.length) + @lastContext = context context + afterRender: -> + super(false) + changeEls = @$el.find('.changes-stub') + models = if @lastContext.levelNeedsSave then [@level] else [] + models = models.concat @lastContext.modifiedComponents + models = models.concat @lastContext.modifiedSystems + models = (m for m in models when m.hasWriteAccess()) + for changeEl, i in changeEls + model = models[i] + try + deltaView = new DeltaView({model:model}) + @insertSubView(deltaView, $(changeEl)) + catch e + console.error "Couldn't create delta view:", e + shouldSaveEntity: (m) -> + return false unless m.hasWriteAccess() return true if m.hasLocalChanges() return true if (m.get('version').major is 0 and m.get('version').minor is 0) or not m.isPublished() and not m.collection # Sometimes we have two versions: one in a search collection and one with a URL. We only save changes to the latter. diff --git a/app/views/editor/level/scripts_tab_view.coffee b/app/views/editor/level/scripts_tab_view.coffee index 8e06b8a58..b2789948f 100644 --- a/app/views/editor/level/scripts_tab_view.coffee +++ b/app/views/editor/level/scripts_tab_view.coffee @@ -3,7 +3,6 @@ template = require 'templates/editor/level/scripts_tab' Level = require 'models/Level' Surface = require 'lib/surface/Surface' nodes = require './treema_nodes' -defaultScripts = require 'lib/scripts/defaultScripts' module.exports = class ScriptsTabView extends View id: "editor-level-scripts-tab-view" @@ -17,14 +16,14 @@ module.exports = class ScriptsTabView extends View super options @world = options.world @files = options.files - + + onLoaded: -> onLevelLoaded: (e) -> @level = e.level @dimensions = @level.dimensions() scripts = $.extend(true, [], @level.get('scripts') ? []) - scripts = _.cloneDeep defaultScripts unless scripts.length treemaOptions = - schema: Level.schema.get('properties').scripts + schema: Level.schema.properties.scripts data: scripts callbacks: change: @onScriptsChanged @@ -54,12 +53,12 @@ module.exports = class ScriptsTabView extends View filePath: "db/level/#{@level.get('original')}" files: @files view: @ - schema: Level.schema.get('properties').scripts.items + schema: Level.schema.properties.scripts.items data: selected.data thangIDs: thangIDs dimensions: @dimensions supermodel: @supermodel - readOnly: true unless me.isAdmin() or @level.hasWriteAccess(me) + readOnly: me.get('anonymous') callbacks: change: @onScriptChanged nodeClasses: diff --git a/app/views/editor/level/settings_tab_view.coffee b/app/views/editor/level/settings_tab_view.coffee index 7556c4a4f..f834e172c 100644 --- a/app/views/editor/level/settings_tab_view.coffee +++ b/app/views/editor/level/settings_tab_view.coffee @@ -3,6 +3,7 @@ template = require 'templates/editor/level/settings_tab' Level = require 'models/Level' Surface = require 'lib/surface/Surface' nodes = require './treema_nodes' +{me} = require 'lib/auth' module.exports = class SettingsTabView extends View id: 'editor-level-settings-tab-view' @@ -20,12 +21,12 @@ module.exports = class SettingsTabView extends View constructor: (options) -> super options - @world = options.world + onLoaded: -> onLevelLoaded: (e) -> @level = e.level data = _.pick @level.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep Level.schema.attributes + schema = _.cloneDeep Level.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings thangIDs = @getThangIDs() @@ -34,7 +35,7 @@ module.exports = class SettingsTabView extends View supermodel: @supermodel schema: schema data: data - readOnly: true unless me.isAdmin() or @level.hasWriteAccess(me) + readOnly: me.get('anonymous') callbacks: {change: @onSettingsChanged} thangIDs: thangIDs nodeClasses: diff --git a/app/views/editor/level/system/add.coffee b/app/views/editor/level/system/add.coffee index a106e3d39..64caa52b4 100644 --- a/app/views/editor/level/system/add.coffee +++ b/app/views/editor/level/system/add.coffee @@ -2,7 +2,7 @@ View = require 'views/kinds/ModalView' template = require 'templates/editor/level/system/add' availableSystemTemplate = require 'templates/editor/level/system/available_system' LevelSystem = require 'models/LevelSystem' -CocoCollection = require 'models/CocoCollection' +CocoCollection = require 'collections/CocoCollection' class LevelSystemSearchCollection extends CocoCollection url: '/db/level_system/search' diff --git a/app/views/editor/level/system/edit.coffee b/app/views/editor/level/system/edit.coffee index c92894cdd..404ac360b 100644 --- a/app/views/editor/level/system/edit.coffee +++ b/app/views/editor/level/system/edit.coffee @@ -1,6 +1,9 @@ View = require 'views/kinds/CocoView' template = require 'templates/editor/level/system/edit' LevelSystem = require 'models/LevelSystem' +VersionHistoryView = require 'views/editor/system/versions_view' +PatchesView = require 'views/editor/patches_view' +SaveVersionModal = require 'views/modal/save_version_modal' module.exports = class LevelSystemEditView extends View id: "editor-level-system-edit-view" @@ -10,6 +13,13 @@ module.exports = class LevelSystemEditView extends View events: 'click #done-editing-system-button': 'endEditing' 'click .nav a': (e) -> $(e.target).tab('show') + 'click #system-patches-tab': -> @patchesView.load() + 'click #system-code-tab': 'buildCodeEditor' + 'click #system-config-schema-tab': 'buildConfigSchemaTreema' + 'click #system-settings-tab': 'buildSettingsTreema' + 'click #system-history-button': 'showVersionHistory' + 'click #patch-system-button': 'startPatchingSystem' + 'click #system-watch-button': 'toggleWatchSystem' constructor: (options) -> super options @@ -26,10 +36,11 @@ module.exports = class LevelSystemEditView extends View @buildSettingsTreema() @buildConfigSchemaTreema() @buildCodeEditor() + @patchesView = @insertSubView(new PatchesView(@levelSystem), @$el.find('.patches-view')) buildSettingsTreema: -> data = _.pick @levelSystem.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep LevelSystem.schema.attributes + schema = _.cloneDeep LevelSystem.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings @@ -38,7 +49,7 @@ module.exports = class LevelSystemEditView extends View schema: schema data: data callbacks: {change: @onSystemSettingsEdited} - treemaOptions.readOnly = true unless me.isAdmin() + treemaOptions.readOnly = me.get('anonymous') @systemSettingsTreema = @$el.find('#edit-system-treema').treema treemaOptions @systemSettingsTreema.build() @systemSettingsTreema.open() @@ -53,25 +64,26 @@ module.exports = class LevelSystemEditView extends View buildConfigSchemaTreema: -> treemaOptions = supermodel: @supermodel - schema: LevelSystem.schema.get('properties').configSchema + schema: LevelSystem.schema.properties.configSchema data: @levelSystem.get 'configSchema' callbacks: {change: @onConfigSchemaEdited} - treemaOptions.readOnly = true unless me.isAdmin() + treemaOptions.readOnly = me.get('anonymous') @configSchemaTreema = @$el.find('#config-schema-treema').treema treemaOptions @configSchemaTreema.build() @configSchemaTreema.open() # TODO: schema is not loaded for the first one here? - @configSchemaTreema.tv4.addSchema('metaschema', LevelSystem.schema.get('properties').configSchema) + @configSchemaTreema.tv4.addSchema('metaschema', LevelSystem.schema.properties.configSchema) onConfigSchemaEdited: => @levelSystem.set 'configSchema', @configSchemaTreema.data Backbone.Mediator.publish 'level-system-edited', levelSystem: @levelSystem buildCodeEditor: -> - editorEl = @$el.find '#system-code-editor' - editorEl.text @levelSystem.get('code') + @editor?.destroy() + editorEl = $('
').text(@levelSystem.get('code')).addClass('inner-editor') + @$el.find('#system-code-editor').empty().append(editorEl) @editor = ace.edit(editorEl[0]) - @editor.setReadOnly(not me.isAdmin()) + @editor.setReadOnly(me.get('anonymous')) session = @editor.getSession() session.setMode 'ace/mode/coffee' session.setTabSize 2 @@ -88,6 +100,21 @@ module.exports = class LevelSystemEditView extends View Backbone.Mediator.publish 'level-system-editing-ended', levelSystem: @levelSystem null + showVersionHistory: (e) -> + versionHistoryView = new VersionHistoryView {}, @levelSystem.id + @openModalView versionHistoryView + Backbone.Mediator.publish 'level:view-switched', e + + startPatchingSystem: (e) -> + @openModalView new SaveVersionModal({model:@levelSystem}) + Backbone.Mediator.publish 'level:view-switched', e + + toggleWatchSystem: -> + console.log 'toggle watch system?' + button = @$el.find('#system-watch-button') + @levelSystem.watch(button.find('.watch').is(':visible')) + button.find('> span').toggleClass('secret') + destroy: -> @editor?.destroy() super() diff --git a/app/views/editor/level/system/new.coffee b/app/views/editor/level/system/new.coffee index 88b92af49..4b91a7df6 100644 --- a/app/views/editor/level/system/new.coffee +++ b/app/views/editor/level/system/new.coffee @@ -32,6 +32,6 @@ module.exports = class LevelSystemNewView extends View console.log "Got errors:", JSON.parse(res.responseText) forms.applyErrorsToForm(@$el, JSON.parse(res.responseText)) res.success => - @supermodel.addModel system + @supermodel.registerModel system Backbone.Mediator.publish 'edit-level-system', original: system.get('_id'), majorVersion: 0 @hide() diff --git a/app/views/editor/level/systems_tab_view.coffee b/app/views/editor/level/systems_tab_view.coffee index 52129e7b8..4d3ed88c6 100644 --- a/app/views/editor/level/systems_tab_view.coffee +++ b/app/views/editor/level/systems_tab_view.coffee @@ -11,7 +11,6 @@ module.exports = class SystemsTabView extends View id: "editor-level-systems-tab-view" template: template className: 'tab-pane' - startsLoading: true subscriptions: 'level-system-added': 'onLevelSystemAdded' @@ -23,53 +22,38 @@ module.exports = class SystemsTabView extends View events: 'click #add-system-button': 'addLevelSystem' 'click #create-new-system-button': 'createNewLevelSystem' + 'click #create-new-system': 'createNewLevelSystem' constructor: (options) -> super options - @toLoad = 0 for system in @buildDefaultSystems() url = "/db/level.system/#{system.original}/version/#{system.majorVersion}" - ls = new LevelSystem() - ls.saveBackups = true - do (url) -> ls.url = -> url - continue if @supermodel.getModelByURL ls.url - ls.fetch() - @listenTo(ls, 'sync', @onSystemLoaded) - ++@toLoad - @onDefaultSystemsLoaded() unless @toLoad - - onSystemLoaded: (ls) -> - @supermodel.addModel ls - --@toLoad - @onDefaultSystemsLoaded() unless @toLoad - - onDefaultSystemsLoaded: -> - @startsLoading = false - @render() # do it again but without the loading screen - @onLevelLoaded level: @level if @level + ls = new LevelSystem().setURL(url) + @supermodel.loadModel(ls, 'system') + afterRender: -> + @buildSystemsTreema() + + onLoaded: -> + super() + onLevelLoaded: (e) -> @level = e.level - return if @startsLoading @buildSystemsTreema() buildSystemsTreema: -> + return unless @level and @supermodel.finished() systems = $.extend(true, [], @level.get('systems') ? []) unless systems.length systems = @buildDefaultSystems() insertedDefaults = true - - systemModels = @supermodel.getModels LevelSystem - systemModelMap = {} - systemModelMap[sys.get('original')] = sys.get('name') for sys in systemModels - systems = _.sortBy systems, (sys) -> systemModelMap[sys.original] - + systems = @getSortedByName systems treemaOptions = # TODO: somehow get rid of the + button, or repurpose it to open the LevelSystemAddView instead supermodel: @supermodel - schema: Level.schema.get('properties').systems + schema: Level.schema.properties.systems data: systems - readOnly: true unless me.isAdmin() or @level.hasWriteAccess(me) + readOnly: me.get('anonymous') callbacks: change: @onSystemsChanged select: @onSystemSelected @@ -83,7 +67,14 @@ module.exports = class SystemsTabView extends View @onSystemsChanged() if insertedDefaults onSystemsChanged: (e) => - @level.set 'systems', @systemsTreema.data + systems = @getSortedByName @systemsTreema.data + @level.set 'systems', systems + + getSortedByName: (systems) => + systemModels = @supermodel.getModels LevelSystem + systemModelMap = {} + systemModelMap[sys.get('original')] = sys.get('name') for sys in systemModels + _.sortBy systems, (sys) -> systemModelMap[sys.original] onSystemSelected: (e, selected) => selected = if selected.length > 1 then selected[0].getLastSelectedTreema() else selected[0] @@ -143,9 +134,9 @@ class LevelSystemNode extends TreemaObjectNode @collection = @system?.attributes?.configSchema?.properties? grabDBComponent: -> - @system = @settings.supermodel.getModelByOriginalAndMajorVersion LevelSystem, @data.original, @data.majorVersion - #@system = _.find @settings.supermodel.getModels(LevelSystem), (m) => - # m.get('original') is @data.original and m.get('version').major is @data.majorVersion + unless _.isString @data.original + return alert('Press the "Add System" button at the bottom instead of the "+". Sorry.') + @system = @settings.supermodel.getModelByOriginalAndMajorVersion(LevelSystem, @data.original, @data.majorVersion) console.error "Couldn't find system for", @data.original, @data.majorVersion, "from models", @settings.supermodel.models unless @system getChildSchema: (key) -> @@ -157,11 +148,12 @@ class LevelSystemNode extends TreemaObjectNode name = "#{@system.get('name')} v#{@system.get('version').major}" @buildValueForDisplaySimply valEl, "#{name}" - onEnterPressed: -> + onEnterPressed: (e) -> + super e Backbone.Mediator.publish 'edit-level-system', original: @data.original, majorVersion: @data.majorVersion - open: -> - super() + open: (depth) -> + super depth cTreema = @childrenTreemas.config if cTreema? and (cTreema.getChildren().length or cTreema.canAddChild()) cTreema.open() diff --git a/app/views/editor/level/thang/edit.coffee b/app/views/editor/level/thang/edit.coffee index 2a92fd0b1..d749eea10 100644 --- a/app/views/editor/level/thang/edit.coffee +++ b/app/views/editor/level/thang/edit.coffee @@ -33,7 +33,9 @@ module.exports = class LevelThangEditView extends View context.thang = @thangData context + onLoaded: -> @render() afterRender: -> + super() options = components: @thangData.components supermodel: @supermodel @@ -50,6 +52,7 @@ module.exports = class LevelThangEditView extends View input.val(thangTypeName) @$el.find('#thang-type-link span').text(thangTypeName) window.input = input + @hideLoading() saveThang: (e) -> # Make sure it validates first? diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index 243b1e540..0e0ac9a74 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -4,7 +4,7 @@ thangs_template = require 'templates/editor/level/thangs_tab' Level = require 'models/Level' ThangType = require 'models/ThangType' LevelComponent = require 'models/LevelComponent' -CocoCollection = require 'models/CocoCollection' +CocoCollection = require 'collections/CocoCollection' {isObjectID} = require 'models/CocoModel' Surface = require 'lib/surface/Surface' Thang = require 'lib/world/thang' @@ -35,21 +35,22 @@ module.exports = class ThangsTabView extends View 'surface:mouse-moved': 'onSurfaceMouseMoved' 'surface:mouse-over': 'onSurfaceMouseOver' 'surface:mouse-out': 'onSurfaceMouseOut' - 'level-loaded': 'onLevelLoaded' 'edit-level-thang': 'editThang' 'level-thang-edited': 'onLevelThangEdited' 'level-thang-done-editing': 'onLevelThangDoneEditing' 'level:view-switched': 'onViewSwitched' - 'sprite:mouse-down': 'onSpriteMouseDown' 'sprite:dragged': 'onSpriteDragged' 'sprite:mouse-up': 'onSpriteMouseUp' 'sprite:double-clicked': 'onSpriteDoubleClicked' - 'surface:stage-mouse-down': 'onStageMouseDown' + 'surface:stage-mouse-up': 'onStageMouseUp' events: 'click #extant-thangs-filter button': 'onFilterExtantThangs' 'click #delete': 'onDeleteClicked' 'click #duplicate': 'onDuplicateClicked' + 'click #thangs-container-toggle': 'toggleThangsContainer' +# 'click #thangs-palette-toggle': 'toggleThangsPalette' +# 'click .add-thang-palette-icon': 'toggleThangsPalette' shortcuts: 'esc': 'selectAddThang' @@ -60,33 +61,18 @@ module.exports = class ThangsTabView extends View constructor: (options) -> super options @world = options.world - @thangTypes = @supermodel.getCollection new ThangTypeSearchCollection() # should load depended-on Components, too - @listenToOnce(@thangTypes, 'sync', @onThangTypesLoaded) - @thangTypes.fetch() - $(document).bind 'contextmenu', @preventDefaultContextMenu + # should load depended-on Components, too + @thangTypes = @supermodel.loadCollection(new ThangTypeSearchCollection(), 'thangs').model # just loading all Components for now: https://github.com/codecombat/codecombat/issues/405 - @componentCollection = @supermodel.getCollection new ComponentsCollection() - @listenToOnce(@componentCollection, 'sync', @onComponentsLoaded) - @componentCollection.fetch() - - onThangTypesLoaded: -> - return if @destroyed - @supermodel.addCollection @thangTypes - @supermodel.populateModel model for model in @thangTypes.models - @startsLoading = not @componentCollection.loaded - @render() # do it again but without the loading screen - @onLevelLoaded level: @level if @level and not @startsLoading - - onComponentsLoaded: -> - return if @destroyed - @supermodel.addCollection @componentCollection - @startsLoading = not @thangTypes.loaded - @render() # do it again but without the loading screen - @onLevelLoaded level: @level if @level and not @startsLoading + @componentCollection = @supermodel.loadCollection(new ComponentsCollection(), 'components').load() + @level = options.level + $(document).bind 'contextmenu', @preventDefaultContextMenu + getRenderData: (context={}) -> context = super(context) + return context unless @supermodel.finished() thangTypes = (thangType.attributes for thangType in @supermodel.getModels(ThangType)) thangTypes = _.uniq thangTypes, false, 'original' thangTypes = _.reject thangTypes, kind: 'Mark' @@ -112,16 +98,21 @@ module.exports = class ThangsTabView extends View $('#thangs-list').height('100%') thangsHeaderHeight = $('#thangs-header').height() oldHeight = $('#thangs-list').height() - $('#thangs-list').height(oldHeight - thangsHeaderHeight - 80) + if $(document).width() < 1050 + $('#thangs-list').height(oldHeight - thangsHeaderHeight - 40) + else + $('#thangs-list').height(oldHeight - thangsHeaderHeight - 80) + afterRender: -> - return if @startsLoading super() - $('.tab-content').click @selectAddThang + return unless @supermodel.finished() + $('.tab-content').mousedown @selectAddThang $('#thangs-list').bind 'mousewheel', @preventBodyScrollingInThangList @$el.find('#extant-thangs-filter button:first').button('toggle') $(window).resize @onWindowResize @addThangsView = @insertSubView new AddThangsView world: @world, supermodel: @supermodel + @buildInterface() # refactor to not have this trigger when this view re-renders? onFilterExtantThangs: (e) -> @$el.find('#extant-thangs-filter button.active').button('toggle') @@ -135,12 +126,12 @@ module.exports = class ThangsTabView extends View @scrollTop += (if e.deltaY < 0 then 1 else -1) * 30 e.preventDefault() - onLevelLoaded: (e) -> - @level = e.level - return if @startsLoading + buildInterface: (e) -> + @level = e.level if e + data = $.extend(true, {}, @level.attributes) treemaOptions = - schema: Level.schema.get('properties').thangs + schema: Level.schema.properties.thangs data: data.thangs supermodel: @supermodel callbacks: @@ -152,6 +143,7 @@ module.exports = class ThangsTabView extends View thang: ThangNode array: ThangsNode world: @world + @thangsTreema = @$el.find('#thangs-treema').treema treemaOptions @thangsTreema.build() @thangsTreema.open() @@ -179,6 +171,7 @@ module.exports = class ThangsTabView extends View destroy: -> @selectAddThangType null @surface.destroy() + $(document).unbind 'contextmenu', @preventDefaultContextMenu super() onViewSwitched: (e) -> @@ -187,13 +180,13 @@ module.exports = class ThangsTabView extends View onSpriteMouseDown: (e) -> # Sprite clicks happen after stage clicks, but we need to know whether a sprite is being clicked. - clearTimeout @backgroundAddClickTimeout - if e.originalEvent.nativeEvent.button == 2 - @onSpriteContextMenu e + # clearTimeout @backgroundAddClickTimeout + # if e.originalEvent.nativeEvent.button == 2 + # @onSpriteContextMenu e - onStageMouseDown: (e) -> + onStageMouseUp: (e) -> if @addThangSprite - # If we click on the background, we need to add @addThangSprite, but not if onSpriteMouseDown will fire. + # If we click on the background, we need to add @addThangSprite, but not if onSpriteMouseUp will fire. @backgroundAddClickTimeout = _.defer => @onExtantThangSelected {} $('#contextmenu').hide() @@ -208,6 +201,9 @@ module.exports = class ThangsTabView extends View @calculateMovement(stageX / w, stageY / h, w / h) onSpriteMouseUp: (e) -> + clearTimeout @backgroundAddClickTimeout + if e.originalEvent.nativeEvent.button == 2 + @onSpriteContextMenu e clearInterval(@movementInterval) if @movementInterval? @movementInterval = null @surface.camera.dragDisabled = false @@ -254,6 +250,7 @@ module.exports = class ThangsTabView extends View # @thangsTreema.deselectAll() selectAddThang: (e) => + return unless e? and $(e.target).closest('#editor-level-thangs-tab-view').length if e then target = $(e.target) else target = @$el.find('.add-thangs-palette') # pretend to click on background if no event return true if target.attr('id') is 'surface' target = target.closest('.add-thang-palette-icon') @@ -282,7 +279,7 @@ module.exports = class ThangsTabView extends View thang = @createAddThang() @addThangSprite = @surface.spriteBoss.addThangToSprites thang, @surface.spriteBoss.spriteLayers["Floating"] @addThangSprite.notOfThisWorld = true - @addThangSprite.displayObject.alpha = 0.75 + @addThangSprite.imageObject.alpha = 0.75 @addThangSprite.playSound? 'selected' pos ?= x: Math.round(@world.width / 2), y: Math.round(@world.height / 2) @adjustThangPos @addThangSprite, thang, pos @@ -329,11 +326,11 @@ module.exports = class ThangsTabView extends View onSurfaceMouseOver: (e) -> return unless @addThangSprite - @addThangSprite.displayObject.visible = true + @addThangSprite.imageObject.visible = true onSurfaceMouseOut: (e) -> return unless @addThangSprite - @addThangSprite.displayObject.visible = false + @addThangSprite.imageObject.visible = false calculateMovement: (pctX, pctY, widthHeightRatio) -> MOVE_TOP_MARGIN = 1.0 - MOVE_MARGIN @@ -402,7 +399,6 @@ module.exports = class ThangsTabView extends View physical.config.pos = x: pos.x, y: pos.y, z: physical.config.pos.z if physical thang = thangType: thangType.get('original'), id: thangID, components: components @thangsTreema.insert '', thang - @supermodel.populateModel thangType # Make sure we grab any new data for the thang we just added editThang: (e) -> if e.target # click event @@ -424,10 +420,11 @@ module.exports = class ThangsTabView extends View @editThangView = null @onThangsChanged() @$el.find('.thangs-column').show() - + preventDefaultContextMenu: (e) -> + return unless $(e.target).closest('#canvas-wrapper').length e.preventDefault() - + onSpriteContextMenu: (e) -> {clientX, clientY} = e.originalEvent.nativeEvent if @addThangType @@ -436,17 +433,25 @@ module.exports = class ThangsTabView extends View $('#duplicate a').html 'Duplicate' $('#contextmenu').css { position: 'fixed', left: clientX, top: clientY } $('#contextmenu').show() - + onDeleteClicked: (e) -> $('#contextmenu').hide() @deleteSelectedExtantThang e - + onDuplicateClicked: (e) -> $('#contextmenu').hide() if !@addThangType thang = @selectedExtantThang.spriteName e.target = $(".add-thang-palette-icon[data-thang-type='" + thang + "']").get 0 @selectAddThang e + + toggleThangsContainer: (e) -> + $('#all-thangs').toggle() + + toggleThangsPalette: (e) -> + $('#add-thangs-column').toggle() + @onWindowResize e + class ThangsNode extends TreemaNode.nodeMap.array valueClass: 'treema-array-replacement' diff --git a/app/views/editor/level/treema_nodes.coffee b/app/views/editor/level/treema_nodes.coffee index fda09a79b..01e1362a2 100644 --- a/app/views/editor/level/treema_nodes.coffee +++ b/app/views/editor/level/treema_nodes.coffee @@ -1,9 +1,9 @@ WorldSelectModal = require './modal/world_select' ThangType = require '/models/ThangType' -makeButton = -> $('') +makeButton = -> $('') shorten = (f) -> parseFloat(f.toFixed(1)) -WIDTH = 1848 +WIDTH = 924 module.exports.WorldPointNode = class WorldPointNode extends TreemaNode.nodeMap.point2d constructor: (args...) -> @@ -13,11 +13,11 @@ module.exports.WorldPointNode = class WorldPointNode extends TreemaNode.nodeMap. buildValueForDisplay: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) buildValueForEditing: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) onClick: (e) -> btn = $(e.target).closest('.treema-map-button') @@ -44,11 +44,11 @@ class WorldRegionNode extends TreemaNode.nodeMap.object buildValueForDisplay: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) buildValueForEditing: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) onClick: (e) -> btn = $(e.target).closest('.treema-map-button') @@ -80,11 +80,11 @@ module.exports.WorldViewportNode = class WorldViewportNode extends TreemaNode.no buildValueForDisplay: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) buildValueForEditing: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) onClick: (e) -> btn = $(e.target).closest('.treema-map-button') @@ -121,11 +121,11 @@ module.exports.WorldBoundsNode = class WorldBoundsNode extends TreemaNode.nodeMa buildValueForDisplay: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) buildValueForEditing: (valEl) -> super(valEl) - valEl.prepend(makeButton()) + valEl.find('.treema-shortened').prepend(makeButton()) onClick: (e) -> btn = $(e.target).closest('.treema-map-button') diff --git a/app/views/editor/patch_modal.coffee b/app/views/editor/patch_modal.coffee new file mode 100644 index 000000000..4603cbc48 --- /dev/null +++ b/app/views/editor/patch_modal.coffee @@ -0,0 +1,64 @@ +ModalView = require 'views/kinds/ModalView' +template = require 'templates/editor/patch_modal' +DeltaView = require 'views/editor/delta' +auth = require 'lib/auth' + +module.exports = class PatchModal extends ModalView + id: "patch-modal" + template: template + plain: true + modalWidthPercent: 60 + + events: + 'click #withdraw-button': 'withdrawPatch' + 'click #reject-button': 'rejectPatch' + 'click #accept-button': 'acceptPatch' + + constructor: (@patch, @targetModel, options) -> + super(options) + targetID = @patch.get('target').id + if targetID is @targetModel.id + @originalSource = @targetModel.clone(false) + else + @originalSource = new @targetModel.constructor({_id:targetID}) + @supermodel.loadModel @originalSource, 'source_document' + + getRenderData: -> + c = super() + c.isPatchCreator = @patch.get('creator') is auth.me.id + c.isPatchRecipient = @targetModel.hasWriteAccess() + c.status = @patch.get 'status' + c.patch = @patch + c + + afterRender: -> + return unless @supermodel.finished() + headModel = null + if @targetModel.hasWriteAccess() + headModel = @originalSource.clone(false) + headModel.set(@targetModel.attributes) + headModel.loaded = true + + pendingModel = @originalSource.clone(false) + pendingModel.applyDelta(@patch.get('delta')) + pendingModel.loaded = true + + @deltaView = new DeltaView({model:pendingModel, headModel:headModel}) + changeEl = @$el.find('.changes-stub') + @insertSubView(@deltaView, changeEl) + super() + + acceptPatch: -> + delta = @deltaView.getApplicableDelta() + @targetModel.applyDelta(delta) + @patch.setStatus('accepted') + @trigger 'accepted-patch' + @hide() + + rejectPatch: -> + @patch.setStatus('rejected') + @hide() + + withdrawPatch: -> + @patch.setStatus('withdrawn') + @hide() diff --git a/app/views/editor/patches_view.coffee b/app/views/editor/patches_view.coffee new file mode 100644 index 000000000..17aef667b --- /dev/null +++ b/app/views/editor/patches_view.coffee @@ -0,0 +1,61 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/patches' +PatchesCollection = require 'collections/PatchesCollection' +nameLoader = require 'lib/NameLoader' +PatchModal = require './patch_modal' + +module.exports = class PatchesView extends CocoView + template: template + className: 'patches-view' + status: 'pending' + + events: + 'change .status-buttons': 'onStatusButtonsChanged' + 'click .patch-icon': 'openPatchModal' + + constructor: (@model, options) -> + super(options) + @initPatches() + + initPatches: -> + @startedLoading = false + @patches = new PatchesCollection([], {}, @model, @status) + + load: -> + @initPatches() + @patches = @supermodel.loadCollection(@patches, 'patches').model + @listenTo @patches, 'sync', @onPatchesLoaded + + onPatchesLoaded: -> + ids = (p.get('creator') for p in @patches.models) + jqxhrOptions = nameLoader.loadNames ids + @supermodel.addRequestResource('user_names', jqxhrOptions).load() if jqxhrOptions + + getRenderData: -> + c = super() + patch.userName = nameLoader.getName(patch.get('creator')) for patch in @patches.models + c.patches = @patches.models + c.status + c + + afterRender: -> + @$el.find(".#{@status}").addClass 'active' + + onStatusButtonsChanged: (e) -> + @status = $(e.target).val() + @reloadPatches() + + reloadPatches: -> + @load() + @render() + + openPatchModal: (e) -> + console.log "open patch modal" + patch = _.find @patches.models, {id:$(e.target).data('patch-id')} + modal = new PatchModal(patch, @model) + @openModalView(modal) + @listenTo modal, 'accepted-patch', -> @trigger 'accepted-patch' + @listenTo modal, 'hide', -> + f = => @reloadPatches() + setTimeout(f, 400) + @stopListening modal diff --git a/app/views/editor/system/versions_view.coffee b/app/views/editor/system/versions_view.coffee new file mode 100755 index 000000000..562d134ad --- /dev/null +++ b/app/views/editor/system/versions_view.coffee @@ -0,0 +1,9 @@ +VersionsModalView = require 'views/modal/versions_modal' + +module.exports = class SystemVersionsView extends VersionsModalView + id: "editor-system-versions-view" + url: "/db/level.system/" + page: "system" + + constructor: (options, @ID) -> + super options, ID, require 'models/LevelSystem' \ No newline at end of file diff --git a/app/views/editor/thang/colors_tab_view.coffee b/app/views/editor/thang/colors_tab_view.coffee index a858f4385..00ba6b16c 100644 --- a/app/views/editor/thang/colors_tab_view.coffee +++ b/app/views/editor/thang/colors_tab_view.coffee @@ -12,7 +12,7 @@ module.exports = class ColorsTabView extends CocoView constructor: (@thangType, options) -> @listenToOnce(@thangType, 'sync', @tryToBuild) - @listenToOnce(@thangType.schema(), 'sync', @tryToBuild) + # @listenToOnce(@thangType.schema(), 'sync', @tryToBuild) @colorConfig = { hue: 0, saturation: 0.5, lightness: 0.5 } @spriteBuilder = new SpriteBuilder(@thangType) f = => @@ -24,7 +24,8 @@ module.exports = class ColorsTabView extends CocoView destroy: -> clearInterval @interval super() - + + onLoaded: -> @render() afterRender: -> super() @createShapeButtons() @@ -112,10 +113,10 @@ module.exports = class ColorsTabView extends CocoView @buttons = buttons tryToBuild: -> - return unless @thangType.loaded and @thangType.schema().loaded + return unless @thangType.loaded data = @thangType.get('colorGroups') data ?= {} - schema = @thangType.schema().attributes.properties?.colorGroups + schema = @thangType.schema().properties?.colorGroups treemaOptions = data: data schema: schema diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 660280e54..3d753ff50 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -9,6 +9,8 @@ View = require 'views/kinds/RootView' ThangComponentEditView = require 'views/editor/components/main' VersionHistoryView = require './versions_view' ColorsTabView = require './colors_tab_view' +PatchesView = require 'views/editor/patches_view' +SaveVersionModal = require 'views/modal/save_version_modal' ErrorView = require '../../error_view' template = require 'templates/editor/thang/edit' @@ -33,6 +35,8 @@ module.exports = class ThangTypeEditView extends View 'click #marker-button': 'toggleDots' 'click #end-button': 'endAnimation' 'click #history-button': 'showVersionHistory' + 'click #save-button': 'openSaveModal' + 'click #patches-tab': -> @patchesView.load() subscriptions: 'save-new-version': 'saveNewThangType' @@ -43,32 +47,13 @@ module.exports = class ThangTypeEditView extends View super options @mockThang = $.extend(true, {}, @mockThang) @thangType = new ThangType(_id: @thangTypeID) + @thangType = @supermodel.loadModel(@thangType, 'thang').model @thangType.saveBackups = true - - @listenToOnce(@thangType, 'error', - () => - @hideLoading() - - # Hack: editor components appear after calling insertSubView. - # So we need to hide them first. - $(@$el).find('.main-content-area').children('*').not('#error-view').remove() - - @insertSubView(new ErrorView()) - ) - - @thangType.fetch() - @thangType.loadSchema() - @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) - @listenToOnce(@thangType, 'sync', @onThangTypeSync) + @listenToOnce @thangType, 'sync', -> + console.log 'files for?', @thangType.id, @thangType.get 'name' + @files = @supermodel.loadCollection(new DocumentFiles(@thangType), 'files').model @refreshAnimation = _.debounce @refreshAnimation, 500 - onThangTypeSync: -> - return unless @thangType.loaded and ThangType.hasSchema() - @startsLoading = false - @files = new DocumentFiles(@thangType) - @files.fetch() - @render() - getRenderData: (context={}) -> context = super(context) context.thangType = @thangType @@ -81,16 +66,17 @@ module.exports = class ThangTypeEditView extends View raw = ("raw:#{name}" for name in raw) main = _.keys(@thangType.get('actions') or {}) main.concat(raw) - + afterRender: -> super() - return unless @thangType.loaded + return unless @supermodel.finished() @initStage() @buildTreema() @initSliders() @initComponents() @insertSubView(new ColorsTabView(@thangType)) - @showReadOnly() unless me.isAdmin() or @thangType.hasWriteAccess(me) + @patchesView = @insertSubView(new PatchesView(@thangType), @$el.find('.patches-view')) + @showReadOnly() if me.get('anonymous') initComponents: => options = @@ -114,10 +100,8 @@ module.exports = class ThangTypeEditView extends View initStage: -> canvas = @$el.find('#canvas') @stage = new createjs.Stage(canvas[0]) - canvasWidth = parseInt(canvas.attr('width'), 10) - canvasHeight = parseInt(canvas.attr('height'), 10) @camera?.destroy() - @camera = new Camera canvasWidth, canvasHeight + @camera = new Camera canvas @torsoDot = @makeDot('blue') @mouthDot = @makeDot('yellow') @@ -211,6 +195,7 @@ module.exports = class ThangTypeEditView extends View # animation select refreshAnimation: -> + return @showRasterImage() if @thangType.get('raster') options = @getSpriteOptions() @thangType.resetSpriteSheetCache() spriteSheet = @thangType.buildSpriteSheet(options) @@ -221,6 +206,13 @@ module.exports = class ThangTypeEditView extends View @showAnimation() @updatePortrait() + showRasterImage: -> + sprite = new CocoSprite(@thangType, @getSpriteOptions()) + @currentSprite?.destroy() + @currentSprite = sprite + @showImageObject(sprite.imageObject) + @updateScale() + showAnimation: (animationName) -> animationName = @$el.find('#animations-select').val() unless _.isString animationName return unless animationName @@ -229,8 +221,8 @@ module.exports = class ThangTypeEditView extends View @showMovieClip(animationName) else @showSprite(animationName) - @updateScale() @updateRotation() + @updateScale() # must happen after update rotation, because updateRotation calls the sprite update() method. showMovieClip: (animationName) -> vectorParser = new SpriteBuilder(@thangType) @@ -240,7 +232,7 @@ module.exports = class ThangTypeEditView extends View if reg movieClip.regX = -reg.x movieClip.regY = -reg.y - @showDisplayObject(movieClip) + @showImageObject(movieClip) getSpriteOptions: -> { resolutionFactor: @resolution, thang: @mockThang} @@ -250,7 +242,7 @@ module.exports = class ThangTypeEditView extends View sprite.queueAction(actionName) @currentSprite?.destroy() @currentSprite = sprite - @showDisplayObject(sprite.displayObject) + @showImageObject(sprite.imageObject) updatePortrait: -> options = @getSpriteOptions() @@ -260,12 +252,12 @@ module.exports = class ThangTypeEditView extends View portrait.addClass 'img-thumbnail' $('#portrait').replaceWith(portrait) - showDisplayObject: (displayObject) -> + showImageObject: (imageObject) -> @clearDisplayObject() - displayObject.x = CENTER.x - displayObject.y = CENTER.y - @stage.addChildAt(displayObject, 1) - @currentObject = displayObject + imageObject.x = CENTER.x + imageObject.y = CENTER.y + @stage.addChildAt(imageObject, 1) + @currentObject = imageObject @updateDots() clearDisplayObject: -> @@ -287,11 +279,16 @@ module.exports = class ThangTypeEditView extends View @currentSprite.update(true) updateScale: => - value = (@scaleSlider.slider('value') + 1) / 10 - fixed = value.toFixed(1) - @scale = value + resValue = (@resolutionSlider.slider('value') + 1) / 10 + scaleValue = (@scaleSlider.slider('value') + 1) / 10 + fixed = scaleValue.toFixed(1) + @scale = scaleValue @$el.find('.scale-label').text " #{fixed}x " - @currentObject.scaleX = @currentObject.scaleY = value if @currentObject? + if @currentSprite + @currentSprite.scaleFactor = scaleValue + @currentSprite.updateScale() + else if @currentObject? + @currentObject.scaleX = @currentObject.scaleY = scaleValue / resValue @updateGrid() @updateDots() @@ -324,8 +321,13 @@ module.exports = class ThangTypeEditView extends View res.success => url = "/editor/thang/#{newThangType.get('slug') or newThangType.id}" - newThangType.uploadGenericPortrait -> - document.location.href = url + portraitSource = null + if @thangType.get('raster') + image = @currentSprite.imageObject.image + portraitSource = imageToPortrait image + # bit of a hacky way to get that portrait + success = -> document.location.href = url + newThangType.uploadGenericPortrait success, portraitSource clearRawData: -> @thangType.resetRawData() @@ -339,7 +341,7 @@ module.exports = class ThangTypeEditView extends View buildTreema: -> data = @getThangData() - schema = _.cloneDeep ThangType.schema.attributes + schema = _.cloneDeep ThangType.schema schema.properties = _.pick schema.properties, (value, key) => not (key in ['components']) options = data: data @@ -382,7 +384,7 @@ module.exports = class ThangTypeEditView extends View bounds = obj.frameBounds[0] obj.regX = bounds.x + bounds.width / 2 obj.regY = bounds.y + bounds.height / 2 - @showDisplayObject(obj) if obj + @showImageObject(obj) if obj obj.y = 200 if obj # truly center the container @showingSelectedNode = true @currentSprite?.destroy() @@ -396,11 +398,25 @@ module.exports = class ThangTypeEditView extends View @showAnimation() @showingSelectedNode = false - destroy: -> - @camera?.destroy() - super() - showVersionHistory: (e) -> versionHistoryView = new VersionHistoryView thangType:@thangType, @thangTypeID @openModalView versionHistoryView Backbone.Mediator.publish 'level:view-switched', e + + openSaveModal: -> + @openModalView(new SaveVersionModal({model: @thangType})) + + destroy: -> + @camera?.destroy() + super() + +imageToPortrait = (img) -> + canvas = document.createElement("canvas") + canvas.width = 100 + canvas.height = 100 + ctx = canvas.getContext("2d") + scaleX = 100 / img.width + scaleY = 100 / img.height + ctx.scale scaleX, scaleY + ctx.drawImage img, 0, 0 + canvas.toDataURL("image/png") \ No newline at end of file diff --git a/app/views/employers_view.coffee b/app/views/employers_view.coffee index d5e2eeb2a..757675cf2 100644 --- a/app/views/employers_view.coffee +++ b/app/views/employers_view.coffee @@ -1,6 +1,190 @@ View = require 'views/kinds/RootView' template = require 'templates/employers' +app = require 'application' +User = require 'models/User' +{me} = require 'lib/auth' +CocoCollection = require 'collections/CocoCollection' +EmployerSignupView = require 'views/modal/employer_signup_modal' + +class CandidatesCollection extends CocoCollection + url: '/db/user/x/candidates' + model: User module.exports = class EmployersView extends View id: "employers-view" template: template + + events: + 'click tbody tr': 'onCandidateClicked' + + constructor: (options) -> + super options + @getCandidates() + checkForEmployerSignupHash: => + if window.location.hash is "#employerSignupLoggingIn" and not ("employer" in me.get("permissions")) + @openModalView application.router.getView("modal/employer_signup","_modal") + window.location.hash = "" + afterRender: -> + super() + @sortTable() if @candidates.models.length + + afterInsert: -> + super() + _.delay @checkForEmployerSignupHash, 500 + + getRenderData: -> + c = super() + c.candidates = @candidates.models + userPermissions = me.get('permissions') ? [] + + c.isEmployer = _.contains userPermissions, "employer" + c.moment = moment + c + + getCandidates: -> + @candidates = new CandidatesCollection() + @candidates.fetch() + # Re-render when we have fetched them, but don't wait and show a progress bar while loading. + @listenToOnce @candidates, 'all', @renderCandidatesAndSetupScrolling + + renderCandidatesAndSetupScrolling: => + @render() + $(".nano").nanoScroller() + if window.history?.state?.lastViewedCandidateID + $(".nano").nanoScroller({scrollTo:$("#" + window.history.state.lastViewedCandidateID)}) + else if window.location.hash.length is 25 + $(".nano").nanoScroller({scrollTo:$(window.location.hash)}) + + sortTable: -> + # http://mottie.github.io/tablesorter/docs/example-widget-bootstrap-theme.html + $.extend $.tablesorter.themes.bootstrap, + # these classes are added to the table. To see other table classes available, + # look here: http://twitter.github.com/bootstrap/base-css.html#tables + table: "table table-bordered" + caption: "caption" + header: "bootstrap-header" # give the header a gradient background + footerRow: "" + footerCells: "" + icons: "" # add "icon-white" to make them white; this icon class is added to the in the header + sortNone: "bootstrap-icon-unsorted" + sortAsc: "icon-chevron-up" # glyphicon glyphicon-chevron-up" # we are still using v2 icons + sortDesc: "icon-chevron-down" # glyphicon-chevron-down" # we are still using v2 icons + active: "" # applied when column is sorted + hover: "" # use custom css here - bootstrap class may not override it + filterRow: "" # filter row class + even: "" # odd row zebra striping + odd: "" # even row zebra striping + + + # e = exact text from cell + # n = normalized value returned by the column parser + # f = search filter input value + # i = column index + # $r = ??? + filterSelectExactMatch = (e, n, f, i, $r) -> e is f + + # call the tablesorter plugin and apply the uitheme widget + @$el.find(".tablesorter").tablesorter + theme: "bootstrap" + widthFixed: true + headerTemplate: "{content} {icon}" + textSorter: + 6: (a, b, direction, column, table) -> + days = [] + for s in [a, b] + n = parseInt s + n = 0 unless _.isNumber n + for [duration, factor] in [ + [/second/i, 1 / (86400 * 1000)] + [/minute/i, 1 / 1440] + [/hour/i, 1 / 24] + [/week/i, 7] + [/month/i, 30.42] + [/year/i, 365.2425] + ] + if duration.test s + n *= factor + break + if /^in /i.test s + n *= -1 + days.push n + days[0] - days[1] + sortList: [[6, 0]] + # widget code contained in the jquery.tablesorter.widgets.js file + # use the zebra stripe widget if you plan on hiding any rows (filter widget) + widgets: ["uitheme", "zebra", "filter"] + widgetOptions: + # using the default zebra striping class name, so it actually isn't included in the theme variable above + # this is ONLY needed for bootstrap theming if you are using the filter widget, because rows are hidden + zebra: ["even", "odd"] + + # extra css class applied to the table row containing the filters & the inputs within that row + filter_cssFilter: "" + + # If there are child rows in the table (rows with class name from "cssChildRow" option) + # and this option is true and a match is found anywhere in the child row, then it will make that row + # visible; default is false + filter_childRows: false + + # if true, filters are collapsed initially, but can be revealed by hovering over the grey bar immediately + # below the header row. Additionally, tabbing through the document will open the filter row when an input gets focus + filter_hideFilters: false + + # Set this option to false to make the searches case sensitive + filter_ignoreCase: true + + # jQuery selector string of an element used to reset the filters + filter_reset: ".reset" + + # Use the $.tablesorter.storage utility to save the most recent filters + filter_saveFilters: true + + # Delay in milliseconds before the filter widget starts searching; This option prevents searching for + # every character while typing and should make searching large tables faster. + filter_searchDelay: 150 + + # Set this option to true to use the filter to find text from the start of the column + # So typing in "a" will find "albert" but not "frank", both have a's; default is false + filter_startsWith: false + + filter_functions: + 2: + "Full-time": filterSelectExactMatch + "Part-time": filterSelectExactMatch + "Contracting": filterSelectExactMatch + "Remote": filterSelectExactMatch + "Internship": filterSelectExactMatch + 5: + "0-1": (e, n, f, i, $r) -> n <= 1 + "2-5": (e, n, f, i, $r) -> 2 <= n <= 5 + "6+": (e, n, f, i, $r) -> 6 <= n + 6: + "Last day": (e, n, f, i, $r) -> + days = parseFloat $($r.find('td')[i]).data('profile-age') + days <= 1 + "Last week": (e, n, f, i, $r) -> + days = parseFloat $($r.find('td')[i]).data('profile-age') + days <= 7 + "Last 4 weeks": (e, n, f, i, $r) -> + days = parseFloat $($r.find('td')[i]).data('profile-age') + days <= 28 + 7: + "✓": filterSelectExactMatch + "✗": filterSelectExactMatch + 8: + "✓": filterSelectExactMatch + "✗": filterSelectExactMatch + + onCandidateClicked: (e) -> + id = $(e.target).closest('tr').data('candidate-id') + if id + if window.history + oldState = _.cloneDeep window.history.state ? {} + oldState["lastViewedCandidateID"] = id + window.history.replaceState(oldState,"") + else + window.location.hash = id + url = "/account/profile/#{id}" + window.open url,"_blank" + else + @openModalView new EmployerSignupView diff --git a/app/views/home_view.coffee b/app/views/home_view.coffee index 7c0eea234..1e314aed0 100644 --- a/app/views/home_view.coffee +++ b/app/views/home_view.coffee @@ -4,6 +4,7 @@ WizardSprite = require 'lib/surface/WizardSprite' ThangType = require 'models/ThangType' Simulator = require 'lib/simulator/Simulator' {me} = require '/lib/auth' +application = require 'application' module.exports = class HomeView extends View id: 'home-view' @@ -16,13 +17,18 @@ module.exports = class HomeView extends View getRenderData: -> c = super() if $.browser - majorVersion = parseInt($.browser.version.split('.')[0]) + majorVersion = $.browser.versionNumber c.isOldBrowser = true if $.browser.mozilla && majorVersion < 21 c.isOldBrowser = true if $.browser.chrome && majorVersion < 17 - c.isOldBrowser = true if $.browser.safari && majorVersion < 536 + c.isOldBrowser = true if $.browser.safari && majorVersion < 6 else console.warn 'no more jquery browser version...' c.isEnglish = (me.get('preferredLanguage') or 'en').startsWith 'en' + c.languageName = me.get('preferredLanguage') + # A/B test: https://github.com/codecombat/codecombat/issues/769 + c.frontPageContent = {0: "video", 1: "screenshot", 2: "nothing"}[me.get('testGroupNumber') % 3] + application.tracker.identify frontPageContent: c.frontPageContent + application.tracker.trackEvent 'Front Page Content', frontPageContent: c.frontPageContent c afterRender: -> diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index 768072a26..f5a69f5bf 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -11,7 +11,6 @@ makeScopeName = -> "view-scope-#{classCount++}" doNothing = -> module.exports = class CocoView extends Backbone.View - startsLoading: false cache: false # signals to the router to keep this view around template: -> '' @@ -27,20 +26,19 @@ module.exports = class CocoView extends Backbone.View # load progress properties loadProgress: - num: 0 - denom: 0 - showing: false - resources: [] # models and collections - requests: [] # jqxhr's - somethings: [] # everything else progress: 0 # Setup, Teardown constructor: (options) -> @loadProgress = _.cloneDeep @loadProgress - @supermodel ?= options?.supermodel or new SuperModel() + @supermodel ?= new SuperModel() @options = options + if options?.supermodel # kind of a hacky way to get each view to store its own progress + @supermodel.models = options.supermodel.models + @supermodel.collections = options.supermodel.collections + @supermodel.shouldSaveBackups = options.supermodel.shouldSaveBackups + @subscriptions = utils.combineAncestralObject(@, 'subscriptions') @events = utils.combineAncestralObject(@, 'events') @scope = makeScopeName() @@ -48,7 +46,13 @@ module.exports = class CocoView extends Backbone.View @subviews = {} @listenToShortcuts() @updateProgressBar = _.debounce @updateProgressBar, 100 + @toggleModal = _.debounce @toggleModal, 100 # Backbone.Mediator handles subscription setup/teardown automatically + + @listenTo(@supermodel, 'loaded-all', @onLoaded) + @listenTo(@supermodel, 'update-progress', @updateProgress) + @listenTo(@supermodel, 'failed', @onResourceLoadFailed) + super options destroy: -> @@ -88,8 +92,13 @@ module.exports = class CocoView extends Backbone.View super() return @template if _.isString(@template) @$el.html @template(@getRenderData()) + + if not @supermodel.finished() + @showLoading() + else + @hideLoading() + @afterRender() - @showLoading() if @startsLoading or @loading() # TODO: Remove startsLoading entirely @$el.i18n() @ @@ -101,103 +110,37 @@ module.exports = class CocoView extends Backbone.View context.fbRef = context.pathname.replace(/[^a-zA-Z0-9+/=\-.:_]/g, '').slice(0, 40) or 'home' context.isMobile = @isMobile() context.isIE = @isIE() + context.moment = moment + context.translate = $.i18n.t context afterRender: -> - - # Resource and request loading management for any given view - - addResourceToLoad: (modelOrCollection, name, value=1) -> - @loadProgress.resources.push {resource:modelOrCollection, value:value, name:name} - @listenToOnce modelOrCollection, 'sync', @updateProgress - @listenTo modelOrCollection, 'error', @onResourceLoadFailed - @updateProgress() - - addRequestToLoad: (jqxhr, name, retryFunc, value=1) -> - @loadProgress.requests.push {request:jqxhr, value:value, name: name, retryFunc: retryFunc} - jqxhr.done @updateProgress - jqxhr.fail @onRequestLoadFailed - addSomethingToLoad: (name, value=1) -> - @loadProgress.somethings.push {loaded: false, name: name, value: value} - @updateProgress() - - somethingLoaded: (name) -> - r = _.find @loadProgress.somethings, {name: name} - return console.error 'Could not find something called', name if not r - r.loaded = true - @updateProgress(name) - - loading: -> - return false if @loaded - for r in @loadProgress.resources - return true if not r.resource.loaded - for r in @loadProgress.requests - return true if not r.request.status - for r in @loadProgress.somethings - return true if not r.loaded - return false - - updateProgress: => - console.debug 'Loaded', r.name if arguments[0] and r = _.find @loadProgress.resources, {resource:arguments[0]} - console.debug 'Loaded', r.name if arguments[2] and r = _.find @loadProgress.requests, {request:arguments[2]} - console.debug 'Loaded', r.name if arguments[0] and r = _.find @loadProgress.somethings, {name:arguments[0]} - - denom = 0 - denom += r.value for r in @loadProgress.resources - denom += r.value for r in @loadProgress.requests - denom += r.value for r in @loadProgress.somethings - num = @loadProgress.num - num += r.value for r in @loadProgress.resources when r.resource.loaded - num += r.value for r in @loadProgress.requests when r.request.status - num += r.value for r in @loadProgress.somethings when r.loaded - #console.log 'update progress', @, num, denom, arguments - - progress = if denom then num / denom else 0 - # sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up + updateProgress: (progress) -> @loadProgress.progress = progress if progress > @loadProgress.progress - @updateProgressBar() - if num is denom and not @loaded - @loaded = true - @onLoaded() - - updateProgressBar: => - prog = "#{parseInt(@loadProgress.progress*100)}%" - @$el.find('.loading-screen .progress-bar').css('width', prog) + @updateProgressBar(progress) - onLoaded: -> - @render() + updateProgressBar: (progress) => + prog = "#{parseInt(progress*100)}%" + @$el?.find('.loading-container .progress-bar').css('width', prog) + + onLoaded: -> @render() # Error handling for loading - - onResourceLoadFailed: (resource, jqxhr) -> - for r, index in @loadProgress.resources - break if r.resource is resource - @$el.find('.loading-screen .errors').append(loadingErrorTemplate({ - status:jqxhr.status, + onResourceLoadFailed: (e) -> + r = e.resource + @$el.find('.loading-container .errors').append(loadingErrorTemplate({ + status: r.jqxhr?.status name: r.name - resourceIndex: index, - responseText: jqxhr.responseText + resourceIndex: r.rid, + responseText: r.jqxhr?.responseText })).i18n() - + onRetryResource: (e) -> - r = @loadProgress.resources[$(e.target).data('resource-index')] - r.resource.fetch() - $(e.target).closest('.loading-error-alert').remove() - - onRequestLoadFailed: (jqxhr) => - for r, index in @loadProgress.requests - break if r.request is jqxhr - @$el.find('.loading-screen .errors').append(loadingErrorTemplate({ - status:jqxhr.status, - name: r.name - requestIndex: index, - responseText: jqxhr.responseText - })) - - onRetryRequest: (e) -> - r = @loadProgress.requests[$(e.target).data('request-index')] - @[r.retryFunc]?() + res = @supermodel.getResource($(e.target).data('resource-index')) + # different views may respond to this call, and not all have the resource to reload + return unless res and res.isFailed + res.load() $(e.target).closest('.loading-error-alert').remove() # Modals @@ -257,7 +200,7 @@ module.exports = class CocoView extends Backbone.View showReadOnly: -> return if me.isAdmin() - warning = $.i18n.t 'editor.read_only_warning', defaultValue: "Note: you can't save any edits here, because you're not logged in as an admin." + warning = $.i18n.t 'editor.read_only_warning2', defaultValue: "Note: you can't save any edits here, because you're not logged in." noty text: warning, layout: 'center', type: 'information', killer: true, timeout: 5000 # Loading ModalViews @@ -296,18 +239,23 @@ module.exports = class CocoView extends Backbone.View # Subviews - insertSubView: (view) -> - @subviews[view.id].destroy() if view.id of @subviews - @$el.find('#'+view.id).after(view.el).remove() + insertSubView: (view, elToReplace=null) -> + key = view.id or (view.constructor.name+classCount++) + key = _.string.underscored(key) + @subviews[key].destroy() if key of @subviews + elToReplace ?= @$el.find('#'+view.id) + elToReplace.after(view.el).remove() view.parent = @ view.render() view.afterInsert() - @subviews[view.id] = view + view.parentKey = key + @subviews[key] = view + view removeSubView: (view) -> view.$el.empty() + delete @subviews[view.parentKey] view.destroy() - delete @subviews[view.id] # Utilities @@ -331,6 +279,9 @@ module.exports = class CocoView extends Backbone.View ua = navigator.userAgent or navigator.vendor or window.opera return ua.search("MSIE") != -1 + isMac: -> + navigator.platform.toUpperCase().indexOf('MAC') isnt -1 + initSlider: ($el, startValue, changeCallback) -> slider = $el.slider({ animate: "fast" }) slider.slider('value', startValue) @@ -339,6 +290,7 @@ module.exports = class CocoView extends Backbone.View slider - -mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i + mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i + +module.exports = CocoView diff --git a/app/views/kinds/ModalView.coffee b/app/views/kinds/ModalView.coffee index 5222df067..26a32081a 100644 --- a/app/views/kinds/ModalView.coffee +++ b/app/views/kinds/ModalView.coffee @@ -5,6 +5,7 @@ module.exports = class ModalView extends CocoView closeButton: true closesOnClickOutside: true modalWidthPercent: null + plain: false shortcuts: 'esc': 'hide' @@ -31,6 +32,7 @@ module.exports = class ModalView extends CocoView @$el.on 'hide.bs.modal', => @onHidden() unless @hidden @hidden = true + @$el.find('.background-wrapper').addClass('plain') if @plain afterInsert: -> super() @@ -43,9 +45,11 @@ module.exports = class ModalView extends CocoView super($el) hide: -> + @trigger 'hide' @$el.removeClass('fade').modal "hide" onHidden: -> + @trigger 'hidden' destroy: -> @hide() unless @hidden diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 2cbcf098c..09d50575b 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -17,6 +17,7 @@ module.exports = class RootView extends CocoView "click #logout-button": "logoutAccount" 'change .language-dropdown': 'onLanguageChanged' 'click .toggle-fullscreen': 'toggleFullscreen' + 'click .auth-button': 'onClickAuthbutton' logoutAccount: -> logoutUser($('#login-email').val()) @@ -26,23 +27,33 @@ module.exports = class RootView extends CocoView subview = new WizardSettingsModal {} @openModalView subview + onClickAuthbutton: -> + AuthModal = require 'views/modal/auth_modal' + @openModalView new AuthModal {} + showLoading: ($el) -> $el ?= @$el.find('.main-content-area') super($el) + renderScrollbar: -> + $('.nano-pane').css('display','none') + $ -> $('.nano').nanoScroller() + afterInsert: -> # force the browser to scroll to the hash # also messes with the browser history, so perhaps come up with a better solution super() - hash = location.hash - location.hash = '' - location.hash = hash - @buildLanguages() + #hash = location.hash + #location.hash = '' + #location.hash = hash + @renderScrollbar() #@$('.antiscroll-wrap').antiscroll() # not yet, buggy afterRender: -> super(arguments...) @chooseTab(location.hash.replace('#','')) if location.hash + @buildLanguages() + $('body').removeClass('is-playing') chooseTab: (category) -> $("a[href='##{category}']", @$el).tab('show') @@ -52,7 +63,7 @@ module.exports = class RootView extends CocoView buildLanguages: -> $select = @$el.find(".language-dropdown").empty() if $select.hasClass("fancified") - $select.parent().find('.options,.trigger').remove() + $select.parent().find('.options, .trigger').remove() $select.unwrap().removeClass("fancified") preferred = me.lang() codes = _.keys(locale) @@ -70,10 +81,8 @@ module.exports = class RootView extends CocoView $.i18n.setLng(newLang, {}) @saveLanguage(newLang) @render() - @buildLanguages() unless newLang.split('-')[0] is "en" @openModalView(application.router.getView("modal/diplomat_suggestion", "_modal")) - $('body').attr('lang', newLang) saveLanguage: (newLang) -> me.set('preferredLanguage', newLang) diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index f49eb4994..209abdd8e 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -8,7 +8,7 @@ class SearchCollection extends Backbone.Collection @url = "#{modelURL}/search?project=true" @url += "&term=#{term}" if @term -module.exports = class ThangTypeHomeView extends View +module.exports = class SearchView extends View template: template className: 'search-view' @@ -32,14 +32,17 @@ module.exports = class ThangTypeHomeView extends View when 'Level' context.currentEditor = 'editor.level_title' context.currentNew = 'editor.new_level_title' + context.currentNewSignup = 'editor.new_level_title_login' context.currentSearch = 'editor.level_search_title' when 'Thang Type' context.currentEditor = 'editor.thang_title' context.currentNew = 'editor.new_thang_title' + context.currentNewSignup = 'editor.new_thang_title_login' context.currentSearch = 'editor.thang_search_title' when 'Article' context.currentEditor = 'editor.article_title' context.currentNew = 'editor.new_article_title' + context.currentNewSignup = 'editor.new_article_title_login' context.currentSearch = 'editor.article_search_title' @$el.i18n() context @@ -96,7 +99,7 @@ module.exports = class ThangTypeHomeView extends View name = @$el.find('#name').val() model = new @model() model.set('name', name) - if @model.schema.get('properties').permissions + if @model.schema.properties.permissions model.set 'permissions', [{access: 'owner', target: me.id}] res = model.save() return unless res diff --git a/app/views/modal/auth_modal.coffee b/app/views/modal/auth_modal.coffee new file mode 100644 index 000000000..43a2e23be --- /dev/null +++ b/app/views/modal/auth_modal.coffee @@ -0,0 +1,83 @@ +View = require 'views/kinds/ModalView' +template = require 'templates/modal/auth' +{loginUser, createUser, me} = require 'lib/auth' +forms = require 'lib/forms' +User = require 'models/User' +application = require 'application' + +module.exports = class AuthModalView extends View + id: "auth-modal" + template: template + mode: 'login' # or 'signup' + + events: + # login buttons + "click #switch-to-signup-button": "onSignupInstead" + "click #signup-confirm-age": "checkAge" + 'submit': 'onSubmitForm' # handles both submit buttons + + subscriptions: + 'server-error': 'onServerError' + 'logging-in-with-facebook': 'onLoggingInWithFacebook' + + getRenderData: -> + c = super() + c.showRequiredError = @options.showRequiredError + c.title = {0: "short", 1: "long"}[me.get('testGroupNumber') % 2] + c.descriptionOn = {0: "yes", 1: "no"}[Math.floor(me.get('testGroupNumber')/2) % 2] + if @mode is 'signup' + application.tracker.identify authModalTitle: c.title + application.tracker.trackEvent 'Started Signup', authModalTitle: c.title, descriptionOn: c.descriptionOn + c.mode = @mode + c.formValues = @previousFormInputs or {} + c + + afterInsert: -> + super() + _.delay application.router.renderLoginButtons, 500 + + onSignupInstead: (e) -> + @mode = 'signup' + @previousFormInputs = forms.formToObject @$el + @render() + _.delay application.router.renderLoginButtons, 500 + + onSubmitForm: (e) -> + e.preventDefault() + if @mode is 'login' then @loginAccount() else @createAccount() + false + + checkAge: (e) -> + $("#signup-button", @$el).prop 'disabled', not $(e.target).prop('checked') + + loginAccount: -> + forms.clearFormAlerts(@$el) + userObject = forms.formToObject @$el + res = tv4.validateMultiple userObject, User.schema + return forms.applyErrorsToForm(@$el, res.errors) unless res.valid + @enableModalInProgress(@$el) # TODO: part of forms + loginUser(userObject) + + createAccount: -> + forms.clearFormAlerts(@$el) + userObject = forms.formToObject @$el + delete userObject.subscribe + delete userObject["confirm-age"] + for key, val of me.attributes when key in ["preferredLanguage", "testGroupNumber", "dateCreated", "wizardColor1", "name", "music", "volume", "emails"] + userObject[key] ?= val + subscribe = @$el.find('#signup-subscribe').prop('checked') + userObject.emails ?= {} + userObject.emails.generalNews ?= {} + userObject.emails.generalNews.enabled = subscribe + res = tv4.validateMultiple userObject, User.schema + return forms.applyErrorsToForm(@$el, res.errors) unless res.valid + window.tracker?.trackEvent 'Finished Signup' + @enableModalInProgress(@$el) + createUser userObject, null, window.nextLevelURL + + onLoggingInWithFacebook: (e) -> + modal = $('.modal:visible', @$el) + @enableModalInProgress(modal) # TODO: part of forms + + onServerError: (e) -> # TODO: work error handling into a separate forms system + @disableModalInProgress(@$el) \ No newline at end of file diff --git a/app/views/modal/contact_modal.coffee b/app/views/modal/contact_modal.coffee index dd3f0c40e..2e313d44a 100644 --- a/app/views/modal/contact_modal.coffee +++ b/app/views/modal/contact_modal.coffee @@ -6,16 +6,15 @@ forms = require 'lib/forms' contactSchema = additionalProperties: false + required: ['email', 'message'] properties: email: - required: true type: 'string' maxLength: 100 minLength: 1 format: 'email' message: - required: true type: 'string' minLength: 1 diff --git a/app/views/modal/diplomat_suggestion_modal.coffee b/app/views/modal/diplomat_suggestion_modal.coffee index 35d173c57..511fe369a 100644 --- a/app/views/modal/diplomat_suggestion_modal.coffee +++ b/app/views/modal/diplomat_suggestion_modal.coffee @@ -11,8 +11,7 @@ module.exports = class DiplomatSuggestionView extends View "click #subscribe-button": "subscribeAsDiplomat" subscribeAsDiplomat: -> - currentSubscriptions = me.get("emailSubscriptions") - me.set("emailSubscriptions", currentSubscriptions.concat ["translator"]) if "translator" not in currentSubscriptions + me.setEmailSubscription 'diplomatNews', true me.save() $("#email_translator").prop("checked", 1) @hide() diff --git a/app/views/modal/employer_signup_modal.coffee b/app/views/modal/employer_signup_modal.coffee new file mode 100644 index 000000000..e7d88ac57 --- /dev/null +++ b/app/views/modal/employer_signup_modal.coffee @@ -0,0 +1,114 @@ +View = require 'views/kinds/ModalView' +template = require 'templates/modal/employer_signup_modal' +forms = require('lib/forms') +User = require 'models/User' +auth = require('lib/auth') +me = auth.me + +module.exports = class EmployerSignupView extends View + id: "employer-signup" + template: template + closeButton: true + + + subscriptions: + "server-error": "onServerError" + 'linkedin-loaded': 'onLinkedInLoaded' + "created-user-without-reload": 'createdAccount' + + events: + "click #contract-agreement-button": "agreeToContract" + "click #create-account-button": "createAccount" + "click .login-link": "setHashToOpenModalAutomatically" + "keydown": "checkForFormSubmissionEnterPress" + + + constructor: (options) -> + super(options) + @authorizedWithLinkedIn = IN?.User?.isAuthorized() + window.tracker?.trackEvent 'Started Employer Signup' + @reloadWhenClosed = false + @linkedinLoaded = Boolean(IN.parse) + @waitingForLinkedIn = false + window.contractCallback = => + @authorizedWithLinkedIn = IN?.User?.isAuthorized() + @render() + + onLinkedInLoaded: => + @linkedinLoaded = true + if @waitingForLinkedIn + @renderLinkedInButton() + + renderLinkedInButton: => + IN.parse() + + onServerError: (e) -> + @disableModalInProgress(@$el) + + afterInsert: -> + super() + linkedInButtonParentElement = document.getElementById("linkedInAuthButton") + if linkedInButtonParentElement + if @linkedinLoaded + @renderLinkedInButton() + else + @waitingForLinkedIn = true + + getRenderData: -> + context = super() + context.userIsAuthorized = @authorizedWithLinkedIn + context.userHasSignedContract = "employer" in me.get("permissions") + context.userIsAnonymous = context.me.get('anonymous') + context + + agreeToContract: -> + application.linkedinHandler.constructEmployerAgreementObject (err, profileData) => + if err? then return handleAgreementFailure err + $.ajax + url: "/db/user/#{me.id}/agreeToEmployerAgreement" + data: profileData + type: "POST" + success: @handleAgreementSuccess + error: @handleAgreementFailure + + handleAgreementSuccess: (result) -> + window.tracker?.trackEvent 'Employer Agreed to Contract' + me.fetch() + window.location.reload() + + handleAgreementFailure: (error) -> + alert "There was an error signing the contract. Please contact team@codecombat.com with this error: #{error.responseText}" + + checkForFormSubmissionEnterPress: (e) -> + if e.which is 13 then @createAccount(e) + + createAccount: (e) => + window.tracker?.trackEvent 'Finished Employer Signup' + e.stopPropagation() + forms.clearFormAlerts(@$el) + userObject = forms.formToObject @$el + delete userObject.subscribe + for key, val of me.attributes when key in ["preferredLanguage", "testGroupNumber", "dateCreated", "wizardColor1", "name", "music", "volume", "emails"] + userObject[key] ?= val + userObject.emails ?= {} + userObject.emails.employerNotes = {enabled: true} + res = tv4.validateMultiple userObject, User.schema + return forms.applyErrorsToForm(@$el, res.errors) unless res.valid + @enableModalInProgress(@$el) + auth.createUserWithoutReload userObject, null + + setHashToOpenModalAutomatically: (e) -> + window.location.hash = "employerSignupLoggingIn" + + createdAccount: -> + @reloadWhenClosed = true + @listenTo me,"sync", => + @render() + IN.parse() + me.fetch() + + destroy: -> + reloadWhenClosed = @reloadWhenClosed + super() + if reloadWhenClosed + window.location.reload() diff --git a/app/views/modal/job_profile_contact_modal.coffee b/app/views/modal/job_profile_contact_modal.coffee new file mode 100644 index 000000000..236301454 --- /dev/null +++ b/app/views/modal/job_profile_contact_modal.coffee @@ -0,0 +1,41 @@ +ContactView = require 'views/modal/contact_modal' +template = require 'templates/modal/job_profile_contact' + +forms = require 'lib/forms' +{sendContactMessage} = require 'lib/contact' + +contactSchema = + additionalProperties: false + required: ['email', 'message'] + properties: + email: + type: 'string' + maxLength: 100 + minLength: 1 + format: 'email' + + subject: + type: 'string' + minLength: 1 + + message: + type: 'string' + minLength: 1 + + recipientID: + type: 'string' + minLength: 1 + +module.exports = class JobProfileContactView extends ContactView + id: "job-profile-contact-modal" + template: template + + contact: -> + forms.clearFormAlerts @$el + contactMessage = forms.formToObject @$el + contactMessage.recipientID = @options.recipientID + res = tv4.validateMultiple contactMessage, contactSchema + return forms.applyErrorsToForm @$el, res.errors unless res.valid + contactMessage.message += '\n\n\n\n[CodeCombat says: please let us know if you end up accepting this job. Thanks!]' + window.tracker?.trackEvent 'Sent Job Profile Message', message: contactMessage + sendContactMessage contactMessage, @$el diff --git a/app/views/modal/login_modal.coffee b/app/views/modal/login_modal.coffee deleted file mode 100644 index 8a433dd28..000000000 --- a/app/views/modal/login_modal.coffee +++ /dev/null @@ -1,46 +0,0 @@ -View = require 'views/kinds/ModalView' -template = require 'templates/modal/login' -{loginUser} = require('lib/auth') -forms = require('lib/forms') -User = require 'models/User' - -filterKeyboardEvents = (allowedEvents, func) -> - return (splat...) -> - e = splat[0] - return unless e.keyCode in allowedEvents or not e.keyCode - return func(splat...) - -module.exports = class LoginModalView extends View - id: "login-modal" - template: template - - events: - "click #login-button": "loginAccount" - "keydown #login-password": "loginAccount" - - subscriptions: - 'server-error': 'onServerError' - 'logging-in-with-facebook': 'onLoggingInWithFacebook' - - onServerError: (e) -> # TODO: work error handling into a separate forms system - @disableModalInProgress(@$el) - - constructor: (options) -> - @loginAccount = filterKeyboardEvents([13], @loginAccount) # TODO: part of forms - super options - - onLoggingInWithFacebook: (e) -> - modal = $('.modal:visible', @$el) - @enableModalInProgress(modal) # TODO: part of forms - - loginAccount: (e) => - forms.clearFormAlerts(@$el) - userObject = forms.formToObject @$el - res = tv4.validateMultiple userObject, User.schema.attributes - return forms.applyErrorsToForm(@$el, res.errors) unless res.valid - @enableModalInProgress(@$el) # TODO: part of forms - loginUser(userObject) - - afterInsert: -> - super() - application.router.renderLoginButtons() diff --git a/app/views/modal/model_modal.coffee b/app/views/modal/model_modal.coffee new file mode 100644 index 000000000..67602431d --- /dev/null +++ b/app/views/modal/model_modal.coffee @@ -0,0 +1,46 @@ +View = require 'views/kinds/ModalView' +template = require 'templates/modal/model' + +module.exports = class ModelModal extends View + id: 'model-modal' + template: template + + constructor: (options) -> + super options + @models = options.models + for model in @models when not model.loaded + @supermodel.loadModel model, 'source_document' + model.fetch() + + getRenderData: -> + c = super() + c.models = @models + c + + afterRender: -> + return unless @supermodel.finished() + for model in @models + data = $.extend true, {}, model.attributes + schema = $.extend true, {}, model.schema() + treemaOptions = + schema: schema + data: data + readOnly: false + modelTreema = @$el.find(".model-treema[data-model-id='#{model.id}']").treema treemaOptions + modelTreema?.build() + modelTreema?.open() + @openTastyTreemas modelTreema, model + + openTastyTreemas: (modelTreema, model) -> + # To save on quick inspection, let's auto-open the properties we're most likely to want to see. + delicacies = ['code'] + for dish in delicacies + child = modelTreema.childrenTreemas[dish] + child?.open() + if child and dish is 'code' and model.type() is 'LevelSession' and team = modelTreema.get('team') + desserts = { + humans: ['programmable-tharin', 'programmable-librarian'] + ogres: ['programmable-brawler', 'programmable-shaman'] + }[team] + for dessert in desserts + child.childrenTreemas[dessert]?.open() diff --git a/app/views/modal/revert_modal.coffee b/app/views/modal/revert_modal.coffee index 91358988c..68094c371 100644 --- a/app/views/modal/revert_modal.coffee +++ b/app/views/modal/revert_modal.coffee @@ -5,16 +5,16 @@ CocoModel = require 'models/CocoModel' module.exports = class RevertModal extends ModalView id: 'revert-modal' template: template - + events: 'click #changed-models button': 'onRevertModel' - + onRevertModel: (e) -> id = $(e.target).val() CocoModel.backedUp[id].revert() $(e.target).closest('tr').remove() @reloadOnClose = true - + getRenderData: -> c = super() models = _.values CocoModel.backedUp @@ -23,5 +23,4 @@ module.exports = class RevertModal extends ModalView c onHidden: -> - console.log 'reload?', @reloadOnClose location.reload() if @reloadOnClose diff --git a/app/views/modal/save_version_modal.coffee b/app/views/modal/save_version_modal.coffee index 86e1ea96b..9b7e0f110 100644 --- a/app/views/modal/save_version_modal.coffee +++ b/app/views/modal/save_version_modal.coffee @@ -1,18 +1,44 @@ ModalView = require 'views/kinds/ModalView' template = require 'templates/modal/save_version' +DeltaView = require 'views/editor/delta' +Patch = require 'models/Patch' +forms = require 'lib/forms' module.exports = class SaveVersionModal extends ModalView id: 'save-version-modal' template: template + plain: true + modalWidthPercent: 60 events: 'click #save-version-button': 'onClickSaveButton' 'click #cla-link': 'onClickCLALink' 'click #agreement-button': 'onAgreedToCLA' - - afterRender: -> + 'click #submit-patch-button': 'submitPatch' + 'submit form': 'submitPatch' + + constructor: (options) -> + super options + @model = options.model or options.level + @isPatch = not @model.hasWriteAccess() + + getRenderData: -> + c = super() + c.isPatch = @isPatch + c.hasChanges = @model.hasLocalChanges() + c + + afterRender: (insertDeltaView=true) -> super() @$el.find(if me.get('signedCLA') then '#accept-cla-wrapper' else '#save-version-button').hide() + changeEl = @$el.find('.changes-stub') + if insertDeltaView + try + deltaView = new DeltaView({model:@model}) + @insertSubView(deltaView, changeEl) + catch e + console.error "Couldn't create delta view:", e + @$el.find('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg')) onClickSaveButton: -> Backbone.Mediator.publish 'save-new-version', { @@ -20,6 +46,28 @@ module.exports = class SaveVersionModal extends ModalView commitMessage: @$el.find('#commit-message').val() } + submitPatch: -> + forms.clearFormAlerts @$el + patch = new Patch() + patch.set 'delta', @model.getDelta() + patch.set 'commitMessage', @$el.find('#commit-message').val() + patch.set 'target', { + 'collection': _.string.underscored @model.constructor.className + 'id': @model.id + } + errors = patch.validate() + forms.applyErrorsToForm(@$el, errors) if errors + patch.set 'editPath', document.location.pathname + res = patch.save() + return unless res + @enableModalInProgress(@$el) + + res.error => + @disableModalInProgress(@$el) + + res.success => + @hide() + onClickCLALink: -> window.open('/cla', 'cla', 'height=800,width=900') @@ -37,4 +85,4 @@ module.exports = class SaveVersionModal extends ModalView @$el.find('#save-version-button').show() onAgreeFailed: => - @$el.find('#agreement-button').text('Failed').prop('disabled', false) \ No newline at end of file + @$el.find('#agreement-button').text('Failed').prop('disabled', false) diff --git a/app/views/modal/signup_modal.coffee b/app/views/modal/signup_modal.coffee index 5ecbc07c5..e368034cf 100644 --- a/app/views/modal/signup_modal.coffee +++ b/app/views/modal/signup_modal.coffee @@ -1,64 +1,4 @@ -View = require 'views/kinds/ModalView' -template = require 'templates/modal/signup' -{createUser, me} = require('lib/auth') -forms = require('lib/forms') -User = require 'models/User' +AuthModal = require 'views/modal/auth_modal' -filterKeyboardEvents = (allowedEvents, func) -> - return (splat...) -> - e = splat[0] - return unless e.keyCode in allowedEvents or not e.keyCode - return func(splat...) - -module.exports = class SignupModalView extends View - id: "signup-modal" - template: template - - events: - "click #signup-confirm-age": "checkAge" - "click #signup-button": "createAccount" - "keydown input": "createAccount" - - subscriptions: - 'server-error': 'onServerError' - 'logging-in-with-facebook': 'onLoggingInWithFacebook' - - onServerError: (e) -> # TODO: work error handling into a separate forms system - @disableModalInProgress(@$el) - - constructor: (options) -> - @createAccount = filterKeyboardEvents([13], @createAccount) # TODO: part of forms - super options - window.tracker?.trackEvent 'Started Signup' - - onLoggingInWithFacebook: (e) -> - modal = $('.modal:visible', @$el) - @enableModalInProgress(modal) # TODO: part of forms - - checkAge: (e) -> - $("#signup-button", @$el).prop 'disabled', not $(e.target).prop('checked') - - getRenderData: -> - c = super() - c.showRequiredError = @options.showRequiredError - c - - createAccount: (e) => - forms.clearFormAlerts(@$el) - userObject = forms.formToObject @$el - delete userObject.subscribe - delete userObject["confirm-age"] - for key, val of me.attributes when key in ["preferredLanguage", "testGroupNumber", "dateCreated", "wizardColor1", "name", "music", "volume", "emailSubscriptions"] - userObject[key] ?= val - subscribe = @$el.find('#signup-subscribe').prop('checked') - userObject.emailSubscriptions ?= [] - if subscribe - userObject.emailSubscriptions.push 'announcement' unless 'announcement' in userObject.emailSubscriptions - userObject.emailSubscriptions.push 'notification' unless 'notification' in userObject.emailSubscriptions - else - userObject.emailSubscriptions = _.without (userObject.emailSubscriptions ? []), 'announcement', 'notification' - res = tv4.validateMultiple userObject, User.schema.attributes - return forms.applyErrorsToForm(@$el, res.errors) unless res.valid - window.tracker?.trackEvent 'Finished Signup' - @enableModalInProgress(@$el) - createUser userObject, null, window.nextLevelURL +module.exports = class SignupModalView extends AuthModal + mode: 'signup' \ No newline at end of file diff --git a/app/views/modal/versions_modal.coffee b/app/views/modal/versions_modal.coffee index 97aeafb33..9a00f5d58 100755 --- a/app/views/modal/versions_modal.coffee +++ b/app/views/modal/versions_modal.coffee @@ -1,6 +1,7 @@ ModalView = require 'views/kinds/ModalView' template = require 'templates/modal/versions' tableTemplate = require 'templates/kinds/table' +DeltaView = require 'views/editor/delta' class VersionsViewCollection extends Backbone.Collection url: "" @@ -13,11 +14,16 @@ class VersionsViewCollection extends Backbone.Collection module.exports = class VersionsModalView extends ModalView template: template startsLoading: true + plain: true + modalWidthPercent: 80 # needs to be overwritten by child id: "" url: "" page: "" + + events: + 'change input.select': 'onSelectionChanged' constructor: (options, @ID, @model) -> super options @@ -34,6 +40,18 @@ module.exports = class VersionsModalView extends ModalView @startsLoading = false @render() + onSelectionChanged: -> + rows = @$el.find 'input.select:checked' + deltaEl = @$el.find '.delta-view' + @removeSubView(@deltaView) if @deltaView + @deltaView = null + if rows.length isnt 2 then return + + laterVersion = new @model(_id:$(rows[0]).val()) + earlierVersion = new @model(_id:$(rows[1]).val()) + @deltaView = new DeltaView({model:earlierVersion, comparisonModel:laterVersion, skipPaths:['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index']}) + @insertSubView(@deltaView, deltaEl) + getRenderData: (context={}) -> context = super(context) context.page = @page diff --git a/app/views/modal/wizard_settings_modal.coffee b/app/views/modal/wizard_settings_modal.coffee index 0223187bd..5715a4c1f 100644 --- a/app/views/modal/wizard_settings_modal.coffee +++ b/app/views/modal/wizard_settings_modal.coffee @@ -22,6 +22,7 @@ module.exports = class WizardSettingsModal extends View WizardSettingsView = require 'views/account/wizard_settings_view' view = new WizardSettingsView() @insertSubView view + super() checkNameExists: => forms.clearFormAlerts(@$el) @@ -31,7 +32,7 @@ module.exports = class WizardSettingsModal extends View forms.applyErrorsToForm(@$el, {property:'name', message:'is already taken'}) if id and id isnt me.id $.ajax("/db/user/#{name}/nameToID", {success: success}) - onWizardSettingsDone: => + onWizardSettingsDone: -> me.set('name', $('#wizard-settings-name').val()) forms.clearFormAlerts(@$el) res = me.validate() @@ -42,10 +43,11 @@ module.exports = class WizardSettingsModal extends View res = me.save() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) - .addClass('btn-info').show().removeClass('btn-danger') + .addClass('btn-info').show().removeClass('btn-danger') res.error => errors = JSON.parse(res.responseText) + console.warn "Got errors saving user:", errors forms.applyErrorsToForm(@$el, errors) @disableModalInProgress(@$el) @@ -53,4 +55,3 @@ module.exports = class WizardSettingsModal extends View @hide() @enableModalInProgress(@$el) - me.save() diff --git a/app/views/play/common/ladder_submission_view.coffee b/app/views/play/common/ladder_submission_view.coffee new file mode 100644 index 000000000..ccb3571a3 --- /dev/null +++ b/app/views/play/common/ladder_submission_view.coffee @@ -0,0 +1,100 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/play/common/ladder_submission' + +module.exports = class LadderSubmissionView extends CocoView + className: "ladder-submission-view" + template: template + + events: + 'click .rank-button': 'rankSession' + 'click .help-simulate': 'onHelpSimulate' + + constructor: (options) -> + super options + @session = options.session + @level = options.level + + getRenderData: -> + ctx = super() + ctx.readyToRank = @session?.readyToRank() + ctx.isRanking = @session?.get('isRanking') + ctx.simulateURL = "/play/ladder/#{@level.get('slug')}#simulate" + ctx.lastSubmitted = moment(submitDate).fromNow() if submitDate = @session?.get('submitDate') + ctx + + afterRender: -> + super() + return unless @supermodel.finished() + @rankButton = @$el.find('.rank-button') + @updateButton() + + updateButton: -> + rankingState = 'unavailable' + if @session?.readyToRank() + rankingState = 'rank' + else if @session?.get 'isRanking' + rankingState = 'ranking' + @setRankingButtonText rankingState + + setRankingButtonText: (spanClass) -> + @rankButton.find('span').hide() + @rankButton.find(".#{spanClass}").show() + @rankButton.toggleClass 'disabled', spanClass isnt 'rank' + helpSimulate = spanClass in ['submitted', 'ranking'] + @$el.find('.help-simulate').toggle(helpSimulate, 'slow') + showLastSubmitted = not (spanClass in ['submitting']) + @$el.find('.last-submitted').toggle(showLastSubmitted) + + rankSession: (e) -> + return unless @session.readyToRank() + @setRankingButtonText 'submitting' + success = => + @setRankingButtonText 'submitted' unless @destroyed + Backbone.Mediator.publish 'ladder:game-submitted', session: @session, level: @level + failure = (jqxhr, textStatus, errorThrown) => + console.log jqxhr.responseText + @setRankingButtonText 'failed' unless @destroyed + transpiledCode = @transpileSession() + + ajaxData = + session: @session.id + levelID: @level.id + originalLevelID: @level.get('original') + levelMajorVersion: @level.get('version').major + transpiledCode: transpiledCode + + $.ajax '/queue/scoring', { + type: 'POST' + data: ajaxData + success: success + error: failure + } + + transpileSession: -> + submittedCode = @session.get('code') + language = @session.get('codeLanguage') or 'javascript' + @session.set('submittedCodeLanguage', language) + @session.save() # TODO: maybe actually use a callback to make sure this works? + transpiledCode = {} + for thang, spells of submittedCode + transpiledCode[thang] = {} + for spellID, spell of spells + unless _.contains(@session.get('teamSpells')[@session.get('team')], thang + "/" + spellID) then continue + #DRY this + aetherOptions = + problems: {} + language: language + functionName: spellID + functionParameters: [] + yieldConditionally: spellID is "plan" + globals: ['Vector', '_'] + protectAPI: true + includeFlow: false + if spellID is "hear" then aetherOptions["functionParameters"] = ["speaker","message","data"] + + aether = new Aether aetherOptions + transpiledCode[thang][spellID] = aether.transpile spell + transpiledCode + + onHelpSimulate: -> + $('a[href="#simulate"]').tab('show') diff --git a/app/views/play/ladder/ladder_tab.coffee b/app/views/play/ladder/ladder_tab.coffee index b9fddcf38..f979ee005 100644 --- a/app/views/play/ladder/ladder_tab.coffee +++ b/app/views/play/ladder/ladder_tab.coffee @@ -2,9 +2,11 @@ CocoView = require 'views/kinds/CocoView' CocoClass = require 'lib/CocoClass' Level = require 'models/Level' LevelSession = require 'models/LevelSession' -CocoCollection = require 'models/CocoCollection' +CocoCollection = require 'collections/CocoCollection' +User = require 'models/User' LeaderboardCollection = require 'collections/LeaderboardCollection' {teamDataFromLevel} = require './utils' +ModelModal = require 'views/modal/model_modal' HIGHEST_SCORE = 1000000 @@ -23,6 +25,8 @@ module.exports = class LadderTabView extends CocoView events: 'click .connect-facebook': 'onConnectFacebook' 'click .connect-google-plus': 'onConnectGPlus' + 'click .name-col-cell': 'onClickPlayerName' + 'click .load-more-ladder-entries': 'onLoadMoreLadderEntries' subscriptions: 'fbapi-loaded': 'checkFriends' @@ -32,7 +36,7 @@ module.exports = class LadderTabView extends CocoView constructor: (options, @level, @sessions) -> super(options) - @addSomethingToLoad("social_network_apis") + @socialNetworkRes = @supermodel.addSomethingResource("social_network_apis", 0) @teams = teamDataFromLevel @level @leaderboards = {} @refreshLadder() @@ -41,18 +45,24 @@ module.exports = class LadderTabView extends CocoView checkFriends: -> return if @checked or (not window.FB) or (not window.gapi) @checked = true - - @addSomethingToLoad("facebook_status") + + # @addSomethingToLoad("facebook_status") + + @fbStatusRes = @supermodel.addSomethingResource("facebook_status", 0) + @fbStatusRes.load() + FB.getLoginStatus (response) => + return if @destroyed @facebookStatus = response.status @loadFacebookFriends() if @facebookStatus is 'connected' - @somethingLoaded("facebook_status") + @fbStatusRes.markLoaded() if application.gplusHandler.loggedIn is undefined @listenToOnce(application.gplusHandler, 'checked-state', @gplusSessionStateLoaded) else @gplusSessionStateLoaded() - @somethingLoaded("social_network_apis") + + @socialNetworkRes.markLoaded() # FACEBOOK @@ -63,23 +73,29 @@ module.exports = class LadderTabView extends CocoView onConnectedWithFacebook: -> location.reload() if @connecting loadFacebookFriends: -> - @addSomethingToLoad("facebook_friends") + # @addSomethingToLoad("facebook_friends") + + @fbFriendRes = @supermodel.addSomethingResource("facebook_friends", 0) + @fbFriendRes.load() + FB.api '/me/friends', @onFacebookFriendsLoaded - + onFacebookFriendsLoaded: (response) => @facebookData = response.data @loadFacebookFriendSessions() - @somethingLoaded("facebook_friends") + @fbFriendRes.markLoaded() loadFacebookFriendSessions: -> levelFrag = "#{@level.get('original')}.#{@level.get('version').major}" url = "/db/level/#{levelFrag}/leaderboard_facebook_friends" - jqxhr = $.ajax url, { + + @fbFriendSessionRes = @supermodel.addRequestResource('facebook_friend_sessions', { + url: url data: { friendIDs: (f.id for f in @facebookData) } method: 'POST' success: @onFacebookFriendSessionsLoaded - } - @addRequestToLoad(jqxhr, 'facebook_friend_sessions', 'loadFacebookFriendSessions') + }) + @fbFriendSessionRes.load() onFacebookFriendSessionsLoaded: (result) => friendsMap = {} @@ -89,7 +105,7 @@ module.exports = class LadderTabView extends CocoView friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans' friend.imageSource = "http://graph.facebook.com/#{friend.facebookID}/picture" @facebookFriendSessions = result - + # GOOGLE PLUS onConnectGPlus: -> @@ -98,26 +114,30 @@ module.exports = class LadderTabView extends CocoView application.gplusHandler.reauthorize() onConnectedWithGPlus: -> location.reload() if @connecting - + gplusSessionStateLoaded: -> if application.gplusHandler.loggedIn - @addSomethingToLoad("gplus_friends") + #@addSomethingToLoad("gplus_friends") + @gpFriendRes = @supermodel.addSomethingResource("gplus_friends", 0) + @gpFriendRes.load() application.gplusHandler.loadFriends @gplusFriendsLoaded gplusFriendsLoaded: (friends) => @gplusData = friends.items @loadGPlusFriendSessions() - @somethingLoaded("gplus_friends") + @gpFriendRes.markLoaded() loadGPlusFriendSessions: -> levelFrag = "#{@level.get('original')}.#{@level.get('version').major}" url = "/db/level/#{levelFrag}/leaderboard_gplus_friends" - jqxhr = $.ajax url, { + + @gpFriendSessionRes = @supermodel.addRequestResource('gplus_friend_sessions', { + url: url data: { friendIDs: (f.id for f in @gplusData) } method: 'POST' success: @onGPlusFriendSessionsLoaded - } - @addRequestToLoad(jqxhr, 'gplus_friend_sessions', 'loadGPlusFriendSessions') + }) + @gpFriendSessionRes.load() onGPlusFriendSessionsLoaded: (result) => friendsMap = {} @@ -127,20 +147,24 @@ module.exports = class LadderTabView extends CocoView friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans' friend.imageSource = friendsMap[friend.gplusID].image.url @gplusFriendSessions = result - + # LADDER LOADING refreshLadder: -> + @supermodel.resetProgress() + @ladderLimit ?= parseInt @getQueryVariable('top_players', 20) for team in @teams - @leaderboards[team.id]?.destroy() + if oldLeaderboard = @leaderboards[team.id] + @supermodel.removeModelResource oldLeaderboard + oldLeaderboard.destroy() teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id - @leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession) - - @addResourceToLoad @leaderboards[team.id], 'leaderboard', 3 + @leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit) + @leaderboardRes = @supermodel.addModelResource(@leaderboards[team.id], 'leaderboard', 3) + @leaderboardRes.load() render: -> super() - + @$el.find('.histogram-display').each (i, el) => histogramWrapper = $(el) team = _.find @teams, name: histogramWrapper.data('team-name') @@ -148,8 +172,8 @@ module.exports = class LadderTabView extends CocoView $.when( $.get("/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}", (data) -> histogramData = data) ).then => - @generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) - + @generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) unless @destroyed + getRenderData: -> ctx = super() ctx.level = @level @@ -166,7 +190,7 @@ module.exports = class LadderTabView extends CocoView #renders twice, hack fix if $("#"+histogramElement.attr("id")).has("svg").length then return histogramData = histogramData.map (d) -> d*100 - + margin = top: 20 right: 20 @@ -175,17 +199,17 @@ module.exports = class LadderTabView extends CocoView width = 300 - margin.left - margin.right height = 125 - margin.top - margin.bottom - + formatCount = d3.format(",.0") - + x = d3.scale.linear().domain([-3000,6000]).range([0,width]) data = d3.layout.histogram().bins(x.ticks(20))(histogramData) - y = d3.scale.linear().domain([0,d3.max(data, (d) -> d.y)]).range([height,0]) - + y = d3.scale.linear().domain([0,d3.max(data, (d) -> d.y)]).range([height,10]) + #create the x axis xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(5).outerTickSize(0) - + svg = d3.select("#"+histogramElement.attr("id")).append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) @@ -194,13 +218,13 @@ module.exports = class LadderTabView extends CocoView barClass = "bar" if teamName.toLowerCase() is "ogres" then barClass = "ogres-bar" if teamName.toLowerCase() is "humans" then barClass = "humans-bar" - + bar = svg.selectAll(".bar") .data(data) .enter().append("g") .attr("class",barClass) - .attr("transform", (d) -> "translate(#{x(d.x)},#{y(d.y)})") - + .attr("transform", (d) -> "translate(#{x(d.x)},#{y(d.y)})") + bar.append("rect") .attr("x",1) .attr("width",width/20) @@ -212,7 +236,7 @@ module.exports = class LadderTabView extends CocoView .enter().append("g") .attr("class","specialbar") .attr("transform", "translate(#{x(playerScore)},#{y(9001)})") - + scorebar.append("rect") .attr("x",1) .attr("width",3) @@ -220,9 +244,13 @@ module.exports = class LadderTabView extends CocoView rankClass = "rank-text" if teamName.toLowerCase() is "ogres" then rankClass = "rank-text ogres-rank-text" if teamName.toLowerCase() is "humans" then rankClass = "rank-text humans-rank-text" - + message = "#{histogramData.length} players" - if @leaderboards[teamName].session? then message="#{@leaderboards[teamName].myRank}/#{histogramData.length}" + if @leaderboards[teamName].session? + if @leaderboards[teamName].myRank <= histogramData.length + message="##{@leaderboards[teamName].myRank} of #{histogramData.length}" + else + message="Rank your session!" svg.append("g") .append("text") .attr("class",rankClass) @@ -230,14 +258,14 @@ module.exports = class LadderTabView extends CocoView .attr("text-anchor","end") .attr("x",width) .text(message) - + #Translate the x-axis up svg.append("g") .attr("class", "x axis") .attr("transform","translate(0," + height + ")") .call(xAxis) - - + + consolidateFriends: -> allFriendSessions = (@facebookFriendSessions or []).concat(@gplusFriendSessions or []) sessions = _.uniq allFriendSessions, false, (session) -> session._id @@ -245,17 +273,30 @@ module.exports = class LadderTabView extends CocoView sessions.reverse() sessions + # Admin view of players' code + onClickPlayerName: (e) -> + return unless me.isAdmin() + row = $(e.target).parent() + player = new User _id: row.data 'player-id' + session = new LevelSession _id: row.data 'session-id' + @openModalView new ModelModal models: [session, player] + + onLoadMoreLadderEntries: (e) -> + @ladderLimit ?= 100 + @ladderLimit += 100 + @refreshLadder() + class LeaderboardData extends CocoClass ### Consolidates what you need to load for a leaderboard into a single Backbone Model-like object. ### - - constructor: (@level, @team, @session) -> + + constructor: (@level, @team, @session, @limit) -> super() @fetch() - + fetch: -> - @topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: 20}) + @topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: @limit}) promises = [] promises.push @topPlayers.fetch() @@ -279,7 +320,7 @@ class LeaderboardData extends CocoClass @trigger 'sync', @ # TODO: cache user ids -> names mapping, and load them here as needed, # and apply them to sessions. Fetching each and every time is too costly. - + onFail: (resource, jqxhr) => return if @destroyed @trigger 'error', @, jqxhr @@ -291,8 +332,8 @@ class LeaderboardData extends CocoClass return [] unless @session?.get('totalScore') l = [] above = @playersAbove.models - above.reverse() l = l.concat(above) + l.reverse() l.push @session l = l.concat(@playersBelow.models) if @myRank diff --git a/app/views/play/ladder/ladder_view.coffee b/app/views/play/ladder/ladder_view.coffee new file mode 100644 index 000000000..6a5f16178 --- /dev/null +++ b/app/views/play/ladder/ladder_view.coffee @@ -0,0 +1,111 @@ +RootView = require 'views/kinds/RootView' +Level = require 'models/Level' +LevelSession = require 'models/LevelSession' +CocoCollection = require 'collections/CocoCollection' +{teamDataFromLevel} = require './utils' +{me} = require 'lib/auth' +application = require 'application' + +LadderTabView = require './ladder_tab' +MyMatchesTabView = require './my_matches_tab' +SimulateTabView = require './simulate_tab' +LadderPlayModal = require './play_modal' +CocoClass = require 'lib/CocoClass' + + +HIGHEST_SCORE = 1000000 + +class LevelSessionsCollection extends CocoCollection + url: '' + model: LevelSession + + constructor: (levelID) -> + super() + @url = "/db/level/#{levelID}/my_sessions" + +module.exports = class LadderView extends RootView + id: 'ladder-view' + template: require 'templates/play/ladder/ladder' + + subscriptions: + 'application:idle-changed': 'onIdleChanged' + + events: + 'click .play-button': 'onClickPlayButton' + 'click a': 'onClickedLink' + + constructor: (options, @levelID) -> + super(options) + @level = @supermodel.loadModel(new Level(_id:@levelID), 'level').model + @sessions = @supermodel.loadCollection(new LevelSessionsCollection(levelID), 'your_sessions').model + + @teams = [] + + onLoaded: -> + @teams = teamDataFromLevel @level + @render() + + getRenderData: -> + ctx = super() + ctx.level = @level + ctx.link = "/play/level/#{@level.get('name')}" + ctx.teams = @teams + ctx.levelID = @levelID + ctx.levelDescription = marked(@level.get('description')) if @level.get('description') + ctx._ = _ + ctx.tournamentTimeLeft = moment(new Date(1402444800000)).fromNow() + ctx + + afterRender: -> + super() + # console.debug 'gintau', 'ladder_view-afterRender', @supermodel.finished() + return unless @supermodel.finished() + @insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions)) + @insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions)) + @insertSubView(@simulateTab = new SimulateTabView()) + @refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 20 * 1000) + hash = document.location.hash[1..] if document.location.hash + if hash and not (hash in ['my-matches', 'simulate', 'ladder', 'prizes', 'rules']) + @showPlayModal(hash) if @sessions.loaded + + fetchSessionsAndRefreshViews: -> + return if @destroyed or application.userIsIdle or (new Date() - 2000 < @lastRefreshTime) or not @supermodel.finished() + @sessions.fetch({"success": @refreshViews}) + + refreshViews: => + return if @destroyed or application.userIsIdle + @lastRefreshTime = new Date() + @ladderTab.refreshLadder() + @myMatchesTab.refreshMatches() + @simulateTab.refresh() + + onIdleChanged: (e) -> + @fetchSessionsAndRefreshViews() unless e.idle + + onClickPlayButton: (e) -> + @showPlayModal($(e.target).closest('.play-button').data('team')) + + showPlayModal: (teamID) -> + return @showApologeticSignupModal() if me.get('anonymous') + session = (s for s in @sessions.models when s.get('team') is teamID)[0] + modal = new LadderPlayModal({}, @level, session, teamID) + @openModalView modal + + showApologeticSignupModal: -> + SignupModal = require 'views/modal/auth_modal' + @openModalView(new SignupModal({showRequiredError:true})) + + onClickedLink: (e) -> + link = $(e.target).closest('a').attr('href') + if link?.startsWith('/play/level') and me.get('anonymous') + e.stopPropagation() + e.preventDefault() + @showApologeticSignupModal() + if link and /#rules$/.test link + @$el.find('a[href="#rules"]').tab('show') + if link and /#prizes/.test link + @$el.find('a[href="#prizes"]').tab('show') + + destroy: -> + clearInterval @refreshInterval + super() diff --git a/app/views/play/ladder/my_matches_tab.coffee b/app/views/play/ladder/my_matches_tab.coffee index e3f0fc62a..3dfe87cbd 100644 --- a/app/views/play/ladder/my_matches_tab.coffee +++ b/app/views/play/ladder/my_matches_tab.coffee @@ -2,6 +2,7 @@ CocoView = require 'views/kinds/CocoView' Level = require 'models/Level' LevelSession = require 'models/LevelSession' LeaderboardCollection = require 'collections/LeaderboardCollection' +LadderSubmissionView = require 'views/play/common/ladder_submission_view' {teamDataFromLevel} = require './utils' module.exports = class MyMatchesTabView extends CocoView @@ -9,12 +10,10 @@ module.exports = class MyMatchesTabView extends CocoView template: require 'templates/play/ladder/my_matches_tab' startsLoading: true - events: - 'click .rank-button': 'rankSession' - constructor: (options, @level, @sessions) -> super(options) @nameMap = {} + @previouslyRankingTeams = {} @refreshMatches() refreshMatches: -> @@ -27,15 +26,19 @@ module.exports = class MyMatchesTabView extends CocoView for session in @sessions.models for match in (session.get('matches') or []) id = match.opponents[0].userID + unless id + console.error "Found bad opponent ID in malformed match:", match, "from session", session + continue ids.push id unless @nameMap[id] return @finishRendering() unless ids.length success = (nameMap) => + return if @destroyed for session in @sessions.models for match in session.get('matches') or [] opponent = match.opponents[0] - @nameMap[opponent.userID] ?= nameMap[opponent.userID] + @nameMap[opponent.userID] ?= nameMap[opponent.userID]?.name ? "" @finishRendering() $.ajax('/db/user/-/names', { @@ -48,8 +51,6 @@ module.exports = class MyMatchesTabView extends CocoView @startsLoading = false @render() - - getRenderData: -> ctx = super() ctx.level = @level @@ -61,6 +62,9 @@ module.exports = class MyMatchesTabView extends CocoView state = 'win' state = 'loss' if match.metrics.rank > opponent.metrics.rank state = 'tie' if match.metrics.rank is opponent.metrics.rank + fresh = match.date > (new Date(new Date() - 20 * 1000)).toISOString() + if fresh + Backbone.Mediator.publish 'play-sound', trigger: 'chat_received' { state: state opponentName: @nameMap[opponent.userID] @@ -68,11 +72,12 @@ module.exports = class MyMatchesTabView extends CocoView when: moment(match.date).fromNow() sessionID: opponent.sessionID stale: match.date < submitDate + fresh: fresh } for team in @teams team.session = (s for s in @sessions.models when s.get('team') is team.id)[0] - team.readyToRank = @readyToRank(team.session) + team.readyToRank = team.session?.readyToRank() team.isRanking = team.session?.get('isRanking') team.matches = (convertMatch(match, team.session.get('submitDate')) for match in team.session?.get('matches') or []) team.matches.reverse() @@ -83,61 +88,48 @@ module.exports = class MyMatchesTabView extends CocoView scoreHistory = team.session?.get('scoreHistory') if scoreHistory?.length > 1 team.scoreHistory = scoreHistory - scoreHistory = _.last scoreHistory, 100 # Chart URL needs to be under 2048 characters for GET - - team.currentScore = Math.round scoreHistory[scoreHistory.length - 1][1] * 100 - team.chartColor = team.primaryColor.replace '#', '' - #times = (s[0] for s in scoreHistory) - #times = ((100 * (t - times[0]) / (times[times.length - 1] - times[0])).toFixed(1) for t in times) - # Let's try being independent of time. - times = (i for s, i in scoreHistory) - scores = (s[1] for s in scoreHistory) - lowest = _.min scores #.concat([0]) - highest = _.max scores #.concat(50) - scores = (Math.round(100 * (s - lowest) / (highest - lowest)) for s in scores) - team.chartData = times.join(',') + '|' + scores.join(',') - team.minScore = Math.round(100 * lowest) - team.maxScore = Math.round(100 * highest) + + if not team.isRanking and @previouslyRankingTeams[team.id] + Backbone.Mediator.publish 'play-sound', trigger: 'cast-end' + @previouslyRankingTeams[team.id] = team.isRanking ctx afterRender: -> super() - @$el.find('.rank-button').each (i, el) => - button = $(el) - sessionID = button.data('session-id') + @removeSubView subview for key, subview of @subviews when subview instanceof LadderSubmissionView + @$el.find('.ladder-submission-view').each (i, el) => + placeholder = $(el) + sessionID = placeholder.data('session-id') session = _.find @sessions.models, {id: sessionID} - rankingState = 'unavailable' - if @readyToRank session - rankingState = 'rank' - else if session.get 'isRanking' - rankingState = 'ranking' - @setRankingButtonText button, rankingState - + ladderSubmissionView = new LadderSubmissionView session: session, level: @level + @insertSubView ladderSubmissionView, placeholder + @$el.find('.score-chart-wrapper').each (i, el) => scoreWrapper = $(el) team = _.find @teams, name: scoreWrapper.data('team-name') @generateScoreLineChart(scoreWrapper.attr('id'), team.scoreHistory, team.name) - + + @$el.find('tr.fresh').removeClass('fresh', 5000) generateScoreLineChart: (wrapperID, scoreHistory,teamName) => - margin = + margin = top: 20 right: 20 bottom: 30 left: 50 - + width = 450 - margin.left - margin.right height = 125 x = d3.time.scale().range([0,width]) y = d3.scale.linear().range([height,0]) - + xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(4).outerTickSize(0) yAxis = d3.svg.axis().scale(y).orient("left").ticks(4).outerTickSize(0) - + line = d3.svg.line().x(((d) -> x(d.date))).y((d) -> y(d.close)) selector = "#" + wrapperID - + svg = d3.select(selector).append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) @@ -150,12 +142,10 @@ module.exports = class MyMatchesTabView extends CocoView date: time close: d[1] * 100 } - + x.domain(d3.extent(data, (d) -> d.date)) y.domain(d3.extent(data, (d) -> d.close)) - - - + svg.append("g") .attr("class", "y axis") .call(yAxis) @@ -172,45 +162,3 @@ module.exports = class MyMatchesTabView extends CocoView .datum(data) .attr("class",lineClass) .attr("d",line) - - - - - readyToRank: (session) -> - return false unless session?.get('levelID') # If it hasn't been denormalized, then it's not ready. - return false unless c1 = session.get('code') - return false unless team = session.get('team') - return true unless c2 = session.get('submittedCode') - thangSpellArr = (s.split("/") for s in session.get('teamSpells')[team]) - for item in thangSpellArr - thang = item[0] - spell = item[1] - return true if c1[thang][spell] isnt c2[thang][spell] - return false - - rankSession: (e) -> - button = $(e.target).closest('.rank-button') - sessionID = button.data('session-id') - session = _.find @sessions.models, {id: sessionID} - return unless @readyToRank(session) - - @setRankingButtonText(button, 'submitting') - success = => - @setRankingButtonText(button, 'submitted') - failure = (jqxhr, textStatus, errorThrown) => - console.log jqxhr.responseText - @setRankingButtonText(button, 'failed') - - ajaxData = {session: sessionID, levelID: @level.id, originalLevelID: @level.attributes.original, levelMajorVersion: @level.attributes.version.major} - console.log "Posting game for ranking from My Matches view." - $.ajax '/queue/scoring', { - type: 'POST' - data: ajaxData - success: success - error: failure - } - - setRankingButtonText: (rankButton, spanClass) -> - rankButton.find('span').addClass('hidden') - rankButton.find(".#{spanClass}").removeClass('hidden') - rankButton.toggleClass 'disabled', spanClass isnt 'rank' diff --git a/app/views/play/ladder/play_modal.coffee b/app/views/play/ladder/play_modal.coffee index 2272be191..7cfd850b8 100644 --- a/app/views/play/ladder/play_modal.coffee +++ b/app/views/play/ladder/play_modal.coffee @@ -11,6 +11,7 @@ module.exports = class LadderPlayModal extends View closeButton: true startsLoading: true @shownTutorialButton: false + tutorialLevelExists: null events: 'click #skip-tutorial-button': 'hideTutorialButtons' @@ -58,9 +59,11 @@ module.exports = class LadderPlayModal extends View # PART 4: Render finishRendering: -> - @startsLoading = false - @render() - @maybeShowTutorialButtons() + @checkTutorialLevelExists (exists) => + @tutorialLevelExists = exists + @startsLoading = false + @render() + @maybeShowTutorialButtons() getRenderData: -> ctx = super() @@ -69,6 +72,7 @@ module.exports = class LadderPlayModal extends View ctx.teamName = _.string.titleize @team ctx.teamID = @team ctx.otherTeamID = @otherTeam + ctx.tutorialLevelExists = @tutorialLevelExists teamsList = teamDataFromLevel @level teams = {} @@ -94,7 +98,7 @@ module.exports = class LadderPlayModal extends View ctx maybeShowTutorialButtons: -> - return if @session or LadderPlayModal.shownTutorialButton + return if @session or LadderPlayModal.shownTutorialButton or not @tutorialLevelExists @$el.find('#normal-view').addClass('secret') @$el.find('.modal-header').addClass('secret') @$el.find('#noob-view').removeClass('secret') @@ -105,6 +109,17 @@ module.exports = class LadderPlayModal extends View @$el.find('.modal-header').removeClass('secret') @$el.find('#noob-view').addClass('secret') + checkTutorialLevelExists: (cb) -> + levelID = @level.get('slug') or @level.id + tutorialLevelID = "#{levelID}-tutorial" + success = => cb true + failure = => cb false + $.ajax + type: "GET" + url: "/db/level/#{tutorialLevelID}/exists" + success: success + error: failure + # Choosing challengers getChallengers: -> diff --git a/app/views/play/ladder/simulate_tab.coffee b/app/views/play/ladder/simulate_tab.coffee new file mode 100644 index 000000000..58e559f6d --- /dev/null +++ b/app/views/play/ladder/simulate_tab.coffee @@ -0,0 +1,143 @@ +CocoView = require 'views/kinds/CocoView' +CocoClass = require 'lib/CocoClass' +SimulatorsLeaderboardCollection = require 'collections/SimulatorsLeaderboardCollection' +Simulator = require 'lib/simulator/Simulator' +{me} = require 'lib/auth' + +module.exports = class SimulateTabView extends CocoView + id: 'simulate-tab-view' + template: require 'templates/play/ladder/simulate_tab' + + events: + 'click #simulate-button': 'onSimulateButtonClick' + 'click #simulate-all-button': 'onSimulateAllButtonClick' + + constructor: (options) -> + super(options) + @simulatorsLeaderboardData = new SimulatorsLeaderboardData(me) + @simulatorsLeaderboardDataRes = @supermodel.addModelResource(@simulatorsLeaderboardData, 'top_simulators') + @simulatorsLeaderboardDataRes.load() + + @simulator = new Simulator() + @listenTo(@simulator, 'statusUpdate', @updateSimulationStatus) + + onLoaded: -> + super() + @render() + + getRenderData: -> + ctx = super() + ctx.simulationStatus = @simulationStatus + ctx.simulatorsLeaderboardData = @simulatorsLeaderboardData + ctx.numberOfGamesInQueue = @simulatorsLeaderboardData.numberOfGamesInQueue + ctx._ = _ + ctx + + afterRender: -> + super() + + # Simulations + + onSimulateButtonClick: (e) -> + application.tracker?.trackEvent 'Simulate Button Click', {} + $("#simulate-button").prop "disabled", true + $("#simulate-button").text "Simulating..." + + @simulator.fetchAndSimulateTask() + + refresh: -> + success = (numberOfGamesInQueue) -> + $("#games-in-queue").text numberOfGamesInQueue + $.ajax "/queue/messagesInQueueCount", {success} + + updateSimulationStatus: (simulationStatus, sessions) -> + @simulationStatus = simulationStatus + try + if sessions? + #TODO: Fetch names from Redis, the creatorName is denormalized + creatorNames = (session.creatorName for session in sessions) + @simulationStatus = "Simulating game between " + for index in [0...creatorNames.length] + unless creatorNames[index] + creatorNames[index] = "Anonymous" + @simulationStatus += (if index != 0 then " and " else "") + creatorNames[index] + @simulationStatus += "..." + catch e + console.log "There was a problem with the named simulation status: #{e}" + $("#simulation-status-text").text @simulationStatus + + resimulateAllSessions: -> + postData = + originalLevelID: @level.get('original') + levelMajorVersion: @level.get('version').major + console.log postData + + $.ajax + url: '/queue/scoring/resimulateAllSessions' + method: 'POST' + data: postData + complete: (jqxhr) -> + console.log jqxhr.responseText + + destroy: -> + clearInterval @refreshInterval + @simulator.destroy() + super() + +class SimulatorsLeaderboardData extends CocoClass + ### + Consolidates what you need to load for a leaderboard into a single Backbone Model-like object. + ### + + constructor: (@me) -> + super() + + fetch: -> + @topSimulators = new SimulatorsLeaderboardCollection({order:-1, scoreOffset: -1, limit: 20}) + promises = [] + promises.push @topSimulators.fetch() + unless @me.get('anonymous') + score = @me.get('simulatedBy') or 0 + queueSuccess = (@numberOfGamesInQueue) => + promises.push $.ajax "/queue/messagesInQueueCount", {success: queueSuccess} + @playersAbove = new SimulatorsLeaderboardCollection({order:1, scoreOffset: score, limit: 4}) + promises.push @playersAbove.fetch() + if score + @playersBelow = new SimulatorsLeaderboardCollection({order:-1, scoreOffset: score, limit: 4}) + promises.push @playersBelow.fetch() + success = (@myRank) => + + promises.push $.ajax "/db/user/me/simulator_leaderboard_rank?scoreOffset=#{score}", {success} + + @promise = $.when(promises...) + @promise.then @onLoad + @promise.fail @onFail + @promise + + onLoad: => + return if @destroyed + @loaded = true + @trigger 'sync', @ + + onFail: (resource, jqxhr) => + return if @destroyed + @trigger 'error', @, jqxhr + + inTopSimulators: -> + return me.id in (user.id for user in @topSimulators.models) + + nearbySimulators: -> + l = [] + above = @playersAbove.models + l = l.concat(above) + l.reverse() + #l.push @me + l = l.concat(@playersBelow.models) if @playersBelow + if @myRank + startRank = @myRank - 4 + user.rank = startRank + i for user, i in l + l + + allResources: -> + resources = [@topSimulators, @playersAbove, @playersBelow] + return (r for r in resources when r) diff --git a/app/views/play/ladder_home.coffee b/app/views/play/ladder_home.coffee new file mode 100644 index 000000000..0150b2284 --- /dev/null +++ b/app/views/play/ladder_home.coffee @@ -0,0 +1,72 @@ +View = require 'views/kinds/RootView' +template = require 'templates/play/ladder_home' +LevelSession = require 'models/LevelSession' +CocoCollection = require 'collections/CocoCollection' + +class LevelSessionsCollection extends CocoCollection + url: '' + model: LevelSession + + constructor: (model) -> + super() + @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID" + +module.exports = class LadderHomeView extends View + id: "ladder-home-view" + template: template + + constructor: (options) -> + super options + @levelStatusMap = {} + @sessions = new LevelSessionsCollection() + @sessions.fetch() + @listenToOnce @sessions, 'sync', @onSessionsLoaded + + onSessionsLoaded: (e) -> + for session in @sessions.models + @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' + @render() + + getRenderData: (context={}) -> + context = super(context) + arenas = [ + { + name: 'Greed' + difficulty: 4 + id: 'greed' + image: '/file/db/level/53558b5a9914f5a90d7ccddb/greed_banner.jpg' + description: "Liked Dungeon Arena and Gold Rush? Put them together in this economic arena!" + } + { + name: 'Dungeon Arena' + difficulty: 3 + id: 'dungeon-arena' + image: '/file/db/level/53173f76c269d400000543c2/Level%20Banner%20Dungeon%20Arena.jpg' + description: "Play head-to-head against fellow Wizards in a dungeon melee!" + } + { + name: 'Gold Rush' + difficulty: 3 + id: 'gold-rush' + image: '/file/db/level/533353722a61b7ca6832840c/Gold-Rush.png' + description: "Prove you are better at collecting gold than your opponent!" + } + { + name: 'Brawlwood' + difficulty: 4 + id: 'brawlwood' + image: '/file/db/level/52d97ecd32362bc86e004e87/Level%20Banner%20Brawlwood.jpg' + description: "Combat the armies of other Wizards in a strategic forest arena! (Fast computer required.)" + } + ] + + context.campaigns = [ + {id: "multiplayer", name: "Multiplayer Arenas", description: "... in which you code head-to-head against other players.", levels: arenas} + ] + context.levelStatusMap = @levelStatusMap + context + + afterRender: -> + super() + @$el.find('.modal').on 'shown.bs.modal', -> + $('input:visible:first', @).focus() diff --git a/app/views/play/ladder_view.coffee b/app/views/play/ladder_view.coffee deleted file mode 100644 index 58366fa58..000000000 --- a/app/views/play/ladder_view.coffee +++ /dev/null @@ -1,158 +0,0 @@ -RootView = require 'views/kinds/RootView' -Level = require 'models/Level' -Simulator = require 'lib/simulator/Simulator' -LevelSession = require 'models/LevelSession' -CocoCollection = require 'models/CocoCollection' -{teamDataFromLevel} = require './ladder/utils' -{me} = require 'lib/auth' -application = require 'application' - -LadderTabView = require './ladder/ladder_tab' -MyMatchesTabView = require './ladder/my_matches_tab' -LadderPlayModal = require './ladder/play_modal' - -HIGHEST_SCORE = 1000000 - -class LevelSessionsCollection extends CocoCollection - url: '' - model: LevelSession - - constructor: (levelID) -> - super() - @url = "/db/level/#{levelID}/my_sessions" - -module.exports = class LadderView extends RootView - id: 'ladder-view' - template: require 'templates/play/ladder' - - subscriptions: - 'application:idle-changed': 'onIdleChanged' - - events: - 'click #simulate-button': 'onSimulateButtonClick' - 'click #simulate-all-button': 'onSimulateAllButtonClick' - 'click .play-button': 'onClickPlayButton' - 'click a': 'onClickedLink' - - constructor: (options, @levelID) -> - super(options) - @level = new Level(_id:@levelID) - @level.fetch() - @sessions = new LevelSessionsCollection(levelID) - @sessions.fetch({}) - @addResourceToLoad(@sessions, 'your_sessions') - @addResourceToLoad(@level, 'level') - @simulator = new Simulator() - @listenTo(@simulator, 'statusUpdate', @updateSimulationStatus) - @teams = [] - - onLoaded: -> - @teams = teamDataFromLevel @level - super() - - getRenderData: -> - ctx = super() - ctx.level = @level - ctx.link = "/play/level/#{@level.get('name')}" - ctx.simulationStatus = @simulationStatus - ctx.teams = @teams - ctx.levelID = @levelID - ctx.levelDescription = marked(@level.get('description')) if @level.get('description') - ctx - - afterRender: -> - super() - return if @loading() - @insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions)) - @insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions)) - @refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 10 * 1000) - hash = document.location.hash[1..] if document.location.hash - if hash and not (hash in ['my-matches', 'simulate', 'ladder']) - @showPlayModal(hash) if @sessions.loaded - - fetchSessionsAndRefreshViews: -> - return if @destroyed or application.userIsIdle or @$el.find('#simulate.active').length or (new Date() - 2000 < @lastRefreshTime) or @loading() - @sessions.fetch({"success": @refreshViews}) - - refreshViews: => - return if @destroyed or application.userIsIdle - @lastRefreshTime = new Date() - @ladderTab.refreshLadder() - @myMatchesTab.refreshMatches() - console.log "Refreshed sessions for ladder and matches." - - onIdleChanged: (e) -> - @fetchSessionsAndRefreshViews() unless e.idle - - # Simulations - - onSimulateAllButtonClick: (e) -> - submitIDs = _.pluck @leaderboards[@teams[0].id].topPlayers.models, "id" - for ID in submitIDs - $.ajax - url: '/queue/scoring' - method: 'POST' - data: - session: ID - $("#simulate-all-button").prop "disabled", true - $("#simulate-all-button").text "Submitted all!" - - onSimulateButtonClick: (e) -> - $("#simulate-button").prop "disabled",true - $("#simulate-button").text "Simulating..." - - @simulator.fetchAndSimulateTask() - - updateSimulationStatus: (simulationStatus, sessions) -> - @simulationStatus = simulationStatus - try - if sessions? - #TODO: Fetch names from Redis, the creatorName is denormalized - creatorNames = (session.creatorName for session in sessions) - @simulationStatus = "Simulating game between " - for index in [0...creatorNames.length] - unless creatorNames[index] - creatorNames[index] = "Anonymous" - @simulationStatus += (if index != 0 then " and " else "") + creatorNames[index] - @simulationStatus += "..." - catch e - console.log "There was a problem with the named simulation status: #{e}" - $("#simulation-status-text").text @simulationStatus - - onClickPlayButton: (e) -> - @showPlayModal($(e.target).closest('.play-button').data('team')) - - resimulateAllSessions: -> - postData = - originalLevelID: @level.get('original') - levelMajorVersion: @level.get('version').major - console.log postData - - $.ajax - url: '/queue/scoring/resimulateAllSessions' - method: 'POST' - data: postData - complete: (jqxhr) -> - console.log jqxhr.responseText - - showPlayModal: (teamID) -> - return @showApologeticSignupModal() if me.get('anonymous') - session = (s for s in @sessions.models when s.get('team') is teamID)[0] - modal = new LadderPlayModal({}, @level, session, teamID) - @openModalView modal - - showApologeticSignupModal: -> - SignupModal = require 'views/modal/signup_modal' - @openModalView(new SignupModal({showRequiredError:true})) - - onClickedLink: (e) -> - link = $(e.target).closest('a').attr('href') - if link?.startsWith('/play/level') and me.get('anonymous') - e.stopPropagation() - e.preventDefault() - @showApologeticSignupModal() - - destroy: -> - clearInterval @refreshInterval - @simulator.destroy() - super() diff --git a/app/views/play/level/control_bar_view.coffee b/app/views/play/level/control_bar_view.coffee index 62f1932d1..9c1076edb 100644 --- a/app/views/play/level/control_bar_view.coffee +++ b/app/views/play/level/control_bar_view.coffee @@ -14,15 +14,15 @@ module.exports = class ControlBarView extends View events: 'click #multiplayer-button': -> - window.tracker?.trackEvent 'Clicked Multiplayer', level: @worldName, label: @worldName + window.tracker?.trackEvent 'Clicked Multiplayer', level: @level.get('name'), label: @level.get('name') @showMultiplayerModal() 'click #docs-button': -> - window.tracker?.trackEvent 'Clicked Docs', level: @worldName, label: @worldName + window.tracker?.trackEvent 'Clicked Docs', level: @level.get('name'), label: @level.get('name') @showGuideModal() 'click #restart-button': -> - window.tracker?.trackEvent 'Clicked Restart', level: @worldName, label: @worldName + window.tracker?.trackEvent 'Clicked Restart', level: @level.get('name'), label: @level.get('name') @showRestartModal() 'click #next-game-button': -> @@ -35,7 +35,6 @@ module.exports = class ControlBarView extends View @session = options.session @level = options.level @playableTeams = options.playableTeams - @ladderGame = options.ladderGame @spectateGame = options.spectateGame ? false super options @@ -55,13 +54,12 @@ module.exports = class ControlBarView extends View super c c.worldName = @worldName c.multiplayerEnabled = @session.get('multiplayer') - c.ladderGame = @ladderGame + c.ladderGame = @level.get('type') is 'ladder' c.spectateGame = @spectateGame - c.homeLink = "/" - levelID = @level.get('slug') - if levelID in ["brawlwood", "brawlwood-tutorial", "dungeon-arena", "dungeon-arena-tutorial"] - levelID = 'brawlwood' if levelID is 'brawlwood-tutorial' - c.homeLink = "/play/ladder/" + levelID + if @level.get('type') in ['ladder', 'ladder-tutorial'] + c.homeLink = '/play/ladder/' + @level.get('slug').replace /\-tutorial$/, '' + else + c.homeLink = '/' c afterRender: -> diff --git a/app/views/play/level/goals_view.coffee b/app/views/play/level/goals_view.coffee index 85d7436ac..21e27e29d 100644 --- a/app/views/play/level/goals_view.coffee +++ b/app/views/play/level/goals_view.coffee @@ -19,12 +19,26 @@ module.exports = class GoalsView extends View 'surface:playback-ended': 'onSurfacePlaybackEnded' events: - 'click': 'toggleCollapse' + 'mouseenter': -> + @mouseEntered = true + @updatePlacement() + + 'mouseleave': -> + @mouseEntered = false + @updatePlacement() toggleCollapse: (e) -> @$el.toggleClass('expanded').toggleClass('collapsed') onNewGoalStates: (e) -> + @$el.find('.goal-status').addClass 'secret' + classToShow = null + classToShow = 'success' if e.overallStatus is 'success' + classToShow = 'failure' if e.overallStatus is 'failure' + classToShow ?= 'timed-out' if e.timedOut + classToShow ?= 'incomplete' + @$el.find('.goal-status.'+classToShow).removeClass 'secret' + list = $('#primary-goals-list', @$el) list.empty() goals = [] @@ -52,14 +66,31 @@ module.exports = class GoalsView extends View @$el.removeClass('secret') if goals.length > 0 onSurfacePlaybackRestarted: -> + @playbackEnded = false @$el.removeClass 'brighter' + @updatePlacement() onSurfacePlaybackEnded: -> + @playbackEnded = true @$el.addClass 'brighter' + @updatePlacement() render: -> super() @$el.addClass('secret').addClass('expanded') + + afterRender: -> + super() + @updatePlacement() + + updatePlacement: -> + if @playbackEnded or @mouseEntered + # expand + @$el.css('top', -10) + else + # collapse + @$el.css('top', 26 - @$el.outerHeight()) onSetLetterbox: (e) -> @$el.toggle not e.on + @updatePlacement() diff --git a/app/views/play/level/gold_view.coffee b/app/views/play/level/gold_view.coffee index e5fcaf03e..6191d4d7a 100644 --- a/app/views/play/level/gold_view.coffee +++ b/app/views/play/level/gold_view.coffee @@ -1,5 +1,6 @@ View = require 'views/kinds/CocoView' template = require 'templates/play/level/gold' +teamTemplate = require 'templates/play/level/team_gold' module.exports = class GoldView extends View id: "gold-view" @@ -9,14 +10,29 @@ module.exports = class GoldView extends View 'surface:gold-changed': 'onGoldChanged' 'level-set-letterbox': 'onSetLetterbox' + constructor: (options) -> + super options + @teamGold = {} + @teamGoldEarned = {} + onGoldChanged: (e) -> @$el.show() + return if @teamGold[e.team] is e.gold and @teamGoldEarned[e.team] is e.goldEarned + @teamGold[e.team] = e.gold + @teamGoldEarned[e.team] = e.goldEarned goldEl = @$el.find('.gold-amount.team-' + e.team) unless goldEl.length - teamEl = $("

") + teamEl = teamTemplate team: e.team @$el.append(teamEl) - goldEl = teamEl.find('.gold-amount.team-' + e.team) - goldEl.text(e.gold) + goldEl = $('.gold-amount.team-' + e.team, teamEl) + text = '' + e.gold + if e.goldEarned and e.goldEarned > e.gold + text += " (#{e.goldEarned})" + goldEl.text text + @updateTitle() + + updateTitle: -> + @$el.attr 'title', ("Team '#{team}' has #{gold} now of #{@teamGoldEarned[team]} gold earned." for team, gold of @teamGold).join ' ' onSetLetterbox: (e) -> @$el.toggle not e.on diff --git a/app/views/play/level/hud_view.coffee b/app/views/play/level/hud_view.coffee index 581a5fb2f..87625d631 100644 --- a/app/views/play/level/hud_view.coffee +++ b/app/views/play/level/hud_view.coffee @@ -109,19 +109,29 @@ module.exports = class HUDView extends View @update() createAvatar: (thangType, thang, colorConfig) -> + unless thangType.isFullyLoaded() + args = arguments + unless @listeningToCreateAvatar + @listenToOnce thangType, 'sync', -> @createAvatar(args...) + @listeningToCreateAvatar = true + return + @listeningToCreateAvatar = false options = thang.getSpriteOptions() or {} options.async = false options.colorConfig = colorConfig if colorConfig - stage = thangType.getPortraitStage options wrapper = @$el.find '.thang-canvas-wrapper' - newCanvas = $(stage.canvas).addClass('thang-canvas') - wrapper.empty().append(newCanvas) team = @thang?.team or @speakerSprite?.thang?.team wrapper.removeClass (i, css) -> (css.match(/\bteam-\S+/g) or []).join ' ' wrapper.addClass "team-#{team}" - stage.update() - @stage?.stopTalking() - @stage = stage + if thangType.get('raster') + wrapper.empty().append($('').attr('src', '/file/'+thangType.get('raster'))) + else + stage = thangType.getPortraitStage options + newCanvas = $(stage.canvas).addClass('thang-canvas') + wrapper.empty().append(newCanvas) + stage.update() + @stage?.stopTalking() + @stage = stage onThangBeganTalking: (e) -> return unless @stage and @thang is e.thang @@ -160,6 +170,8 @@ module.exports = class HUDView extends View setMessage: (message, mood, responses) -> message = marked message + # Fix old HTML icons like in the Markdown + message = message.replace /<i class='(.+?)'><\/i>/, "" clearInterval(@messageInterval) if @messageInterval @bubble = $('.dialogue-bubble', @$el) @bubble.removeClass(@lastMood) if @lastMood @@ -249,7 +261,7 @@ module.exports = class HUDView extends View return null # included in the bar context = prop: prop - hasIcon: prop in ["health", "pos", "target", "inventory", "gold", "visualRange", "attackDamage", "attackRange", "maxSpeed"] + hasIcon: prop in ["health", "pos", "target", "inventory", "gold", "bountyGold", "visualRange", "attackDamage", "attackRange", "maxSpeed"] hasBar: prop in ["health"] $(prop_template(context)) @@ -306,10 +318,11 @@ module.exports = class HUDView extends View for actionName, action of @thang.actions @updateActionElement(actionName, @timespans[actionName], @thang.action is actionName) tableContainer = @$el.find('.table-container') - timelineWidth = tableContainer.find('tr:not(.secret) .action-timeline').width() - right = (1 - (@timeProgress ? 0)) * timelineWidth arrow = tableContainer.find('.progress-arrow') - arrow.css 'right', right - arrow.width() / 2 + @timelineWidth ||= tableContainer.find('tr:not(.secret) .action-timeline').width() + @actionArrowWidth ||= arrow.width() + right = (1 - (@timeProgress ? 0)) * @timelineWidth + arrow.css 'right', right - @actionArrowWidth / 2 tableContainer.find('.progress-line').css 'right', right buildActionTimespans: -> diff --git a/app/views/play/level/level_loading_view.coffee b/app/views/play/level/level_loading_view.coffee index 32da3377e..217b4bc9b 100644 --- a/app/views/play/level/level_loading_view.coffee +++ b/app/views/play/level/level_loading_view.coffee @@ -6,9 +6,7 @@ module.exports = class LevelLoadingView extends View id: "level-loading-view" template: template - subscriptions: - 'level-loader:progress-changed': 'onLevelLoaderProgressChanged' - + onLoaded: -> afterRender: -> @$el.find('.tip.rare').remove() if _.random(1, 10) < 9 tips = @$el.find('.tip').addClass('to-remove') @@ -16,24 +14,19 @@ module.exports = class LevelLoadingView extends View $(tip).removeClass('to-remove') @$el.find('.to-remove').remove() - onLevelLoaderProgressChanged: (e) -> - @progress = e.progress - @progress = 0.01 if @progress < 0.01 - @updateProgressBar() - - updateProgressBar: -> - @$el.find('.progress-bar').css('width', (100 * @progress) + '%') - showReady: -> + return if @shownReady + @shownReady = true ready = $.i18n.t('play_level.loading_ready', defaultValue: 'Ready!') @$el.find('#tip-wrapper .tip').addClass('ready').text ready - Backbone.Mediator.publish 'play-sound', trigger: 'loading_ready', volume: 0.75 + Backbone.Mediator.publish 'play-sound', trigger: 'level_loaded', volume: 0.75 # old: loading_ready unveil: -> _.delay @reallyUnveil, 1000 reallyUnveil: => return if @destroyed + @$el.addClass 'unveiled' loadingDetails = @$el.find('.loading-details') duration = parseFloat loadingDetails.css 'transition-duration' loadingDetails.css 'top', -loadingDetails.outerHeight(true) @@ -43,4 +36,4 @@ module.exports = class LevelLoadingView extends View onUnveilEnded: => return if @destroyed - Backbone.Mediator.publish 'onLoadingViewUnveiled', view: @ + Backbone.Mediator.publish 'level:loading-view-unveiled', view: @ diff --git a/app/views/play/level/modal/docs_modal.coffee b/app/views/play/level/modal/docs_modal.coffee index 88336fb64..8c6a56a16 100644 --- a/app/views/play/level/modal/docs_modal.coffee +++ b/app/views/play/level/modal/docs_modal.coffee @@ -13,6 +13,7 @@ module.exports = class DocsModal extends View 'enter': 'hide' constructor: (options) -> + @firstOnly = options.firstOnly @docs = options?.docs general = @docs.generalArticles or [] specific = @docs.specificArticles or [] @@ -24,8 +25,8 @@ module.exports = class DocsModal extends View general = (article.attributes for article in general when article) @docs = specific.concat(general) - marked.setOptions {gfm: true, sanitize: false, smartLists: true, breaks: false} @docs = $.extend(true, [], @docs) + @docs = [@docs[0]] if @firstOnly and @docs[0] doc.html = marked(utils.i18n doc, 'body') for doc in @docs doc.name = (utils.i18n doc, 'name') for doc in @docs doc.slug = _.string.slugify(doc.name) for doc in @docs @@ -48,6 +49,10 @@ module.exports = class DocsModal extends View clickTab: (e) => @$el.find('li.active').removeClass('active') + + afterInsert: -> + super() + Backbone.Mediator.publish 'level:docs-shown' onHidden: -> Backbone.Mediator.publish 'level:docs-hidden' diff --git a/app/views/play/level/modal/editor_config_modal.coffee b/app/views/play/level/modal/editor_config_modal.coffee index 6be98b44d..5047be476 100644 --- a/app/views/play/level/modal/editor_config_modal.coffee +++ b/app/views/play/level/modal/editor_config_modal.coffee @@ -23,11 +23,20 @@ module.exports = class EditorConfigModal extends View constructor: (options) -> super(options) + @session = options.session getRenderData: -> @aceConfig = _.cloneDeep me.get('aceConfig') ? {} @aceConfig = _.defaults @aceConfig, @defaultConfig c = super() + c.languages = [ + {id: 'javascript', name: 'JavaScript'} + {id: 'coffeescript', name: 'CoffeeScript'} + {id: 'python', name: 'Python (Experimental)'} + {id: 'clojure', name: 'Clojure (Experimental)'} + {id: 'lua', name: 'Lua (Experimental)'} + ] + c.sessionLanguage = @session.get('codeLanguage') ? @aceConfig.language c.language = @aceConfig.language c.keyBindings = @aceConfig.keyBindings c.invisibles = @aceConfig.invisibles @@ -35,6 +44,9 @@ module.exports = class EditorConfigModal extends View c.behaviors = @aceConfig.behaviors c + updateSessionLanguage: -> + @session.set 'codeLanguage', @$el.find('#tome-session-language').val() + updateLanguage: -> @aceConfig.language = @$el.find('#tome-language').val() @@ -54,7 +66,9 @@ module.exports = class EditorConfigModal extends View super() onHidden: -> - oldLanguage = @aceConfig.language + oldLanguage = @session.get('codeLanguage') ? @aceConfig.language + newLanguage = @$el.find('#tome-session-language').val() + @session.set 'codeLanguage', newLanguage @aceConfig.language = @$el.find('#tome-language').val() @aceConfig.invisibles = @$el.find('#tome-invisibles').prop('checked') @aceConfig.keyBindings = @$el.find('#tome-key-bindings').val() @@ -62,7 +76,8 @@ module.exports = class EditorConfigModal extends View @aceConfig.behaviors = @$el.find('#tome-behaviors').prop('checked') me.set 'aceConfig', @aceConfig Backbone.Mediator.publish 'tome:change-config' - Backbone.Mediator.publish 'tome:change-language' unless @aceConfig.language isnt oldLanguage + Backbone.Mediator.publish 'tome:change-language', language: newLanguage unless newLanguage is oldLanguage + @session.save() unless newLanguage is oldLanguage me.save() destroy: -> diff --git a/app/views/play/level/modal/infinite_loop_modal.coffee b/app/views/play/level/modal/infinite_loop_modal.coffee index f26185103..ff89e229f 100644 --- a/app/views/play/level/modal/infinite_loop_modal.coffee +++ b/app/views/play/level/modal/infinite_loop_modal.coffee @@ -8,3 +8,4 @@ module.exports = class InfiniteLoopModal extends View events: 'click #restart-level-infinite-loop-retry-button': -> Backbone.Mediator.publish 'tome:cast-spell' 'click #restart-level-infinite-loop-confirm-button': -> Backbone.Mediator.publish 'restart-level' + 'click #restart-level-infinite-loop-comment-button': -> Backbone.Mediator.publish 'tome:comment-my-code' diff --git a/app/views/play/level/modal/keyboard_shortcuts_modal.coffee b/app/views/play/level/modal/keyboard_shortcuts_modal.coffee new file mode 100644 index 000000000..816bd3f10 --- /dev/null +++ b/app/views/play/level/modal/keyboard_shortcuts_modal.coffee @@ -0,0 +1,17 @@ +View = require 'views/kinds/ModalView' +template = require 'templates/play/level/modal/keyboard_shortcuts' + +module.exports = class KeyboardShortcutsModal extends View + id: 'keyboard-shortcuts-modal' + template: template + + getRenderData: -> + c = super() + c.ctrl = if @isMac() then '⌘' else '^' + c.ctrlName = if @isMac() then 'Cmd' else 'Ctrl' + c.alt = if @isMac() then '⌥' else 'alt' + c.altName = if @isMac() then 'Opt' else 'Alt' + c.enter = $.i18n.t 'keyboard_shortcuts.enter' + c.space = $.i18n.t 'keyboard_shortcuts.space' + c.escapeKey = $.i18n.t 'keyboard_shortcuts.escape' + c diff --git a/app/views/play/level/modal/multiplayer_modal.coffee b/app/views/play/level/modal/multiplayer_modal.coffee index 973bc8b47..c7e02b157 100644 --- a/app/views/play/level/modal/multiplayer_modal.coffee +++ b/app/views/play/level/modal/multiplayer_modal.coffee @@ -1,45 +1,54 @@ View = require 'views/kinds/ModalView' template = require 'templates/play/level/modal/multiplayer' -{me} = require('lib/auth') +{me} = require 'lib/auth' +LadderSubmissionView = require 'views/play/common/ladder_submission_view' module.exports = class MultiplayerModal extends View id: 'level-multiplayer-modal' template: template + subscriptions: + 'ladder:game-submitted': 'onGameSubmitted' + events: 'click textarea': 'onClickLink' 'change #multiplayer': 'updateLinkSection' - constructor: (options) -> super(options) @session = options.session @level = options.level @listenTo(@session, 'change:multiplayer', @updateLinkSection) @playableTeams = options.playableTeams - @ladderGame = options.ladderGame - console.log 'ladder game is', @ladderGame getRenderData: -> c = super() c.joinLink = (document.location.href.replace(/\?.*/, '').replace('#', '') + '?session=' + @session.id) - c.multiplayer = @session.get('multiplayer') + c.multiplayer = @session.get 'multiplayer' c.team = @session.get 'team' - c.levelSlug = @level?.get('slug') + c.levelSlug = @level?.get 'slug' c.playableTeams = @playableTeams - c.ladderGame = @ladderGame # For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet. + if @level?.get('type') is 'ladder' + c.ladderGame = true + c.readyToRank = @session?.readyToRank() c afterRender: -> super() @updateLinkSection() + @ladderSubmissionView = new LadderSubmissionView session: @session, level: @level + @insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view') onClickLink: (e) -> e.target.select() + onGameSubmitted: (e) -> + ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches" + Backbone.Mediator.publish 'router:navigate', route: ladderURL + updateLinkSection: -> multiplayer = @$el.find('#multiplayer').prop('checked') la = @$el.find('#link-area') diff --git a/app/views/play/level/modal/victory_modal.coffee b/app/views/play/level/modal/victory_modal.coffee index 3fe539b99..ecc883426 100644 --- a/app/views/play/level/modal/victory_modal.coffee +++ b/app/views/play/level/modal/victory_modal.coffee @@ -1,6 +1,7 @@ View = require 'views/kinds/ModalView' template = require 'templates/play/level/modal/victory' {me} = require 'lib/auth' +LadderSubmissionView = require 'views/play/common/ladder_submission_view' LevelFeedback = require 'models/LevelFeedback' utils = require 'lib/utils' @@ -8,9 +9,11 @@ module.exports = class VictoryModal extends View id: 'level-victory-modal' template: template + subscriptions: + 'ladder:game-submitted': 'onGameSubmitted' + events: 'click .next-level-button': 'onPlayNextLevel' - 'click .rank-game-button': 'onRankGame' # review events 'mouseover .rating i': (e) -> @showStars(@starNum($(e.target))) @@ -58,21 +61,9 @@ module.exports = class VictoryModal extends View @saveReview() if @$el.find('.review textarea').val() Backbone.Mediator.publish('play-next-level') - onRankGame: (e) -> - button = @$el.find('.rank-game-button') - button.text($.i18n.t('play_level.victory_ranking_game', defaultValue: 'Submitting...')) - button.prop 'disabled', true - ajaxData = session: @session.id, levelID: @level.id, originalLevelID: @level.get('original'), levelMajorVersion: @level.get('version').major + onGameSubmitted: (e) -> ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches" - goToLadder = -> Backbone.Mediator.publish 'router:navigate', route: ladderURL - console.log "Posting game for ranking from victory modal." - $.ajax '/queue/scoring', - type: 'POST' - data: ajaxData - success: goToLadder - failure: (response) -> - console.error "Couldn't submit game for ranking:", response - goToLadder() + Backbone.Mediator.publish 'router:navigate', route: ladderURL getRenderData: -> c = super() @@ -82,9 +73,7 @@ module.exports = class VictoryModal extends View c.levelName = utils.i18n @level.attributes, 'name' c.level = @level if c.level.get('type') is 'ladder' - c1 = @session?.get('code') - c2 = @session?.get('submittedCode') - c.readyToRank = @session.get('levelID') and c1 and not _.isEqual(c1, c2) + c.readyToRank = @session.readyToRank() if me.get 'hourOfCode' # Show the Hour of Code "I'm Done" tracking pixel after they played for 30 minutes elapsed = (new Date() - new Date(me.get('dateCreated'))) @@ -101,6 +90,8 @@ module.exports = class VictoryModal extends View afterRender: -> super() + @ladderSubmissionView = new LadderSubmissionView session: @session, level: @level + @insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view') afterInsert: -> super() diff --git a/app/views/play/level/playback_view.coffee b/app/views/play/level/playback_view.coffee index 4a3e4d359..8bc581e1e 100644 --- a/app/views/play/level/playback_view.coffee +++ b/app/views/play/level/playback_view.coffee @@ -3,6 +3,7 @@ template = require 'templates/play/level/playback' {me} = require 'lib/auth' EditorConfigModal = require './modal/editor_config_modal' +KeyboardShortcutsModal = require './modal/keyboard_shortcuts_modal' module.exports = class PlaybackView extends View id: "playback-view" @@ -29,6 +30,7 @@ module.exports = class PlaybackView extends View 'click #grid-toggle': 'onToggleGrid' 'click #edit-wizard-settings': 'onEditWizardSettings' 'click #edit-editor-config': 'onEditEditorConfig' + 'click #view-keyboard-shortcuts': 'onViewKeyboardShortcuts' 'click #music-button': 'onToggleMusic' 'click #zoom-in-button': -> Backbone.Mediator.publish('camera-zoom-in') unless @shouldIgnore() 'click #zoom-out-button': -> Backbone.Mediator.publish('camera-zoom-out') unless @shouldIgnore() @@ -107,9 +109,12 @@ module.exports = class PlaybackView extends View @hookUpScrubber() @updateMusicButton() $(window).on('resize', @onWindowResize) + ua = navigator.userAgent.toLowerCase() + if /safari/.test(ua) and not /chrome/.test(ua) + @$el.find('.toggle-fullscreen').hide() updatePopupContent: -> - @timePopup.updateContent "

#{@timeToString @newTime}

#{@formatTime(@current, @currentTime)}
#{@formatTime(@total, @totalTime)}" + @timePopup?.updateContent "

#{@timeToString @newTime}

#{@formatTime(@current, @currentTime)}
#{@formatTime(@total, @totalTime)}" # These functions could go to some helper class @@ -151,18 +156,16 @@ module.exports = class PlaybackView extends View @newTime = 0 @currentTime = 0 - @timePopup = new HoverPopup unless @timePopup? + @timePopup ?= new HoverPopup - - #TODO: Why do we need defaultValues here at all? Fallback language has been set to 'en'... oO t = $.i18n.t - @second = t 'units.second', defaultValue: 'second' - @seconds = t 'units.seconds', defaultValue: 'seconds' - @minute = t 'units.minute', defaultValue: 'minute' - @minutes = t 'units.minutes', defaultValue: 'minutes' - @goto = t 'play_level.time_goto', defaultValue: "Go to:" - @current = t 'play_level.time_current', defaultValue: "Now:" - @total = t 'play_level.time_total', defaultValue: "Max:" + @second = t 'units.second' + @seconds = t 'units.seconds' + @minute = t 'units.minute' + @minutes = t 'units.minutes' + @goto = t 'play_level.time_goto' + @current = t 'play_level.time_current' + @total = t 'play_level.time_total' onToggleDebug: -> return if @shouldIgnore() @@ -178,9 +181,13 @@ module.exports = class PlaybackView extends View Backbone.Mediator.publish 'edit-wizard-settings' onEditEditorConfig: -> - @openModalView(new EditorConfigModal()) + @openModalView new EditorConfigModal session: @options.session - onCastSpells: -> + onViewKeyboardShortcuts: -> + @openModalView new KeyboardShortcutsModal() + + onCastSpells: (e) -> + return if e.preload @casting = true @$progressScrubber.slider('disable', true) @@ -190,9 +197,9 @@ module.exports = class PlaybackView extends View $('button', @$el).addClass('disabled') try @$progressScrubber.slider('disable', true) - catch e - #console.warn('error disabling scrubber') - @timePopup.disable() + catch error + console.warn('error disabling scrubber', error) + @timePopup?.disable() $('#volume-button', @$el).removeClass('disabled') onEnableControls: (e) -> @@ -201,9 +208,9 @@ module.exports = class PlaybackView extends View $('button', @$el).removeClass('disabled') try @$progressScrubber.slider('enable', true) - catch e - #console.warn('error enabling scrubber') - @timePopup.enable() + catch error + console.warn('error enabling scrubber', error) + @timePopup?.enable() onSetPlaying: (e) -> @playing = (e ? {}).playing ? true @@ -235,36 +242,36 @@ module.exports = class PlaybackView extends View @currentTime = e.frame / e.world.frameRate # Game will sometimes stop at 29.97, but with only one digit, this is unnecesary. # @currentTime = @totalTime if Math.abs(@totalTime - @currentTime) < 0.04 - @updatePopupContent() + @updatePopupContent() if @timePopup?.shown @updateProgress(e.progress) @updatePlayButton(e.progress) @lastProgress = e.progress onProgressEnter: (e) -> - #Why it needs itself as parameter you ask? Ask Twitter instead.. - @timePopup.enter @timePopup + # Why it needs itself as parameter you ask? Ask Twitter instead. + @timePopup?.enter @timePopup onProgressLeave: (e) -> - @timePopup.leave @timePopup + @timePopup?.leave @timePopup onProgressHover: (e) -> timeRatio = @$progressScrubber.width() / @totalTime @newTime = e.offsetX / timeRatio @updatePopupContent() - @timePopup.onHover e + @timePopup?.onHover e - #Show it instantaniously if close enough to current time. - if Math.abs(@currentTime - @newTime) < 1 and not @timePopup.shown - @timePopup.show() unless @timePopup.shown + # Show it instantaneously if close enough to current time. + if @timePopup and Math.abs(@currentTime - @newTime) < 1 and not @timePopup.shown + @timePopup.show() updateProgress: (progress) -> $('.scrubber .progress-bar', @$el).css('width', "#{progress*100}%") updatePlayButton: (progress) -> - if progress >= 1.0 and @lastProgress < 1.0 + if progress >= 0.99 and @lastProgress < 0.99 $('#play-button').removeClass('playing').removeClass('paused').addClass('ended') - if progress < 1.0 and @lastProgress >= 1.0 + if progress < 0.99 and @lastProgress >= 0.99 b = $('#play-button').removeClass('ended') if @playing then b.addClass('playing') else b.addClass('paused') diff --git a/app/views/play/level/thang_avatar_view.coffee b/app/views/play/level/thang_avatar_view.coffee index 3c22fc5cb..fe9babf5b 100644 --- a/app/views/play/level/thang_avatar_view.coffee +++ b/app/views/play/level/thang_avatar_view.coffee @@ -14,16 +14,31 @@ module.exports = class ThangAvatarView extends View super options @thang = options.thang @includeName = options.includeName + @thangType = @getSpriteThangType() + if not @thangType + console.error 'Thang avatar view expected a thang type to be provided.' + return + + unless @thangType.isFullyLoaded() or @thangType.loading + @thangType.fetch() + + # couldn't get the level view to load properly through the supermodel + # so just doing it manually this time. + @listenTo @thangType, 'sync', @render + @listenTo @thangType, 'build-complete', @render + + getSpriteThangType: -> + thangs = @supermodel.getModels(ThangType) + thangs = (t for t in thangs when t.get('name') is @thang.spriteName) + loadedThangs = (t for t in thangs when t.isFullyLoaded()) + return loadedThangs[0] or thangs[0] # try to return one with all the goods, otherwise a projection getRenderData: (context={}) -> context = super context context.thang = @thang - thangs = @supermodel.getModels(ThangType) - thangs = (t for t in thangs when t.get('name') is @thang.spriteName) - thang = thangs[0] options = @thang?.getSpriteOptions() or {} - options.async = false - context.avatarURL = thang.getPortraitSource(options) + options.async = true + context.avatarURL = @thangType.getPortraitSource(options) unless @thangType.loading context.includeName = @includeName context diff --git a/app/views/play/level/tome/cast_button_view.coffee b/app/views/play/level/tome/cast_button_view.coffee index 5b2f0e094..133cfbfcc 100644 --- a/app/views/play/level/tome/cast_button_view.coffee +++ b/app/views/play/level/tome/cast_button_view.coffee @@ -20,7 +20,6 @@ module.exports = class CastButtonView extends View super options @spells = options.spells @levelID = options.levelID - isMac = navigator.platform.toUpperCase().indexOf('MAC') isnt -1 @castShortcut = "⇧↵" @castShortcutVerbose = "Shift+Enter" @@ -36,7 +35,7 @@ module.exports = class CastButtonView extends View @castOptions = $('.autocast-delays', @$el) delay = me.get('autocastDelay') delay ?= 5000 - if @levelID in ['brawlwood', 'brawlwood-tutorial', 'dungeon-arena', 'dungeon-arena-tutorial'] + if @levelID in ['brawlwood', 'brawlwood-tutorial', 'dungeon-arena', 'dungeon-arena-tutorial', 'gold-rush', 'greed'] delay = 90019001 @setAutocastDelay delay @@ -47,7 +46,6 @@ module.exports = class CastButtonView extends View Backbone.Mediator.publish 'tome:manual-cast', {} onCastOptionsClick: (e) => - console.log 'cast options click', $(e.target) Backbone.Mediator.publish 'focus-editor' @castButtonGroup.removeClass 'open' @setAutocastDelay $(e.target).attr 'data-delay' @@ -57,8 +55,11 @@ module.exports = class CastButtonView extends View @updateCastButton() onCastSpells: (e) -> + return if e.preload @casting = true - Backbone.Mediator.publish 'play-sound', trigger: 'cast', volume: 0.5 + if @hasStartedCastingOnce # Don't play this sound the first time + Backbone.Mediator.publish 'play-sound', trigger: 'cast', volume: 0.5 + @hasStartedCastingOnce = true @updateCastButton() @onWorldLoadProgressChanged progress: 0 @@ -69,21 +70,27 @@ module.exports = class CastButtonView extends View onNewWorld: (e) -> @casting = false - Backbone.Mediator.publish 'play-sound', trigger: 'cast-end', volume: 0.5 + if @hasCastOnce # Don't play this sound the first time + Backbone.Mediator.publish 'play-sound', trigger: 'cast-end', volume: 0.5 + @hasCastOnce = true @updateCastButton() updateCastButton: -> return if _.some @spells, (spell) => not spell.loaded - castable = _.some @spells, (spell) => spell.hasChangedSignificantly spell.getSource() - @castButtonGroup.toggleClass('castable', castable).toggleClass('casting', @casting) - if @casting - s = $.i18n.t("play_level.tome_cast_button_casting", defaultValue: "Casting") - else if castable - s = $.i18n.t("play_level.tome_cast_button_castable", defaultValue: "Cast Spell") + " " + @castShortcut - else - s = $.i18n.t("play_level.tome_cast_button_cast", defaultValue: "Spell Cast") - @castButton.text s - @castButton.prop 'disabled', not castable + + async.some _.values(@spells), (spell, callback) => + spell.hasChangedSignificantly spell.getSource(), null, callback + , (castable) => + Backbone.Mediator.publish 'tome:spell-has-changed-significantly-calculation', hasChangedSignificantly: castable + @castButtonGroup.toggleClass('castable', castable).toggleClass('casting', @casting) + if @casting + s = $.i18n.t("play_level.tome_cast_button_casting", defaultValue: "Casting") + else if castable + s = $.i18n.t("play_level.tome_cast_button_castable", defaultValue: "Cast Spell") + " " + @castShortcut + else + s = $.i18n.t("play_level.tome_cast_button_cast", defaultValue: "Spell Cast") + @castButton.text s + @castButton.prop 'disabled', not castable setAutocastDelay: (delay) -> #console.log "Set autocast delay to", delay diff --git a/app/views/play/level/tome/problem.coffee b/app/views/play/level/tome/problem.coffee index 2e1267785..9dc6db442 100644 --- a/app/views/play/level/tome/problem.coffee +++ b/app/views/play/level/tome/problem.coffee @@ -17,9 +17,9 @@ module.exports = class Problem @removeMarkerRange() buildAnnotation: -> - return unless @aetherProblem.ranges + return unless @aetherProblem.range text = @aetherProblem.message.replace /^Line \d+: /, '' - start = @aetherProblem.ranges[0][0] + start = @aetherProblem.range[0] @annotation = row: start.row, column: start.col, @@ -33,8 +33,8 @@ module.exports = class Problem $(@ace.container).append @alertView.el buildMarkerRange: -> - return unless @aetherProblem.ranges - [start, end] = @aetherProblem.ranges[0] + return unless @aetherProblem.range + [start, end] = @aetherProblem.range clazz = "problem-marker-#{@aetherProblem.level}" @markerRange = new Range start.row, start.col, end.row, end.col @markerRange.start = @ace.getSession().getDocument().createAnchor @markerRange.start diff --git a/app/views/play/level/tome/problem_alert_view.coffee b/app/views/play/level/tome/problem_alert_view.coffee index 2d77ac523..6ab41327d 100644 --- a/app/views/play/level/tome/problem_alert_view.coffee +++ b/app/views/play/level/tome/problem_alert_view.coffee @@ -17,8 +17,15 @@ module.exports = class ProblemAlertView extends View getRenderData: (context={}) -> context = super context - format = (s) -> s?.replace("\n", "
").replace('<', '<').replace('>', '>') - context.message = format @problem.aetherProblem.message + format = (s) -> s?.replace(//g, '>').replace(/\n/g, '
') + message = @problem.aetherProblem.message + age = @problem.aetherProblem.userInfo.age + if age? + if /^Line \d+:/.test message + message = message.replace /^(Line \d+)/, "$1, time #{age.toFixed(1)}" + else + message = "Time #{age.toFixed(1)}: #{message}" + context.message = format message context.hint = format @problem.aetherProblem.hint context diff --git a/app/views/play/level/tome/spell.coffee b/app/views/play/level/tome/spell.coffee index a0cb680cb..0608c90f2 100644 --- a/app/views/play/level/tome/spell.coffee +++ b/app/views/play/level/tome/spell.coffee @@ -2,6 +2,9 @@ SpellView = require './spell_view' SpellListTabEntryView = require './spell_list_tab_entry_view' {me} = require 'lib/auth' +Aether.addGlobal 'Vector', require 'lib/world/vector' +Aether.addGlobal '_', _ + module.exports = class Spell loaded: false view: null @@ -11,26 +14,31 @@ module.exports = class Spell @spellKey = options.spellKey @pathComponents = options.pathComponents @session = options.session + @spectateView = options.spectateView @supermodel = options.supermodel - @skipFlow = options.skipFlow @skipProtectAPI = options.skipProtectAPI @worker = options.worker p = options.programmableMethod @name = p.name - @source = @session.getSourceFor(@spellKey) ? p.source - @originalSource = p.source - @parameters = p.parameters @permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams + teamSpells = @session.get('teamSpells') + team = @session.get('team') ? 'humans' + @useTranspiledCode = @permissions.readwrite.length and ((teamSpells and not _.contains(teamSpells[team], @spellKey)) or (@session.get('creator') isnt me.id) or @spectateView) + #console.log @spellKey, "using transpiled code?", @useTranspiledCode + @source = @originalSource = p.source + @parameters = p.parameters + if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey) + @source = sessionSource + @language = if @canWrite() then options.language else 'javascript' @thangs = {} - @view = new SpellView {spell: @, session: @session} + @view = new SpellView {spell: @, session: @session, worker: @worker} @view.render() # Get it ready and code loaded in advance @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel @tabView.render() @team = @permissions.readwrite[0] ? "common" Backbone.Mediator.publish 'tome:spell-created', spell: @ - destroy: -> @view.destroy() @tabView.destroy() @@ -61,11 +69,16 @@ module.exports = class Spell else source = @getSource() [pure, problems] = [null, null] + if @useTranspiledCode + transpiledCode = @session.get('code') for thangID, spellThang of @thangs unless pure - pure = spellThang.aether.transpile source - problems = spellThang.aether.problems - #console.log "aether transpiled", source.length, "to", pure.length, "for", thangID, @spellKey + if @useTranspiledCode and transpiledSpell = transpiledCode[@spellKey.split('/')[0]]?[@name] + spellThang.aether.pure = transpiledSpell + else + pure = spellThang.aether.transpile source + problems = spellThang.aether.problems + #console.log "aether transpiled", source.length, "to", spellThang.aether.pure.length, "for", thangID, @spellKey else spellThang.aether.pure = pure spellThang.aether.problems = problems @@ -75,45 +88,64 @@ module.exports = class Spell hasChanged: (newSource=null, currentSource=null) -> (newSource ? @originalSource) isnt (currentSource ? @source) - hasChangedSignificantly: (newSource=null, currentSource=null) -> + hasChangedSignificantly: (newSource=null, currentSource=null, cb) -> for thangID, spellThang of @thangs aether = spellThang.aether break unless aether console.error @toString(), "couldn't find a spellThang with aether of", @thangs - return false - aether.hasChangedSignificantly (newSource ? @originalSource), (currentSource ? @source), true, true + cb false + workerMessage = + function: "hasChangedSignificantly" + a: (newSource ? @originalSource) + spellKey: @spellKey + b: (currentSource ? @source) + careAboutLineNumbers: true + careAboutLint: true + @worker.addEventListener "message", (e) => + workerData = JSON.parse e.data + if workerData.function is "hasChangedSignificantly" and workerData.spellKey is @spellKey + @worker.removeEventListener "message", arguments.callee, false + cb(workerData.hasChanged) + @worker.postMessage JSON.stringify(workerMessage) createAether: (thang) -> aceConfig = me.get('aceConfig') ? {} + writable = @permissions.readwrite.length > 0 aetherOptions = problems: jshint_W040: {level: "ignore"} jshint_W030: {level: "ignore"} # aether_NoEffect instead - aether_MissingThis: {level: (if thang.requiresThis then 'error' else 'warning')} - language: aceConfig.language ? 'javascript' + jshint_W038: {level: "ignore"} # eliminates hoisting problems + jshint_W091: {level: "ignore"} # eliminates more hoisting problems + jshint_E043: {level: "ignore"} # https://github.com/codecombat/codecombat/issues/813 -- since we can't actually tell JSHint to really ignore things + jshint_Unknown: {level: "ignore"} # E043 also triggers Unknown, so ignore that, too + aether_MissingThis: {level: 'error'} + language: if @canWrite() then @language else 'javascript' functionName: @name functionParameters: @parameters yieldConditionally: thang.plan? - requiresThis: thang.requiresThis + globals: ['Vector', '_'] # TODO: Gridmancer doesn't currently work with protectAPI, so hack it off - protectAPI: not (@skipProtectAPI or window.currentView?.level.get('name').match("Gridmancer")) and @permissions.readwrite.length > 0 # If anyone can write to this method, we must protect it. - includeFlow: not @skipFlow and @canRead() - #callIndex: 0 - #timelessVariables: ['i'] - #statementIndex: 9001 - if not (me.team in @permissions.readwrite) or window.currentView?.sessionID is "52bfb88099264e565d001349" # temp fix for debugger explosion bug - #console.log "Turning off includeFlow for", @spellKey - aetherOptions.includeFlow = false + protectAPI: not (@skipProtectAPI or window.currentView?.level.get('name').match("Gridmancer")) and writable # If anyone can write to this method, we must protect it. + includeFlow: false #console.log "creating aether with options", aetherOptions aether = new Aether aetherOptions + workerMessage = + function: "createAether" + spellKey: @spellKey + options: aetherOptions + @worker.postMessage JSON.stringify workerMessage aether - updateLanguageAether: -> - aceConfig = me.get('aceConfig') ? {} + updateLanguageAether: (@language) -> for thangId, spellThang of @thangs - spellThang.aether?.setLanguage (aceConfig.language ? 'javascript') + spellThang.aether?.setLanguage @language spellThang.castAether = null + workerMessage = + function: "updateLanguageAether" + newLanguage: @language + @worker.postMessage JSON.stringify workerMessage @transpile() toString: -> diff --git a/app/views/play/level/tome/spell_debug_view.coffee b/app/views/play/level/tome/spell_debug_view.coffee index 32f7b0182..c48d3beab 100644 --- a/app/views/play/level/tome/spell_debug_view.coffee +++ b/app/views/play/level/tome/spell_debug_view.coffee @@ -13,6 +13,11 @@ module.exports = class DebugView extends View subscriptions: 'god:new-world-created': 'onNewWorld' + 'god:debug-value-return': 'handleDebugValue' + 'tome:spell-shown': 'changeCurrentThangAndSpell' + 'tome:cast-spells': 'onTomeCast' + 'surface:frame-changed': 'onFrameChanged' + 'tome:spell-has-changed-significantly-calculation': 'onSpellChangedCalculation' events: {} @@ -20,11 +25,72 @@ module.exports = class DebugView extends View super options @ace = options.ace @thang = options.thang + @spell = options.spell @variableStates = {} - @globals = {Math: Math, _: _} # ... add more as documented - for className, klass of serializedClasses - @globals[className] = klass + @globals = {Math: Math, _: _, String: String, Number: Number, Array: Array, Object: Object} # ... add more as documented + for className, serializedClass of serializedClasses + @globals[className] = serializedClass + @onMouseMove = _.throttle @onMouseMove, 25 + @cache = {} + @lastFrameRequested = -1 + @workerIsSimulating = false + @spellHasChanged = false + + + + pad2: (num) -> + if not num? or num is 0 then "00" else ((if num < 10 then "0" else "") + num) + + calculateCurrentTimeString: => + time = @currentFrame / @frameRate + mins = Math.floor(time / 60) + secs = (time - mins * 60).toFixed(1) + "#{mins}:#{@pad2 secs}" + + + setTooltipKeyAndValue: (key, value) => + message = "Time: #{@calculateCurrentTimeString()}\n#{key}: #{value}" + @$el.find("code").text message + @$el.show().css(@pos) + + setTooltipText: (text) => + #perhaps changing styling here in the future + @$el.find("code").text text + @$el.show().css(@pos) + + onTomeCast: -> + @invalidateCache() + + invalidateCache: -> @cache = {} + + retrieveValueFromCache: (thangID,spellID,variableChain,frame) -> + joinedVariableChain = variableChain.join() + value = @cache[frame]?[thangID]?[spellID]?[joinedVariableChain] + return value ? undefined + + updateCache: (thangID, spellID, variableChain, frame, value) -> + currentObject = @cache + keys = [frame,thangID,spellID,variableChain.join()] + for keyIndex in [0...(keys.length - 1)] + key = keys[keyIndex] + unless key of currentObject + currentObject[key] = {} + currentObject = currentObject[key] + currentObject[keys[keys.length - 1]] = value + + + changeCurrentThangAndSpell: (thangAndSpellObject) -> + @thang = thangAndSpellObject.thang + @spell = thangAndSpellObject.spell + + handleDebugValue: (returnObject) -> + @workerIsSimulating = false + {key, value} = returnObject + @updateCache(@thang.id,@spell.name,key.split("."),@lastFrameRequested,value) + if @variableChain and not key is @variableChain.join(".") then return + @setTooltipKeyAndValue(key,value) + afterRender: -> super() @@ -33,17 +99,18 @@ module.exports = class DebugView extends View setVariableStates: (@variableStates) -> @update() + isIdentifier: (t) -> + t and (t.type is 'identifier' or t.value is 'this' or @globals[t.value]) + onMouseMove: (e) => return if @destroyed pos = e.getDocumentPosition() - endOfDoc = pos.row is @ace.getSession().getDocument().getLength() - 1 it = new TokenIterator e.editor.session, pos.row, pos.column - isIdentifier = (t) => t and (t.type is 'identifier' or t.value is 'this' or @globals[t.value]) - while it.getCurrentTokenRow() is pos.row and not isIdentifier(token = it.getCurrentToken()) + endOfLine = it.getCurrentToken()?.index is it.$rowTokens.length - 1 + while it.getCurrentTokenRow() is pos.row and not @isIdentifier(token = it.getCurrentToken()) + break if endOfLine or not token # Don't iterate beyond end or beginning of line it.stepBackward() - break unless token - break if endOfDoc # Don't iterate backward on last line, since we might be way below. - if isIdentifier token + if @isIdentifier token # This could be a property access, like "enemy.target.pos" or "this.spawnedRectangles". # We have to realize this and dig into the nesting of the objects. start = it.getCurrentTokenColumn() @@ -53,11 +120,12 @@ module.exports = class DebugView extends View break unless it.getCurrentToken()?.value is "." it.stepBackward() token = null # If we're doing a complex access like this.getEnemies().length, then length isn't a valid var. - break unless isIdentifier(prev = it.getCurrentToken()) + break unless @isIdentifier(prev = it.getCurrentToken()) token = prev start = it.getCurrentTokenColumn() chain.unshift token.value - if token and (token.value of @variableStates or token.value is "this" or @globals[token.value]) + #Highlight all tokens, so true overrides all other conditions TODO: Refactor this later + if token and (true or token.value of @variableStates or token.value is "this" or @globals[token.value]) @variableChain = chain offsetX = e.domEvent.offsetX ? e.clientX - $(e.domEvent.target).offset().left offsetY = e.domEvent.offsetY ? e.clientY - $(e.domEvent.target).offset().top @@ -75,12 +143,35 @@ module.exports = class DebugView extends View onNewWorld: (e) -> @thang = @options.thang = e.world.thangMap[@thang.id] if @thang + @frameRate = e.world.frameRate + onFrameChanged: (data) -> + @currentFrame = data.frame + @frameRate = data.world.frameRate + onSpellChangedCalculation: (data) -> + @spellHasChanged = data.hasChangedSignificantly + update: -> if @variableChain - {key, value} = @deserializeVariableChain @variableChain - @$el.find("code").text "#{key}: #{value}" - @$el.show().css(@pos) + if @spellHasChanged + @setTooltipText("You've changed this spell! \nPlease recast to use the hover debugger.") + else if @variableChain.length is 2 and @variableChain[0] is "this" + @setTooltipKeyAndValue(@variableChain.join("."),@stringifyValue(@thang[@variableChain[1]],0)) + else if @variableChain.length is 1 and Aether.globals[@variableChain[0]] + @setTooltipKeyAndValue(@variableChain.join("."),@stringifyValue(Aether.globals[@variableChain[0]],0)) + else if @workerIsSimulating + @setTooltipText("World is simulating, please wait...") + else if @currentFrame is @lastFrameRequested and (cacheValue = @retrieveValueFromCache(@thang.id, @spell.name, @variableChain, @currentFrame)) + @setTooltipKeyAndValue(@variableChain.join("."),cacheValue) + else + Backbone.Mediator.publish 'tome:spell-debug-value-request', + thangID: @thang.id + spellID: @spell.name + variableChain: @variableChain + frame: @currentFrame + if @currentFrame isnt @lastFrameRequested then @workerIsSimulating = true + @lastFrameRequested = @currentFrame + @setTooltipText("Finding value...") else @$el.hide() if @variableChain?.length is 2 @@ -90,21 +181,6 @@ module.exports = class DebugView extends View @notifyPropertyHovered() @updateMarker() - notifyPropertyHovered: => - clearTimeout @hoveredPropertyTimeout if @hoveredPropertyTimeout - @hoveredPropertyTimeout = null - oldHoveredProperty = @hoveredProperty - @hoveredProperty = if @variableChain?.length is 2 then owner: @variableChain[0], property: @variableChain[1] else {} - unless _.isEqual oldHoveredProperty, @hoveredProperty - Backbone.Mediator.publish 'tome:spell-debug-property-hovered', @hoveredProperty - - updateMarker: -> - if @marker - @ace.getSession().removeMarker @marker - @marker = null - if @markerRange - @marker = @ace.getSession().addMarker @markerRange, "ace_bracket", "text" - stringifyValue: (value, depth) -> return value if not value or _.isString value if _.isFunction value @@ -138,27 +214,21 @@ module.exports = class DebugView extends View prefix ?= "Object" if isObject prefix = if prefix then prefix + " " else "" return "#{prefix}#{brackets[0]}#{sep} #{values.join(sep + ' ')}#{sep}#{brackets[1]}" + notifyPropertyHovered: => + clearTimeout @hoveredPropertyTimeout if @hoveredPropertyTimeout + @hoveredPropertyTimeout = null + oldHoveredProperty = @hoveredProperty + @hoveredProperty = if @variableChain?.length is 2 then owner: @variableChain[0], property: @variableChain[1] else {} + unless _.isEqual oldHoveredProperty, @hoveredProperty + Backbone.Mediator.publish 'tome:spell-debug-property-hovered', @hoveredProperty + + updateMarker: -> + if @marker + @ace.getSession().removeMarker @marker + @marker = null + if @markerRange + @marker = @ace.getSession().addMarker @markerRange, "ace_bracket", "text" - deserializeVariableChain: (chain) -> - keys = [] - for prop, i in chain - if prop is "this" - value = @thang - else if i is 0 - value = @variableStates[prop] - if typeof value is "undefined" then value = @globals[prop] - else - value = value[prop] - keys.push prop - break unless value - if theClass = serializedClasses[value.CN] - if value.CN is "Thang" - thang = @thang.world.thangMap[value.id] - value = thang or "" - else - value = theClass.deserializeFromAether(value) - value = @stringifyValue value, 0 - key: keys.join("."), value: value destroy: -> @ace?.removeEventListener "mousemove", @onMouseMove diff --git a/app/views/play/level/tome/spell_palette_entry_view.coffee b/app/views/play/level/tome/spell_palette_entry_view.coffee index ba6665931..5a610d984 100644 --- a/app/views/play/level/tome/spell_palette_entry_view.coffee +++ b/app/views/play/level/tome/spell_palette_entry_view.coffee @@ -6,9 +6,6 @@ filters = require 'lib/image_filter' {downTheChain} = require 'lib/world/world_utils' window.Vector = require 'lib/world/vector' # So we can document it -# If we use marked somewhere else, we'll have to make sure to preserve options -marked.setOptions {gfm: true, sanitize: false, smartLists: true, breaks: true} - safeJSONStringify = (input, maxDepth) -> recursion = (input, path, depth) -> output = {} diff --git a/app/views/play/level/tome/spell_palette_view.coffee b/app/views/play/level/tome/spell_palette_view.coffee index 3a1056733..e14ff1507 100644 --- a/app/views/play/level/tome/spell_palette_view.coffee +++ b/app/views/play/level/tome/spell_palette_view.coffee @@ -39,6 +39,7 @@ module.exports = class SpellPaletteView extends View for entry in entryColumn col.append entry.el entry.render() # Render after appending so that we can access parent container for popover + $('.nano').nanoScroller() createPalette: -> lcs = @supermodel.getModels LevelComponent @@ -50,19 +51,24 @@ module.exports = class SpellPaletteView extends View allDocs['__' + doc.name].push doc if doc.type is 'snippet' then doc.owner = 'snippets' - propStorage = - 'this': 'programmableProperties' - more: 'moreProgrammableProperties' - Math: 'programmableMathProperties' - Array: 'programmableArrayProperties' - Object: 'programmableObjectProperties' - String: 'programmableStringProperties' - Vector: 'programmableVectorProperties' - snippets: 'programmableSnippets' + if @options.programmable + propStorage = + 'this': 'programmableProperties' + more: 'moreProgrammableProperties' + Math: 'programmableMathProperties' + Array: 'programmableArrayProperties' + Object: 'programmableObjectProperties' + String: 'programmableStringProperties' + Vector: 'programmableVectorProperties' + snippets: 'programmableSnippets' + else + propStorage = + 'this': 'apiProperties' count = 0 propGroups = {} for owner, storage of propStorage - added = propGroups[owner] = _.sortBy(@thang[storage] ? []).slice() + props = _.reject @thang[storage] ? [], (prop) -> prop[0] is '_' # no private properties + added = propGroups[owner] = _.sortBy(props).slice() count += added.length shortenize = count > 6 @@ -77,7 +83,7 @@ module.exports = class SpellPaletteView extends View doc ?= prop @entries.push @addEntry(doc, shortenize, tabbify, owner is 'snippets') groupForEntry = (entry) -> - return 'more' if entry.doc.owner is 'this' and entry.doc.name in propGroups.more + return 'more' if entry.doc.owner is 'this' and entry.doc.name in (propGroups.more ? []) entry.doc.owner @entries = _.sortBy @entries, (entry) -> order = ['this', 'more', 'Math', 'Vector', 'snippets'] diff --git a/app/views/play/level/tome/spell_view.coffee b/app/views/play/level/tome/spell_view.coffee index 4951b6915..75f559bdf 100644 --- a/app/views/play/level/tome/spell_view.coffee +++ b/app/views/play/level/tome/spell_view.coffee @@ -18,6 +18,9 @@ module.exports = class SpellView extends View editModes: 'javascript': 'ace/mode/javascript' 'coffeescript': 'ace/mode/coffee' + 'clojure': 'ace/mode/clojure' + 'lua': 'ace/mode/lua' + 'python': 'ace/mode/python' keyBindings: 'default': null @@ -47,10 +50,11 @@ module.exports = class SpellView extends View constructor: (options) -> super options + @worker = options.worker @session = options.session @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @spell = options.spell - @problems = {} + @problems = [] @writable = false unless me.team in @spell.permissions.readwrite # TODO: make this do anything @highlightCurrentLine = _.throttle @highlightCurrentLine, 100 @@ -63,7 +67,7 @@ module.exports = class SpellView extends View @createFirepad() else # needs to happen after the code generating this view is complete - setTimeout @onAllLoaded, 1 + _.defer @onAllLoaded createACE: -> # Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html @@ -72,7 +76,7 @@ module.exports = class SpellView extends View @aceSession = @ace.getSession() @aceDoc = @aceSession.getDocument() @aceSession.setUseWorker false - @aceSession.setMode @editModes[aceConfig.language ? 'javascript'] + @aceSession.setMode @editModes[@spell.language] @aceSession.setWrapLimitRange null @aceSession.setUseWrapMode true @aceSession.setNewLineMode "unix" @@ -96,8 +100,12 @@ module.exports = class SpellView extends View aceCommands.push c.name addCommand name: 'run-code' - bindKey: {win: 'Shift-Enter|Ctrl-Enter|Ctrl-S', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter|Command-S|Ctrl-S'} + bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'} exec: -> Backbone.Mediator.publish 'tome:manual-cast', {} + addCommand + name: 'no-op' + bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'} + exec: -> # just prevent page save call addCommand name: 'toggle-playing' bindKey: {win: 'Ctrl-P', mac: 'Command-P|Ctrl-P'} @@ -106,8 +114,8 @@ module.exports = class SpellView extends View name: 'end-current-script' bindKey: {win: 'Shift-Space', mac: 'Shift-Space'} passEvent: true # https://github.com/ajaxorg/ace/blob/master/lib/ace/keyboard/keybinding.js#L114 - # No easy way to selectively cancel shift+space, since we don't get access to the event. - # Maybe we could temporarily set ourselves to read-only if we somehow know that a script is active? + # No easy way to selectively cancel shift+space, since we don't get access to the event. + # Maybe we could temporarily set ourselves to read-only if we somehow know that a script is active? exec: -> Backbone.Mediator.publish 'level:shift-space-pressed' addCommand name: 'end-all-scripts' @@ -145,6 +153,11 @@ module.exports = class SpellView extends View name: 'spell-beautify' bindKey: {win: 'Ctrl-Shift-B', mac: 'Command-Shift-B|Ctrl-Shift-B'} exec: -> Backbone.Mediator.publish 'spell-beautify' + addCommand + name: 'prevent-line-jump' + bindKey: {win: 'Ctrl-L', mac: 'Command-L'} + passEvent: true + exec: -> # just prevent default ACE go-to-line alert fillACE: -> @ace.setValue @spell.source @@ -157,7 +170,7 @@ module.exports = class SpellView extends View @firepad?.dispose() createFirepad: -> - # load from firebase or the original source if there's nothing there + # load from firebase or the original source if there's nothing there return if @firepadLoading @eventsSuppressed = true @loaded = false @@ -188,7 +201,7 @@ module.exports = class SpellView extends View @createToolbarView() createDebugView: -> - @debugView = new SpellDebugView ace: @ace, thang: @thang + @debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell @$el.append @debugView.render().$el.hide() createToolbarView: -> @@ -209,11 +222,11 @@ module.exports = class SpellView extends View @createDebugView() unless @debugView @debugView.thang = @thang @toolbarView?.toggleFlow false - @updateAether false, true + @updateAether false, false @highlightCurrentLine() - cast: -> - Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang + cast: (preload=false) -> + Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload notifySpellChanged: => Backbone.Mediator.publish 'tome:spell-changed', spell: @spell @@ -278,35 +291,27 @@ module.exports = class SpellView extends View ] @onCodeChangeMetaHandler = => return if @eventsSuppressed - if not @spellThang or @spell.hasChangedSignificantly @getSource(), @spellThang.aether.raw - callback() for callback in onSignificantChange # Do these first - callback() for callback in onAnyChange # Then these + @spell.hasChangedSignificantly @getSource(), @spellThang.aether.raw, (hasChanged) => + if not @spellThang or hasChanged + callback() for callback in onSignificantChange # Do these first + callback() for callback in onAnyChange # Then these @aceDoc.on 'change', @onCodeChangeMetaHandler - setRecompileNeeded: (needed=true) => - if needed - @recompileNeeded = needed # and @recompileValid # todo, remove if not caring about validity - else - @recompileNeeded = false + setRecompileNeeded: (@recompileNeeded) => onCursorActivity: => @currentAutocastHandler?() # Design for a simpler system? - # * Turn off ACE's JSHint worker # * Keep Aether linting, debounced, on any significant change - # - Don't send runtime errors from in-progress worlds # - All problems just vanish when you make any change to the code # * You wouldn't accept any Aether updates/runtime information/errors unless its code was current when you got it - # * Store the last run Aether in each spellThang and use it whenever its code actually is current - # This suffers from the problem that any whitespace/comment changes will lose your info, but what else - # could you do other than somehow maintain a mapping from current to original code locations? - # I guess you could use dynamic markers for problem ranges and keep annotations/alerts in when insignificant + # * Store the last run Aether in each spellThang and use it whenever its code actually is current. + # Use dynamic markers for problem ranges and keep annotations/alerts in when insignificant # changes happen, but always treat any change in the (trimmed) number of lines as a significant change. - # Ooh, that's pretty nice. Gets you most of the way there and is simple. # - All problems have a master representation as a Problem, and we can easily generate all Problems from # any Aether instance. Then when we switch contexts in any way, we clear, recreate, and reapply the Problems. - # * Problem alerts will have their own templated ProblemAlertViews + # * Problem alerts have their own templated ProblemAlertViews. # * We'll only show the first problem alert, and it will always be at the bottom. # Annotations and problem ranges can show all, I guess. # * The editor will reserve space for one annotation as a codeless area. @@ -317,20 +322,39 @@ module.exports = class SpellView extends View # to a new spellThang, we may want to refresh our Aether display. return unless aether = @spellThang?.aether source = @getSource() - codeHasChangedSignificantly = force or @spell.hasChangedSignificantly source, aether.raw - needsUpdate = codeHasChangedSignificantly or @spellThang isnt @lastUpdatedAetherSpellThang - return if not needsUpdate and aether is @displayedAether - castAether = @spellThang.castAether - codeIsAsCast = castAether and not @spell.hasChangedSignificantly source, castAether.raw - aether = castAether if codeIsAsCast - return if not needsUpdate and aether is @displayedAether + @spell.hasChangedSignificantly source, aether.raw, (hasChanged) => + codeHasChangedSignificantly = force or hasChanged + needsUpdate = codeHasChangedSignificantly or @spellThang isnt @lastUpdatedAetherSpellThang + return if not needsUpdate and aether is @displayedAether + castAether = @spellThang.castAether + codeIsAsCast = castAether and source is castAether.raw + aether = castAether if codeIsAsCast + return if not needsUpdate and aether is @displayedAether - # Now that that's figured out, perform the update. - @clearAetherDisplay() - aether.transpile source if codeHasChangedSignificantly and not codeIsAsCast - @displayAether aether - @lastUpdatedAetherSpellThang = @spellThang - @guessWhetherFinished aether if fromCodeChange + # Now that that's figured out, perform the update. + # The web worker Aether won't track state, so don't have to worry about updating it + finishUpdatingAether = (aether) => + @displayAether aether + @lastUpdatedAetherSpellThang = @spellThang + @guessWhetherFinished aether if fromCodeChange + + @clearAetherDisplay() + if codeHasChangedSignificantly and not codeIsAsCast + workerMessage = + function: "transpile" + spellKey: @spell.spellKey + source: source + + @worker.addEventListener "message", (e) => + workerData = JSON.parse e.data + if workerData.function is "transpile" and workerData.spellKey is @spell.spellKey + @worker.removeEventListener "message", arguments.callee, false + aether.problems = workerData.problems + aether.raw = source + finishUpdatingAether(aether) + @worker.postMessage JSON.stringify(workerMessage) + else + finishUpdatingAether(aether) clearAetherDisplay: -> problem.destroy() for problem in @problems @@ -341,6 +365,8 @@ module.exports = class SpellView extends View displayAether: (aether) -> @displayedAether = aether isCast = not _.isEmpty(aether.metrics) or _.some aether.problems.errors, {type: 'runtime'} + isCast = isCast or @spell.language isnt 'javascript' # Since we don't have linting for other languages + problem.destroy() for problem in @problems # Just in case another problem was added since clearAetherDisplay() ran. @problems = [] annotations = [] seenProblemKeys = {} @@ -362,28 +388,36 @@ module.exports = class SpellView extends View # Autocast: # Goes immediately if the code is a) changed and b) complete/valid and c) the cursor is at beginning or end of a line - # We originall thought it would: + # We originally thought it would: # - Go after specified delay if a) and b) but not c) # - Go only when manually cast or deselecting a Thang when there are errors # But the error message display was delayed, so now trying: # - Go after specified delay if a) and not b) or c) guessWhetherFinished: (aether) -> - return if @autocastDelay > 60000 - #@recompileValid = not aether.getAllProblems().length valid = not aether.getAllProblems().length cursorPosition = @ace.getCursorPosition() currentLine = _.string.rtrim(@aceDoc.$lines[cursorPosition.row].replace(/[ \t]*\/\/[^"']*/g, '')) # trim // unless inside " endOfLine = cursorPosition.column >= currentLine.length # just typed a semicolon or brace, for example beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example #console.log "finished?", valid, endOfLine, beginningOfLine, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine - if valid and endOfLine or beginningOfLine - @recompile() - #console.log "recompile now!" - #else if not valid - # # if this works, we can get rid of all @recompileValid logic - # console.log "not valid, but so we'll wait to do it in", @autocastDelay + "ms" - #else - # console.log "valid but not at end of line; recompile in", @autocastDelay + "ms" + if valid and (endOfLine or beginningOfLine) + if @autocastDelay > 60000 + @preload() + else + @recompile() + + preload: -> + # Send this code over to the God for preloading, but don't change the cast state. + oldSource = @spell.source + oldSpellThangAethers = {} + for thangID, spellThang of @spell.thangs + oldSpellThangAethers[thangID] = spellThang.aether.serialize() # Get raw, pure, and problems + @spell.transpile @getSource() + @cast true + @spell.source = oldSource + for thangID, spellThang of @spell.thangs + for key, value of oldSpellThangAethers[thangID] + spellThang.aether[key] = value onSpellChanged: (e) -> @spellHasChanged = true @@ -400,10 +434,11 @@ module.exports = class SpellView extends View return @onInfiniteLoop e if e.problem.id is "runtime_InfiniteLoop" return unless e.problem.userInfo.methodName is @spell.name return unless spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID - return if @spell.hasChangedSignificantly @getSource() # don't show this error if we've since edited the code - spellThang.aether.addProblem e.problem - @lastUpdatedAetherSpellThang = null # force a refresh without a re-transpile - @updateAether false, false + @spell.hasChangedSignificantly @getSource(), null, (hasChanged) => + return if hasChanged + spellThang.aether.addProblem e.problem + @lastUpdatedAetherSpellThang = null # force a refresh without a re-transpile + @updateAether false, false onInfiniteLoop: (e) -> return unless @spellThang @@ -418,7 +453,7 @@ module.exports = class SpellView extends View aether = e.world.userCodeMap[thangID]?[@spell.name] # Might not be there if this is a new Programmable Thang. spellThang.castAether = aether spellThang.aether = @spell.createAether thang - #console.log thangID, @spell.spellKey, "ran", aether.metrics.callsExecuted, "times over", aether.metrics.statementsExecuted, "statements, with max recursion depth", aether.metrics.maxDepth, "and full flow/metrics", aether.metrics, aether.flow + #console.log thangID, @spell.spellKey, "ran", aether.metrics.callsExecuted, "times over", aether.metrics.statementsExecuted, "statements, with max recursion depth", aether.metrics.maxDepth, "and full flow/metrics", aether.metrics, aether.flow @spell.transpile() @updateAether false, false @@ -438,7 +473,7 @@ module.exports = class SpellView extends View @highlightCurrentLine() onCoordinateSelected: (e) -> - return unless e.x? and e.y? + return unless @ace.isFocused() and e.x? and e.y? @ace.insert "{x: #{e.x}, y: #{e.y}}" @highlightCurrentLine() @@ -469,7 +504,7 @@ module.exports = class SpellView extends View break _.last(executed).push state executedRows[state.range[0].row] = true - #state.executing = true if state.userInfo?.time is @thang.world.age # no work + #state.executing = true if state.userInfo?.time is @thang.world.age # no work currentCallIndex ?= callNumber - 1 #console.log "got call index", currentCallIndex, "for time", @thang.world.age, "out of", states.length @@ -571,14 +606,14 @@ module.exports = class SpellView extends View @ace.setDisplayIndentGuides aceConfig.indentGuides # default false @ace.setShowInvisibles aceConfig.invisibles # default false @ace.setKeyboardHandler @keyBindings[aceConfig.keyBindings ? 'default'] - # @aceSession.setMode @editModes[aceConfig.language ? 'javascript'] onChangeLanguage: (e) -> - aceConfig = me.get('aceConfig') ? {} - @aceSession.setMode @editModes[aceConfig.language ? 'javascript'] + if @spell.canWrite() + @aceSession.setMode @editModes[e.language] dismiss: -> - @recompile() if @spell.hasChangedSignificantly @getSource() + @spell.hasChangedSignificantly @getSource(), null, (hasChanged) => + @recompile() if hasChanged destroy: -> $(@ace?.container).find('.ace_gutter').off 'click', '.ace_error, .ace_warning, .ace_info', @onAnnotationClick diff --git a/app/views/play/level/tome/thang_list_entry_view.coffee b/app/views/play/level/tome/thang_list_entry_view.coffee index 6135a6d53..388a5d10f 100644 --- a/app/views/play/level/tome/thang_list_entry_view.coffee +++ b/app/views/play/level/tome/thang_list_entry_view.coffee @@ -156,9 +156,16 @@ module.exports = class ThangListEntryView extends View @$el.toggleClass('disabled', not enabled) onFrameChanged: (e) -> + # Optimize return unless currentThang = e.world.thangMap[@thang.id] - @$el.toggle Boolean(currentThang.exists) - @$el.toggleClass 'dead', currentThang.health <= 0 if currentThang.exists + exists = Boolean currentThang.exists + if @thangDidExist isnt exists + @$el.toggle exists + @thangDidExist = exists + dead = exists and currentThang.health <= 0 + if @thangWasDead isnt dead + @$el.toggleClass 'dead', dead + @thangWasDead = dead destroy: -> @avatar?.destroy() diff --git a/app/views/play/level/tome/tome_view.coffee b/app/views/play/level/tome/tome_view.coffee index e7cc1d7e8..1366722e8 100644 --- a/app/views/play/level/tome/tome_view.coffee +++ b/app/views/play/level/tome/tome_view.coffee @@ -36,7 +36,7 @@ ThangListView = require './thang_list_view' SpellPaletteView = require './spell_palette_view' CastButtonView = require './cast_button_view' -window.SHIM_WORKER_PATH = '/javascripts/workers/catiline_worker_shim.coffee' +window.SHIM_WORKER_PATH = '/javascripts/workers/catiline_worker_shim.js' module.exports = class TomeView extends View id: 'tome-view' @@ -51,6 +51,7 @@ module.exports = class TomeView extends View 'tome:change-language': 'updateLanguageForAllSpells' 'surface:sprite-selected': 'onSpriteSelected' 'god:new-world-created': 'onNewWorld' + 'tome:comment-my-code': 'onCommentMyCode' events: 'click #spell-view': 'onSpellViewClick' @@ -72,33 +73,22 @@ module.exports = class TomeView extends View delete @options.thangs onNewWorld: (e) -> - thangs = _.filter e.world.thangs, 'isSelectable' + thangs = _.filter e.world.thangs, 'inThangList' programmableThangs = _.filter thangs, 'isProgrammable' @createSpells programmableThangs, e.world @thangList.adjustThangs @spells, thangs @spellList.adjustSpells @spells + onCommentMyCode: (e) -> + for spellKey, spell of @spells when spell.canWrite() + console.log "Commenting out", spellKey + commentedSource = 'return; // Commented out to stop infinite loop.\n' + spell.getSource() + spell.view.updateACEText commentedSource + spell.view.recompile false + @cast() + createWorker: -> - return - # In progress - worker = cw - initialize: (scope) -> - self.window = self - self.global = self - console.log 'Tome worker initialized.' - doIt: (data, callback, scope) -> - console.log 'doing', what - try - importScripts '/javascripts/tome_aether.js' - catch err - console.log err.toString() - a = new Aether() - callback 'good' - undefined - onAccepted = (s) -> console.log 'accepted', s - onRejected = (s) -> console.log 'rejected', s - worker.doIt('hmm').then onAccepted, onRejected - worker + return new Worker("/javascripts/workers/aether_worker.js") generateTeamSpellMap: (spellObject) -> teamSpellMap = {} @@ -115,6 +105,7 @@ module.exports = class TomeView extends View return teamSpellMap createSpells: (programmableThangs, world) -> + language = @options.session.get('codeLanguage') ? me.get('aceConfig')?.language ? 'javascript' pathPrefixComponents = ['play', 'level', @options.levelID, @options.session.id, 'code'] @spells ?= {} @thangSpells ?= {} @@ -129,9 +120,18 @@ module.exports = class TomeView extends View spellKey = pathComponents.join '/' @thangSpells[thang.id].push spellKey unless method.cloneOf - skipProtectAPI = @getQueryVariable "skip_protect_api", not @options.ladderGame - skipFlow = @getQueryVariable "skip_flow", @options.levelID is 'brawlwood' - spell = @spells[spellKey] = new Spell programmableMethod: method, spellKey: spellKey, pathComponents: pathPrefixComponents.concat(pathComponents), session: @options.session, supermodel: @supermodel, skipFlow: skipFlow, skipProtectAPI: skipProtectAPI, worker: @worker + skipProtectAPI = @getQueryVariable "skip_protect_api", (@options.levelID in ['gridmancer']) + spell = @spells[spellKey] = new Spell + programmableMethod: method + spellKey: spellKey + pathComponents: pathPrefixComponents.concat(pathComponents) + session: @options.session + supermodel: @supermodel + skipProtectAPI: skipProtectAPI + worker: @worker + language: language + spectateView: @options.spectateView + for thangID, spellKeys of @thangSpells thang = world.getThangByID thangID if thang @@ -149,19 +149,10 @@ module.exports = class TomeView extends View onCastSpell: (e) -> # A single spell is cast. # Hmm; do we need to make sure other spells are all cast here? - @cast() + @cast e?.preload - cast: -> - if @options.levelID is 'brawlwood' - # For performance reasons, only includeFlow on the currently Thang. - for spellKey, spell of @spells - for thangID, spellThang of spell.thangs - hadFlow = Boolean spellThang.aether.options.includeFlow - willHaveFlow = spellThang is @spellView?.spellThang - spellThang.aether.options.includeFlow = spellThang.aether.originalOptions.includeFlow = willHaveFlow - spellThang.aether.transpile spell.source unless hadFlow is willHaveFlow - #console.log "set includeFlow to", spellThang.aether.options.includeFlow, "for", thangID, "of", spellKey - Backbone.Mediator.publish 'tome:cast-spells', spells: @spells + cast: (preload=false) -> + Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload onToggleSpellList: (e) -> @spellList.rerenderEntries() @@ -188,14 +179,12 @@ module.exports = class TomeView extends View thang = e.thang spellName = e.spellName @spellList?.$el.hide() - return @clearSpellView() unless thang?.isProgrammable - selectedThangSpells = (@spells[spellKey] for spellKey in @thangSpells[thang.id]) - if spellName - spell = _.find selectedThangSpells, {name: spellName} - else - spell = @thangList.topSpellForThang thang - #spell = selectedThangSpells[0] # TODO: remember last selected spell for this thang - return @clearSpellView() unless spell?.canRead() + return @clearSpellView() unless thang + spell = @spellFor thang, spellName + unless spell?.canRead() + @clearSpellView() + @updateSpellPalette thang, spell + return unless spell.view is @spellView @clearSpellView() @spellView = spell.view @@ -208,18 +197,32 @@ module.exports = class TomeView extends View @spellList.setThangAndSpell thang, spell @spellView?.setThang thang @spellTabView?.setThang thang - if @spellPaletteView?.thang isnt thang - @spellPaletteView = @insertSubView new SpellPaletteView thang: thang, supermodel: @supermodel - @spellPaletteView.toggleControls {}, spell.view.controlsEnabled # TODO: know when palette should have been disabled but didn't exist + @updateSpellPalette thang, spell + + updateSpellPalette: (thang, spell) -> + return unless thang and @spellPaletteView?.thang isnt thang and thang.programmableProperties or thang.apiProperties + @spellPaletteView = @insertSubView new SpellPaletteView thang: thang, supermodel: @supermodel, programmable: spell?.canRead() + @spellPaletteView.toggleControls {}, spell.view.controlsEnabled if spell # TODO: know when palette should have been disabled but didn't exist + + spellFor: (thang, spellName) -> + return null unless thang?.isProgrammable + selectedThangSpells = (@spells[spellKey] for spellKey in @thangSpells[thang.id]) + if spellName + spell = _.find selectedThangSpells, {name: spellName} + else + spell = @thangList.topSpellForThang thang + #spell = selectedThangSpells[0] # TODO: remember last selected spell for this thang + spell reloadAllCode: -> - spell.view.reloadCode false for spellKey, spell of @spells when spell.team is me.team - Backbone.Mediator.publish 'tome:cast-spells', spells: @spells + spell.view.reloadCode false for spellKey, spell of @spells when spell.team is me.team or (spell.team in ["common", "neutral", null]) + Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: false - updateLanguageForAllSpells: -> - spell.updateLanguageAether() for spellKey, spell of @spells + updateLanguageForAllSpells: (e) -> + spell.updateLanguageAether e.language for spellKey, spell of @spells when spell.canWrite() + @cast() destroy: -> spell.destroy() for spellKey, spell of @spells - @worker?._close() + @worker?.terminate() super() diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index bd60b867f..392198638 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -12,7 +12,7 @@ Surface = require 'lib/surface/Surface' God = require 'lib/God' GoalManager = require 'lib/world/GoalManager' ScriptManager = require 'lib/scripts/ScriptManager' -LevelBus = require('lib/LevelBus') +LevelBus = require 'lib/LevelBus' LevelLoader = require 'lib/LevelLoader' LevelSession = require 'models/LevelSession' Level = require 'models/Level' @@ -60,7 +60,6 @@ module.exports = class PlayLevelView extends View 'surface:world-set-up': 'onSurfaceSetUpNewWorld' 'level:session-will-save': 'onSessionWillSave' 'level:set-team': 'setTeam' - 'god:new-world-created': 'loadSoundsForWorld' 'level:started': 'onLevelStarted' 'level:loading-view-unveiled': 'onLoadingViewUnveiled' @@ -70,64 +69,56 @@ module.exports = class PlayLevelView extends View shortcuts: 'ctrl+s': 'onCtrlS' + # Initial Setup ############################################################# + constructor: (options, @levelID) -> console.profile?() if PROFILE_ME super options if not me.get('hourOfCode') and @getQueryVariable "hour_of_code" - me.set 'hourOfCode', true - me.save() - $('body').append($("")) - window.tracker?.trackEvent 'Hour of Code Begin', {} + @setUpHourOfCode() @isEditorPreview = @getQueryVariable 'dev' @sessionID = @getQueryVariable 'session' $(window).on('resize', @onWindowResize) - @listenToOnce(@supermodel, 'error', @onLevelLoadError) @saveScreenshot = _.throttle @saveScreenshot, 30000 if @isEditorPreview - f = => - @supermodel.shouldSaveBackups = (model) -> - model.constructor.className in ['Level', 'LevelComponent', 'LevelSystem'] - @load() unless @levelLoader + # wait to see if it's just given to us through setLevel + f = => @load() unless @levelLoader setTimeout f, 100 else @load() + application.tracker?.trackEvent 'Started Level Load', level: @levelID, label: @levelID - onLevelLoadError: (e) -> - application.router.navigate "/play?not_found=#{@levelID}", {trigger: true} + setUpHourOfCode: -> + me.set 'hourOfCode', true + me.save() + $('body').append($("")) + application.tracker?.trackEvent 'Hour of Code Begin', {} - setLevel: (@level, @supermodel) -> - @god?.level = @level.serialize @supermodel + setLevel: (@level, givenSupermodel) -> + @supermodel.models = givenSupermodel.models + @supermodel.collections = givenSupermodel.collections + @supermodel.shouldSaveBackups = givenSupermodel.shouldSaveBackups + + serializedLevel = @level.serialize @supermodel + @god?.setLevel serializedLevel if @world - serializedLevel = @level.serialize(@supermodel) @world.loadFromLevel serializedLevel, false else @load() load: -> + @loadStartTime = new Date() + @god = new God debugWorker: true @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team") - @listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded) - @listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged) - @god = new God() + @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded - getRenderData: -> - c = super() - c.world = @world - if me.get('hourOfCode') and me.lang() is 'en-US' - # Show the Hour of Code footer explanation until it's been more than a day - elapsed = (new Date() - new Date(me.get('dateCreated'))) - c.explainHourOfCode = elapsed < 86400 * 1000 - c + # CocoView overridden methods ############################################### - afterRender: -> - window.onPlayLevelViewLoaded? @ # still a hack - @insertSubView @loadingView = new LoadingView {} - @$el.find('#level-done-button').hide() - super() - - onLevelLoaderProgressChanged: -> + updateProgress: (progress) -> + super(progress) return if @seenDocs return unless @levelLoader.session.loaded and @levelLoader.level.loaded return unless showFrequency = @levelLoader.level.get('showsGuide') @@ -142,56 +133,66 @@ module.exports = class PlayLevelView extends View showGuide: -> @seenDocs = true DocsModal = require './level/modal/docs_modal' - options = {docs: @levelLoader.level.get('documentation'), supermodel: @supermodel} + options = + docs: @levelLoader.level.get('documentation') + supermodel: @supermodel + firstOnly: true @openModalView(new DocsModal(options), true) - Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelLoaderLoaded, @ + Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelStarted, @ return true - onLevelLoaderLoaded: -> - return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early - @loadingView.showReady() - if window.currentModal and not window.currentModal.destroyed - return Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelLoaderLoaded, @ + getRenderData: -> + c = super() + c.world = @world + if me.get('hourOfCode') and me.lang() is 'en-US' + # Show the Hour of Code footer explanation until it's been more than a day + elapsed = (new Date() - new Date(me.get('dateCreated'))) + c.explainHourOfCode = elapsed < 86400 * 1000 + c - # Save latest level played in local storage - if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial']) - me.set('lastLevel', @levelID) - me.save() + afterRender: -> + super() + window.onPlayLevelViewLoaded? @ # still a hack + @insertSubView @loadingView = new LoadingView {} + @$el.find('#level-done-button').hide() + $('body').addClass('is-playing') + + afterInsert: -> + super() + @showWizardSettingsModal() if not me.get('name') + + # Partially Loaded Setup #################################################### + + onWorldNecessitiesLoaded: -> + # Called when we have enough to build the world, but not everything is loaded @grabLevelLoaderData() team = @getQueryVariable("team") ? @world.teamForPlayer(0) @loadOpponentTeam(team) - @god.level = @level.serialize @supermodel - @god.worldClassMap = @world.classMap + @setupGod() @setTeam team - @initSurface() @initGoalManager() - @initScriptManager() - @insertSubviews ladderGame: (@level.get('type') is "ladder") + @insertSubviews() @initVolume() @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @originalSessionState = $.extend(true, {}, @session.get('state')) @register() @controlBar.setBus(@bus) - @surface.showLevel() - if @otherSession - # TODO: colorize name and cloud by team, colorize wizard by user's color config - @surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team') + @initScriptManager() grabLevelLoaderData: -> @session = @levelLoader.session @world = @levelLoader.world @level = @levelLoader.level @otherSession = @levelLoader.opponentSession - @levelLoader.destroy() - @levelLoader = null loadOpponentTeam: (myTeam) -> opponentSpells = [] for spellTeam, spells of @session.get('teamSpells') ? @otherSession?.get('teamSpells') ? {} continue if spellTeam is myTeam or not myTeam opponentSpells = opponentSpells.concat spells - - opponentCode = @otherSession?.get('submittedCode') or {} + if (not @session.get('teamSpells')) and @otherSession?.get('teamSpells') + @session.set('teamSpells',@otherSession.get('teamSpells')) + opponentCode = @otherSession?.get('transpiledCode') or {} myCode = @session.get('code') or {} for spell in opponentSpells [thang, spell] = spell.split '/' @@ -203,41 +204,107 @@ module.exports = class PlayLevelView extends View # For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet. @session.set 'multiplayer', false - onLevelStarted: (e) -> - @loadingView?.unveil() + setupGod: -> + @god.setLevel @level.serialize @supermodel + @god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id] + @god.setWorldClassMap @world.classMap - onLoadingViewUnveiled: (e) -> - @removeSubView @loadingView - @loadingView = null + setTeam: (team) -> + team = team?.team unless _.isString team + team ?= 'humans' + me.team = team + Backbone.Mediator.publish 'level:team-set', team: team + @team = team - onSupermodelLoadedOne: => - @modelsLoaded ?= 0 - @modelsLoaded += 1 - @updateInitString() + initGoalManager: -> + @goalManager = new GoalManager(@world, @level.get('goals'), @team) + @god.setGoalManager @goalManager - updateInitString: -> - return if @surface - @modelsLoaded ?= 0 - canvas = @$el.find('#surface')[0] - ctx = canvas.getContext('2d') - ctx.font="20px Georgia" - ctx.clearRect(0, 0, canvas.width, canvas.height) - ctx.fillText("Loaded #{@modelsLoaded} thingies",50,50) - - insertSubviews: (subviewOptions) -> - @insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel, ladderGame: subviewOptions.ladderGame - @insertSubView new PlaybackView {} + insertSubviews: -> + @insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel + @insertSubView new PlaybackView session: @session @insertSubView new GoalsView {} @insertSubView new GoldView {} @insertSubView new HUDView {} @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session worldName = utils.i18n @level.attributes, 'name' - @controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, playableTeams: @world.playableTeams, ladderGame: subviewOptions.ladderGame} - #Backbone.Mediator.publish('level-set-debug', debug: true) if me.displayName() is 'Nick!' + @controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, playableTeams: @world.playableTeams} + #Backbone.Mediator.publish('level-set-debug', debug: true) if me.displayName() is 'Nick!' - afterInsert: -> - super() - @showWizardSettingsModal() if not me.get('name') + initVolume: -> + volume = me.get('volume') + volume = 1.0 unless volume? + Backbone.Mediator.publish 'level-set-volume', volume: volume + + initScriptManager: -> + @scriptManager = new ScriptManager({scripts: @world.scripts or [], view:@, session: @session}) + @scriptManager.loadFromSession() + + register: -> + @bus = LevelBus.get(@levelID, @session.id) + @bus.setSession(@session) + @bus.setSpells @tome.spells + @bus.connect() if @session.get('multiplayer') + + # Load Completed Setup ###################################################### + + onLoaded: -> + _.defer => @onLevelLoaded() + + onLevelLoaded: -> + # Everything is now loaded + return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early + + # Save latest level played in local storage + if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial']) + me.set('lastLevel', @levelID) + me.save() + @levelLoader.destroy() + @levelLoader = null + @initSurface() + + initSurface: -> + surfaceCanvas = $('canvas#surface', @$el) + @surface = new Surface(@world, surfaceCanvas, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview) + worldBounds = @world.getBounds() + bounds = [{x:worldBounds.left, y:worldBounds.top}, {x:worldBounds.right, y:worldBounds.bottom}] + @surface.camera.setBounds(bounds) + @surface.camera.zoomTo({x:0, y:0}, 0.1, 0) + + # Once Surface is Loaded #################################################### + + onLevelStarted: -> + @loadingView.showReady() + if window.currentModal and not window.currentModal.destroyed + return Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelStarted, @ + @surface.showLevel() + if @otherSession + # TODO: colorize name and cloud by team, colorize wizard by user's color config + @surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team') + @loadingView?.unveil() + + onLoadingViewUnveiled: (e) -> + @loadingView.$el.remove() + @removeSubView @loadingView + @loadingView = null + unless @isEditorPreview + @loadEndTime = new Date() + loadDuration = @loadEndTime - @loadStartTime + console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s" + application.tracker?.trackEvent 'Finished Level Load', level: @levelID, label: @levelID, loadDuration: loadDuration + application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID + + onSurfaceSetUpNewWorld: -> + return if @alreadyLoadedState + @alreadyLoadedState = true + state = @originalSessionState + if state.frame and @level.get('type') isnt 'ladder' # https://github.com/codecombat/codecombat/issues/714 + Backbone.Mediator.publish 'level-set-time', { time: 0, frameOffset: state.frame } + if state.selected + # TODO: Should also restore selected spell here by saving spellName + Backbone.Mediator.publish 'level-select-sprite', { thangID: state.selected, spellName: null } + if state.playing? + Backbone.Mediator.publish 'level-set-playing', { playing: state.playing } # callbacks @@ -272,28 +339,30 @@ module.exports = class PlayLevelView extends View $('#level-done-button').show() @showVictory() if e.showModal setTimeout(@preloadNextLevel, 3000) + return if @victorySeen + @victorySeen = true + victoryTime = (new Date()) - @loadEndTime + if victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level + application.tracker?.trackEvent 'Saw Victory', level: @level.get('name'), label: @level.get('name') + application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID, 100 showVictory: -> options = {level: @level, supermodel: @supermodel, session: @session} docs = new VictoryModal(options) @openModalView(docs) - window.tracker?.trackEvent 'Saw Victory', level: @world.name, label: @world.name if me.get('anonymous') - window.nextLevelURL = @getNextLevelID() # Signup will go here on completion instead of reloading. + window.nextLevelURL = @getNextLevelURL() # Signup will go here on completion instead of reloading. onRestartLevel: -> @tome.reloadAllCode() Backbone.Mediator.publish 'level:restarted' $('#level-done-button', @$el).hide() - window.tracker?.trackEvent 'Confirmed Restart', level: @world.name, label: @world.name - - onNewWorld: (e) -> - @world = e.world + application.tracker?.trackEvent 'Confirmed Restart', level: @level.get('name'), label: @level.get('name') onInfiniteLoop: (e) -> return unless e.firstWorld @openModalView new InfiniteLoopModal() - window.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @world.name, label: @world.name + application.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @level.get('name'), label: @level.get('name') onPlayNextLevel: -> nextLevelID = @getNextLevelID() @@ -304,15 +373,17 @@ module.exports = class PlayLevelView extends View viewArgs: [{supermodel:@supermodel}, nextLevelID]} getNextLevel: -> - nextLevelOriginal = @level.get('nextLevel')?.original + return null unless nextLevelOriginal = @level.get('nextLevel')?.original levels = @supermodel.getModels(Level) return l for l in levels when l.get('original') is nextLevelOriginal getNextLevelID: -> - nextLevel = @getNextLevel() + return null unless nextLevel = @getNextLevel() nextLevelID = nextLevel.get('slug') or nextLevel.id - getNextLevelURL: -> "/play/level/#{@getNextLevelID()}" + getNextLevelURL: -> + return null unless @getNextLevelID() + "/play/level/#{@getNextLevelID()}" onHighlightDom: (e) -> if e.delay @@ -361,14 +432,15 @@ module.exports = class PlayLevelView extends View .css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)") .css('top', target_top - 50) .css('left', target_left - 50) - setTimeout((=> + setTimeout(()=> + return if @destroyed @animatePointer() clearInterval(@pointerInterval) @pointerInterval = setInterval(@animatePointer, 1200) - ), 1) + , 1) - animatePointer: -> + animatePointer: => pointer = $('#pointer') pointer.css('transition', 'all 0.6s ease-out') pointer.css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance-50}px)") @@ -388,46 +460,11 @@ module.exports = class PlayLevelView extends View @bus.removeFirebaseData => @bus.disconnect() - # initialization - addPointer: -> p = $('#pointer') return if p.length @$el.append($('')) - initSurface: -> - surfaceCanvas = $('canvas#surface', @$el) - @surface = new Surface(@world, surfaceCanvas, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview) - worldBounds = @world.getBounds() - bounds = [{x:worldBounds.left, y:worldBounds.top}, {x:worldBounds.right, y:worldBounds.bottom}] - @surface.camera.setBounds(bounds) - @surface.camera.zoomTo({x:0, y:0}, 0.1, 0) - - initGoalManager: -> - @goalManager = new GoalManager(@world, @level.get('goals')) - @god.goalManager = @goalManager - - initScriptManager: -> - @scriptManager = new ScriptManager({scripts: @world.scripts or [], view:@, session: @session}) - @scriptManager.loadFromSession() - - initVolume: -> - volume = me.get('volume') - volume = 1.0 unless volume? - Backbone.Mediator.publish 'level-set-volume', volume: volume - - onSurfaceSetUpNewWorld: -> - return if @alreadyLoadedState - @alreadyLoadedState = true - state = @originalSessionState - if state.frame - Backbone.Mediator.publish 'level-set-time', { time: 0, frameOffset: state.frame } - if state.selected - # TODO: Should also restore selected spell here by saving spellName - Backbone.Mediator.publish 'level-select-sprite', { thangID: state.selected, spellName: null } - if state.playing? - Backbone.Mediator.publish 'level-set-playing', { playing: state.playing } - preloadNextLevel: => # TODO: Loading models in the middle of gameplay causes stuttering. Most of the improvement in loading time is simply from passing the supermodel from this level to the next, but if we can find a way to populate the level early without it being noticeable, that would be even better. # return if @destroyed @@ -436,34 +473,24 @@ module.exports = class PlayLevelView extends View # @supermodel.populateModel nextLevel # @preloaded = true - register: -> - @bus = LevelBus.get(@levelID, @session.id) - @bus.setSession(@session) - @bus.setSpells @tome.spells - @bus.connect() if @session.get('multiplayer') - onSessionWillSave: (e) -> # Something interesting has happened, so (at a lower frequency), we'll save a screenshot. - @saveScreenshot e.session + #@saveScreenshot e.session # Throttled saveScreenshot: (session) => return unless screenshot = @surface?.screenshot() session.save {screenshot: screenshot}, {patch: true} - setTeam: (team) -> - team = team?.team unless _.isString team - team ?= 'humans' - me.team = team - Backbone.Mediator.publish 'level:team-set', team: team - # Dynamic sound loading - loadSoundsForWorld: (e) -> + onNewWorld: (e) -> return if @headless - world = e.world + scripts = @world.scripts # Since these worlds don't have scripts, preserve them. + @world = e.world + @world.scripts = scripts thangTypes = @supermodel.getModels(ThangType) - for [spriteName, message] in world.thangDialogueSounds() + for [spriteName, message] in @world.thangDialogueSounds() continue unless thangType = _.find thangTypes, (m) -> m.get('name') is spriteName continue unless sound = AudioPlayer.soundForDialogue message, thangType.get('soundTriggers') AudioPlayer.preloadSoundReference sound diff --git a/app/views/play/spectate_view.coffee b/app/views/play/spectate_view.coffee index da6f5e611..76e5430ca 100644 --- a/app/views/play/spectate_view.coffee +++ b/app/views/play/spectate_view.coffee @@ -8,7 +8,7 @@ World = require 'lib/world/world' # tools Surface = require 'lib/surface/Surface' -God = require 'lib/God' +God = require 'lib/God' # 'lib/Buddha' GoalManager = require 'lib/world/GoalManager' ScriptManager = require 'lib/scripts/ScriptManager' LevelLoader = require 'lib/LevelLoader' @@ -90,9 +90,9 @@ module.exports = class SpectateLevelView extends View application.router.navigate "/play?not_found=#{@levelID}", {trigger: true} setLevel: (@level, @supermodel) -> - @god?.level = @level.serialize @supermodel + serializedLevel = @level.serialize @supermodel + @god?.setLevel serializedLevel if @world - serializedLevel = @level.serialize(@supermodel) @world.loadFromLevel serializedLevel, false else @load() @@ -106,8 +106,7 @@ module.exports = class SpectateLevelView extends View spectateMode: true team: @getQueryVariable("team") @listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded) - @listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged) - @god = new God maxWorkerPoolSize: 1, maxAngels: 1 + @god = new God maxAngels: 1 getRenderData: -> c = super() @@ -119,8 +118,10 @@ module.exports = class SpectateLevelView extends View @insertSubView @loadingView = new LoadingView {} @$el.find('#level-done-button').hide() super() + $('body').addClass('is-playing') - onLevelLoaderProgressChanged: -> + updateProgress: (progress) -> + super(progress) return if @seenDocs return unless showFrequency = @levelLoader.level.get('showGuide') session = @levelLoader.session @@ -140,7 +141,10 @@ module.exports = class SpectateLevelView extends View Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelLoaderLoaded, @ return true - onLevelLoaderLoaded: -> + onLoaded: -> + _.defer => @onLevelLoaded() + + onLevelLoaded: -> return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early # Save latest level played in local storage if window.currentModal and not window.currentModal.destroyed @@ -151,13 +155,14 @@ module.exports = class SpectateLevelView extends View #at this point, all requisite data is loaded, and sessions are not denormalized team = @world.teamForPlayer(0) @loadOpponentTeam(team) - @god.level = @level.serialize @supermodel - @god.worldClassMap = @world.classMap + @god.setLevel @level.serialize @supermodel + @god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id] + @god.setWorldClassMap @world.classMap @setTeam team @initSurface() @initGoalManager() @initScriptManager() - @insertSubviews ladderGame: @otherSession? + @insertSubviews() @initVolume() @originalSessionState = $.extend(true, {}, @session.get('state')) @@ -191,13 +196,14 @@ module.exports = class SpectateLevelView extends View continue if spellTeam is myTeam or not myTeam opponentSpells = opponentSpells.concat spells - opponentCode = @otherSession?.get('submittedCode') or {} - myCode = @session.get('submittedCode') or {} + opponentCode = @otherSession?.get('transpiledCode') or {} + myCode = @session.get('transpiledCode') or {} for spell in opponentSpells [thang, spell] = spell.split '/' c = opponentCode[thang]?[spell] myCode[thang] ?= {} if c then myCode[thang][spell] = c else delete myCode[thang][spell] + @session.set('code', myCode) if @session.get('multiplayer') and @otherSession? # For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet. @@ -207,8 +213,9 @@ module.exports = class SpectateLevelView extends View @loadingView?.unveil() onLoadingViewUnveiled: (e) -> - @removeSubView @loadingView - @loadingView = null + # Don't remove it; we want its decoration around on large screens. + #@removeSubView @loadingView + #@loadingView = null onSupermodelLoadedOne: => @modelsLoaded ?= 0 @@ -224,8 +231,8 @@ module.exports = class SpectateLevelView extends View ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.fillText("Loaded #{@modelsLoaded} thingies",50,50) - insertSubviews: (subviewOptions) -> - @insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel, ladderGame: subviewOptions.ladderGame + insertSubviews: -> + @insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel, spectateView: true @insertSubView new PlaybackView {} @insertSubView new GoldView {} @@ -382,7 +389,7 @@ module.exports = class SpectateLevelView extends View initGoalManager: -> @goalManager = new GoalManager(@world, @level.get('goals')) - @god.goalManager = @goalManager + @god.setGoalManager @goalManager initScriptManager: -> if @world.scripts diff --git a/app/views/play_view.coffee b/app/views/play_view.coffee index 349f900c7..8662ecef4 100644 --- a/app/views/play_view.coffee +++ b/app/views/play_view.coffee @@ -1,7 +1,7 @@ View = require 'views/kinds/RootView' template = require 'templates/play' LevelSession = require 'models/LevelSession' -CocoCollection = require 'models/CocoCollection' +CocoCollection = require 'collections/CocoCollection' class LevelSessionsCollection extends CocoCollection url: '' @@ -138,11 +138,19 @@ module.exports = class PlayView extends View difficulty: 5 id: 'gridmancer' image: '/file/db/level/52ae2460ef42c52f13000008/gridmancer_icon.png' - description: "Challenge! Beat this level, get a job!" + description: "Super algorithm challenge level!" } ] arenas = [ + { + name: 'Greed' + difficulty: 4 + id: 'greed' + image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png' + description: "Liked Dungeon Arena and Gold Rush? Put them together in this economic arena!" + levelPath: 'ladder' + } { name: 'Dungeon Arena' difficulty: 3 @@ -151,6 +159,14 @@ module.exports = class PlayView extends View description: "Play head-to-head against fellow Wizards in a dungeon melee!" levelPath: 'ladder' } + { + name: 'Gold Rush' + difficulty: 3 + id: 'gold-rush' + image: '/file/db/level/52602ecb026e8481e7000001/generic_1.png' + description: "Prove you are better at collecting gold than your opponent!" + levelPath: 'ladder' + } { name: 'Brawlwood' difficulty: 4 @@ -204,6 +220,21 @@ module.exports = class PlayView extends View image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png' description: "Transfer a stack of ogres while preserving their honor. - by Alexandru" } + { + name: 'Find the Spy' + difficulty: 2 + id: 'find-the-spy' + image: '/file/db/level/526ae95c1e5cd30000000008/zone_of_danger_icon.png' + description: "Identify the spies hidden among your soldiers - by Nathan Gossett" + } + { + name: 'Harvest Time' + difficulty: 2 + id: 'harvest-time' + image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png' + description: "Collect a hundred mushrooms in just five lines of code - by Nathan Gossett" + } + #{ # name: 'Enemy Artillery' # difficulty: 1 diff --git a/bin/coco-brunch b/bin/coco-brunch index 6ebf85db5..9afe5e7c7 100755 --- a/bin/coco-brunch +++ b/bin/coco-brunch @@ -9,7 +9,7 @@ import sys current_directory = os.path.dirname(os.path.realpath(sys.argv[0])) coco_path = os.getenv("COCO_DIR",os.path.join(current_directory,os.pardir)) brunch_path = coco_path + os.sep + "node_modules" + os.sep + ".bin" + os.sep + "brunch" -subprocess.Popen("ulimit -n 10000",shell=True) +subprocess.Popen("ulimit -n 100000",shell=True) while True: print("Starting brunch. After the first compile, it'll keep running and watch for changes.") call(brunch_path + " w",shell=True,cwd=coco_path) diff --git a/bin/coco-dev-server b/bin/coco-dev-server index c76923672..e30367130 100755 --- a/bin/coco-dev-server +++ b/bin/coco-dev-server @@ -8,5 +8,5 @@ current_directory = os.path.dirname(os.path.realpath(sys.argv[0])) coco_path = os.getenv("COCO_DIR",os.path.join(current_directory,os.pardir)) nodemon_path = coco_path + os.sep + "node_modules" + os.sep + ".bin" + os.sep + "nodemon" -call(nodemon_path + " . --ext \".coffee|.js\" --watch server --watch app.js --watch server_config.js --watch server_setup.coffee",shell=True,cwd=coco_path) +call(nodemon_path + " . --ext \".coffee|.js\" --watch server --watch server_config.js --watch server_setup.coffee --watch app" + os.sep + "schemas",shell=True,cwd=coco_path) diff --git a/bower.json b/bower.json index e73834bc5..f04a51d91 100644 --- a/bower.json +++ b/bower.json @@ -24,19 +24,26 @@ "test" ], "dependencies": { - "jquery": "~2.0.3", + "jquery": "~2.1.0", "lodash": "~2.4.1", "backbone": "1.1.0", "jquery-mousewheel": "~3.1.9", - "i18next": "~1.7.1", + "i18next": "git://github.com/nwinter/i18next.git", "firepad": "~0.1.2", "marked": "~0.3.0", "moment": "~2.5.0", - "aether": "~0.1.18", + "aether": "~0.2.8", "underscore.string": "~2.3.3", "firebase": "~1.0.2", "catiline": "~2.9.3", - "d3": "~3.4.4" + "d3": "~3.4.4", + "jsondiffpatch": "~0.1.5", + "nanoscroller": "~0.8.0", + "jquery.tablesorter": "~2.15.13", + "treema": "~0.0.8", + "bootstrap": "~3.1.1", + "validated-backbone-mediator": "~0.1.3", + "jquery.browser": "~0.0.6" }, "overrides": { "backbone": { @@ -50,6 +57,29 @@ }, "underscore.string": { "main": "lib/underscore.string.js" + }, + "jsondiffpatch": { + "main": [ + "build/bundle-full.js", + "build/formatters.js", + "src/formatters/html.css" + ] + }, + "jquery.tablesorter": { + "main": [ + "js/jquery.tablesorter.js", + "js/jquery.tablesorter.widgets.js", + "css/theme.bootstrap.css" + ] + }, + "bootstrap": { + "main": [ + "./dist/js/bootstrap.js", + "./dist/fonts/glyphicons-halflings-regular.eot", + "./dist/fonts/glyphicons-halflings-regular.svg", + "./dist/fonts/glyphicons-halflings-regular.ttf", + "./dist/fonts/glyphicons-halflings-regular.woff" + ] } } } diff --git a/config.coffee b/config.coffee index 1a860cb76..55b9888b4 100644 --- a/config.coffee +++ b/config.coffee @@ -11,6 +11,7 @@ exports.config = ignored: (path) -> startsWith(sysPath.basename(path), '_') workers: enabled: false # turned out to be much, much slower than without workers + sourceMaps: true files: javascripts: defaultExtension: 'coffee' @@ -21,58 +22,54 @@ exports.config = |(app[\/\\]lib[\/\\]utils.coffee) |(vendor[\/\\]scripts[\/\\]Box2dWeb-2.1.a.3) |(vendor[\/\\]scripts[\/\\]string_score.js) - |(bower_components[\/\\]lodash[\/\\]dist[\/\\]lodash.js) - |(bower_components[\/\\]aether[\/\\]build[\/\\]aether.js) )/// 'javascripts/app.js': /^app/ 'javascripts/vendor.js': ///^( vendor[\/\\](?!scripts[\/\\]Box2d) - |bower_components + |bower_components[\/\\](?!aether) )/// 'javascripts/vendor_with_box2d.js': ///^( vendor[\/\\] - |bower_components # include box2dweb for profiling (and for IE9...) + |bower_components[\/\\](?!aether) # include box2dweb for profiling (and for IE9...) )/// - 'javascripts/tome_aether.js': ///^( + 'javascripts/lodash.js': ///^( (bower_components[\/\\]lodash[\/\\]dist[\/\\]lodash.js) - |(bower_components[\/\\]aether[\/\\]build[\/\\]aether.js) )/// - 'test/javascripts/test.js': /^test[\/\\](?!vendor)/ - 'test/javascripts/test-vendor.js': /^test[\/\\](?=vendor)/ + 'javascripts/aether.js': ///^( + (bower_components[\/\\]aether[\/\\]build[\/\\]aether.js) + )/// +# 'test/javascripts/test.js': /^test[\/\\](?!vendor)/ +# 'test/javascripts/test-vendor.js': /^test[\/\\](?=vendor)/ order: before: [ - 'bower_components/jquery/jquery.js' + 'bower_components/jquery/dist/jquery.js' 'bower_components/lodash/dist/lodash.js' 'bower_components/backbone/backbone.js' # Twitter Bootstrap jquery plugins - 'vendor/scripts/bootstrap/transition.js' - 'vendor/scripts/bootstrap/affix.js' - 'vendor/scripts/bootstrap/alert.js' - 'vendor/scripts/bootstrap/button.js' - 'vendor/scripts/bootstrap/carousel.js' - 'vendor/scripts/bootstrap/collapse.js' - 'vendor/scripts/bootstrap/dropdown.js' - 'vendor/scripts/bootstrap/modal.js' - 'vendor/scripts/bootstrap/scrollspy.js' - 'vendor/scripts/bootstrap/tab.js' - 'vendor/scripts/bootstrap/tooltip.js' + 'bower_components/bootstrap/dist/bootstrap.js' # CreateJS dependencies 'vendor/scripts/easeljs-NEXT.combined.js' 'vendor/scripts/preloadjs-NEXT.combined.js' 'vendor/scripts/soundjs-NEXT.combined.js' 'vendor/scripts/tweenjs-NEXT.combined.js' 'vendor/scripts/movieclip-NEXT.min.js' + # Validated Backbone Mediator dependencies + 'bower_components/tv4/tv4.js' # Aether before box2d for some strange Object.defineProperty thing 'bower_components/aether/build/aether.js' 'bower_components/d3/d3.min.js' + 'vendor/scripts/async.js' ] stylesheets: defaultExtension: 'sass' joinTo: - 'stylesheets/app.css': /^(app|vendor)/ + 'stylesheets/app.css': /^(app|vendor|bower_components)/ order: - before: ['app/styles/bootstrap.scss'] + before: [ + 'app/styles/bootstrap.scss' + 'vendor/styles/nanoscroller.scss' + ] templates: defaultExtension: 'jade' joinTo: 'javascripts/app.js' diff --git a/headless_client.coffee b/headless_client.coffee new file mode 100644 index 000000000..bdd7eeded --- /dev/null +++ b/headless_client.coffee @@ -0,0 +1,217 @@ +### +This file will simulate games on node.js by emulating the browser environment. +### +simulateOneGame = false +if process.argv[2] is "one-game" + #calculate result of one game here + simulateOneGame = true + console.log "Simulating #{process.argv[3]} vs #{process.argv[4]}" +bowerComponentsPath = "./bower_components/" +headlessClientPath = "./headless_client/" + +# SETTINGS +options = + workerCode: require headlessClientPath + 'worker_world' + debug: false # Enable logging of ajax calls mainly + testing: false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. + testFile: require headlessClientPath + 'test.js' + leakTest: false # Install callback that tries to find leaks automatically + exitOnLeak: false # Exit if leak is found. Only useful if leaktest is set to true, obviously. + heapdump: false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. + headlessClient: true + +options.heapdump = require('heapdump') if options.heapdump +server = if options.testing then "http://127.0.0.1:3000" else "http://codecombat.com" + +# Disabled modules +disable = [ + 'lib/AudioPlayer' + 'locale/locale' + '../locale/locale' +] + +# Start of the actual code. Setting up the enivronment to match the environment of the browser + +# the path used for the loader. __dirname is module dependent. +path = __dirname + +m = require 'module' +request = require 'request' +Deferred = require "JQDeferred" +originalLoader = m._load + +unhook = () -> + m._load = originalLoader + +hook = () -> + m._load = hookedLoader + + +JASON = require 'jason' + +# Global emulated stuff +GLOBAL.window = GLOBAL +GLOBAL.document = location: pathname: "headless_client" +GLOBAL.console.debug = console.log + +GLOBAL.Worker = require('webworker-threads').Worker +Worker::removeEventListener = (what) -> + if what is 'message' + @onmessage = -> #This webworker api has only one event listener at a time. + +GLOBAL.tv4 = require('tv4').tv4 + +GLOBAL.marked = setOptions: -> + +store = {} +GLOBAL.localStorage = + getItem: (key) => store[key] + setItem: (key, s) => store[key] = s + removeItem: (key) => delete store[key] + +# Hook node.js require. See https://github.com/mfncooper/mockery/blob/master/mockery.js +# The signature of this function *must* match that of Node's Module._load, +# since it will replace that. +# (Why is there no easier way?) +hookedLoader = (request, parent, isMain) -> + if request in disable or ~request.indexOf('templates') + console.log 'Ignored ' + request if options.debug + return class fake + else if '/' in request and not (request[0] is '.') or request is 'application' + request = path + '/app/' + request + else if request is 'underscore' + request = 'lodash' + + console.log "loading " + request if options.debug + originalLoader request, parent, isMain + + +#jQuery wrapped for compatibility purposes. Poorly. +GLOBAL.$ = GLOBAL.jQuery = (input) -> + console.log 'Ignored jQuery: ' + input if options.debug + append: (input)-> exports: ()-> + +cookies = request.jar() +$.when = Deferred.when + +$.ajax = (options) -> + responded = false + url = options.url + if url.indexOf('http') + url = '/' + url unless url[0] is '/' + url = server + url + + data = options.data + + + #if (typeof data) is 'object' + #console.warn JSON.stringify data + #data = JSON.stringify data + + console.log "Requesting: " + JSON.stringify options if options.debug + console.log "URL: " + url if options.debug + + deferred = Deferred() + + request + url: url + jar: cookies + json: options.parse + method: options.type + body: data + , (error, response, body) -> + console.log "HTTP Request:" + JSON.stringify options if options.debug and not error + + if responded + console.log "\t↳Already returned before." if options.debug + return + + if (error) + console.warn "\t↳Returned: error: #{error}" + options.error(error) if options.error? + deferred.reject() + + else + console.log "\t↳Returned: statusCode #{response.statusCode}: #{if options.parse then JSON.stringify body else body}" if options.debug + options.success(body, response, status: response.statusCode) if options.success? + deferred.resolve() + + statusCode = response.statusCode if response? + options.complete(status: statusCode) if options.complete? + responded = true + + deferred.promise() + + +$.extend = (deep, into, from) -> + copy = _.clone(from, deep); + if into + _.assign into, copy + copy = into + copy + +$.isArray = (object) -> + _.isArray object + +$.isPlainObject = (object) -> + _.isPlainObject object + + +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.string = _.str + _.mixin _.str.exports() + + +# load Backbone. Needs hooked loader to reroute underscore to lodash. +hook() +GLOBAL.Backbone = require bowerComponentsPath + 'backbone/backbone' +unhook() +Backbone.$ = $ + +require bowerComponentsPath + 'validated-backbone-mediator/backbone-mediator' +# Instead of mediator, dummy might be faster yet suffice? +#Mediator = class Mediator +# publish: (id, object) -> +# console.Log "Published #{id}: #{object}" +# @subscribe: () -> +# @unsubscribe: () -> + +GLOBAL.Aether = require 'aether' + +# Set up new loader. +hook() + +login = require './login.coffee' #should contain an object containing they keys 'username' and 'password' + +#Login user and start the code. +$.ajax + url: '/auth/login' + type: "POST" + data: login + parse: true + error: (error) -> "Bad Error. Can't connect to server or something. " + error + success: (response) -> + console.log "User: " + JSON.stringify response + GLOBAL.window.userObject = response # JSON.parse response + + User = require 'models/User' + + World = require 'lib/world/world' + LevelLoader = require 'lib/LevelLoader' + GoalManager = require 'lib/world/GoalManager' + + SuperModel = require 'models/SuperModel' + + log = require 'winston' + + CocoClass = require 'lib/CocoClass' + + Simulator = require 'lib/simulator/Simulator' + + sim = new Simulator options + if simulateOneGame + sim.fetchAndSimulateOneGame(process.argv[3],process.argv[4]) + else + sim.fetchAndSimulateTask() diff --git a/headless_client/test.js b/headless_client/test.js new file mode 100644 index 000000000..fdd51bf83 --- /dev/null +++ b/headless_client/test.js @@ -0,0 +1,87 @@ +module.exports = { + "messageGenerated": 1396792689279, + "sessions": [ + { + "sessionID": "533a2c4893b95d9319a58049", + "submitDate": "2014-04-06T06:31:11.806Z", + "team": "humans", + "code": { + "ogre-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// Choose your hero! You can only build one hero.\nvar hero;\n//hero = 'ironjaw'; // A leaping juggernaut hero, type 'brawler'.\nhero = 'yugargen'; // A devious spellcaster hero, type 'shaman'.\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Munchkins are weak melee units with 1.25s build cooldown.\n// Throwers are fragile, deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['munchkin', 'thrower', 'munchkin', 'thrower', 'munchkin', 'thrower'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);\n//this.say(\"Move\", {to:{x:20, y:30}});{x: 68, y: 29}{x: 70, y: 30}" + }, + "programmable-shaman": { + "chooseAction": "if (this.hero !== undefined) {\n this.hero = this.getNearest(enemy);\n}\n// Shamans are spellcasters with a weak magic attack\n// and three spells: 'shrink', 'grow', and 'poison-cloud'.\n// Shrink: target has 2/3 health, 1.5x speed for 5s.\n// Grow: target has double health, half speed for 5s.\n// Once per match, she can cast poison cloud, which does\n// 5 poison dps for 10s to enemies in a 10m radius.\nvar right = 0;\nif(right === 0){this.move({x: 70, y: 40});\n}\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\nif(this.canCast('shrink', enemy)) \n{\n this.castShrink(enemy);\n}\nelse\n{\n this.castGrow(friend);\n}\n\nvar enemiesinpoisonrange = 0;\nfor (var i = 0; i < enemies.lenght; ++i) {\n var enemi = enemies[i];\n if (this.distance(enemi) <= 10) {\n enemiesinpoisonrange++;\n }\n}\nif (enemiesinpoisonrange >= 7) {\n this.castPoisonCloud(enemy);\n}\n//if (this.distance(ogrebase) > 10) {\n// this.move({x: 70, y: 30});\n//}\n//this.say(\"Defend!\", {targetPos: {x: 45, y: 30}});\n\n//this.say(\"Defend!\", {targetPos: {x: 35, y: 30}});\n\n//this.say(\"Defend!\", {targetPos: {x: 25, y: 30}});\n\n//this.say(\"Attack!\", {to:{x:20, y:30}});\n\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('shrink', enemy)) this.castShrink(enemy);\n//if(this.canCast('grow', friend)) this.castGrow(friend);\n//if(this.canCast('poison-cloud', enemy)) this.castPoisonCloud(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 45, y: 30}});\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});" + }, + "programmable-brawler": { + "chooseAction": "// The Brawler is a huge melee hero with mighty mass.\n// this.throw() hurls an enemy behind him.\n// this.jumpTo() leaps to a target within 20m every 10s.\n// this.stomp() knocks everyone away, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('jump')) this.jumpTo(enemy.pos);\n//if(!this.getCooldown('stomp') && this.distance(enemy) < 10) this.stomp();\n//if(!this.getCooldown('throw')) this.throw(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;{x: 68, y: 29}{x: 70, y: 30}" + }, + "programmable-librarian": { + "chooseAction": "var enemies = this.getEnemies();\nif (enemies.length === 0)\n return;\nvar enemy = this.getNearest(enemies);\nvar friends = this.getFriends();\nvar friend = this.getNearest(friends);\nvar archer = this.getFriends(type, \"archer\");\nvar soldier = this.getFriends(type, \"soldier\");\nvar hero = this.getFriends(type, \"hushbaum\");\nvar rand = Math.random();\nvar xmove;\nvar ymove;\nfor (var i = 0; i < enemies.length / 3; i += 1) {\n var e = enemies[i];\n var ehealth = Math.floor(e.health);\n if (this.canCast(\"haste\", friend)) {\n this.say(\"Godspeed \" + friend.id + \"!\");\n this.castHaste(friend);\n }\n if (this.canCast(\"haste\", this)) {\n this.say(\"I am Godspeed!\");\n this.castHaste(this);\n }\n if (this.canCast(\"slow\", e)) {\n this.say(\"Chill Out \" + e.id + \"!\");\n this.castSlow(e);\n }\n if (this.distance(e) < 45) {\n this.attack(e);\n this.say(\"Attacking \" + e.id + \" life is \" + ehealth + \".\");\n }\n if (this.health < this.maxHealth * 0.75) {\n if (this.pos.x > 20) {\n this.move({\n x: this.pos.x - 20,\n y: this.pos.y\n });\n } else {\n this.move({\n x: this.pos.x + 20,\n y: this.pos.y\n });\n }\n }\n if (this.canCast(\"regen\", this)) {\n this.castRegen(this);\n this.say(\"I won't die today bitch!\");\n }\n if (friend.health < friend.maxHealth * 0.5) {\n if (this.canCast(\"regen\", friend)) {\n this.say(\"You won't die today \" + friend.id + \".\");\n this.castRegen(friend);\n }\n }\n}\n;" + }, + "programmable-tharin": { + "chooseAction": "// Tharin is a melee fighter with shield, warcry, and terrify skills.\n// this.shield() lets him take one-third damage while defending.\n// this.warcry() gives allies within 10m 30% haste for 5s, every 10s.\n// this.terrify() sends foes within 30m fleeing for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('warcry')) this.warcry();\n//if(!this.getCooldown('terrify')) this.terrify();\n//this.shield();\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 40, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;" + }, + "human-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// CHOOSE YOUR HERO! You can only build one hero.\nvar hero;\n//hero = 'tharin'; // A fierce knight with battlecry abilities.\nhero = 'hushbaum'; // A fiery spellcaster hero.\n\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Soldiers are hard-to-kill, low damage melee units with 2s build cooldown.\n// Archers are fragile but deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['soldier', 'soldier', 'archer', 'archer', 'soldier', 'soldier'];\nvar type = buildOrder[this.built.length % buildOrder.length];\nthis.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);\n\n " + } + }, + "teamSpells": { + "ogres": [ + "programmable-brawler/chooseAction", + "programmable-shaman/chooseAction", + "ogre-base/chooseAction" + ], + "humans": [ + "programmable-librarian/chooseAction", + "programmable-tharin/chooseAction", + "human-base/chooseAction" + ] + }, + "levelID": "dungeon-arena", + "creator": "5338c38c4811eff221de2347", + "creatorName": "iC0DE" + }, + { + "sessionID": "532a777c2042708b711a6c29", + "submitDate": "2014-03-20T05:45:54.691Z", + "team": "ogres", + "code": { + "ogre-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// Choose your hero! You can only build one hero.\nvar hero;\n//hero = 'ironjaw'; // A leaping juggernaut hero, type 'brawler'.\nhero = 'yugargen'; // A devious spellcaster hero, type 'shaman'.\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Munchkins are weak melee units with 1.25s build cooldown.\n// Throwers are fragile, deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['munchkin', 'munchkin', 'munchkin', 'thrower'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);" + }, + "programmable-shaman": { + "chooseAction": "// Shamans are spellcasters with a weak magic attack\n// and three spells: 'shrink', 'grow', and 'poison-cloud'.\n// Shrink: target has 2/3 health, 1.5x speed for 5s.\n// Grow: target has double health, half speed for 5s.\n// Once per match, she can cast poison cloud, which does\n// 5 poison dps for 10s to enemies in a 10m radius.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) {\n return; // Chill if all enemies are dead.\n}\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\nif (enemies.length > 5) {\n if(this.canCast('poison-cloud', enemy)) {\n this.castPoisonCloud(enemy);\n return;\n }\n}\n\nif (friends.length > 4) {\n this.attack(enemy); \n}\nfor (var i = 0; i < friends.length; ++i) {\n if (friends[i].health < 0) {\n continue;\n }\n if(friends[i].type == \"thrower\" && this.canCast('shrink', friends[i])) {\n this.castShrink(friends[i]);\n return;\n } \n if(friends[i].type == \"munchkin\" && this.canCast('grow', friends[i])) {\n this.castGrow(friends[i]);\n return;\n } \n}\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('shrink', enemy)) this.castShrink(enemy);\n//if(this.canCast('grow', friend)) this.castGrow(friend);\n//if(this.canCast('poison-cloud', enemy)) this.castPoisonCloud(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});" + }, + "programmable-brawler": { + "chooseAction": "// The Brawler is a huge melee hero with mighty mass.\n// this.throw() hurls an enemy behind him.\n// this.jumpTo() leaps to a target within 20m every 10s.\n// this.stomp() knocks everyone away, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('jump')) this.jumpTo(enemy.pos);\n//if(!this.getCooldown('stomp') && this.distance(enemy) < 10) this.stomp();\n//if(!this.getCooldown('throw')) this.throw(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;" + }, + "human-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// CHOOSE YOUR HERO! You can only build one hero.\nvar hero;\nhero = 'tharin'; // A fierce knight with battlecry abilities.\n//hero = 'hushbaum'; // A fiery spellcaster hero.\n\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Soldiers are hard-to-kill, low damage melee units with 2s build cooldown.\n// Archers are fragile but deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['archer', 'archer', 'soldier', 'archer', 'soldier'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);" + }, + "programmable-tharin": { + "chooseAction": "this.findTypeInRange = function(units, type) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if (unit.type === type && this.distance(unit) < 20)\n return unit;\n }\n return null;\n};\n\nthis.findType = function(units, type) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if (unit.type === type)\n return unit;\n }\n return null;\n};\n\nthis.findHeroInRange = function(units, range) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if ((unit.type === 'shaman' || unit.type === 'brawler') && this.distance(unit) < range)\n return unit;\n }\n return null;\n};\n\n// Tharin is a melee fighter with shield, warcry, and terrify skills.\n// this.shield() lets him take one-third damage while defending.\n// this.warcry() gives allies within 10m 30% haste for 5s, every 10s.\n// this.terrify() sends foes within 30m fleeing for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\n\n//Enemies\nvar enemyBase = this.findType(enemies, 'base');\nvar brawler = this.findTypeInRange(enemies, 'brawler');\nvar shaman = this.findTypeInRange(enemies, 'shaman');\n\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('warcry')) this.warcry();\n//if(!this.getCooldown('terrify')) this.terrify();\n//this.shield();\n\nif((brawler || shaman) && !this.attackTime)\n{\n this.attackTime = true;\n if(brawler)\n this.say(\"Attack!\", {target: brawler});\n else if(shaman)\n this.say(\"Attack!\", {target: shaman});\n}\nelse if(this.health < 15 && this.getCooldown('terrify'))\n{\n this.terrify();\n}\nelse if(this.findHeroInRange(enemies, 30) && this.getCooldown('terrify'))\n{\n this.terrify();\n}\nelse if(this.health < 25)\n{\n this.shield();\n}\nelse if(brawler && this.distance(brawler) <=10)\n{\n this.attack(brawler);\n}\nelse\n{\n this.attack(enemy);\n}\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 40, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;" + }, + "programmable-librarian": { + "chooseAction": "// The Librarian is a spellcaster with a fireball attack\n// plus three useful spells: 'slow', 'regen', and 'haste'.\n// Slow makes a target move and attack at half speed for 5s.\n// Regen makes a target heal 10 hp/s for 10s.\n// Haste speeds up a target by 4x for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('slow', enemy)) this.castSlow(enemy);\n//if(this.canCast('regen', friend)) this.castRegen(friend);\n//if(this.canCast('haste', friend)) this.castHaste(friend);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});" + } + }, + "teamSpells": { + "ogres": [ + "programmable-brawler/chooseAction", + "programmable-shaman/chooseAction", + "ogre-base/chooseAction" + ], + "humans": [ + "programmable-librarian/chooseAction", + "programmable-tharin/chooseAction", + "human-base/chooseAction" + ] + }, + "levelID": "dungeon-arena", + "creator": "53291a80b112e7240f324667", + "creatorName": "Imbal Oceanrage" + } +], + "taskID": "53415d71942d00aa43dbf3e9", + "receiptHandle": "cd50e44db7dbd4cc0bcce047aa822ba2fe3556cf" +} \ No newline at end of file diff --git a/headless_client/worker_world.coffee b/headless_client/worker_world.coffee new file mode 100644 index 000000000..e40317bfe --- /dev/null +++ b/headless_client/worker_world.coffee @@ -0,0 +1,220 @@ +# function to use inside a webworker. +# This function needs to run inside an environment that has a 'self'. +# This specific worker is targeted towards the node.js headless_client environment. + +JASON = require 'jason' +fs = require 'fs' +GLOBAL.Aether = Aether = require 'aether' +GLOBAL._ = _ = require 'lodash' + +betterConsole = () -> + + self.logLimit = 200; + self.logsLogged = 0; + + self.transferableSupported = () -> true + + self.console = log: -> + if self.logsLogged++ is self.logLimit + self.postMessage + type: "console-log" + args: ["Log limit " + self.logLimit + " reached; shutting up."] + id: self.workerID + + else if self.logsLogged < self.logLimit + args = [].slice.call(arguments) + i = 0 + + while i < args.length + args[i] = args[i].toString() if args[i].constructor.className is "Thang" or args[i].isComponent if args[i] and args[i].constructor + ++i + try + self.postMessage + type: "console-log" + args: args + id: self.workerID + + catch error + self.postMessage + type: "console-log" + args: [ + "Could not post log: " + args + error.toString() + error.stack + error.stackTrace + ] + id: self.workerID + + # so that we don't crash when debugging statements happen + self.console.error = self.console.warn = self.console.info = self.console.debug = self.console.log + GLOBAL.console = console = self.console + self.console + + +work = () -> + console.log "starting..." + + console.log = -> + + World = self.require('lib/world/world') + GoalManager = self.require('lib/world/GoalManager') + + Aether.addGlobal('Vector', require('lib/world/vector')) + Aether.addGlobal('_', _) + + self.cleanUp = -> + self.world = null + self.goalManager = null + self.postedErrors = {} + self.t0 = null + self.logsLogged = 0 + + self.runWorld = (args) -> + console.log "Running world inside worker." + self.postedErrors = {} + self.t0 = new Date() + self.postedErrors = false + self.logsLogged = 0 + + try + self.world = new World(args.userCodeMap) + self.world.levelSessionIDs = args.levelSessionIDs + self.world.loadFromLevel args.level, true if args.level + self.world.headless = args.headless + self.goalManager = new GoalManager(self.world) + self.goalManager.setGoals args.goals + self.goalManager.setCode args.userCodeMap + self.goalManager.worldGenerationWillBegin() + self.world.setGoalManager self.goalManager + catch error + console.log "There has been an error inside the worker." + self.onWorldError error + return + Math.random = self.world.rand.randf # so user code is predictable + console.log "Loading frames." + + self.postMessage type: "start-load-frames" + + + self.world.loadFrames self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress, true + + + self.onWorldLoaded = onWorldLoaded = -> + self.goalManager.worldGenerationEnded() + goalStates = self.goalManager.getGoalStates() + self.postMessage type: "end-load-frames", goalStates: goalStates + + t1 = new Date() + diff = t1 - self.t0 + if (self.world.headless) + return console.log("Headless simulation completed in #{diff}ms."); + + transferableSupported = self.transferableSupported() + try + serialized = serializedWorld: self.world.serialize() + transferableSupported = false + catch error + console.log "World serialization error:", error.toString() + "\n" + error.stack or error.stackTrace + t2 = new Date() + + # console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); + try + message = + type: "new-world" + serialized: serialized.serializedWorld + goalStates: goalStates + if transferableSupported + self.postMessage message, serialized.transferableObjects + else + self.postMessage message + + catch error + console.log "World delivery error:", error.toString() + "\n" + error.stack or error.stackTrace + t3 = new Date() + console.log "And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms" + self.cleanUp() + + + self.onWorldError = onWorldError = (error) -> + self.postMessage type: "end-load-frames" + if error instanceof Aether.problems.UserCodeProblem + #console.log "Aether userCodeProblem occured." + unless self.postedErrors[error.key] + problem = error.serialize() + self.postMessage + type: "user-code-problem" + problem: problem + + self.postedErrors[error.key] = problem + else + console.log "Non-UserCodeError:", error.toString() + "\n" + error.stack or error.stackTrace + self.cleanUp() + + self.onWorldLoadProgress = onWorldLoadProgress = (progress) -> + #console.log "Worker onWorldLoadProgress" + self.postMessage + type: "world-load-progress-changed" + progress: progress + + self.abort = abort = -> + #console.log "Abort called for worker." + if self.world + #console.log "About to abort:", self.world.name, typeof self.world.abort + self.world.abort() + self.world = null + self.postMessage type: "abort" + self.cleanUp() + + self.reportIn = reportIn = -> + console.log "Reporting in." + self.postMessage type: "report-in" + + self.addEventListener "message", (event) -> + #console.log JSON.stringify event + self[event.data.func] event.data.args + + self.postMessage type: "worker-initialized" + +worldCode = fs.readFileSync "./public/javascripts/world.js", 'utf8' +lodashCode = fs.readFileSync "./public/javascripts/lodash.js", 'utf8' +aetherCode = fs.readFileSync "./public/javascripts/aether.js", 'utf8' + +#window.BOX2D_ENABLED = true; + +newConsole = "newConsole = #{}JASON.stringify newConsole}()"; + +ret = """ + + GLOBAL = root = window = self; + GLOBAL.window = window; + + self.workerID = "Worker"; + + console = #{JASON.stringify betterConsole}(); + + try { + // the world javascript file + #{worldCode}; + #{lodashCode}; + #{aetherCode}; + + // Don't let user generated code access stuff from our file system! + self.importScripts = importScripts = null; + self.native_fs_ = native_fs_ = null; + + // the actual function + #{JASON.stringify work}(); + }catch (error) { + self.postMessage({"type": "console-log", args: ["An unhandled error occured: ", error.toString(), error.stack], id: -1}); + } +""" + + +#console = #{JASON.stringify createConsole}(); +# +# console.error = console.info = console.log; +#self.console = console; +#GLOBAL.console = console; + + +module.exports = new Function(ret) diff --git a/package.json b/package.json index 665ac0131..3986248e1 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "mongoose": "3.8.x", "mongoose-text-search": "~0.0.2", "request": "2.12.x", - "tv4": "1.0.11", - "lodash": "~2.0.0", + "tv4": "~1.0.16", + "lodash": "~2.4.1", "underscore.string": "2.3.x", "async": "0.2.x", "connect": "2.7.x", @@ -60,9 +60,14 @@ "gridfs-stream": "0.4.x", "stream-buffers": "0.2.x", "sendwithus": "2.0.x", - "aws-sdk":"~2.0.0", - "bayesian-battle":"0.0.x", - "redis": "" + "aws-sdk": "~2.0.0", + "bayesian-battle": "0.0.x", + "redis": "", + "webworker-threads": "~0.4.11", + "node-gyp": "~0.13.0", + "aether": "~0.2.8", + "JASON": "~0.1.3", + "JQDeferred": "~2.1.0" }, "devDependencies": { "jade": "0.33.x", @@ -73,7 +78,6 @@ "css-brunch": "> 1.0 < 1.8", "jade-brunch": "> 1.0 < 1.8", "uglify-js-brunch": "~1.7.4", - "clean-css-brunch": "> 1.0 < 1.8", "auto-reload-brunch": "> 1.0 < 1.8", "brunch": "~1.7.4", "jasmine-node": "1.13.x", @@ -92,7 +96,9 @@ "karma-requirejs": "~0.2.1", "karma-phantomjs-launcher": "~0.1.1", "karma": "~0.10.9", - "karma-coverage": "~0.1.4" + "karma-coverage": "~0.1.4", + "compressible": "~1.0.1", + "jasmine-spec-reporter": "~0.3.0" }, "license": "MIT for the code, and CC-BY for the art and music", "private": true, diff --git a/scripts/devSetup/directoryController.py b/scripts/devSetup/directoryController.py index 6585ff5f8..5087db888 100644 --- a/scripts/devSetup/directoryController.py +++ b/scripts/devSetup/directoryController.py @@ -20,23 +20,30 @@ class DirectoryController(object): def bin_directory(self): return self.root_install_directory + def mkdir(self, path): + if os.path.exists(path): + print(u"Skipping creation of " + path + " because it exists.") + else: + os.mkdir(path) + def create_directory_in_tmp(self,subdirectory): - os.mkdir(self.generate_path_for_directory_in_tmp(subdirectory)) + path = self.generate_path_for_directory_in_tmp(subdirectory) + self.mkdir(path) def generate_path_for_directory_in_tmp(self,subdirectory): return self.tmp_directory + os.sep + subdirectory def create_directory_in_bin(self,subdirectory): full_path = self.bin_directory + os.sep + subdirectory - os.mkdir(full_path) + self.mkdir(full_path) def create_base_directories(self): shutil.rmtree(self.root_dir + os.sep + "coco" + os.sep + "node_modules",ignore_errors=True) #just in case try: - if os.path.exists(self.tmp_directory): - self.remove_tmp_directory() - os.mkdir(self.tmp_directory) + if os.path.exists(self.tmp_directory): + self.remove_tmp_directory() + os.mkdir(self.tmp_directory) except: - raise errors.CoCoError(u"There was an error creating the directory structure, do you have correct permissions? Please remove all and start over.") + raise errors.CoCoError(u"There was an error creating the directory structure, do you have correct permissions? Please remove all and start over.") def remove_directories(self): shutil.rmtree(self.bin_directory + os.sep + "node",ignore_errors=True) diff --git a/scripts/devSetup/factories.py b/scripts/devSetup/factories.py index f14f922ec..1eab847bb 100644 --- a/scripts/devSetup/factories.py +++ b/scripts/devSetup/factories.py @@ -37,10 +37,12 @@ class SetupFactory(object): try: mongo_version_string = subprocess.check_output("mongod --version",shell=True) mongo_version_string = mongo_version_string.decode(encoding='UTF-8') - except: - print("Mongod not found.") + except Exception as e: + print("Mongod not found: %s"%e) if "v2.6." not in mongo_version_string: - print("MongoDB not found, so installing...") + if mongo_version_string: + print("Had MongoDB version: %s"%mongo_version_string) + print("MongoDB not found, so installing a local copy...") self.mongo.download_dependencies() self.mongo.install_dependencies() self.node.download_dependencies() diff --git a/scripts/devSetup/mongo.py b/scripts/devSetup/mongo.py index 88de736af..eb57ea452 100644 --- a/scripts/devSetup/mongo.py +++ b/scripts/devSetup/mongo.py @@ -8,7 +8,7 @@ import os from configuration import Configuration from dependency import Dependency import sys - +import shutil class MongoDB(Dependency): def __init__(self,configuration): @@ -32,13 +32,20 @@ class MongoDB(Dependency): def bashrc_string(self): return "COCO_MONGOD_PATH=" + self.config.directory.bin_directory + os.sep + u"mongo" + os.sep +"bin" + os.sep + "mongod" + def download_dependencies(self): - self.downloader.download() - self.downloader.decompress() + install_directory = self.config.directory.bin_directory + os.sep + u"mongo" + if os.path.exists(install_directory): + print(u"Skipping MongoDB download because " + install_directory + " exists.") + else: + self.downloader.download() + self.downloader.decompress() def install_dependencies(self): install_directory = self.config.directory.bin_directory + os.sep + u"mongo" - import shutil - shutil.copytree(self.findUnzippedMongoBinPath(),install_directory) + if os.path.exists(install_directory): + print(u"Skipping creation of " + install_directory + " because it exists.") + else: + shutil.copytree(self.findUnzippedMongoBinPath(),install_directory) def findUnzippedMongoBinPath(self): return self.downloader.download_directory + os.sep + \ diff --git a/scripts/devSetup/node.py b/scripts/devSetup/node.py index 065634aad..8fb1265d8 100644 --- a/scripts/devSetup/node.py +++ b/scripts/devSetup/node.py @@ -37,19 +37,27 @@ class Node(Dependency): return self.config.directory.bin_directory def download_dependencies(self): - self.downloader.download() - self.downloader.decompress() + install_directory = self.config.directory.bin_directory + os.sep + u"node" + if os.path.exists(install_directory): + print(u"Skipping Node download because " + install_directory + " exists.") + else: + self.downloader.download() + self.downloader.decompress() def bashrc_string(self): return "COCO_NODE_PATH=" + self.config.directory.bin_directory + os.sep + u"node" + os.sep + "bin" + os.sep +"node" def install_dependencies(self): install_directory = self.config.directory.bin_directory + os.sep + u"node" #check for node here - unzipped_node_path = self.findUnzippedNodePath() if self.config.system.operating_system in ["mac","linux"] and not which("node"): + unzipped_node_path = self.findUnzippedNodePath() print("Copying node into /usr/local/bin/...") shutil.copy(unzipped_node_path + os.sep + "bin" + os.sep + "node","/usr/local/bin/") os.chmod("/usr/local/bin/node",S_IRWXG|S_IRWXO|S_IRWXU) - shutil.copytree(self.findUnzippedNodePath(),install_directory) + if os.path.exists(install_directory): + print(u"Skipping creation of " + install_directory + " because it exists.") + else: + unzipped_node_path = self.findUnzippedNodePath() + shutil.copytree(self.findUnzippedNodePath(),install_directory) wants_to_upgrade = True if self.check_if_executable_installed(u"npm"): warning_string = u"A previous version of npm has been found. \nYou may experience problems if you have a version of npm that's too old.Would you like to upgrade?(y/n) " diff --git a/scripts/mail.coffee b/scripts/mail.coffee new file mode 100644 index 000000000..b9983b619 --- /dev/null +++ b/scripts/mail.coffee @@ -0,0 +1,104 @@ +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.mixin _.str.exports() + +async = require 'async' + +serverSetup = require '../server_setup' +sendwithus = require '../server/sendwithus' +User = require '../server/users/User.coffee' +Level = require '../server/levels/Level.coffee' +LevelSession = require '../server/levels/sessions/LevelSession.coffee' + +alreadyEmailed = [] + +DEBUGGING = true + +sendInitialRecruitingEmail = -> + leaderboards = [ + {slug: 'brawlwood', team: 'humans', limit: 55, name: "Brawlwood", original: "52d97ecd32362bc86e004e87", majorVersion: 0} + {slug: 'brawlwood', team: 'ogres', limit: 40, name: "Brawlwood", original: "52d97ecd32362bc86e004e87", majorVersion: 0} + {slug: 'dungeon-arena', team: 'humans', limit: 200, name: "Dungeon Arena", original: "53173f76c269d400000543c2", majorVersion: 0} + {slug: 'dungeon-arena', team: 'ogres', limit: 150, name: "Dungeon Arena", original: "53173f76c269d400000543c2", majorVersion: 0} + ] + async.waterfall [ + (callback) -> async.map leaderboards, grabSessions, callback + (sessionLists, callback) -> async.map collapseSessions(sessionLists), grabUser, callback + (users, callback) -> async.map users, emailUser, callback + ], (err, results) -> + return console.log "Error:", err if err + console.log "Looked at sending to #{results.length} users; sent to #{_.filter(results).length}." + console.log "Sent to: ['#{(user.email for user in results when user).join('\', \'')}']" + +grabSessions = (levelInfo, callback) -> + queryParameters = + level: {original: levelInfo.original, majorVersion: levelInfo.majorVersion} + team: levelInfo.team + submitted: true + sortParameters = totalScore: -1 + selectString = 'totalScore creator' + query = LevelSession + .find(queryParameters) + .limit(levelInfo.limit) + .sort(sortParameters) + .select(selectString) + .lean() + query.exec (err, sessions) -> + return callback err if err + for session, rank in sessions + session.levelInfo = levelInfo + session.rank = rank + 1 + callback null, sessions + +collapseSessions = (sessionLists) -> + userRanks = {} + for sessionList in sessionLists + for session in sessionList + ranks = userRanks[session.creator] ? [] + ranks.push session + userRanks[session.creator] = _.sortBy ranks, 'rank' + topSessions = [] + for userID, ranks of userRanks + topSessions.push ranks[0] + topSessions + +grabUser = (session, callback) -> + findParameters = _id: session.creator + selectString = 'email emailSubscriptions emails name jobProfile' + query = User + .findOne(findParameters) + .select(selectString) + .lean() + query.exec (err, user) -> + return callback err if err + user.session = session + callback null, user + +totalEmailsSent = 0 +emailUser = (user, callback) -> + #return callback null, false if user.emails?.anyNotes?.enabled is false # TODO: later, uncomment to obey also "anyNotes" when that's untangled + return callback null, false if user.emails?.recruitNotes?.enabled is false + return callback null, false if user.email in alreadyEmailed + return callback null, false if DEBUGGING and (totalEmailsSent > 1 or Math.random() > 0.1) + ++totalEmailsSent + name = if user.firstName and user.lastName then "#{user.firstName}" else user.name + name = "Wizard" if not name or name is "Anoner" + team = user.session.levelInfo.team + team = team.substr(0, team.length - 1) + context = + email_id: sendwithus.templates.one_time_recruiting_email + recipient: + address: if DEBUGGING then 'nick@codecombat.com' else user.email + name: name + email_data: + name: name + level_name: user.session.levelInfo.name + place: "##{user.session.rank}" # like "#31" + level_race: team + sendwithus.api.send context, (err, result) -> + return callback err if err + callback null, user + +serverSetup.connectToDatabase() +sendInitialRecruitingEmail() diff --git a/scripts/mongodb/migrations/2014-04-22-migrate-emails.js b/scripts/mongodb/migrations/2014-04-22-migrate-emails.js new file mode 100644 index 000000000..07ccd32ad --- /dev/null +++ b/scripts/mongodb/migrations/2014-04-22-migrate-emails.js @@ -0,0 +1,35 @@ +// Did not migrate anonymous users because they get their properties setup on signup. + +// migrate the most common subscription configs with mass update commands +db.users.update({anonymous:false, emailSubscriptions:['announcement', 'notification'], emails:{$exists:false}}, {$set:{emails:{}}}, {multi:true}); +db.users.update({anonymous:false, emailSubscriptions:[], emails:{$exists:false}}, {$set:{emails:{anyNotes:{enabled:false}, generalNews:{enabled:false}}}}, {multi:true}); + +// migrate the rest one by one +emailMap = { + announcement: 'generalNews', + developer: 'archmageNews', + tester: 'adventurerNews', + level_creator: 'artisanNews', + article_editor: 'scribeNews', + translator: 'diplomatNews', + support: 'ambassadorNews', + notification: 'anyNotes' +}; + +db.users.find({anonymous:false, emails:{$exists:false}}).forEach(function(u) { + emails = {anyNotes:{enabled:false}, generalNews:{enabled:false}}; + var oldEmailSubs = u.emailSubscriptions || ['notification', 'announcement']; + for(var email in oldEmailSubs) { + var oldEmailName = oldEmailSubs[email]; + var newEmailName = emailMap[oldEmailName]; + if(!newEmailName) { + print('STOP, COULD NOT FIND EMAIL NAME', oldEmailName); + return false; + } + emails[newEmailName] = {enabled:true}; + } + u.emails = emails; + db.users.save(u); +}); + +// Done. No STOP error when this was run. \ No newline at end of file diff --git a/scripts/mongodb/migrations/2014-05-08-populate-watchers.js b/scripts/mongodb/migrations/2014-05-08-populate-watchers.js new file mode 100644 index 000000000..b6a56b475 --- /dev/null +++ b/scripts/mongodb/migrations/2014-05-08-populate-watchers.js @@ -0,0 +1,70 @@ +var scott = ObjectId('5162fab9c92b4c751e000274'); +var nick = ObjectId('512ef4805a67a8c507000001'); +//var collections = [db.levels, db.level.components, db.level.systems]; +var collection = db.levels; +//var collection = db.level.components; +//var collection = db.level.systems; +var permission; + +collection.find({slug:{$exists:1}}).forEach(function(doc) { + print('--------------------------------------------------', doc.name); + var official = false; + var owner = null; + var changed = false; + for (var j in doc.permissions) { + permission = doc.permissions[j]; + if(permission.access !== 'owner') + continue; + owner = permission.target; + if(owner === scott+'') { + print('Owner of', doc.name, 'is Scott'); + official = true; + } + else if(owner === nick+'') { + print('Owner of', doc.name, 'is Nick'); + official = true; + } + else { + print('Owner of', doc.name, 'is', owner); + } + } + if(!doc.watchers) { + print('Init watchers, was', doc.watchers); + doc.watchers = []; + } + if(official) { + var hasNick = false; + var hasScott = false; + for(var k in doc.watchers) { + var watcher = doc.watchers[k]; + if(watcher.equals(nick)) hasNick = true; + if(watcher.equals(scott)) hasScott = true; + } + if(!hasNick) { + doc.watchers.push(nick); + print('Added Nick to', doc.name); + changed = true; + } + if(!hasScott) { + doc.watchers.push(scott); + print('Added Scott to', doc.name); + changed = true; + } + } + else { + var hasOwner = false; + for(var l in doc.watchers) { + var watcher = doc.watchers[l]; + if(watcher+'' === owner) hasOwner = true; + } + if(!hasOwner) { + doc.watchers.push(ObjectId(owner)); + print('Added owner to', doc.name); + changed = true; + } + } + if(changed) { + print('Changed, so saving'); + collection.save(doc); + } +}); \ No newline at end of file diff --git a/scripts/mongodb/queries/patches.js b/scripts/mongodb/queries/patches.js new file mode 100644 index 000000000..418ebbe18 --- /dev/null +++ b/scripts/mongodb/queries/patches.js @@ -0,0 +1,13 @@ +// Finds all patches and denorms their target names and creators. + +var patches = db.patches.find({status:'pending'}).toArray(); +for(var i in patches) { + var patch = patches[i]; + var collection = null; + if(patch.target.collection === 'level') collection = db.levels; + if(patch.target.collection === 'level_component') collection = db.level.components; + if(patch.target.collection === 'level_system') collection = db.level.systems; + var target = collection.findOne({original:patch.target.original, name:{$exists:true}}); + var creator = db.users.findOne({_id:patch.creator}); + print(target.name, 'made by', creator.name); + } \ No newline at end of file diff --git a/scripts/simulate.coffee b/scripts/simulate.coffee new file mode 100644 index 000000000..01eccb5f9 --- /dev/null +++ b/scripts/simulate.coffee @@ -0,0 +1,23 @@ +spawn = require("child_process").spawn + +[sessionOne, sessionTwo] = [process.argv[2],process.argv[3]] +homeDirectory = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE +unless sessionOne and sessionTwo and sessionOne.length is 24 and sessionTwo.length is 24 + console.log "Not enough games to continue!" + process.exit(1) +run = (cb) -> + command = spawn("coffee",["headless_client.coffee","one-game",sessionOne,sessionTwo],{cwd: homeDirectory + "/codecombat/"}) + result = "" + command.stdout.on 'data', (data) -> + result += data.toString() + command.stdout.on 'close', -> + return cb(result) +run (result) -> + lines = result.split("\n") + for line in lines + if line.slice(0, 10) is "GAMERESULT" + process.stdout.write line.slice(11) + process.exit(0) + process.exit(0) + + \ No newline at end of file diff --git a/scripts/transpile.coffee b/scripts/transpile.coffee new file mode 100644 index 000000000..0cb4b462f --- /dev/null +++ b/scripts/transpile.coffee @@ -0,0 +1,81 @@ +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.mixin _.str.exports() +Aether = require "aether" +async = require 'async' + +serverSetup = require '../server_setup' +Level = require '../server/levels/Level.coffee' +LevelSession = require '../server/levels/sessions/LevelSession.coffee' + +Aether.addGlobal 'Vector', require '../app/lib/world/vector' +Aether.addGlobal '_', _ + +transpileLevelSession = (sessionID, cb) -> + query = LevelSession.findOne("_id": sessionID).select("submittedCode").lean() + query.exec (err, session) -> + if err then return cb err + submittedCode = session.submittedCode + transpiledCode = {} + console.log "Updating session #{sessionID}" + for thang, spells of submittedCode + transpiledCode[thang] = {} + for spellID, spell of spells + + aetherOptions = + problems: {} + language: "javascript" + functionName: spellID + functionParameters: [] + yieldConditionally: spellID is "plan" + globals: ['Vector', '_'] + protectAPI: true + includeFlow: false + if spellID is "hear" then aetherOptions["functionParameters"] = ["speaker","message","data"] + + aether = new Aether aetherOptions + transpiledCode[thang][spellID] = aether.transpile spell + conditions = + "_id": sessionID + update = + "transpiledCode": transpiledCode + "submittedCodeLanguage": "javascript" + query = LevelSession.update(conditions,update) + + query.exec (err, numUpdated) -> cb err + +findLadderLevelSessions = (levelID, cb) -> + queryParameters = + "level.original": levelID + "" + submitted: true + + selectString = "_id" + query = LevelSession.find(queryParameters).select(selectString).lean() + + query.exec (err, levelSessions) -> + if err then return cb err + levelSessionIDs = _.pluck levelSessions, "_id" + async.eachSeries levelSessionIDs, transpileLevelSession, (err) -> + if err then return cb err + cb null + + +transpileLadderSessions = -> + queryParameters = + type: "ladder" + "version.isLatestMajor": true + "version.isLatestMinor": true + selectString = "original" + query = Level.find(queryParameters).select(selectString).lean() + + query.exec (err, ladderLevels) -> + throw err if err + ladderLevels = _.pluck ladderLevels, "original" + async.eachSeries ladderLevels, findLadderLevelSessions, (err) -> + throw err if err + +serverSetup.connectToDatabase() +transpileLadderSessions() + + \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/config.coco b/scripts/windows/coco-dev-setup/batch/config/config.coco index eba46b0f4..da381689f 100755 --- a/scripts/windows/coco-dev-setup/batch/config/config.coco +++ b/scripts/windows/coco-dev-setup/batch/config/config.coco @@ -1,8 +1,9 @@ - 1.2 + 3.5 GlenDC CodeCombat.com © 2013-2014 https://github.com/codecombat/codecombat.git git@github.com:codecombat/codecombat.git + http://23.21.59.137/dump.tar.gz \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/downloads.coco b/scripts/windows/coco-dev-setup/batch/config/downloads.coco index 771189954..f8906cbb4 100755 --- a/scripts/windows/coco-dev-setup/batch/config/downloads.coco +++ b/scripts/windows/coco-dev-setup/batch/config/downloads.coco @@ -11,7 +11,6 @@ http://nodejs.org/dist/v0.10.25/x64/node-v0.10.25-x64.msi http://dl.bintray.com/oneclick/rubyinstaller/rubyinstaller-2.0.0-p353-x64.exe?direct http://s3.amazonaws.com/CodeCombatLargeFiles/python-64.msi - http://download.microsoft.com/download/A/6/A/A6AC035D-DA3F-4F0C-ADA4-37C8E5D34E3D/winsdk_web.exe http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe @@ -19,20 +18,28 @@ http://download.microsoft.com/download/C/6/D/C6D0FD4E-9E53-4897-9B91-836EBA2AACD3/vcredist_x86.exe - + - http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip - http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.6.0.zip + + + + + https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip + + + https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.6.0.zip - http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip - http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2.6.0.zip - \ No newline at end of file + diff --git a/scripts/windows/coco-dev-setup/batch/config/finished_header.coco b/scripts/windows/coco-dev-setup/batch/config/finished_header.coco old mode 100755 new mode 100644 index 3e8f64b02..9163183ca --- a/scripts/windows/coco-dev-setup/batch/config/finished_header.coco +++ b/scripts/windows/coco-dev-setup/batch/config/finished_header.coco @@ -1,7 +1,7 @@ - ______ _____ _ _ _____ _____ _ _ ___________ - | ___|_ _| \ | |_ _/ ___| | | || ___| _ \ - | |_ | | | \| | | | \ `--.| |_| || |__ | | | | - | _| | | | . ` | | | `--. \ _ || __|| | | | - | | _| |_| |\ |_| |_/\__/ / | | || |___| |/ / - \_| \___/\_| \_/\___/\____/\_| |_/\____/|___/ + ______ _____ _ _ _____ _____ _ _ ___________ + | ___|_ _| \ | |_ _/ ___| | | || ___| _ \ + | |_ | | | \| | | | \ `--.| |_| || |__ | | | | + | _| | | | . ` | | | `--. \ _ || __|| | | | + | | _| |_| |\ |_| |_/\__/ / | | || |___| |/ / + \_| \___/\_| \_/\___/\____/\_| |_/\____/|___/ \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/github_header.coco b/scripts/windows/coco-dev-setup/batch/config/github_header.coco old mode 100755 new mode 100644 index dd979561d..ce71943fc --- a/scripts/windows/coco-dev-setup/batch/config/github_header.coco +++ b/scripts/windows/coco-dev-setup/batch/config/github_header.coco @@ -1,7 +1,7 @@ - _____ _____ _____ _ _ _ _______ - | __ \_ _|_ _| | | | | | | ___ \ - | | \/ | | | | | |_| | | | | |_/ / - | | __ | | | | | _ | | | | ___ \ - | |_\ \_| |_ | | | | | | |_| | |_/ / - \____/\___/ \_/ \_| |_/\___/\____/ + _____ _____ _____ _ _ _ _______ + | __ \_ _|_ _| | | | | | | ___ \ + | | \/ | | | | | |_| | | | | |_/ / + | | __ | | | | | _ | | | | ___ \ + | |_\ \_| |_ | | | | | | |_| | |_/ / + \____/\___/ \_/ \_| |_/\___/\____/ \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/install_header.coco b/scripts/windows/coco-dev-setup/batch/config/install_header.coco old mode 100755 new mode 100644 index a804e1db3..e99e50e70 --- a/scripts/windows/coco-dev-setup/batch/config/install_header.coco +++ b/scripts/windows/coco-dev-setup/batch/config/install_header.coco @@ -1,7 +1,7 @@ - _____ ___________ _____ _ _ ___ ______ _____ - / ___|| _ | ___|_ _| | | |/ _ \ | ___ \ ___| - \ `--. | | | | |_ | | | | | / /_\ \| |_/ / |__ - `--. \| | | | _| | | | |/\| | _ || /| __| - /\__/ /\ \_/ / | | | \ /\ / | | || |\ \| |___ - \____/ \___/\_| \_/ \/ \/\_| |_/\_| \_\____/ + _____ ___________ _____ _ _ ___ ______ _____ + / ___|| _ | ___|_ _| | | |/ _ \ | ___ \ ___| + \ `--. | | | | |_ | | | | | / /_\ \| |_/ / |__ + `--. \| | | | _| | | | |/\| | _ || /| __| + /\__/ /\ \_/ / | | | \ /\ / | | || |\ \| |___ + \____/ \___/\_| \_/ \/ \/\_| |_/\_| \_\____/ \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/license.coco b/scripts/windows/coco-dev-setup/batch/config/localized/license-nl.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/config/license.coco rename to scripts/windows/coco-dev-setup/batch/config/localized/license-nl.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/license-ru.coco b/scripts/windows/coco-dev-setup/batch/config/localized/license-ru.coco new file mode 100644 index 000000000..48ad97ea1 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/license-ru.coco @@ -0,0 +1,10 @@ + +‹¨æ¥­§¨ï MIT (Œ’ˆ) + +Copyright (c) 2014 CodeCombat Inc. ¨ ¤à㣨¥ ãç áâ­¨ª¨ + +„ ­­ ï «¨æ¥­§¨ï à §à¥è ¥â «¨æ ¬, ¯®«ã稢訬 ª®¯¨î ¤ ­­®£® ¯à®£à ¬¬­®£® ®¡¥á¯¥ç¥­¨ï ¨ ᮯãâáâ¢ãî饩 ¤®ªã¬¥­â æ¨¨ (¢ ¤ «ì­¥©è¥¬ ¨¬¥­ã¥¬ë¬¨ <ணࠬ¬­®¥ Ž¡¥á¯¥ç¥­¨¥>), ¡¥§¢®§¬¥§¤­® ¨á¯®«ì§®¢ âì ணࠬ¬­®¥ Ž¡¥á¯¥ç¥­¨¥ ¡¥§ ®£à ­¨ç¥­¨©, ¢ª«îç ï ­¥®£à ­¨ç¥­­®¥ ¯à ¢® ­  ¨á¯®«ì§®¢ ­¨¥, ª®¯¨à®¢ ­¨¥, ¨§¬¥­¥­¨¥, ¤®¡ ¢«¥­¨¥, ¯ã¡«¨ª æ¨î, à á¯à®áâà ­¥­¨¥, áã¡«¨æ¥­§¨à®¢ ­¨¥ ¨/¨«¨ ¯à®¤ ¦ã ª®¯¨© ணࠬ¬­®£® Ž¡¥á¯¥ç¥­¨ï, â ª¦¥ ª ª ¨ «¨æ ¬, ª®â®àë¬ ¯à¥¤®áâ ¢«ï¥âáï ¤ ­­®¥ ணࠬ¬­®¥ Ž¡¥á¯¥ç¥­¨¥, ¯à¨ ᮡ«î¤¥­¨¨ á«¥¤ãîé¨å ãá«®¢¨©: + +“ª § ­­®¥ ¢ëè¥ ã¢¥¤®¬«¥­¨¥ ®¡  ¢â®à᪮¬ ¯à ¢¥ ¨ ¤ ­­ë¥ ãá«®¢¨ï ¤®«¦­ë ¡ëâì ¢ª«îç¥­ë ¢® ¢á¥ ª®¯¨¨ ¨«¨ §­ ç¨¬ë¥ ç á⨠¤ ­­®£® ணࠬ¬­®£® Ž¡¥á¯¥ç¥­¨ï. + +„€Ž… Žƒ€ŒŒŽ… Ž…‘…—…ˆ… …„Ž‘’€‚‹Ÿ…’‘Ÿ <Š€Š …‘’œ>, …‡ Š€Šˆ•-‹ˆŽ ƒ€€’ˆ‰, Ÿ‚Ž ‚›€†…›• ˆ‹ˆ Ž„€‡“Œ…‚€…Œ›•, ‚Š‹ž—€Ÿ, Ž … Žƒ€ˆ—ˆ‚€Ÿ‘œ ƒ€€’ˆŸŒˆ ’Ž‚€Ž‰ ˆƒŽ„Ž‘’ˆ, ‘ŽŽ’‚…’‘’‚ˆŸ Ž …ƒŽ ŠŽŠ…’ŽŒ“ €‡€—…ˆž ˆ Ž’‘“’‘’‚ˆŸ €“˜…ˆ‰ €‚. ˆ ‚ Š€ŠŽŒ ‘‹“—€… €‚’Ž› ˆ‹ˆ €‚ŽŽ‹€„€’…‹ˆ … …‘“’ Ž’‚…’‘’‚…Ž‘’ˆ Ž ˆ‘Š€Œ Ž ‚Ž‡Œ…™…ˆˆ “™…€, “›’ŠŽ‚ ˆ‹ˆ „“ƒˆ• ’…Ž‚€ˆ‰ Ž „…‰‘’‚“ž™ˆŒ ŠŽ’€Š’€Œ, „…‹ˆŠ’€Œ ˆ‹ˆ ˆŽŒ“, ‚Ž‡ˆŠ˜ˆŒ ˆ‡, ˆŒ…ž™ˆŒ ˆ—ˆŽ‰ ˆ‹ˆ ‘‚Ÿ‡€›Œ ‘ Žƒ€ŒŒ›Œ Ž…‘…—…ˆ…Œ ˆ‹ˆ ˆ‘Ž‹œ‡Ž‚€ˆ…Œ Žƒ€ŒŒŽƒŽ Ž…‘…—…ˆŸ ˆ‹ˆ ˆ›Œˆ „…‰‘’‚ˆŸŒˆ ‘ Žƒ€ŒŒ›Œ Ž…‘…—…ˆ…Œ. \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/license.coco b/scripts/windows/coco-dev-setup/batch/config/localized/license.coco new file mode 100755 index 000000000..9b753bf10 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/license.coco @@ -0,0 +1,10 @@ + +The MIT License (MIT) + +Copyright (c) 2014 CodeCombat Inc. and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in allcopies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN sCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE. diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco new file mode 100755 index 000000000..97258bb1b --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco @@ -0,0 +1,30 @@ + _____ _ _____ _ _ + / __ \ | | / __ \ | | | | + | / \/ ___ __| | ___ | / \/ ___ _ __ ___ | |__ __ _| |_ + | | / _ \ / _` |/ _ \ | | / _ \| '_ ` _ \| '_ \ / _` | __| + | \__/\ (_) | (_| | __/ | \__/\ (_) | | | | | | |_) | (_| | |_ + \____/\___/ \__,_|\___| \____/\___/|_| |_| |_|_.__/ \__,_|\__| + +============================================================================= + +Gefeliciteerd, je bent nu een deel van de CodeCombat gemeenschap. +Nu dat je ontwikkelingsomgeving volledig klaar is kun je beginnen met bijdragen +en ons helpen de wereld een betere plek te maken. + +Heb je enige vragen of wil je ons ontmoeten? +Praat met ons op hipchat @ https://www.hipchat.com/g3plnOKqa + +Je kunt ons ook bereiken via de forums. +Deze zijn te vinden @ http://discourse.codecombat.com/ + +De laatste ontwikkelingen kun je ook altijd volgen op onze blog. +Deze is te vinden @ http://blog.codecombat.com/ + +Tenslotte kun je de meeste documentatie en informatie vinden +op onze wiki @ https://github.com/codecombat/codecombat/wiki + +We hopen dat je het naar je zin zult hebben en net zoveel plezier zult beleven als wij. + + + - Nick, George, Scott, Michael, Jeremy and Glen + diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/readme-ru.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme-ru.coco new file mode 100644 index 000000000..c62f89900 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/readme-ru.coco @@ -0,0 +1,29 @@ + _____ _ _____ _ _ + / __ \ | | / __ \ | | | | + | / \/ ___ __| | ___ | / \/ ___ _ __ ___ | |__ __ _| |_ + | | / _ \ / _` |/ _ \ | | / _ \| '_ ` _ \| '_ \ / _` | __| + | \__/\ (_) | (_| | __/ | \__/\ (_) | | | | | | |_) | (_| | |_ + \____/\___/ \__,_|\___| \____/\___/|_| |_| |_|_.__/ \__,_|\__| + +============================================================================= + +®§¤à ¢«ï¥¬, ⥯¥àì ¢ë ç áâì á®®¡é¥á⢠ CodeCombat. +’¥¯¥àì, ª®£¤  ¢ è  á।  ࠧࠡ®â稪  ãáâ ­®¢«¥­ , ¢ë £®â®¢ë ­ ç âì +¢­®á¨âì ¢ª« ¤ ¨ ¯®¬®çì ­ ¬ ᤥ« âì íâ®â ¬¨à «ãçè¥. + +“ ¢ á ¥áâì ¢®¯à®áë ¨«¨ ¢ë å®â¥«¨ ¡ë á ­ ¬¨ ¢áâà¥â¨âìáï? +®£®¢®à¨â¥ á ­ ¬¨ ¢ hipchat @ https://www.hipchat.com/g3plnOKqa + +…é¥ ®¤¨­ ᯮᮡ ¤®á⨦¥­¨ï í⮣® - ¯®á¥é¥­¨¥ ­ è¥£® ä®à㬠. +‚ë ¬®¦¥â¥ ­ ©â¨ ¥£® @ http://discourse.codecombat.com/ + +‚ë ¬®¦¥â¥ ¯à®ç¨â âì ® ¯®á«¥¤­¨å ¤®á⨦¥­¨ïå ¢ ­ è¥¬ ¡«®£¥. +…£® ¬®¦­® ­ ©â¨ @ http://blog.codecombat.com/ + +ˆ ¯®á«¥¤­¥¥, ­® ­¥ ¯® §­ ç¥­¨î, - ¢ë ¬®¦¥â¥ ­ ©â¨ ¡®«ìèãî ç áâì ¤®ªã¬¥­â æ¨¨ +¨ ¨­ä®à¬ æ¨¨ ¢ ­ è¥© ¢¨ª¨ @ https://github.com/codecombat/codecombat/wiki + +Œë ­ ¤¥¥¬áï, çâ® ¢ë ¡ã¤¥â¥ ­ á« ¦¤ âìáï ¢ ­ è¥¬ á®®¡é¥á⢥ â ª ¦¥, ª ª ¨ ¬ë. + + + - ¨ª, „¦®à¤¦, ‘ª®ââ, Œ¨å í«ì, „¦¥à¥¬¨ ¨ ƒ«¥­ diff --git a/scripts/windows/coco-dev-setup/batch/config/readme.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/config/readme.coco rename to scripts/windows/coco-dev-setup/batch/config/localized/readme.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco new file mode 100755 index 000000000..bc12d3bf5 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco @@ -0,0 +1,8 @@ + 1) Antwoord voorzichtig en juist, indien er een vraag gesteld wordt. + 2) Deze installatie is nog steeds in beta en kan bugs bevatten. + 3) Rapporteer bugs op 'https://github.com/codecombat/codecombat/issues' + 4) Heb je vragen of suggesties? Praat met ons op HipChat via CodeCombat.com + + Je kan een Engelstalige stappengids + voor deze installatie vinden op onze wiki: + github.com/codecombat/codecombat/wiki/Setup-on-Windows:-a-step-by-step-guide \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/tips-ru.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips-ru.coco new file mode 100644 index 000000000..90d791d16 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/tips-ru.coco @@ -0,0 +1,7 @@ + 1) Š®£¤  § ¤ ñâáï ¢®¯à®á, ¯®¦ «ã©áâ , ®â¢¥ç ©â¥ ¢­¨¬ â¥«ì­® ¨ ¯à ¢¨«ì­® + 2) „ ­­ë© ãáâ ­®¢é¨ª ­ å®¤¨âáï ¢ áâ ¤¨¨ ¡¥âë ¨ ¬®¦¥â ᮤ¥à¦ âì ¡ £¨ + 3) ‚ë ¬®¦¥â¥ á®®¡é âì ® ¡ £ å @ 'https://github.com/codecombat/codecombat/issues' + 4) …áâì ¢®¯à®áë/¯à¥¤«®¦¥­¨ï? ®£®¢®à¨â¥ á ­ ¬¨ ¢ HipChat ç¥à¥§ CodeCombat.com + + ‚ë ¬®¦¥â¥ ­ ©â¨ ¯®è £®¢®¥ à㪮¢®¤á⢮ ¤«ï ¤ ­­®£® ãáâ ­®¢é¨ª  ¢ ­ è¥© ¢¨ª¨. + github.com/codecombat/codecombat/wiki/Setup-on-Windows:-a-step-by-step-guide \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/tips.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/config/tips.coco rename to scripts/windows/coco-dev-setup/batch/config/localized/tips.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/npm_and_brunch_header.coco b/scripts/windows/coco-dev-setup/batch/config/npm_and_brunch_header.coco old mode 100755 new mode 100644 index 968e651dd..53a47af88 --- a/scripts/windows/coco-dev-setup/batch/config/npm_and_brunch_header.coco +++ b/scripts/windows/coco-dev-setup/batch/config/npm_and_brunch_header.coco @@ -1,7 +1,7 @@ - _ _ _________ ___ ____________ _ _ _ _ _____ _ _ - | \ | || ___ \ \/ | | ___ \ ___ \ | | | \ | / __ \| | | | - | \| || |_/ / . . | ______ | |_/ / |_/ / | | | \| | / \/| |_| | - | . ` || __/| |\/| | |______| | ___ \ /| | | | . ` | | | _ | - | |\ || | | | | | | |_/ / |\ \| |_| | |\ | \__/\| | | | - \_| \_/\_| \_| |_/ \____/\_| \_|\___/\_| \_/\____/\_| |_/ + _ _ _________ ___ ____________ _ _ _ _ _____ _ _ + | \ | || ___ \ \/ | | ___ \ ___ \ | | | \ | / __ \| | | | + | \| || |_/ / . . | ______ | |_/ / |_/ / | | | \| | / \/| |_| | + | . ` || __/| |\/| | |______| | ___ \ /| | | | . ` | | | _ | + | |\ || | | | | | | |_/ / |\ \| |_| | |\ | \__/\| | | | + \_| \_/\_| \_| |_/ \____/\_| \_|\___/\_| \_/\____/\_| |_/ \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/configuration.exe b/scripts/windows/coco-dev-setup/batch/configuration.exe new file mode 100755 index 000000000..28177aeab Binary files /dev/null and b/scripts/windows/coco-dev-setup/batch/configuration.exe differ diff --git a/scripts/windows/coco-dev-setup/batch/localisation/fr.coco b/scripts/windows/coco-dev-setup/batch/localisation/fr.coco deleted file mode 100755 index 1c92b433c..000000000 --- a/scripts/windows/coco-dev-setup/batch/localisation/fr.coco +++ /dev/null @@ -1,82 +0,0 @@ - - - - français - From now on we'll send our feedback in English! - - - - -bit computer detected. - The operating system - was detected. - We don't support Windows XP, installation cancelled. - - - Have you already installed all the software needed for CodeCombat? - We recommand that you reply negative in case you're not sure. - Skipping the installation of the software... - CodeCombat couldn't be developed without third-party software. - That's why you'll need to install this software, - in order to start contributing to our community. - Cancel the installation if you already have the application. - Make sure to select the option that adds the application to your Windows Path, if the option is available. - Do you already have the latest version of - installed? - is downloading... - is installing... - is unzipping... - is cleaning... - Please define the full path where mongodb should be installed - - - - - CodeCombat is opensource, like you already know. - All our sourcecode can be found online at Github. - You can choose to do the entire Git setup yourself. - However we recommend that you instead let us handle it instead. - - - Do you want to do the Local Git setup manually yourself? - Make sure you have correctly setup your repository before processing. - Do not close this window please. - When you're ready, press any key to continue... - - - Please give the full path of your CodeCombat git repository: - Please enter the full path where you want to install your CodeCombat environment - This installation requires Git Bash. - Git bash is by default installed at 'C:\Program Files (x86)\Git'. - Git bash is by default installed at 'C:\Program Files\Git'. - Please enter the full path where git bash is installed or just press enter if it's in the default location - Do you want to checkout the repository via ssh? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - - - That path already exists, are you sure you want to overwrite it? - That path doesn't exist. Please try again... - - - The setup of the CodeCombat Dev. Environment was succesfull. - Thank you already for your contribution and see you soon. - Do you want to read the README for more information? - - - From now on you can start the dev. environment at - the touch of a single mouse click. - 1) Just double click - and let the environment start up. - 2) Now just open 'localhost:3000' in your prefered browser. - That's it, you're now ready to start working on CodeCombat! - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco b/scripts/windows/coco-dev-setup/batch/localisation/nl.coco deleted file mode 100755 index 654d45c97..000000000 --- a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco +++ /dev/null @@ -1,82 +0,0 @@ - - - - Nederlands - Vanaf nu geven we onze feedback in het Nederlands! - - - - -bit computer gedetecteerd. - Het besturingsysteem - is gedetecteerd. - Wij ondersteunen Windows XP niet, installatie geanulleerd. - - - Heb je alle benodige software al geinstalleerd? - We raden aan dat je negatief antwoord indien je niet zeker bent. - De installatie van software wordt geanulleerd... - CodeCombat kon niet worden ontwikkeld zonder third-party software. - Dat is waarom je deze software moet installeren, - zodat je je kan beginnen met het bijdragen tot onze gemeenschap. - Annuleer de installatie als je de applicatie al hebt. - Zorg er zeker voor dat je de optie selecteert dat de applicatie aan je Windows Path toevoegt, als de optie beschikbaar is. - Heb je al de laatste versie van - geinstalleerd? - is aan het downloaden... - is aan het installeren... - is aan het uitpakken... - is aan het opkuisen... - Geef het volledige pad op, waar mongodb mag worden geinstalleerd - - - - - CodeCombat is opensource, zoals je waarschijnlijk wel al weet. - Je kan al onze sourcecode vinden op Github. - Indien je wil, kan je de Git setup manueel doen. - Maar wij raden aan dat je ons dit automatisch laat afhandellen. - - - Wil je de lokale Git setup manueel doen? - Zorg er zeker voor dat jouw git repository correct is. - Sluit dit venster niet alsjeblieft. - Wanneer je klaar bent, druk dan eender welke toets om verder te gaan... - - - Geef alsjeblieft het volledige pad van je CodeCombat git repository: - Geef alsjeblieft het volledige pad waar je de CodeCombat Ontwikkelings omgeving will installeren - Deze installatie maakt gebruik van Git Bash. - Git bash is normaal geinstalleerd in 'C:\Program Files (x86)\Git'. - Git bash is normaal geinstalleerd in 'C:\Program Files\Git'. - Geef alsjeblieft het volledige pad op van Git Bash of druk gewoon op enter indien je het pad niet gewijzigd heeft - Wil je het git project downloaden via ssh? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - - - Dat pad bestaat al, ben je zeker dat je het wil overschrijven? - Dat pad bestaat niet, probeer alsjeblieft opnieuw... - - - De installatie van de CodeCombat-Ontwikkelings omgeving was succesvol. - Alvast bedankt voor al je werk en tot binnenkort. - Wil je de LEESMIJ lezen voor meer informatie? - - - Vanaf nu kan je de ontwikkelings omgeving opstarten - met het gemak van een enkele muisklik. - 1) Dubbelklik op - en laat de omgeving opstarten. - 2) Nu kan je 'localhost:3000' openen in je browser naar voorkeur. - Dat is het, je bent nu klaar om te starten met je werk aan CodeCombat. - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco deleted file mode 100755 index 2eca2705c..000000000 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco +++ /dev/null @@ -1,82 +0,0 @@ - - - - 简体中文 - From now on we'll send our feedback in English! - - - - -bit computer detected. - The operating system - was detected. - We don't support Windows XP, installation cancelled. - - - Have you already installed all the software needed for CodeCombat? - We recommand that you reply negative in case you're not sure. - Skipping the installation of the software... - CodeCombat couldn't be developed without third-party software. - That's why you'll need to install this software, - in order to start contributing to our community. - Cancel the installation if you already have the application. - Make sure to select the option that adds the application to your Windows Path, if the option is available. - Do you already have the latest version of - installed? - is downloading... - is installing... - is unzipping... - is cleaning... - Please define the full path where mongodb should be installed - - - - - CodeCombat is opensource, like you already know. - All our sourcecode can be found online at Github. - You can choose to do the entire Git setup yourself. - However we recommend that you instead let us handle it instead. - - - Do you want to do the Local Git setup manually yourself? - Make sure you have correctly setup your repository before processing. - Do not close this window please. - When you're ready, press any key to continue... - - - Please give the full path of your CodeCombat git repository: - Please enter the full path where you want to install your CodeCombat environment - This installation requires Git Bash. - Git bash is by default installed at 'C:\Program Files (x86)\Git'. - Git bash is by default installed at 'C:\Program Files\Git'. - Please enter the full path where git bash is installed or just press enter if it's in the default location - Do you want to checkout the repository via ssh? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - - - That path already exists, are you sure you want to overwrite it? - That path doesn't exist. Please try again... - - - The setup of the CodeCombat Dev. Environment was succesfull. - Thank you already for your contribution and see you soon. - Do you want to read the README for more information? - - - From now on you can start the dev. environment at - the touch of a single mouse click. - 1) Just double click - and let the environment start up. - 2) Now just open 'localhost:3000' in your prefered browser. - That's it, you're now ready to start working on CodeCombat! - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh.coco deleted file mode 100755 index b84d84146..000000000 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh.coco +++ /dev/null @@ -1,82 +0,0 @@ - - - - 中文 - From now on we'll send our feedback in English! - - - - -bit computer detected. - The operating system - was detected. - We don't support Windows XP, installation cancelled. - - - Have you already installed all the software needed for CodeCombat? - We recommand that you reply negative in case you're not sure. - Skipping the installation of the software... - CodeCombat couldn't be developed without third-party software. - That's why you'll need to install this software, - in order to start contributing to our community. - Cancel the installation if you already have the application. - Make sure to select the option that adds the application to your Windows Path, if the option is available. - Do you already have the latest version of - installed? - is downloading... - is installing... - is unzipping... - is cleaning... - Please define the full path where mongodb should be installed - - - - - CodeCombat is opensource, like you already know. - All our sourcecode can be found online at Github. - You can choose to do the entire Git setup yourself. - However we recommend that you instead let us handle it instead. - - - Do you want to do the Local Git setup manually yourself? - Make sure you have correctly setup your repository before processing. - Do not close this window please. - When you're ready, press any key to continue... - - - Please give the full path of your CodeCombat git repository: - Please enter the full path where you want to install your CodeCombat environment - This installation requires Git Bash. - Git bash is by default installed at 'C:\Program Files (x86)\Git'. - Git bash is by default installed at 'C:\Program Files\Git'. - Please enter the full path where git bash is installed or just press enter if it's in the default location - Do you want to checkout the repository via ssh? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - - - That path already exists, are you sure you want to overwrite it? - That path doesn't exist. Please try again... - - - The setup of the CodeCombat Dev. Environment was succesfull. - Thank you already for your contribution and see you soon. - Do you want to read the README for more information? - - - From now on you can start the dev. environment at - the touch of a single mouse click. - 1) Just double click - and let the environment start up. - 2) Now just open 'localhost:3000' in your prefered browser. - That's it, you're now ready to start working on CodeCombat! - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/de.coco b/scripts/windows/coco-dev-setup/batch/localization/de.coco old mode 100755 new mode 100644 similarity index 75% rename from scripts/windows/coco-dev-setup/batch/localisation/de.coco rename to scripts/windows/coco-dev-setup/batch/localization/de.coco index fc257a64d..f4f93fdb2 --- a/scripts/windows/coco-dev-setup/batch/localisation/de.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/de.coco @@ -1,82 +1,108 @@ - - - - Deutsch - Ab jetzt senden wir unser Feedback in Englisch! - - - - -Bit System erkannt. - Es wurde das Betriebssystem - erkannt. - Windows XP wird nicht unterstützt. Installation abgebrochen. - - - Sind die für CodeCombat benötigten Programme bereits installiert? - Wir empfehlen Ihnen, mit „Nein“ zu antorten, falls Sie unsicher sind. - Ãœberspringe Installation der Programme... - Ohne Software von Drittanbietern könnte CodeCombat nicht entwickelt werden. - Aus diesem Grund müssen Sie diese Software installieren, - um sich in der Community zu engagieren. - Wenn Sie ein Programm bereits installiert haben, brechen Sie die Installation bitte ab. - Make sure to select the option that adds the application to your Windows Path, if the option is available. - Haben Sie bereits die aktuellste Version von - installiert? - wird heruntergeladen... - wird installiert... - wird entpackt... - wird aufgeräumt... - Bitte geben Sie den kompletten Pfad an, an dem MongoDB installiert werden soll - - - - - Wie Du bereits weißt, ist CodeCombat Open Source. - Unser Quellcode ist komplett auf Github. - Wenn Du möchtest, kannst du das komplette Git Repository selbst herunterladen und nach deinen wünschen einrichten. - Allerdings empfehlen wir, dass du den Prozess statt dessen uns überlässt. - - - Willst du das lokale Git Setup selbst vornehmen? - Bit vergewissere dich, dass das Repository korrekt heruntergeladen wurde, bevor du fortfährst. - Bitte schließe dieses Fenster nicht. - Wenn du fertig bist, drücke eine beliebige Taste zum Fortfahren... - - - Gebe bitte den kompletten Pfad zu deinem CodeCombat Git Repository ein: - Bitte gib den kompletten Pfad ein, an dem du die CodeCombat Umgebung einrichten willst - Diese Installation benötigt die Git Bash. - Die Git Bash ist standardmäßig in 'C:\Program Files (x86)\Git' installiert. - Die Git Bash ist standardmäßig in 'C:\Program Files\Git' installiert. - Bitte gebe den kompletten Pfad zur Git Bash ein, oder drücke Enter, um den Standardpfad zu verwenden - Willst du das Repository via SSH auschecken? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - - - Dieser Pfad existiert bereits. Willst du ihn wirklich überschreiben? - Dieser Pfad exisitert nicht. Bitte versuche es erneut... - - - Die CodeCombat Entwicklungsumgebung wurde erfoglreich installiert. - Vielen Dank für die Unterstützung und bis bald. - Willst du das README lesen, um weitere Informationen zu erhalten? - - - Von nun an kannst du die Entwicklungsumgebung starten unter - einmal mit der Maus klicken. - 1) Einfach Doppelklicken - und warten bis die Entwicklungsumgebung fertig geladen hat. - 2) Jetzt 'localhost:3000' in deinem bevorzugten Browser aufrufen. - Fertig. Du bist nun bereit, bei CodeCombat mitzuarbeiten! - + + + + Deutsch + German + Before we start the installation, here are some tips: + Press any key to exit... + + + You have choosen Deutsch as your language. + Ab jetzt senden wir unser Feedback in Deutsch. + + + In order to continue the installation of the developers environment + you will have to read and agree with the following license: + Have you read the license and do you agree with it? + This setup can't happen without an agreement. + Installation and Setup of the CodeCombat environment is cancelled. + + + + -Bit System erkannt. + Es wurde das Betriebssystem + erkannt. + Windows XP wird nicht unterstützt. Installation abgebrochen. + + + Sind die für CodeCombat benötigten Programme bereits installiert? + Wir empfehlen Ihnen, mit „Nein“ zu antorten, falls Sie unsicher sind. + Ãœberspringe Installation der Programme... + Ohne Software von Drittanbietern könnte CodeCombat nicht entwickelt werden. + Aus diesem Grund müssen Sie diese Software installieren, + um sich in der Community zu engagieren. + Wenn Sie ein Programm bereits installiert haben, brechen Sie die Installation bitte ab. + Make sure to select the option that adds the application to your Windows Path, if the option is available. + Haben Sie bereits die aktuellste Version von + installiert? + wird heruntergeladen... + wird installiert... + wird entpackt... + wird aufgeräumt... + Bitte geben Sie den kompletten Pfad an, an dem MongoDB installiert werden soll + + + + + Wie Du bereits weißt, ist CodeCombat Open Source. + Unser Quellcode ist komplett auf Github. + Wenn Du möchtest, kannst du das komplette Git Repository selbst herunterladen und nach deinen wünschen einrichten. + Allerdings empfehlen wir, dass du den Prozess statt dessen uns überlässt. + + + Willst du das lokale Git Setup selbst vornehmen? + Bit vergewissere dich, dass das Repository korrekt heruntergeladen wurde, bevor du fortfährst. + Bitte schließe dieses Fenster nicht. + Wenn du fertig bist, drücke eine beliebige Taste zum Fortfahren... + + + Gebe bitte den kompletten Pfad zu deinem CodeCombat Git Repository ein: + Bitte gib den kompletten Pfad ein, an dem du die CodeCombat Umgebung einrichten willst + Diese Installation benötigt die Git Bash. + Die Git Bash ist standardmäßig in 'C:\Program Files (x86)\Git' installiert. + Die Git Bash ist standardmäßig in 'C:\Program Files\Git' installiert. + Bitte gebe den kompletten Pfad zur Git Bash ein, oder drücke Enter, um den Standardpfad zu verwenden + Willst du das Repository via SSH auschecken? + + + You should have forked CodeCombat to your own GitHub Account by now... + Please enter your github information, to configure your local repository. + Username: + Password: + Thank you... Configuring your local repistory right now... + + + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + + + Installing bower, brunch, nodemon and sendwithus... + Installing bower packages... + Installing sass... + Installing npm... + Starting brunch.... + Setting up a MongoDB database for you... + Downloading the last version of the CodeCombat database... + + Don't close! + + + Dieser Pfad existiert bereits. Willst du ihn wirklich überschreiben? + Dieser Pfad exisitert nicht. Bitte versuche es erneut... + + + Die CodeCombat Entwicklungsumgebung wurde erfoglreich installiert. + Vielen Dank für die Unterstützung und bis bald. + Willst du das README lesen, um weitere Informationen zu erhalten? + + + Von nun an kannst du die Entwicklungsumgebung starten unter + einmal mit der Maus klicken. + 1) Einfach Doppelklicken + und warten bis die Entwicklungsumgebung fertig geladen hat. + 2) Jetzt 'localhost:3000' in deinem bevorzugten Browser aufrufen. + Fertig. Du bist nun bereit, bei CodeCombat mitzuarbeiten! + \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/en.coco b/scripts/windows/coco-dev-setup/batch/localization/en.coco similarity index 72% rename from scripts/windows/coco-dev-setup/batch/localisation/en.coco rename to scripts/windows/coco-dev-setup/batch/localization/en.coco index 947890ee8..bef7c9ae6 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/en.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/en.coco @@ -2,8 +2,21 @@ English - From now on we'll send our feedback in English! + English + Before we start the installation, here are some tips: + Press any key to exit... + + You have choosen English as your language. + From now on we'll send our feedback in English. + + + In order to continue the installation of the developers environment + you will have to read and agree with the following license: + Have you read the license and do you agree with it? + This setup can't happen without an agreement. + Installation and Setup of the CodeCombat environment is cancelled. + -bit computer detected. @@ -51,7 +64,19 @@ Please enter the full path where git bash is installed or just press enter if it's in the default location Do you want to checkout the repository via ssh? + + You should have forked CodeCombat to your own GitHub Account by now... + Please enter your github information, to configure your local repository. + Username: + Password: + Thank you... Configuring your local repistory right now... + + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + Installing bower, brunch, nodemon and sendwithus... Installing bower packages... @@ -61,6 +86,7 @@ Setting up a MongoDB database for you... Downloading the last version of the CodeCombat database... + Don't close! That path already exists, are you sure you want to overwrite it? diff --git a/scripts/windows/coco-dev-setup/batch/localisation/languages.coco b/scripts/windows/coco-dev-setup/batch/localization/languages.coco similarity index 66% rename from scripts/windows/coco-dev-setup/batch/localisation/languages.coco rename to scripts/windows/coco-dev-setup/batch/localization/languages.coco index a267d65d0..2f3e2fe0d 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/languages.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/languages.coco @@ -1,7 +1,6 @@ en +ru nl de -fr -zh zh-HANT zh-HANS \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localization/nl.coco b/scripts/windows/coco-dev-setup/batch/localization/nl.coco new file mode 100755 index 000000000..a969efb31 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/localization/nl.coco @@ -0,0 +1,108 @@ + + + + Nederlands + Dutch + Voor we verder gaan met de installatie hier volgen enkele tips: + Druk een willekeurige toets in om af te sluiten... + + + Je hebt Nederlands gekozen als jouw taal naar keuze. + Vanaf nu geven we onze feedback in het Nederlands. + + + Om verder te gaan met de installatie van jouw CodeCombat omgeving + moet je de licentieovereenkomst lezen en ermee akkoord gaan. + Heb je de licentieovereenkomst gelezen en ga je ermee akkoord? + Deze installatie kan niet doorgaan zonder jouw akkoord. + De installatie van jouw Developers omgeving is nu geannulleerd. + + + + -bit computer gedetecteerd. + Het besturingssysteem + is gedetecteerd. + Wij ondersteunen Windows XP niet, installatie geannuleerd. + + + Heb je alle benodige software al geïnstalleerd? + We raden aan dat je negatief antwoord indien je niet zeker bent. + De installatie van software wordt geannuleerd... + CodeCombat kon niet worden ontwikkeld zonder third-party software. + Dat is waarom je deze software moet installeren, + zodat je kan beginnen met het bijdragen tot onze gemeenschap. + Annuleer de installatie als je de applicatie al hebt. + Zorg er zeker voor dat je de optie selecteert die de applicatie aan je Windows Path toevoegt, als deze optie beschikbaar is. + Heb je al de laatste versie van + geïnstalleerd? + is aan het downloaden... + is aan het installeren... + is aan het uitpakken... + is aan het opkuisen... + Geef het volledige pad op waar mongodb mag worden geïnstalleerd + + + + + CodeCombat is open-source, zoals je waarschijnlijk wel al weet. + Je kunt al onze source code vinden op Github. + Indien je wil, kan je de Git setup ook manueel doen. + Maar wij raden aan dat je ons dit automatisch laat afhandelen. + + + Wil je de lokale Git setup manueel doen? + Zorg er zeker voor dat jouw git repository correct is. + Sluit dit venster alsjeblieft niet. + Wanneer je klaar bent, druk dan op eender welke toets om verder te gaan... + + + Geef alsjeblieft het volledige pad in van je CodeCombat git repository: + Geef alsjeblieft het volledige pad in waar je de CodeCombat ontwikkelingsomgeving wilt installeren + Deze installatie maakt gebruik van Git Bash. + Git bash is normaal gezien geïnstalleerd in 'C:\Program Files (x86)\Git'. + Git bash is normaal gezien geïnstalleerd in 'C:\Program Files\Git'. + Geef alsjeblieft het volledige pad op van Git Bash of druk gewoon op enter indien je het pad niet gewijzigd hebt. + Wil je het git project downloaden via ssh? + + + Je zou nu al een eigen CodeCombat-fork moeten hebben gekoppeld aan jouw GitHub account... + Geef jou GitHub informatie alstublieft, zodat wij jou lokale repositorie kunnen configureren. + Gebruikersnaam: + Wachtwoord: + Dank u, jouw lokaal project wordt nu geconfigureerd... + + + + De installatie van jouw lokale omgeving was een succes! + Je kan nu deze setup sluiten. + Nadien, kan je de 'configuration' setup openen om jouw omgeving automatisch te configureren... + + + Bezig met het installeren van bower, brunch, nodemon en sendwithus... + Bower packages worden geïnstalleerd... + Sass wordt geïnstalleerd... + Npm wordt geïnstalleerd... + Brunch wordt gestart... + De MongoDB database wordt voor je klaargemaakt... + De laatste versie van de CodeCombat database wordt gedownload... + + Niet sluiten! + + + Dat pad bestaat al, ben je zeker dat je het wil overschrijven? + Dat pad bestaat niet, probeer alsjeblieft opnieuw... + + + De installatie van de CodeCombat ontwikkelingsomgeving was succesvol. + Alvast bedankt voor al je werk en tot binnenkort. + Wil je de LEESMIJ lezen voor meer informatie? + + + Vanaf nu kan je de ontwikkelingsomgeving opstarten + met het gemak van een enkele muisklik. + 1) Dubbelklik op + en laat de omgeving opstarten. + 2) Nu kan je 'localhost:3000' openen in je browser naar voorkeur. + Dat is het, je bent nu klaar om te starten met je werk aan CodeCombat. + + \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localization/ru.coco b/scripts/windows/coco-dev-setup/batch/localization/ru.coco new file mode 100644 index 000000000..9914b5d44 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/localization/ru.coco @@ -0,0 +1,108 @@ + + + + ðóññêèé + Russian + Ïåðåä òåì, êàê ìû íà÷í¸ì óñòàíîâêó, âîò íåñêîëüêî ñîâåòîâ: + Íàæìèòå Enter äëÿ âûõîäà... + + + Âû âûáðàëè ðóññêèé â êà÷åñòâå âàøåãî ÿçûêà. + C äàííîãî ìîìåíòà ìû áóäåì îáùàòüñÿ íà ðóññêîì. + + + Äëÿ òîãî, ÷òîáû ïðîäîëæèòü óñòàíîâêó ñðåäû ðàçðàáîò÷èêà + âû äîëæíû ïðî÷èòàòü è ñîãëàñèòüñÿ ñî ñëåäóþùåé ëèöåíçèåé: + Âû ïðî÷èòàëè ëèöåíçèþ è ñîãëàñíû ñ íåé? + Äàííàÿ óñòàíîâêà íå íà÷í¸òñÿ áåç ñîãëàñèÿ. + Óñòàíîâêà è íàñòðîéêà ñðåäû CodeCombat îòìåíåíà. + + + + -áèòíûé êîìïüþòåð îáíàðóæåí. + Îáíàðóæåíà îïåðàöèîííàÿ ñèñòåìà + . + Ìû íå ïîääåðæèâàåì Windows XP, óñòàíîâêà îòìåíåíà. + + + Âû óæå óñòàíîâèëè âñ¸ ïðîãðàììíîå îáåñïå÷åíèå, íåîáõîäèìîå äëÿ CodeCombat? + Ìû ðåêóìåíäóåì îòâåòèòü îòðèöàòåëüíî, åñëè âû íå óâåðåíû. + Ïðîïóñê óñòàíîâêè ïðîãðàììíîãî îáåñïå÷åíèÿ... + CodeCombat íå ìîã áû áûòü ðàçðàáîòàí áåç ñòîðîííåãî ïðîãðàììíîãî îáåñïå÷åíèÿ. + Âîò ïî÷åìó âû äîëæíû áóäåòå óñòàíîâèòü ýòî ïðîãðàììíîå îáåñïå÷åíèå + äëÿ òîãî, ÷òîáû íà÷àòü âíîñèòü âêëàä â íàøå ñîîáùåñòâî. + Îòìåíèòå óñòàíîâêó, åñëè ó âàñ óæå åñòü ïðèëîæåíèå. + Óáåäèòåñü â âûáîðå îïöèè, êîòîðàÿ äîáàâëÿåò ïðèëîæåíèå â Windows PATH, åñëè îïöèÿ äîñòóïíà. + Ó âàñ óæå åñòü ïîñëåäíÿÿ âåðñèÿ + ? + çàãðóæàåòñÿ... + óñòàíàâëèâàåòñÿ... + ðàñïàêîâûâàåòñÿ... + ïðèáèðàåòñÿ... + Ïîæàëóéñòà, îïðåäåëèòå ïîëíûé ïóòü, êóäà äîëæåí áûòü óñòàíîâëåí MongoDB + + + + + Èñõîäíûé êîä CodeCombat îòêðûò, êàê âû óæå çíàåòå. + Âåñü íàø èñõîäíûé êîä ìîæåò áûòü íàéäåí îíëàéí â Github. + Âû ìîæåòå âûáðàòü öåëèêîì ñàìîñòîÿòåëüíóþ óñòàíîâêó Git. + Îäíàêî ìû ðåêîìåíäóåì, âìåñòî ýòîãî, ïåðåäàòü óïðàâëåíèå íàì. + + + Âû õîòèòå ïðîâåñòè óñòàíîâêó Local Git âðó÷íóþ ñàìîñòîÿòåëüíî? + Óáåäèòåñü, ÷òî âû ïðàâèëüíî íàñòðîèëè ðåïîçèòîðèé ïåðåä âûïîëíåíèåì. + Íå çàêðûâàéòå ýòî îêíî, ïîæàëóéñòà. + Êîãäà âû áóäåòå ãîòîâû, íàæìèòå Enter äëÿ ïðîäîëæåíèÿ... + + + Ïîæàëóéñòà, óêàæèòå ïîëíûé ïóòü äî âàøåãî CodeCombat ðåïîçèòîðèÿ git: + Ïîæàëóéñòà, ââåäèòå ïîëíûé ïóòü, êóäà âû õîòèòå óñòàíîâèòü ñðåäó CodeCombat + Äàííàÿ óñòàíîâêà òðåáóåò Git Bash. + Git bash ïî óìîë÷àíèþ óñòàíîâëåí â 'C:\Program Files (x86)\Git'. + Git bash ïî óìîë÷àíèþ óñòàíîâëåí â 'C:\Program Files\Git'. + Ïîæàëóéñòà, ââåäèòå ïîëíûé ïóòü, êóäà óñòàíîâëåí git bash èëè ïðîñòî íàæìèòå Enter, åñëè îí íàõîäèòñÿ â ïàïêå ïî óìîë÷àíèþ + Âû õîòèòå ïðîâåðÿòü ðåïîçèòîðèé ÷åðåç ssh? + + + Âû äîëæíû áûëè ñäåëàòü ôîðê CodeCombat íà âàøåì àêêàóíòå GitHub... + Ïîæàëóéñòà, ââåäèòå âàøè äàííûå github, ÷òîáû íàñòðîèòü ëîêàëüíûé ðåïîçèòîðèé. + Èìÿ ïîëüçîâàòåëÿ: + Ïàðîëü: + Ñïàñèáî... Èä¸ò íàñòðîéêà âàøåãî ëîêàëüíîãî ðåïîçèòîðèÿ... + + + + Óñòàíîâêà âàøåé ëîêàëüíîé ñðåäû óñïåøíî çàâåðøåíà! + Òåïåðü âû ìîæåòå çàêðûòü äàííûé óñòàíîâùèê. + Ïîñëå ýòîãî âû äîëæíû îòêðûòü óñòàíîâùèê íàñòðîåê äëÿ àâòîìàòè÷åñêîé êîíôèãóðàöèè âàøåé ñðåäû... + + + Óñòàíîâêà bower, brunch, nodemon è sendwithus... + Óñòàíîâêà ïàêåòîâ bower... + Óñòàíîâêà sass... + Óñòàíîâêà npm... + Çàïóñê brunch.... + Óñòàíîâêà áàçû äàííûõ MongoDB... + Ñêà÷èâàíèå ïîñëåäíåé âåðñèè áàçû äàííûõ CodeCombat... + + Íå çàêðûâàéòå! + + + Ýòîò ïóòü óæå ñóùåñòâóåò, âû óâåðåíû, ÷òî õîòèòå ïåðåçàïèñàòü åãî? + Ýòîò ïóòü íå ñóùåñòâóåò. Ïîæàëóéñòà, ïîïðîáóéòå åù¸ ðàç... + + + Óñòàíîâêà ñðåäû ðàçðàáîò÷èêà CodeCombat óñïåøíî çàâåðøåíà. + Çàðàíåå ñïàñèáî çà âàø âêëàä è äî ñêîðîé âñòðå÷è. + Âû õîòèòå ïðî÷èòàòü README äëÿ ïîëó÷åíèÿ äîïîëíèòåëüíîé èíôîðìàöèè? + + + Ñ ýòîãî ìîìåíòà âû ìîæåòå çàïóñêàòü ñðåäó ðàçðàáîò÷èêà + ñ ïîìîùüþ ùåë÷êà ìûøè. + 1) Äâàæäû ù¸ëêíèòå + è äàéòå ñðåäå çàïóñòèòüñÿ. + 2) Òåïåðü ïðîñòî îòêðîéòå 'localhost:3000' â âàøåì ëþáèìîì áðàóçåðå. + Âîò è âñ¸, òåïåðü âû ãîòîâû ïðèñòóïèòü ê ðàáîòå íàä CodeCombat! + + \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco new file mode 100644 index 000000000..62e3a4b6a --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco @@ -0,0 +1,108 @@ + + + + 简体中文 + Traditional Chinese + Before we start the installation, here are some tips: + Press any key to exit... + + + You have choosen 简体中文 as your language. + ç›®å‰æˆ‘们åªèƒ½ç”¨è‹±æ–‡ç»™ä½ å馈 + + + In order to continue the installation of the developers environment + you will have to read and agree with the following license: + Have you read the license and do you agree with it? + This setup can't happen without an agreement. + Installation and Setup of the CodeCombat environment is cancelled. + + + + -ä½ç³»ç»Ÿ. + æ“作系统 + 被侦测到. + 我们ä¸æ”¯æŒ Windows XP, 安装å–消. + + + 你是å¦å·²ç»å®‰è£…好è¿è¡Œ CodeCombat 所需的所有软件? + 如果你ä¸ç¡®å®šçš„è¯è¯·å›žç­” No. + 正在跳过此软件的安装... + CodeCombat 无法在ä¸ä½¿ç”¨ç¬¬ä¸‰æ–¹æœåŠ¡çš„情况下开å‘. + 这就是为什么你需è¦å®‰è£…这些软件, + 为了开始给我们的开æºç¤¾åŒºåšè´¡çŒ®. + 如果你已ç»æœ‰äº†è¿™äº›è½¯ä»¶ 请å–消安装. + Make sure to select the option that adds the application to your Windows Path, if the option is available. + 你是å¦å·²ç»å®‰è£…了最新版本的 + ? + 正在下载... + 正在安装... + 正在解压... + 正在清ç†... + 请输入你希望安装 mongodb 的文件夹的全路径 + + + + + CodeCombat 是开æºçš„. + 我们的所有æºä»£ç éƒ½æ”¾åœ¨äº† Github. + ä½ å¯ä»¥é€‰æ‹©è‡ªå·±æ‰‹å·¥å®‰è£… Git. + 但我们ä»ç„¶å»ºè®®è®©ç¨‹åºè‡ªåŠ¨æ›¿ä½ å®Œæˆ. + + + 你是å¦æƒ³è‡ªå·±æ‰‹å·¥å®‰è£…本地 Git 安装? + 请确ä¿åœ¨å¼€å§‹å¤„ç†å‰, 你有正确设置好你的库. + 请ä¸è¦å…³é—­æ­¤çª—å£. + 如果你准备好了, 请按任æ„键继续... + + + 请输入你 CodeCombat git库的全路径: + 请输入你想安装 CodeCombat 环境的全路径 + è¿™é¡¹å®‰è£…éœ€è¦ Git Bash. + Git bash 默认安装在 'C:\Program Files (x86)\Git'. + Git bash 默认安装在 'C:\Program Files\Git'. + 请输入 git bash 的安装全路径, 如果你安装的是默认路径, 那么直接输入回车å³å¯ + 你是å¦æƒ³ä½¿ç”¨ ssh æ¥æ£€å‡º(checkout)库(repository)? + + + You should have forked CodeCombat to your own GitHub Account by now... + Please enter your github information, to configure your local repository. + Username: + Password: + Thank you... Configuring your local repistory right now... + + + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + + + 正在安装 bower, brunch, nodemon å’Œ sendwithus... + 正在用 bower 安装ä¾èµ–包... + 正在安装 sass... + 正在安装 npm... + æ­£åœ¨å¼€å¯ brunch.... + 正在为你设置 MongoDB æ•°æ®åº“... + 正在下载 CodeCombat æ•°æ®åº“的最新版本... + + Don't close! + + + 这个路径已ç»å­˜åœ¨, 你想è¦è¦†ç›–它å—? + 这个路径ä¸å­˜åœ¨, 请å†æ¬¡å°è¯•... + + + CodeCombat å¼€å‘环境的æ­å»ºå·²æˆåŠŸ. + æ„Ÿè°¢~ 我们会很快å†æ¬¡è§é¢çš„ :) + 你是å¦æƒ³é˜…读 README 文件以了解更多信æ¯? + + + From now on you can start the dev. environment at + the touch of a single mouse click. + 1) åŒå‡»æ–‡ä»¶ + å¯åŠ¨å¼€å‘环境. + 2) 在æµè§ˆå™¨é‡Œè®¿é—® 'localhost:3000' + 好了,你现在å¯ä»¥å¼€å§‹å¼€å‘ CodeCombat 了! + + \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco old mode 100755 new mode 100644 similarity index 74% rename from scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco rename to scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco index 93bcefd87..2cdaba96e --- a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco @@ -1,82 +1,108 @@ - - - - ç¹ä½“中文 - From now on we'll send our feedback in English! - - - - -bit computer detected. - The operating system - was detected. - We don't support Windows XP, installation cancelled. - - - Have you already installed all the software needed for CodeCombat? - We recommand that you reply negative in case you're not sure. - Skipping the installation of the software... - CodeCombat couldn't be developed without third-party software. - That's why you'll need to install this software, - in order to start contributing to our community. - Cancel the installation if you already have the application. - Make sure to select the option that adds the application to your Windows Path, if the option is available. - Do you already have the latest version of - installed? - is downloading... - is installing... - is unzipping... - is cleaning... - Please define the full path where mongodb should be installed - - - - - CodeCombat is opensource, like you already know. - All our sourcecode can be found online at Github. - You can choose to do the entire Git setup yourself. - However we recommend that you instead let us handle it instead. - - - Do you want to do the Local Git setup manually yourself? - Make sure you have correctly setup your repository before processing. - Do not close this window please. - When you're ready, press any key to continue... - - - Please give the full path of your CodeCombat git repository: - Please enter the full path where you want to install your CodeCombat environment - This installation requires Git Bash. - Git bash is by default installed at 'C:\Program Files (x86)\Git'. - Git bash is by default installed at 'C:\Program Files\Git'. - Please enter the full path where git bash is installed or just press enter if it's in the default location - Do you want to checkout the repository via ssh? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - - - That path already exists, are you sure you want to overwrite it? - That path doesn't exist. Please try again... - - - The setup of the CodeCombat Dev. Environment was succesfull. - Thank you already for your contribution and see you soon. - Do you want to read the README for more information? - - - From now on you can start the dev. environment at - the touch of a single mouse click. - 1) Just double click - and let the environment start up. - 2) Now just open 'localhost:3000' in your prefered browser. - That's it, you're now ready to start working on CodeCombat! - + + + + ç¹ä½“中文 + Simplified Chinese + Before we start the installation, here are some tips: + Press any key to exit... + + + You have choosen ç¹ä½“中文 as your language. + From now on we'll send our feedback in ç¹ä½“中文. + + + In order to continue the installation of the developers environment + you will have to read and agree with the following license: + Have you read the license and do you agree with it? + This setup can't happen without an agreement. + Installation and Setup of the CodeCombat environment is cancelled. + + + + -bit computer detected. + The operating system + was detected. + We don't support Windows XP, installation cancelled. + + + Have you already installed all the software needed for CodeCombat? + We recommand that you reply negative in case you're not sure. + Skipping the installation of the software... + CodeCombat couldn't be developed without third-party software. + That's why you'll need to install this software, + in order to start contributing to our community. + Cancel the installation if you already have the application. + Make sure to select the option that adds the application to your Windows Path, if the option is available. + Do you already have the latest version of + installed? + is downloading... + is installing... + is unzipping... + is cleaning... + Please define the full path where mongodb should be installed + + + + + CodeCombat is opensource, like you already know. + All our sourcecode can be found online at Github. + You can choose to do the entire Git setup yourself. + However we recommend that you instead let us handle it instead. + + + Do you want to do the Local Git setup manually yourself? + Make sure you have correctly setup your repository before processing. + Do not close this window please. + When you're ready, press any key to continue... + + + Please give the full path of your CodeCombat git repository: + Please enter the full path where you want to install your CodeCombat environment + This installation requires Git Bash. + Git bash is by default installed at 'C:\Program Files (x86)\Git'. + Git bash is by default installed at 'C:\Program Files\Git'. + Please enter the full path where git bash is installed or just press enter if it's in the default location + Do you want to checkout the repository via ssh? + + + You should have forked CodeCombat to your own GitHub Account by now... + Please enter your github information, to configure your local repository. + Username: + Password: + Thank you... Configuring your local repistory right now... + + + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + + + Installing bower, brunch, nodemon and sendwithus... + Installing bower packages... + Installing sass... + Installing npm... + Starting brunch.... + Setting up a MongoDB database for you... + Downloading the last version of the CodeCombat database... + + Don't close! + + + That path already exists, are you sure you want to overwrite it? + That path doesn't exist. Please try again... + + + The setup of the CodeCombat Dev. Environment was succesfull. + Thank you already for your contribution and see you soon. + Do you want to read the README for more information? + + + From now on you can start the dev. environment at + the touch of a single mouse click. + 1) Just double click + and let the environment start up. + 2) Now just open 'localhost:3000' in your prefered browser. + That's it, you're now ready to start working on CodeCombat! + \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/ask_question.bat b/scripts/windows/coco-dev-setup/batch/scripts/ask_question.bat old mode 100755 new mode 100644 index fe7ffdd8c..6633ba71d --- a/scripts/windows/coco-dev-setup/batch/scripts/ask_question.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/ask_question.bat @@ -1,5 +1,5 @@ -set /p res="%1 [Y/N]: " -set "result=unset" -if "%res%"=="Y" (set "result=true") -if "%res%"=="y" (set "result=true") +set /p res="%1 [Y/N]: " +set "result=unset" +if "%res%"=="Y" (set "result=true") +if "%res%"=="y" (set "result=true") if "%result%"=="unset" (set "result=false") \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/configuration.bat b/scripts/windows/coco-dev-setup/batch/scripts/configuration.bat new file mode 100755 index 000000000..f70748a7a --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/configuration.bat @@ -0,0 +1,47 @@ +@echo off +setlocal EnableDelayedExpansion + +call read_cache + +call configuration_cmd + +call npm_and_brunch_setup + +call print_finished_header +call print_dashed_seperator + +call get_local_text end_succesfull end succesfull +call get_local_text end_thankyou end thankyou +echo %end_succesfull% +echo %end_thankyou% + +call print_dashed_seperator + +call get_local_text start_s1 start s1 +call get_local_text start_s2 start s2 +call get_local_text start_s3 start s3 +call get_local_text start_s4 start s4 +call get_local_text start_s5 start s5 +call get_local_text start_s6 start s6 + +echo !start_s1! +echo !start_s2! +echo. +echo !start_s3! '!repository_path!\coco\SCOCODE.bat' +echo !start_s4! +echo !start_s5! +echo. +echo !start_s6! + +call print_dashed_seperator + +call get_local_text end_readme end readme +call ask_question "!end_readme!" + +if "%result%"=="true" ( + call open_readme +) + +exit + +endlocal \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/configuration_cmd.bat b/scripts/windows/coco-dev-setup/batch/scripts/configuration_cmd.bat new file mode 100755 index 000000000..3c614e671 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/configuration_cmd.bat @@ -0,0 +1,4 @@ +Color 0A +mode con: cols=79 lines=55 + +TITLE CodeCombat.com - Development Environment Installation \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat old mode 100755 new mode 100644 index 8d85be8b1..760b40889 --- a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat @@ -1,68 +1,141 @@ -set "temp_directory=c:\.coco\" -set "curl_app=..\utilities\curl.exe" -set "zu_app=..\utilities\7za.exe" - -if NOT exist "%temp_directory%" ( - md %temp_directory% -) - -call get_local_text install_process_prefix install process prefix -call get_local_text install_process_sufix install process sufix - -call ask_question "!install_process_prefix! %1 !install_process_sufix!" - -if "%result%"=="true" ( - goto:exit_installation -) - -call print_dashed_seperator - -call get_extension %2 download_extension -call get_local_text install_process_downloading install process downloading -echo %1 !install_process_downloading! -set "install_file=!temp_directory!%1.!download_extension!" -%curl_app% -k %2 -o !install_file! - -if "%download_extension%"=="zip" ( - set "package_path=!temp_directory!%1\" - - %zu_app% x !install_file! -o!package_path! -y - - for /f "delims=" %%a in ('dir !package_path! /on /ad /b') do @set mongodb_original_directory=%%a - - call print_dashed_seperator - goto:get_mongodb_path - - :get_mongodb_path - call get_local_text install_process_mongodbpath install process mongodbpath - set /p "mongodb_path=!install_process_mongodbpath!: " - if exist "%mongodb_path%" ( - call get_local_text error_path error path - call ask_question "!error_path!" - if "!result!"=="false" ( - call print_dashed_seperator - goto:get_mongodb_path - ) else ( - rmdir /s /q %mongodb_path% - ) - ) - md %mongodb_path% - - %systemroot%\System32\xcopy !package_path!!mongodb_original_directory! !mongodb_path! /r /h /s /e /y - goto:clean_up -) - -call get_local_text install_process_installing install process installing -echo %1 !install_process_installing! -echo. -start /WAIT !install_file! -goto:clean_up - -:clean_up - call get_local_text install_process_cleaning install process cleaning - echo %1 !install_process_cleaning! - rmdir /s /q "!temp_directory!" - goto:exit_installation - -:exit_installation +<<<<<<< HEAD +set "temp_directory=c:\.coco\" +set "curl_app=..\utilities\curl.exe" +set "zu_app=..\utilities\7za.exe" + +if NOT exist "%temp_directory%" ( + md %temp_directory% +) + +call get_local_text install_process_prefix install process prefix +call get_local_text install_process_sufix install process sufix + +call ask_question "!install_process_prefix! %1 !install_process_sufix!" + +if "%result%"=="true" ( + goto:exit_installation +) + +call print_dashed_seperator + +call get_extension %2 download_extension +call get_local_text install_process_downloading install process downloading +echo %1 !install_process_downloading! +set "install_file=!temp_directory!%1.!download_extension!" +%curl_app% -k %2 -o !install_file! + +if "%download_extension%"=="zip" ( + set "package_path=!temp_directory!%1\" + + %zu_app% x !install_file! -o!package_path! -y + + for /f "delims=" %%a in ('dir !package_path! /on /ad /b') do @set mongodb_original_directory=%%a + + call print_dashed_seperator + goto:get_mongodb_path + + :get_mongodb_path + call get_local_text install_process_mongodbpath install process mongodbpath + set /p "mongodb_path=!install_process_mongodbpath!: " + if exist "%mongodb_path%" ( + call get_local_text error_path error path + call ask_question "!error_path!" + if "!result!"=="false" ( + call print_dashed_seperator + goto:get_mongodb_path + ) else ( + rmdir /s /q %mongodb_path% + ) + ) + md %mongodb_path% + + %systemroot%\System32\xcopy !package_path!!mongodb_original_directory! !mongodb_path! /r /h /s /e /y + goto:clean_up +) + +call get_local_text install_process_installing install process installing +echo %1 !install_process_installing! +echo. +start /WAIT !install_file! +goto:clean_up + +:clean_up + call get_local_text install_process_cleaning install process cleaning + echo %1 !install_process_cleaning! + rmdir /s /q "!temp_directory!" + goto:exit_installation + +:exit_installation +======= +set "temp_directory=c:\.coco\" +set "curl_app=..\utilities\curl.exe" +set "zu_app=..\utilities\7za.exe" + +if NOT exist "%temp_directory%" ( + md %temp_directory% +) + +call get_local_text install_process_prefix install process prefix +call get_local_text install_process_sufix install process sufix + +call ask_question "!install_process_prefix! %1 !install_process_sufix!" + +if "%result%"=="true" ( + goto:exit_installation +) + +call print_dashed_seperator + +call get_extension %2 download_extension +call get_local_text install_process_downloading install process downloading +echo %1 !install_process_downloading! +set "install_file=!temp_directory!%1.!download_extension!" +start /wait cmd.exe /c "TITLE %1 !install_process_downloading! && %curl_app% -k -m 10800 --retry 100 -o !install_file! %2" + +if "%download_extension%"=="zip" ( + set "package_path=!temp_directory!%1\" + + %zu_app% x !install_file! -o!package_path! -y + + for /f "delims=" %%a in ('dir !package_path! /on /ad /b') do @set mongodb_original_directory=%%a + + call print_dashed_seperator + goto:get_mongodb_path + + :get_mongodb_path + call get_local_text install_process_mongodbpath install process mongodbpath + set /p "mongodb_path=!install_process_mongodbpath!: " + if exist "%mongodb_path%" ( + call get_local_text error_path error path + call ask_question "!error_path!" + if "!result!"=="false" ( + call print_dashed_seperator + goto:get_mongodb_path + ) else ( + rmdir /s /q %mongodb_path% + ) + ) + md %mongodb_path% + + %systemroot%\System32\xcopy !package_path!!mongodb_original_directory! !mongodb_path! /r /h /s /e /y + + call set_environment_var "!mongodb_path!\bin" + + goto:clean_up +) + +call get_local_text install_process_installing install process installing +echo %1 !install_process_installing! +echo. +start /WAIT !install_file! +goto:clean_up + +:clean_up + call get_local_text install_process_cleaning install process cleaning + echo %1 !install_process_cleaning! + rmdir /s /q "!temp_directory!" + goto:exit_installation + +:exit_installation +>>>>>>> 072729acc34123c42250d361955438cfd8c210d7 call print_dashed_seperator \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat old mode 100755 new mode 100644 index 3c5f798fd..f99680802 --- a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat @@ -1,53 +1,54 @@ -call print_install_header -call print_dashed_seperator - -call get_local_text install_process_sks install process sks -echo !install_process_sks! - -call get_local_text install_process_skq install process skq -call ask_question "!install_process_skq!" - -call print_dashed_seperator - -if "%result%"=="true" ( - call get_local_text install_process_skc install process skc - echo !install_process_skc! - call print_dashed_seperator - goto:exit_setup -) - -call get_system_information -call print_dashed_seperator - -if %system_info_os% == XP ( - call get_local_text install_system_xp install system xp - echo !install_system_xp! - call print_exit -) - -call get_variables ..\\config\\downloads.coco downloads download_names downloads_count 0 general general -call get_variables ..\\config\\downloads.coco downloads download_names downloads_count 2 %system_info_os% b%system_info_bit% -call get_variables ..\\config\\downloads.coco downloads download_names downloads_count 3 general b%system_info_bit% - -call get_local_text install_process_s1 install process s1 -call get_local_text install_process_s2 install process s2 -call get_local_text install_process_s3 install process s3 -call get_local_text install_process_s4 install process s4 -call get_local_text install_process_winpath install process winpath - -echo !install_process_s1! -echo !install_process_s2! -echo !install_process_s3! -echo !install_process_s4! -echo. -echo !install_process_winpath! - -call print_dashed_seperator - -for /l %%i in (1, 1, !downloads_count!) do ( - call download_and_install_app !download_names[%%i]! !downloads[%%i]! -) - -goto:exit_setup - +call print_install_header +call print_dashed_seperator + +call get_local_text install_process_sks install process sks +echo !install_process_sks! + +call get_local_text install_process_skq install process skq +call ask_question "!install_process_skq!" + +call print_dashed_seperator + +if "%result%"=="true" ( + call get_local_text install_process_skc install process skc + echo !install_process_skc! + call print_dashed_seperator + goto:exit_setup +) + +call get_system_information +call print_dashed_seperator + +if %system_info_os% == XP ( + call get_local_text install_system_xp install system xp + echo !install_system_xp! + call print_exit +) + +call get_variables ..\\config\\downloads.coco downloads download_names downloads_count 0 general general +call get_variables ..\\config\\downloads.coco downloads download_names downloads_count 2 %system_info_os% b%system_info_bit% +call get_variables ..\\config\\downloads.coco downloads download_names downloads_count 3 general b%system_info_bit% + +call get_local_text install_process_s1 install process s1 +call get_local_text install_process_s2 install process s2 +call get_local_text install_process_s3 install process s3 +call get_local_text install_process_s4 install process s4 +call get_local_text install_process_winpath install process winpath + +echo !install_process_s1! +echo !install_process_s2! +echo !install_process_s3! +echo. +echo !install_process_s4! +echo. +echo !install_process_winpath! + +call print_dashed_seperator + +for /l %%i in (1, 1, !downloads_count!) do ( + call download_and_install_app !download_names[%%i]! !downloads[%%i]! +) + +goto:exit_setup + :exit_setup \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_array.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_array.bat old mode 100755 new mode 100644 index a699365ad..a11f2375e --- a/scripts/windows/coco-dev-setup/batch/scripts/get_array.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_array.bat @@ -1,6 +1,6 @@ -set "file=%1" -set /a %3=0 -for /F "usebackq delims=" %%a in ("%file%") do ( - set /A %3+=1 - call set %2[%%%3%%]=%%a +set "file=%1" +set /a %3=0 +for /F "usebackq delims=" %%a in ("%file%") do ( + set /A %3+=1 + call set %2[%%%3%%]=%%a ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_cache_var.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_cache_var.bat new file mode 100755 index 000000000..d7eebbcfe --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_cache_var.bat @@ -0,0 +1,3 @@ +for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\config\\cache.coco %1') do ( + set "%1=%%F" +) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_config.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_config.bat old mode 100755 new mode 100644 index 21c975aaf..c335263d6 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_config.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_config.bat @@ -1,3 +1,3 @@ -for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\config\\config.coco %1') do ( - set "%1=%%F" +for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\config\\config.coco %1') do ( + set "%1=%%F" ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_download.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_download.bat old mode 100755 new mode 100644 index 2f884b366..1b81e6a0c --- a/scripts/windows/coco-dev-setup/batch/scripts/get_download.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_download.bat @@ -1,3 +1,3 @@ -for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\config\\downloads.coco %2 %3 %4 %5') do ( - set "%1=%%F" +for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\config\\downloads.coco %2 %3 %4 %5') do ( + set "%1=%%F" ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_extension.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_extension.bat old mode 100755 new mode 100644 index bbcd05b5e..71b381ebb --- a/scripts/windows/coco-dev-setup/batch/scripts/get_extension.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_extension.bat @@ -1,3 +1,3 @@ -for /F "delims=" %%F in ('call run_script .\\get_extension.ps1 %1') do ( - set "%2=%%F" +for /F "delims=" %%F in ('call run_script .\\get_extension.ps1 %1') do ( + set "%2=%%F" ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_extension.ps1 b/scripts/windows/coco-dev-setup/batch/scripts/get_extension.ps1 old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat old mode 100755 new mode 100644 index eb9c88ef5..04f859da8 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat @@ -1,36 +1,39 @@ -echo Some feedback is sent in your system's language -echo but most feedback is sent and localised by us. -echo Here is a list of languages: -call print_dashed_seperator - -call get_array ..\\localisation\\languages.coco languages language_count -for /l %%i in (1,1,%language_count%) do ( - call get_text !languages[%%i]! global_native global native - echo [%%i] !global_native! -) - -goto:get_localisation_id - -:get_localisation_id - call print_dashed_seperator - set /p "localisation_id=Enter the language ID of your preference and press : " - goto:validation_check - -:validation_check - set "localisation_is_false=" - set /a local_id = %localisation_id% - if !local_id! EQU 0 set localisation_is_false=1 - if !local_id! LSS 1 set localisation_is_false=1 - if !local_id! GTR !language_count! set localisation_is_false=1 - if defined localisation_is_false ( - echo The id you entered is invalid, please try again... - goto:get_localisation_id - ) else ( - set language_id=!languages[%local_id%]! - call get_text !language_id! global_native global native - call print_dashed_seperator - echo You have choosen !global_native! as your language. - call get_text !language_id! global_intro global intro - echo !global_intro! - call print_seperator +echo Some feedback is sent in your system's language +echo but most feedback is sent and localised by us. +echo Here is a list of languages: +call print_dashed_seperator + +call get_array ..\\localization\\languages.coco languages language_count +for /l %%i in (1,1,%language_count%) do ( + call get_text !languages[%%i]! global_description global description + echo [%%i] !global_description! +) + +goto:get_localization_id + +:get_localization_id + call print_dashed_seperator + set /p "localization_id=Enter the language ID of your preference and press : " + goto:validation_check + +:validation_check + set "localization_is_false=" + set /a local_id = %localization_id% + if !local_id! EQU 0 set localization_is_false=1 + if !local_id! LSS 1 set localization_is_false=1 + if !local_id! GTR !language_count! set localization_is_false=1 + if defined localization_is_false ( + echo The id you entered is invalid, please try again... + goto:get_localization_id + ) else ( + set language_id=!languages[%local_id%]! + call print_dashed_seperator + + call get_local_text language_choosen language choosen + echo !language_choosen! + + call get_local_text language_feedback language feedback + echo !language_feedback! + + call print_seperator ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_local_text.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_local_text.bat old mode 100755 new mode 100644 index 9a54a78c5..aae9bf110 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_local_text.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_local_text.bat @@ -1 +1 @@ -call get_text !language_id! %1 %2 %3 %4 %5 \ No newline at end of file +call get_text %language_id% %1 %2 %3 %4 %5 \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_path_safe.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_path_safe.bat old mode 100755 new mode 100644 index 8e2560551..c76707670 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_path_safe.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_path_safe.bat @@ -1,10 +1,10 @@ -goto:get_safe_path - -:get_safe_path - set /p "tmp_safe_path=%1" - if not exist "%tmp_safe_path%" ( - call get_local_text error-exist - echo !error_exist! - call print_dashed_seperator - goto:get_safe_path +goto:get_safe_path + +:get_safe_path + set /p "tmp_safe_path=%1" + if not exist "%tmp_safe_path%" ( + call get_local_text error-exist + echo !error_exist! + call print_dashed_seperator + goto:get_safe_path ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat old mode 100755 new mode 100644 index 908399932..04c66c1a3 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat @@ -1,31 +1,29 @@ -if exist "%PROGRAMFILES(X86)%" ( - call:set_bit 64 -) else ( - call:set_bit 32 -) - -for /f "tokens=4-5 delims=. " %%i in ('ver') do set VERSION=%%i.%%j -if "%version%" == "5.2" ( call:set_os XP ) -if "%version%" == "6.0" ( call:set_os Vista ) -if "%version%" == "6.1" ( call:set_os Win7 ) -:: we handle win8.0 as win7 -if "%version%" == "6.2" ( call:set_os Win7 ) -:: we handle win8.1 as win7 -if "%version%" == "6.3" ( call:set_os Win7 ) - -goto:end - -:set_bit - call get_local_text install_system_bit install system bit - set system_info_bit=%~1 - echo %system_info_bit%%install_system_bit% -goto:eof - -:set_os - set system_info_os=%~1 - call get_local_text install_system_prefix install system prefix - call get_local_text install_system_sufix install system sufix - echo %install_system_prefix% %system_info_os% %install_system_sufix% -goto:eof - -:end +if exist "%PROGRAMFILES(X86)%" ( + call:set_bit 64 +) else ( + call:set_bit 32 +) + +for /f "tokens=4-5 delims=. " %%i in ('ver') do set VERSION=%%i.%%j +if "%version%" == "5.2" ( call:set_os XP ) +if "%version%" == "6.0" ( call:set_os Vista ) +if "%version%" == "6.1" ( call:set_os Win7 ) +if "%version%" == "6.2" ( call:set_os Win8 ) +if "%version%" == "6.3" ( call:set_os Win8 ) + +goto:end + +:set_bit + call get_local_text install_system_bit install system bit + set system_info_bit=%~1 + echo %system_info_bit%%install_system_bit% +goto:eof + +:set_os + set system_info_os=%~1 + call get_local_text install_system_prefix install system prefix + call get_local_text install_system_sufix install system sufix + echo %install_system_prefix% %system_info_os% %install_system_sufix% +goto:eof + +:end diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat old mode 100755 new mode 100644 index aacdf94f2..617d5b00a --- a/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat @@ -1,3 +1,3 @@ -for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\localisation\\%1.coco %3 %4 %5 %6') do ( - set "%2=%%F" +for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\localization\\%1.coco %3 %4 %5 %6') do ( + set "%2=%%F" ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_var.ps1 b/scripts/windows/coco-dev-setup/batch/scripts/get_var.ps1 old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_variables.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_variables.bat old mode 100755 new mode 100644 index a53805fac..f46c187bc --- a/scripts/windows/coco-dev-setup/batch/scripts/get_variables.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_variables.bat @@ -1,4 +1,4 @@ -set count=0 -for /F "delims=" %%F in ('call run_script.bat .\\get_variables.ps1 %*') do ( - %%F +set count=0 +for /F "delims=" %%F in ('call run_script.bat .\\get_variables.ps1 %*') do ( + %%F ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_variables.ps1 b/scripts/windows/coco-dev-setup/batch/scripts/get_variables.ps1 old mode 100755 new mode 100644 index 6d94b4324..38fa11b7c --- a/scripts/windows/coco-dev-setup/batch/scripts/get_variables.ps1 +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_variables.ps1 @@ -1,33 +1,33 @@ -$xml_file = [xml](get-content $args[0]) -$arr_value = $args[1] -$arr_name = $args[2] -$arr_counter = $args[3] -$counter = $args[4] - -if($args.count -eq 6) -{ - $root = $xml_file.variables.($args[5]) -} -elseif($args.count -eq 7) -{ - $root = $xml_file.variables.($args[5]).($args[6]) -} -elseif($args.count -eq 8) -{ - $root = $xml_file.variables.($args[5]).($args[6]).($args[7]) -} -elseif($args.count -eq 9) -{ - $nodes = $xml_file.variables.($args[5]).($args[6]).($args[7]).($args[8]) -} - -foreach ($node in $root.ChildNodes) -{ - $counter += 1 - $value = $node.InnerText - $name = $node.Name - Write-Host set "$arr_value[$counter]=$value" - Write-Host set "$arr_name[$counter]=$name" -} - +$xml_file = [xml](get-content $args[0]) +$arr_value = $args[1] +$arr_name = $args[2] +$arr_counter = $args[3] +$counter = $args[4] + +if($args.count -eq 6) +{ + $root = $xml_file.variables.($args[5]) +} +elseif($args.count -eq 7) +{ + $root = $xml_file.variables.($args[5]).($args[6]) +} +elseif($args.count -eq 8) +{ + $root = $xml_file.variables.($args[5]).($args[6]).($args[7]) +} +elseif($args.count -eq 9) +{ + $nodes = $xml_file.variables.($args[5]).($args[6]).($args[7]).($args[8]) +} + +foreach ($node in $root.ChildNodes) +{ + $counter += 1 + $value = $node.InnerText + $name = $node.Name + Write-Host set "$arr_value[$counter]=$value" + Write-Host set "$arr_name[$counter]=$name" +} + Write-Host set "$arr_counter=$counter" \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat b/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat old mode 100755 new mode 100644 index e9add16ca..38234f51b --- a/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat @@ -1,115 +1,149 @@ -call print_github_header -call print_dashed_seperator - -call get_local_text github_intro_opensource github intro opensource -call get_local_text github_intro_online github intro online -call get_local_text github_intro_manual github intro manual -call get_local_text github_intro_norec github intro norec - -echo !github_intro_opensource! -echo !github_intro_online! -echo !github_intro_manual! -echo !github_intro_norec! - -call print_dashed_seperator - -call get_local_text github_skip_question github skip question -call ask_question "!github_skip_question!" -call print_dashed_seperator - -if "%result%"=="true" ( - call get_local_text github_skip_consequence github skip consequence - echo !github_skip_consequence! - - call get_local_text github_skip_donotclose github skip donotclose - echo !github_skip_donotclose! - - call get_local_text github_skip_wait github skip wait - set /p "github_skip_wait=!github_skip_wait!" - - call print_dashed_seperator - - call get_local_text github_process_path github process path - call get_path_safe "!github_process_path!" - set "repository_path=!tmp_safe_path!" - - goto:exit_git_setup -) - -goto:get_bash_path - -:get_bash_path - call get_local_text github_process_bashi github process bashi - echo !github_process_bashi! - - if not defined install_system_bit ( - call print_dashed_seperator - call get_system_information - call print_dashed_seperator - ) - - if "%system_info_bit%"=="64" ( - call get_local_text github_process_bashp64 github process bashp64 - echo !github_process_bashp64! - ) else ( - call get_local_text github_process_bashp32 github process bashp32 - echo !github_process_bashp32! - ) - - call get_local_text github_process_bashq github process bashq - set /p "git_bash_path=!github_process_bashq!: " - - if not defined git_bash_path ( - if "%system_info_bit%"=="64" ( - set "git_bash_path=C:\Program Files (x86)\Git" - ) else ( - set "git_bash_path=C:\Program Files\Git" - ) - goto:get_git_path - ) - - if not exist "%git_bash_path%" ( - call get_local_text error_exist error exist - echo !error_exist! - call print_dashed_seperator - goto:get_bash_path - ) else ( - goto:get_git_path - ) -goto:eof - -:get_git_path - call print_dashed_seperator - call get_local_text github_process_checkout github process checkout - set /p "repository_path=!github_process_checkout!: " - if exist !repository_path! ( - call get_local_text error_path error path - call ask_question "!error_path!" - if "!result!"=="false" ( - call print_dashed_seperator - goto:get_git_path - ) else ( - rmdir /s /q %repository_path% - goto:git_checkout - ) - ) else ( - goto:git_checkout - ) -goto:eof - -:git_checkout - md "%repository_path%" - set "repository_path=%repository_path%\coco" - - call print_dashed_seperator - set "git_app_path=%git_bash_path%\bin\git.exe" - - call get_config github_url - "%git_app_path%" clone "!github_url!" "%repository_path%" - - goto:exit_git_setup -goto:eof - -:exit_git_setup - call print_dashed_seperator +call print_github_header +call print_dashed_seperator + +call get_local_text github_intro_opensource github intro opensource +call get_local_text github_intro_online github intro online +call get_local_text github_intro_manual github intro manual +call get_local_text github_intro_norec github intro norec + +echo !github_intro_opensource! +echo !github_intro_online! +echo !github_intro_manual! +echo !github_intro_norec! + +call print_dashed_seperator + +call get_local_text github_skip_question github skip question +call ask_question "!github_skip_question!" +call print_dashed_seperator + +if "%result%"=="true" ( + call get_local_text github_skip_consequence github skip consequence + echo !github_skip_consequence! + + call get_local_text github_skip_donotclose github skip donotclose + echo !github_skip_donotclose! + + call get_local_text github_skip_wait github skip wait + set /p "github_skip_wait=!github_skip_wait!" + + call print_dashed_seperator + + call get_local_text github_process_path github process path + call get_path_safe "!github_process_path!" + set "repository_path=!tmp_safe_path!" + + goto:exit_git_setup +) + +goto:get_bash_path + +:get_bash_path + call get_local_text github_process_bashi github process bashi + echo !github_process_bashi! + + if not defined install_system_bit ( + call print_dashed_seperator + call get_system_information + call print_dashed_seperator + ) + + if "%system_info_bit%"=="64" ( + call get_local_text github_process_bashp64 github process bashp64 + echo !github_process_bashp64! + ) else ( + call get_local_text github_process_bashp32 github process bashp32 + echo !github_process_bashp32! + ) + + call get_local_text github_process_bashq github process bashq + set /p "git_bash_path=!github_process_bashq!: " + + if not defined git_bash_path ( + if "%system_info_bit%"=="64" ( + set "git_bash_path=C:\Program Files (x86)\Git" + ) else ( + set "git_bash_path=C:\Program Files\Git" + ) + goto:get_git_path + ) + + if not exist "%git_bash_path%" ( + call get_local_text error_exist error exist + echo !error_exist! + call print_dashed_seperator + goto:get_bash_path + ) else ( + goto:get_git_path + ) +goto:eof + +:get_git_path + call print_dashed_seperator + call get_local_text github_process_checkout github process checkout + set /p "repository_path=!github_process_checkout!: " + if exist !repository_path! ( + call get_local_text error_path error path + call ask_question "!error_path!" + if "!result!"=="false" ( + call print_dashed_seperator + goto:get_git_path + ) else ( + rmdir /s /q %repository_path% + goto:git_checkout + ) + ) else ( + goto:git_checkout + ) +goto:eof + +:git_checkout + md "%repository_path%" + set "repository_path=%repository_path%" + + call print_dashed_seperator + set "git_app_path=%git_bash_path%\bin\git.exe" + + call get_config github_url + "%git_app_path%" clone "!github_url!" "%repository_path%\coco" + + goto:git_configuration +goto:eof + +:git_configuration + call print_dashed_seperator + + call get_local_text github_config_intro github config intro + echo !github_config_intro! + + call get_local_text github_config_info github config info + echo !github_config_info! + + call print_dashed_seperator + + call get_local_text github_config_username github config username + set /p "git_username=!github_config_username!" + + call get_local_text github_config_password github config password + + set /p "git_password=!github_config_password!" + + call print_dashed_seperator + + call get_local_text github_config_process github config process + echo !github_config_process! + + set cur_dir=%CD% + cd !repository_path!\coco + + "%git_app_path%" remote rm origin + "%git_app_path%" remote add origin https://!git_username!:!git_password!@github.com/!git_username!/codecombat.git + + cd !cur_dir! + + goto:exit_git_setup +goto:eof + +:exit_git_setup + call print_dashed_seperator goto:eof \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat new file mode 100755 index 000000000..7048f0180 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat @@ -0,0 +1,7 @@ +call print_dashed_seperator +call get_local_text npm_script npm script +echo %npm_script% + +echo start cmd.exe cmd /c "TITLE CodeCombat.com - mongodb database & mongod --setParameter textSearchEnabled=true --dbpath %~2">%~1\SCOCODE.bat +echo start cmd.exe cmd /c "TITLE CodeCombat.com - nodemon server & nodemon index.js">>%~1\SCOCODE.bat +echo start cmd.exe cmd /c "TITLE CodeCombat.com - brunch - live compiler & brunch w">>%~1\SCOCODE.bat \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_bower.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_bower.bat new file mode 100755 index 000000000..283d5e522 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_bower.bat @@ -0,0 +1,7 @@ +call print_dashed_seperator +call get_local_text npm_binstall npm binstall +echo %npm_binstall% + +cd /D %~1 +start /wait cmd /c "echo %npm_binstall% & bower cache clean & bower install" +cd /D %work_directory% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat new file mode 100755 index 000000000..84d9a3691 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat @@ -0,0 +1,37 @@ +call print_dashed_seperator +call get_local_text npm_mongodb npm mongodb +echo %npm_mongodb% + +if exist %~1 ( + rmdir /s /q %~1 +) + +md %~1 + +call print_dashed_seperator +call get_local_text npm_db npm db +echo %npm_db% + +call get_config database_backup + +call get_local_text npm_close npm close + +cd /D %~1 + +start cmd /c "TITLE MongoDB - %npm_close% & mongod --setParameter textSearchEnabled=true --dbpath %~1" + +start /wait cmd.exe /c "TITLE downloading database backup... && %work_directory%\%curl_app% -k -m 10800 --retry 100 -o dump.tar.gz %database_backup%" + +start /wait cmd /c "%work_directory%\%zu_app% e dump.tar.gz && del dump.tar.gz && %work_directory%\%zu_app% x dump.tar && del dump.tar" + +start /wait cmd /c "mongorestore dump" + +rmdir /s /q dump + +call %work_directory%\print_dashed_seperator + +taskkill /F /fi "IMAGENAME eq mongod.exe" + +del /F %~1\mongod.lock + +cd /D %work_directory% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_npm.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_npm.bat new file mode 100755 index 000000000..6c6e5d3c2 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_npm.bat @@ -0,0 +1,6 @@ +call get_local_text npm_install npm install +echo %npm_install% + +cd /D %~1 +start /wait cmd /c "echo %npm_install% & npm install -g bower brunch nodemon sendwithus" +cd /D %work_directory% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_npm_all.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_npm_all.bat new file mode 100755 index 000000000..cb858c029 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_npm_all.bat @@ -0,0 +1,7 @@ +call print_dashed_seperator +call get_local_text npm_npm npm npm +echo %npm_npm% + +cd /D %~1 +start /wait cmd /c "echo %npm_npm% & npm install" +cd /D %work_directory% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_sass.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_sass.bat new file mode 100755 index 000000000..6cece7ced --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_sass.bat @@ -0,0 +1,7 @@ +call print_dashed_seperator +call get_local_text npm_sass npm sass +echo %npm_sass% + +cd /D %~1 +start /wait cmd /c "echo %npm_sass% & gem install sass" +cd /D %work_directory% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/npm_and_brunch_setup.bat b/scripts/windows/coco-dev-setup/batch/scripts/npm_and_brunch_setup.bat old mode 100755 new mode 100644 index ae3572b36..4f6a7964a --- a/scripts/windows/coco-dev-setup/batch/scripts/npm_and_brunch_setup.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/npm_and_brunch_setup.bat @@ -1,89 +1,24 @@ -call print_npm_and_brunch_header -call print_dashed_seperator - -set work_directory=%CD% - -set "curl_app=..\utilities\curl.exe" -set "zu_app=..\utilities\7za.exe" -set "keystuff=..\utilities\keystuff.exe" - -set "coco_root=!repository_path!\coco" - -goto:automatic_script - -call get_local_text npm-install -echo !npm_install! - -cd !coco_root! -start /wait cmd /c "echo !npm_install! & npm install -g bower brunch nodemon sendwithus" -cd !work_directory! - -call print_dashed_seperator -call get_local_text npm-binstall -echo !npm_binstall! - -cd "!coco_root!" -start /wait cmd /c "echo !npm_binstall! & bower install" -cd "!work_directory!" - -call print_dashed_seperator -call get_local_text npm-sass -echo !npm_sass! - -cd "!coco_root!" -start /wait cmd /c "echo !npm_sass! & gem install sass" -cd "!work_directory!" - -call print_dashed_seperator -call get_local_text npm-npm -echo !npm_npm! - -cd "!coco_root!" -start /wait cmd /c "echo !npm_npm! & npm install" -cd "!work_directory!" - -:: --- MONGODB - -:mongodb -call print_dashed_seperator -call get_local_text npm-mongodb -echo !npm_mongodb! - -set "mdb_directory=!repository_path!\cocodb" - -if exist mdb_directory ( - rmdir /s /q "!mdb_directory!" -) - -md !mdb_directory! - -call print_dashed_seperator -call get_local_text npm-db -echo !npm_db! - -call get_config database_backup - -cd !mdb_directory! - -start cmd /c "%work_directory%\%keystuff% Alt-Tab && mongod --setParameter textSearchEnabled=true --dbpath !mdb_directory!" - -%curl_app% -k !database_backup! -o dump.tar.gz - -start /wait cmd /c "%work_directory%\%keystuff% Alt-Tab && %zu_app% e dump.tar.gz && del dump.tar.gz && %zu_app% x dump.tar && del dump.tar" - -start /wait cmd /c "mongorestore dump" - -rmdir /s /q db - -:: --- AUTOMATIC SCRIPT - -::automatic_script -call print_dashed_seperator -call get_local_text npm-script -echo !npm_script! - -:: --- END - -call print_dashed_seperator - -pause \ No newline at end of file +call print_npm_and_brunch_header +call print_dashed_seperator + +set work_directory=%CD% + +set "curl_app=..\utilities\curl.exe" +set "zu_app=..\utilities\7za.exe" + +set coco_root=%repository_path%\coco +set coco_db=%repository_path%\cocodb + +call nab_install_npm %coco_root% + +call nab_install_bower %coco_root% + +call nab_install_sass %coco_root% + +call nab_install_npm_all %coco_root% + +call nab_install_mongodb %coco_db% + +call nab_automatic_script.bat %coco_root% %coco_db% + +call print_dashed_seperator diff --git a/scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat new file mode 100755 index 000000000..bee9f5dd6 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat @@ -0,0 +1,6 @@ +set "LFTP=%1-%language_id%.coco" +if not exist "%LFTP%" ( + call open_text_file %1.coco +) else ( + call open_text_file %LFTP% +) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat b/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat old mode 100755 new mode 100644 index 484f3dd75..730a3f577 --- a/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat @@ -1 +1 @@ -call open_text_file ..\\config\\readme.coco \ No newline at end of file +call open_localized_text_file ..\\config\\localized\\readme \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/open_text_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/open_text_file.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat old mode 100755 new mode 100644 index 727d7e61c..cf1c60ada --- a/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat @@ -1,3 +1,3 @@ -echo. -echo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +echo. +echo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - echo. \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat old mode 100755 new mode 100644 index 6f1051cc6..107790d88 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat @@ -1,2 +1,3 @@ -set /p res="Press any key to exit..." +call get_local_text global_exit global exit +set /p res="%global_exit%" exit \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_file.bat old mode 100755 new mode 100644 index f40867969..de46b68ee --- a/scripts/windows/coco-dev-setup/batch/scripts/print_file.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_file.bat @@ -1,4 +1,4 @@ -set "file=%1" -for /f "usebackq tokens=* delims=;" %%a in ("%file%") do ( - echo.%%a +set "file=%1" +for /f "usebackq tokens=* delims=;" %%a in ("%file%") do ( + echo.%%a ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_finished_header.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_finished_header.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_github_header.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_github_header.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_header.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_header.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_info.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_info.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_install_header.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_install_header.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat old mode 100755 new mode 100644 index a208ca559..3acee4bcc --- a/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat @@ -1 +1 @@ -print_file ..\\config\\license.coco \ No newline at end of file +call print_localized_file ..\\config\\localized\\license \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat new file mode 100755 index 000000000..e71fe3364 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat @@ -0,0 +1,6 @@ +set "LFTP=%1-%language_id%.coco" +if not exist "%LFTP%" ( + call print_file %1.coco +) else ( + call print_file %LFTP% +) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_npm_and_brunch_header.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_npm_and_brunch_header.bat old mode 100755 new mode 100644 diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat old mode 100755 new mode 100644 index c68792d46..50f484b80 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat @@ -1,3 +1,3 @@ -echo. -echo ----------------------------------------------------------------------------- +echo. +echo ------------------------------------------------------------------------------- echo. \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat old mode 100755 new mode 100644 index c00833574..0a2e3033a --- a/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat @@ -1 +1 @@ -print_file ..\\config\\tips.coco \ No newline at end of file +call print_localized_file ..\\config\\localized\\tips \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/read_cache.bat b/scripts/windows/coco-dev-setup/batch/scripts/read_cache.bat new file mode 100755 index 000000000..13f96330d --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/read_cache.bat @@ -0,0 +1,2 @@ +call get_cache_var language_id +call get_cache_var repository_path \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/run_script.bat b/scripts/windows/coco-dev-setup/batch/scripts/run_script.bat old mode 100755 new mode 100644 index 1e4797008..c18af72b7 --- a/scripts/windows/coco-dev-setup/batch/scripts/run_script.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/run_script.bat @@ -1,2 +1,2 @@ -@echo off +@echo off PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& "%*" \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/set_environment_var.bat b/scripts/windows/coco-dev-setup/batch/scripts/set_environment_var.bat new file mode 100755 index 000000000..05d5362a5 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/set_environment_var.bat @@ -0,0 +1 @@ +setx path ";%~1" \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/setup.bat b/scripts/windows/coco-dev-setup/batch/scripts/setup.bat old mode 100755 new mode 100644 index 7c137563f..fe24e6855 --- a/scripts/windows/coco-dev-setup/batch/scripts/setup.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/setup.bat @@ -1,67 +1,29 @@ -@echo off -setlocal EnableDelayedExpansion - -Color 0A -mode con: cols=79 lines=55 - -call print_header -call print_dashed_seperator - -call get_config.bat version -call get_config.bat author -call get_config.bat copyright -echo Welcome to the automated Installation of the CodeCombat Dev. Environment! -echo v%version% authored by %author% and published by %copyright%. -call print_seperator - -echo Before we start the installation, here are some tips: -call print_tips -call print_seperator - -call sign_license - -call get_language - -call download_and_install_applications - -call github_setup - -:: This will be available in v2.0 -::call npm_and_brunch_setup - -call print_finished_header -call print_dashed_seperator - -call get_local_text end_succesfull end succesfull -call get_local_text end_thankyou end thankyou -echo %end_succesfull% -echo %end_thankyou% - -call print_dashed_seperator - -call get_local_text start_s1 start s1 -call get_local_text start_s2 start s2 -call get_local_text start_s3 start s3 -call get_local_text start_s4 start s4 -call get_local_text start_s5 start s5 -call get_local_text start_s6 start s6 - -echo !start_s1! -echo !start_s2! -echo. -echo !start_s3! '!repository_path!\coco\SCOCODE.bat' -echo !start_s4! -echo !start_s5! -echo. -echo !start_s6! - -call print_dashed_seperator - -call get_local_text end_readme end readme -call ask_question "!end_readme!" - -if "%result%"=="true" ( - call open_readme -) - +@echo off +setlocal EnableDelayedExpansion + +call configuration_cmd + +call print_header +call print_dashed_seperator + +call get_config.bat version +call get_config.bat author +call get_config.bat copyright +echo Welcome to the automated Installation of the CodeCombat Dev. Environment! +echo v%version% authored by %author% and published by %copyright%. +call print_seperator + +call get_language + +call get_local_text global_tips global tips +echo !global_tips! +call print_tips +call print_seperator + +call sign_license + +call download_and_install_applications + +start cmd /c "setup_p2.bat" + endlocal \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat b/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat new file mode 100755 index 000000000..f6e0d3b21 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat @@ -0,0 +1,20 @@ +@echo off +setlocal EnableDelayedExpansion + +call configuration_cmd + +call github_setup + +call write_cache + +call get_local_text switch_install switch install +call get_local_text switch_close switch close +call get_local_text switch_open switch open + +echo %switch_install% +echo %switch_close% +echo. + +set /p "dummy=%switch_open%" + +endlocal \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat b/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat old mode 100755 new mode 100644 index 139ddfd80..a3d3a3f3f --- a/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat @@ -1,15 +1,27 @@ -echo In order to continue the installation of the developers environment -echo you will have to read and agree with the following license: -call print_dashed_seperator - -call print_license -call print_dashed_seperator - -call ask_question "Have you read the license and do you agree with it?" -call print_dashed_seperator - -if "%result%"=="false" ( - echo This setup can't happen without an agreement. - echo Installation and Setup of the CodeCombat environment is cancelled. - call print_exit +call get_local_text license_s1 license s1 +echo !license_s1! + +call get_local_text license_s2 license s2 +echo !license_s2! + +call print_dashed_seperator + +call print_license +call print_dashed_seperator + +call get_local_text license_q1 license q1 +call ask_question "%license_q1%" + +call print_dashed_seperator + +if "%result%"=="false" ( + call get_local_text license_a1 license a1 + echo !license_a1! + + call get_local_text license_a2 license a2 + echo !license_a2! + + echo. + + call print_exit ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/write_cache.bat b/scripts/windows/coco-dev-setup/batch/scripts/write_cache.bat new file mode 100755 index 000000000..2e46ca4c3 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/write_cache.bat @@ -0,0 +1,10 @@ +set "cache=..\\config\\cache.coco" + +echo ^>%cache% + +echo ^>>%cache% + +echo ^%language_id%^>>%cache% +echo ^%repository_path%^>>%cache% + +echo ^>>%cache% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/setup.bat b/scripts/windows/coco-dev-setup/batch/setup.bat old mode 100755 new mode 100644 index aa57d4523..1246a77c1 --- a/scripts/windows/coco-dev-setup/batch/setup.bat +++ b/scripts/windows/coco-dev-setup/batch/setup.bat @@ -1,2 +1,2 @@ -cd scripts +cd scripts setup.bat \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/setup.exe b/scripts/windows/coco-dev-setup/batch/setup.exe new file mode 100755 index 000000000..f36e70dfa Binary files /dev/null and b/scripts/windows/coco-dev-setup/batch/setup.exe differ diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/config.coco b/scripts/windows/coco-dev-setup/last_step_succesfull/config.coco deleted file mode 100755 index ae8c66f56..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/config.coco +++ /dev/null @@ -1,6 +0,0 @@ - - - 1.0 - GlenDC - CodeCombat.com © 2013-2014 - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/downloads.coco b/scripts/windows/coco-dev-setup/last_step_succesfull/downloads.coco deleted file mode 100755 index 2a0472c41..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/downloads.coco +++ /dev/null @@ -1,24 +0,0 @@ - - - - - http://nodejs.org/dist/v0.10.25/node-v0.10.25-x86.msi - http://dl.bintray.com/oneclick/rubyinstaller/rubyinstaller-2.0.0-p353.exe?direct - http://www.python.org/ftp/python/2.7.6/python-2.7.6.msi - - - http://nodejs.org/dist/v0.10.25/x64/node-v0.10.25-x64.msi - http://dl.bintray.com/oneclick/rubyinstaller/rubyinstaller-2.0.0-p353-x64.exe?direct - http://www.python.org/ftp/python/2.7.6/python-2.7.6.amd64.msi - - https://msysgit.googlecode.com/files/Git-1.8.5.2-preview20131230.exe - - - mongodb=http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.5.4.zip - mongodb=http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.5.4.zip - - - mongodb=http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.5.4.zip - mongodb=http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2.5.4.zip - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/en.coco b/scripts/windows/coco-dev-setup/last_step_succesfull/en.coco deleted file mode 100755 index a2e1f9fca..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/en.coco +++ /dev/null @@ -1,53 +0,0 @@ - - - - English - Bye Bye! - - - Installation has begun, this can take a while... Please stay tuned... - Don't close any windows please, unless specified explicitly. - - - [DOWNLOADING AND INSTALLING 3RD PARTY SOFTWARE] - downloading: - installing: - Download and Installation cancelled... - Software has been installed... - Installation of the Developers Environment is complete! - Installation has been stopped... - unpacking and moving: - Installing bower, brunch, nodemon and sendwithus... - - - CodeCombat is safely stored on a git repository. - Therefore you need a git command-line application (Git-bash). - Examples: git-bash, CygWin, ... - Do you already have git-bash? - Enter the path to where you installed Git-bash - Checking out the Git Repository... - Please enter your github username: - - - Do you already have the latest version of node-js installed? - Please enter the full path of the location you installed nodejs to: - - - Do you already have the latest version of ruby installed? - - - Do you already have the latest version of mongo-db installed? - Enter the path where you would like to install MongoDB: - - - Do you already have the latest version of python installed? - - - Sadly we can't support Windows XP... Please upgrade your OS! - Machine OS cannot be determined... - Report your OS to the developers @ CodeCombat.com... - ... Cleaning up has been disabled... Terminating Script! - The path to your git application is incorrect, please try again... - The path you entered is invalid, please try again... - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/get_config.bat b/scripts/windows/coco-dev-setup/last_step_succesfull/get_config.bat deleted file mode 100755 index 3849e22c2..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/get_config.bat +++ /dev/null @@ -1,3 +0,0 @@ -powershell .\get_var.ps1 config.coco %1 > var.tmp -set /p %1= < var.tmp -del /q var.tmp \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/get_download.bat b/scripts/windows/coco-dev-setup/last_step_succesfull/get_download.bat deleted file mode 100755 index fde3799e3..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/get_download.bat +++ /dev/null @@ -1,4 +0,0 @@ -@ECHO off -powershell .\get_var.ps1 downloads.coco %2 %3 %4 %5 %6 > var.tmp -set /p %1= < var.tmp -del /q var.tmp \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/get_text.bat b/scripts/windows/coco-dev-setup/last_step_succesfull/get_text.bat deleted file mode 100755 index 5cae1d431..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/get_text.bat +++ /dev/null @@ -1,4 +0,0 @@ -@ECHO off -powershell .\get_var.ps1 %1.coco %3 %4 %5 %6 %7 > var.tmp -set /p %2= < var.tmp -del /q var.tmp \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/get_var.ps1 b/scripts/windows/coco-dev-setup/last_step_succesfull/get_var.ps1 deleted file mode 100755 index 77573929f..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/get_var.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$xml_file = [xml](get-content $args[0]) -if($args.count -eq 2) -{ - $xml_file.variables.($args[1]) -} -elseif($args.count -eq 3) -{ - $xml_file.variables.($args[1]).($args[2]) -} -elseif($args.count -eq 4) -{ - $xml_file.variables.($args[1]).($args[2]).($args[3]) -} -elseif($args.count -eq 5) -{ - $xml_file.variables.($args[1]).($args[2]).($args[3]).($args[4]) -} \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/last_step_succesfull/run_script.bat b/scripts/windows/coco-dev-setup/last_step_succesfull/run_script.bat deleted file mode 100755 index dfc6e6cc0..000000000 --- a/scripts/windows/coco-dev-setup/last_step_succesfull/run_script.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -powershell "& "%*" \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/recycle_bin/dev-setup.bat b/scripts/windows/coco-dev-setup/recycle_bin/dev-setup.bat deleted file mode 100755 index 97e7d1ee2..000000000 --- a/scripts/windows/coco-dev-setup/recycle_bin/dev-setup.bat +++ /dev/null @@ -1,533 +0,0 @@ -@echo off -setlocal EnableDelayedExpansion - -Color 0A - -mode con: cols=78 lines=60 - -:: Global Variables -set "temp-dir=C:\Coco-Temp" -set install-log=%temp-dir%\coco-dev-install-log.txt - -:: set correct curl app -IF EXIST "%PROGRAMFILES(X86)%" ( - (set "curl-app=utilities\curl\64bit\curl.exe") -) ELSE ( - set "curl-app=utilities\curl\32bit\curl.exe" -) - -set "ZU-app=utilities\7za.exe" - -:: BUGS: - :: + DEBUG ALL STEPS UNTILL NOW DONE - - -:: TODO: -:: + Write code to install vs if it's not yet installed on users pc - -:: + Configuraton and installation checklist: -:: 1) cd codecombat -:: 2) npm install -g bower brunch nodemon sendwithus -:: 3) bower install -:: 4) gem install sass -:: 5) npm install -:: 6) brunch -w -:: Extra... @ Fail run npm install - -:: + Copy the automated dev batch file to root folder -:: => Let user define mongo-db directory -:: + Start the dev environment - -:: Create The Temporary Directory -IF EXIST %temp-dir% rmdir %temp-dir% /s /q -mkdir %temp-dir% - -:: Create Log File -copy /y nul %install-log% > nul - -call:parse_aa_and_draw "config\header" -call:draw_dss - -call:parse_file_new "config\config" cnfg n - -call:log "Welcome to the automated Installation of the CodeCombat Dev. Environment!" -call:log_sse "v%%cnfg[1]%% authored by %%cnfg[2]%% and published by %%cnfg[3]%%." - -:: Language Agreement Stuff - -call:log "In order to continue the installation of the developers environment" -call:log "you will have to read and agree with the following license: -call:draw_dss -echo. -call:parse_aa_and_draw "license.txt" -echo. -call:draw_dss -call:strict_user_yn_question "Have you read the license and do you agree with it?" - -if "%res%"=="false" ( - call:log "Sorry to hear that, have a good day..." - call:log_sse "Installation and Setup of the CodeCombat environment is cancelled." - GOTO:END -) - -:: Tips -call:log "Before we start the installation, here are some tips:" -echo. - -call:parse_aa_and_draw "config\tips" - -call:draw_ss - -:: Read Language Index -call:parse_file_new "localisation\languages" lang lang_c - -:: Read Download URLs -call:parse_file_new "config\downloads" downloads n -call:parse_file_new "config\downloads_32" downloads_32 n -call:parse_file_new "config\downloads_64" downloads_64 n -call:parse_file_new "config\downloads_vista_32" downloads_vista_32 n -call:parse_file_new "config\downloads_vista_64" downloads_vista_64 n -call:parse_file_new "config\downloads_7_32" downloads_7_32 n -call:parse_file_new "config\downloads_7_64" downloads_7_64 n - -:: Parse all Localisation Files -for /L %%i in (1,1,%lang_c%) do ( - call:parse_file "localisation\%%lang[%%i]%%" languages languages_c -) - -set /A "wc = %languages_c% / %lang_c%" - -:: Start install with language question (Localisation) -call:log "Which language do you prefer?" - -set /A c=0 -for /L %%i in (1,%wc%,%languages_c%) do ( - set /A "n = %%i - 1" - call:log " [%%c%%] %%languages[%%i]%%" - set /A c+=1 -) - -set "lang_id=-1" -call:user_enter_language_id -goto:user_pick_language - -:user_enter_language_id - set /p lang_id= "Enter the language ID and press : " -goto:eof - -:user_pick_language - set res=false - if %lang_id% LSS 0 set res=true - if %lang_id% GEQ %lang_c% set res=true - if "%res%"=="true" ( - call:log "Invalid id! Please enter a correct id from the numbers listed above..." - call:draw_dss - call:user_enter_language_id - goto:user_pick_language - ) - -call:get_lw word 0 -call:log_ds "You choose '%word%', from now on all feedback will be logged in it." - -call:log_lw 1 -call:log_lw_sse 2 - -:: downloads for all version... - -:: [TODO] The choice between Cygwin && Git ?! Is => HAVE EXTERNAL GIT APPLICATION LIST!!! - -call:log_lw_sse 3 - -call:log_lw 6 -call:log_lw 7 -call:log_lw 8 -call:install_software_o "git" "%%downloads[1]%%" exe 9 -call:draw_dss -call:get_lw word 11 - -:: [TODO] Add downloads for windows visual studio ?! - -call:user_set_git_path - -:user_set_git_path_fail - if not exist "%git_exe_path%" ( - call:log_lw 27 - call:draw_dss - call:user_set_git_path - ) - :: architecture specific downloads... - IF EXIST "%PROGRAMFILES(X86)%" (GOTO 64BIT) ELSE (GOTO 32BIT) -goto:eof - -:user_set_git_path - set /p git_exe_path="%word%: " - call:user_set_git_path_fail -goto:eof - -:go_to_platform - call:log_ds "Windows %~1 detected..." - GOTO %~2 -goto:eof - -:64BIT - call:log_ds "64-bit computer detected..." - - call:install_software_o "node-js" "%%downloads_64[1]%%" msi 12 - call:draw_dss - - call:get_path_from_user 41 42 - set "node_js_path=%user_tmp_path%" - Call:draw_dss - - call:install_software_o "ruby" "%%downloads_64[2]%%" exe 13 - call:draw_dss - call:install_software_o "python" "%%downloads_64[3]%%" msi 26 - - :: Some installations require specific windows versions - for /f "tokens=4-5 delims=. " %%i in ('ver') do set VERSION=%%i.%%j - if "%version%" == "5.2" ( call:go_to_platform "XP" ver_XP_64 ) - if "%version%" == "6.0" ( call:go_to_platform "Vista" ver_Vista_64 ) - if "%version%" == "6.1" ( call:go_to_platform "7" ver_Win7_8_64 ) - if "%version%" == "6.2" ( call:go_to_platform "8.0" ver_Win7_8_64 ) - if "%version%" == "6.3" ( call:go_to_platform "8.1" ver_Win7_8_64 ) - GOTO warn_and_exit -GOTO END - -:32BIT - call:log_ds "32-bit computer detected..." - - call:install_software_o "node-js" "%%downloads_32[1]%%" msi 12 - call:draw_dss - - call:get_path_from_user 41 42 - set "node_js_path=%user_tmp_path%" - Call:draw_dss - - call:install_software_o "ruby" "%%downloads_32[2]%%" exe 13 - call:draw_dss - call:install_software_o "python" "%%downloads_32[3]%%" msi 26 - - :: Some installations require specific windows versions - for /f "tokens=4-5 delims=. " %%i in ('ver') do set VERSION=%%i.%%j - if "%version%" == "5.2" ( call:go_to_platform "XP" ver_XP_32 ) - if "%version%" == "6.0" ( call:go_to_platform "Vista" ver_Vista_32 ) - if "%version%" == "6.1" ( call:go_to_platform "7" ver_Win7_8_32 ) - if "%version%" == "6.2" ( call:go_to_platform "8.0" ver_Win7_8_32 ) - if "%version%" == "6.3" ( call:go_to_platform "8.1" ver_Win7_8_32 ) - GOTO warn_and_exit -GOTO END - -:ver_Win7_8_32 - call:install_packed_software_o "mongo-db" "%%downloads_7_32[1]%%" 25 14 - set "mong-db-path = %packed_software_path%" -goto git_rep_checkout - -:ver_Vista_32 - call:install_packed_software_o "mongo-db" "%%downloads_vista_32[1]%%" 25 14 - set "mong-db-path = %packed_software_path%" -goto git_rep_checkout - -:ver_XP_32 - call:log_lw_ds 15 -goto END - -:ver_Win7_8_64 - call:install_packed_software_o "mongo-db" "%%downloads_7_64[1]%%" 25 14 - set "mong-db-path = %packed_software_path%" -goto git_rep_checkout - -:ver_Vista_64 - call:install_packed_software_o "mongo-db" "%%downloads_vista_64[1]%%" 25 14 - set "mong-db-path = %packed_software_path%" -goto git_rep_checkout - -:ver_XP_64 - call:log_lw_ds 15 -goto END - -:git_rep_checkout - call:log_lw_ss 16 - call:log_lw_sse 17 - - set "PATH=%PATH%;%git_exe_path%\bin;%git_exe_path%\cmd" /M - - call:log_lw 36 - call:log_lw 37 - call:log_lw 38 - - call:draw_dss - - call:get_lw word 39 - set /p git_username="%word% " - - call:draw_dss - - call:get_empty_path_from_user 32 - set "git_repository_path=%user_tmp_path%" - -goto:git_rep_checkout_auto - -:git_rep_checkout_auto - git clone https://github.com/%git_username%/codecombat.git "%git_repository_path%" -goto:git_repo_configuration - -:git_repo_configuration - call:log_lw_ss 35 - call:log_lw_sse 36 - - SET "PATH=%PATH%;%node_js_path%" /M - setx -m git "%git_exe_path%\bin" - - call:log_lw 40 - start cmd /k "npm install -g bower brunch nodemon sendwithus & exit" - -goto report_ok - -:report_ok - call:log_lw 18 - call:log_lw_sse 19 - - :: Open README file - call:open_readme - -goto clean_up - -:open_readme - call:open_txt_file "config/info" -goto:eof - -:warn_and_exit - call:log_lw_ss 20 - call:log_lw_sse 21 -goto error_report - -:error_report - call:log_lw_ds 22 -goto END - -:clean_up - call:log_lw_sse 23 - rmdir %temp-dir% /s /q -goto END - -:: ============================ INSTALL SOFTWARE FUNCTIONS ====================== - -:download_software - call:get_lw word 4 - call:log "%word% %~1..." - %curl-app% -sS -k %~2 -o %temp-dir%\%~1-setup.%~3 -goto:eof - -:install_software - call:download_software %~1 %~2 %~3 - call:get_lw word 5 - call:log "%word% %~1..." - START /WAIT %temp-dir%\%~1-setup.%~3 -goto:eof - -:install_software_o - call:get_lw word %~4 - call:user_yn_question "%word%" - if "%res%"=="true" ( - call:install_software %~1 %~2 %~3 - ) else ( - call:log_lw 10 - ) -goto:eof - -:install_packed_software - call:download_software %~1 %~2 zip - - call:draw_dss - - call:get_lw word %~3 - - set /p packed_software_path="%word% " - - :: remove chosen directory of user if it already exists (to prevent a window from popping up) - IF EXIST %packed_software_path% rmdir %packed_software_path% /s /q - - %ZU-app% x %temp-dir%\%~1-setup.zip -o%packed_software_path% - - call:draw_dss - - for /f "delims=" %%a in ('dir "%packed_software_path%\" /on /ad /b') do @set temp_dir=%%a - for /f "delims=" %%a in ('dir "%packed_software_path%\%temp_dir%\" /on /ad /b') do ( - xcopy %packed_software_path%\%temp_dir% %packed_software_path%\ /S /E - ) - - call:draw_dss - rmdir %packed_software_path%\%temp_dir%\ /s /q -goto:eof - -:user_yn_question - set /p result="%~1 [Y/N]: " - call:draw_dss - set "res=false" - if "%result%"=="N" (set "res=true") - if "%result%"=="n" (set "res=true") -goto:eof - -:strict_user_yn_question - set /p result="%~1 [Y/N]: " - call:draw_dss - set "res=unset" - if "%result%"=="N" (set "res=false") - if "%result%"=="n" (set "res=false") - if "%result%"=="Y" (set "res=true") - if "%result%"=="y" (set "res=true") - - if "%res%"=="unset" ( - call:log "Please answer the question with either Y or N..." - call:draw_dss - call:strict_user_yn_question "%~1" - ) -goto:eof - -:install_packed_software_o - call:get_lw word %~4 - call:user_yn_question "%word%" - if "%res%"=="true" ( - call:install_packed_software %~1 %~2 %~3 - ) else ( - call:log_lw 10 - ) -goto:eof - -:: ===================== USER - INTERACTION - FUNCTIONS ======================== - -:get_path_from_user - call:get_lw word %~1 - set /p user_tmp_path="%word% " - if not exist "%user_tmp_path%" ( - call:log_lw 43 - call:draw_dss - call:get_path_from_user %~1 %~2 - ) -goto:eof - -:get_empty_path_from_user - call:get_lw word %~1 - set /p user_tmp_path="%word% " - if exist "%user_tmp_path%" ( - call:log_lw 33 - call:draw_dss - call:get_path_from_user %~1 - ) -goto:eof - -:: ============================== FUNCTIONS ==================================== - -:log - echo %~1 - echo %~1 >> %install-log% -goto:eof - -:draw_ss - echo. - call:log "-----------------------------------------------------------------------------" - echo. -goto:eof - -:draw_dss - echo. - call:log "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" - echo. -goto:eof - -:draw_seperator - echo. - echo + + + + + + + + - echo. -goto:eof - -:log_ss - call:draw_ss - call:log "%~1" -goto:eof - -:log_sse - call:log "%~1" - call:draw_ss -goto:eof - -:log_ds - call:log_ss "%~1" - call:draw_ss -goto:eof - -:: ============================== IO FUNCTIONS ==================================== - -:open_txt_file - start "" notepad.exe %~1 -goto:eof - -:parse_aa_and_draw - set "file=%~1" - for /f "usebackq tokens=* delims=;" %%a in ("%file%") do ( - echo.%%a - ) -goto:eof - -:parse_file - set "file=%~1" - for /F "usebackq delims=" %%a in ("%file%") do ( - set /A %~3+=1 - call set %~2[%%%~3%%]=%%a - ) -goto:eof - -:parse_file_new - set /A %~3=0 - call:parse_file %~1 %~2 %~3 -goto:eof - -:: ============================== LOCALISATION FUNCTIONS ================ - -:get_lw - call:get_lw_id %~1 %lang_id% %~2 -goto:eof - -:get_lw_id - set /A count = %~2 * %wc% + %~3 + 1 - set "%~1=!languages[%count%]!" -goto:eof - -:log_lw - call:get_lw str %~1 - call:log "%str%" -goto:eof - -:log_lw_prfx - call:get_lw str %~1 - call:log "%~2%str%" -goto:eof - -:log_lw_ss - call:get_lw str %~1 - call:log_ss "%str%" -goto:eof - -:log_lw_ds - call:get_lw str %~1 - call:log_ds "%str%" -goto:eof - -:log_lw_sse - call:get_lw str %~1 - call:log_sse "%str%" -goto:eof - -:: ============================== WINDOWS FUNCTIONS ====================== - -:set_env_var - setx -m %~1 %~2 -goto:eof - -:: ============================== EOF ==================================== - -:END - exit -goto:eof - -endlocal \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/recycle_bin/git-test.bat b/scripts/windows/coco-dev-setup/recycle_bin/git-test.bat deleted file mode 100755 index adee59c3e..000000000 --- a/scripts/windows/coco-dev-setup/recycle_bin/git-test.bat +++ /dev/null @@ -1,50 +0,0 @@ -@echo off -setlocal EnableDelayedExpansion - -:: + Configuraton and installation checklist: -:: 1) cd codecombat -:: 2) npm install -g bower brunch nodemon sendwithus -:: 3) bower install -:: 4) gem install sass -:: 5) npm install -:: 6) brunch -w -:: Extra... @ Fail run npm install - -echo "Moving to your git repository..." -C: -cd C:\CodeCombat - -PAUSE - -SET "PATH=%PATH%;C:\Program Files\Nodejs" /M -setx -m git "C:\Program Files (x86)\Git\bin" -SET "PATH=%PATH%;C:\Program Files (x86)\Git\bin;C:\Program Files (x86)\Git\cmd" /M - -PAUSE - -echo "Installing bower, brunch, nodemon and sendwithus..." -start cmd /k "npm install -g bower brunch nodemon sendwithus & exit" - -PAUSE - -echo "running npm install..." -start cmd /k "npm install & exit" - -PAUSE - -echo "Activating bower install..." -start cmd /k "bower install & PAUSE & exit" - -PAUSE - -echo "Installing sass via gem..." -start cmd /k "install sass & PAUSE & exit" - -PAUSE - -echo "comping repository via brunch..." -start cmd /k "brunch -w & exit" - -PAUSE - -endlocal \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/src/get_category.cpp b/scripts/windows/coco-dev-setup/src/get_category.cpp deleted file mode 100755 index 971c3aefb..000000000 --- a/scripts/windows/coco-dev-setup/src/get_category.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include "stdafx.h" -#include -#include -#include -#include -#include - -#define tstring std::wstring -#define tcout std::wcout - -static const tstring DEF_URL = L"http://www.google.com"; - -int ErrorReport(const tstring & str, int value = 0) -{ - tcout << str.c_str(); - return value; -} - -void GetHashInfo(tstring id, std::vector & info) { - while(id.size() > 0) - { - size_t pos = id.find(L'-'); - - tstring substr = - id.substr(0, pos == tstring::npos ? id.length() : pos); - info.push_back(substr); - - if(pos == tstring::npos) id = L""; - else - { - ++pos; - id = id.substr(pos, id.length() - pos); - } - } -} - -void SetArrayVariable( - const tstring & name, - int id, - const tstring & line - ) -{ - tcout << L"set \""; - tcout << name; - tcout << L"[" << id << "]"; - tcout << L"=" << line; - tcout << L"\"" << std::endl; -} - -void FillArray( - const std::vector & info, - const tstring & name, - const tstring & id_array_name, - const tstring & file, - int & id - ) -{ - if(info.size() == 0) return; - - auto it = info.begin(); - size_t indention = 0; - unsigned int nlc = 0; - - std::wifstream infile(file.c_str(), std::ifstream::in); - - if(!infile) - { - #ifdef _DEBUG - tcout << file.c_str() << std::endl; - tcout << strerror(errno) << std::endl; - #endif - return; - } - - tstring line; - int counter = 1; - while (std::getline(infile, line)) - { - size_t cpos = line.find('['); - if(cpos == tstring::npos) - { - cpos = line.find_first_not_of(L" \t\r\n"); - } - if(nlc++ == 0 || cpos == indention) - { - indention = cpos; - if(it == info.end()) - { - size_t pos = line.find(L'=') + 1; - SetArrayVariable( - name, id, - line.substr(pos, line.size() - pos) - ); - SetArrayVariable( - id_array_name, id++, - line.substr(cpos, pos - 3) - ); - ++counter; - } - else if(line.find(*it) != tstring::npos) - { - ++it; - nlc = 0; - } - } - else if(counter > 1) - { - return; - } - } - - infile.close(); - return; -} - -int _tmain(int argc, _TCHAR* argv[]) -{ - if(argc == 1) - return ErrorReport(L"Please specify a localisation file."); - else if(argc == 2) - return ErrorReport(L"Please specify the name of the array."); - else if(argc == 3) - return ErrorReport(L"Please specify the name of the name-array."); - else if(argc == 4) - return ErrorReport(L"Please specify the counter parameter."); - else if(argc == 5) - return ErrorReport(L"Please specify one or more categories you are looking for."); - - tstring file, name, counter_name, id_array_name; - file = argv[1]; - name = argv[2]; - id_array_name = argv[3]; - counter_name = argv[4]; - int id = 1; - - for(int i = 5 ; i < argc ; ++i) - { - std::vector information; - GetHashInfo(argv[i], information); - FillArray(information, name, id_array_name, file, id); - } - - tcout << L"set \"" << counter_name << L"=" << (id - 1) << L"\""; - - return 0; -} diff --git a/scripts/windows/coco-dev-setup/src/get_extension.cpp b/scripts/windows/coco-dev-setup/src/get_extension.cpp deleted file mode 100755 index f311ac93f..000000000 --- a/scripts/windows/coco-dev-setup/src/get_extension.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "stdafx.h" -#include -#include -#include -#include -#include - -#define tstring std::wstring -#define tcout std::wcout - -int ErrorReport(const tstring & str, int value = 0) -{ - tcout << str.c_str(); - return value; -} - -int _tmain(int argc, _TCHAR* argv[]) -{ - if(argc == 1) - return ErrorReport(L"Please specify a download URL."); - if(argc == 2) - return ErrorReport(L"Please specify a name for your variable."); - - tstring url, name, extension; - url = argv[1]; - name = argv[2]; - - if(url.find(L"exe") != tstring::npos) extension = L"exe"; - else if(url.find(L"msi") != tstring::npos) extension = L"msi"; - else if(url.find(L"zip") != tstring::npos) extension = L"zip"; - - tcout << L"set \"" << name << L"="; - tcout << extension << L"\""; - - return 0; -} \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/src/get_var.cpp b/scripts/windows/coco-dev-setup/src/get_var.cpp deleted file mode 100755 index e69b0be1c..000000000 --- a/scripts/windows/coco-dev-setup/src/get_var.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "stdafx.h" -#include -#include -#include -#include -#include - -#define tstring std::wstring -#define tcout std::wcout - -static const tstring DEF_URL = L"http://www.google.com"; - -int ErrorReport(const tstring & str, int value = 0) -{ - tcout << str.c_str(); - return value; -} - -void GetHashInfo(tstring id, std::vector & info) { - while(id.size() > 0) - { - size_t pos = id.find(L'-'); - - tstring substr = - id.substr(0, pos == tstring::npos ? id.length() : pos); - info.push_back(substr); - - if(pos == tstring::npos) id = L""; - else - { - ++pos; - id = id.substr(pos, id.length() - pos); - } - } -} - -std::wstring GetText(const std::vector & info, const tstring & file) -{ - if(info.size() == 0) return L"Info Size is 0."; - - auto it = info.begin(); - auto last = info.end() - 1; - size_t indention = 0; - unsigned int nlc = 0; - - std::wifstream infile(file.c_str(), std::ifstream::in); - - if(!infile) - { - #ifdef _DEBUG - tcout << file.c_str() << std::endl; - tcout << strerror(errno) << std::endl; - #endif - return L"File couldn't be opened."; - } - - tstring line; - while (std::getline(infile, line)) - { - size_t cpos = line.find('['); - if(nlc++ == 0 || cpos == indention) - { - indention = cpos; - if(line.find(*it) != tstring::npos) - { - if(it == last) - { - size_t pos = line.find(L'=') + 1; - infile.close(); - return line.substr(pos, line.size() - pos); - } - else - { - ++it; - nlc = 0; - } - } - } - } - - infile.close(); - return L"Var couldn't be found."; -} - -int _tmain(int argc, _TCHAR* argv[]) -{ - if(argc == 1) - return ErrorReport(L"Please specify a localisation file."); - else if(argc == 2) - return ErrorReport(L"Please specify the ID you are looking for."); - - tstring file, hash; - file = argv[1]; - hash = argv[2]; - - std::vector information; - GetHashInfo(hash, information); - - size_t size = information.size(); - for(unsigned int i = 0 ; i < size ; ++i) - { - tcout << information[i]; - if(i != size - 1) tcout << L"_"; - } - tcout << L"=" << GetText(information, file); - - return 0; -} diff --git a/server/articles/Article.coffee b/server/articles/Article.coffee index 19a1e3253..626fc779c 100644 --- a/server/articles/Article.coffee +++ b/server/articles/Article.coffee @@ -1,12 +1,11 @@ mongoose = require('mongoose') plugins = require('../plugins/plugins') -ArticleSchema = new mongoose.Schema( - body: String, -) +ArticleSchema = new mongoose.Schema(body: String, {strict:false}) ArticleSchema.plugin(plugins.NamedPlugin) ArticleSchema.plugin(plugins.VersionedPlugin) ArticleSchema.plugin(plugins.SearchablePlugin, {searchable: ['body', 'name']}) +ArticleSchema.plugin(plugins.PatchablePlugin) module.exports = mongoose.model('article', ArticleSchema) diff --git a/server/articles/article_handler.coffee b/server/articles/article_handler.coffee index b519b8b9f..8aa2d26dc 100644 --- a/server/articles/article_handler.coffee +++ b/server/articles/article_handler.coffee @@ -4,8 +4,13 @@ Handler = require('../commons/Handler') ArticleHandler = class ArticleHandler extends Handler modelClass: Article editableProperties: ['body', 'name', 'i18n'] + jsonSchema: require '../../app/schemas/models/article' hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() + hasAccessToDocument: (req, document, method=null) -> + return true if req.method is 'GET' or method is 'get' or req.user?.isAdmin() + return false + module.exports = new ArticleHandler() diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index f38885fd9..81293b4ad 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -3,6 +3,9 @@ mongoose = require('mongoose') Grid = require 'gridfs-stream' errors = require './errors' log = require 'winston' +Patch = require '../patches/Patch' +User = require '../users/User' +sendwithus = require '../sendwithus' PROJECT = {original:1, name:1, version:1, description: 1, slug:1, kind: 1} FETCH_LIMIT = 200 @@ -20,15 +23,14 @@ module.exports = class Handler hasAccessToDocument: (req, document, method=null) -> return true if req.user?.isAdmin() if @modelClass.schema.uses_coco_permissions - return document.hasPermissionsForMethod(req.user, method or req.method) + return document.hasPermissionsForMethod?(req.user, method or req.method) return true formatEntity: (req, document) -> document?.toObject() getEditableProperties: (req, document) -> props = @editableProperties.slice() isBrandNew = req.method is 'POST' and not req.body.original - if isBrandNew - props = props.concat @postEditableProperties + props = props.concat @postEditableProperties if isBrandNew if @modelClass.schema.uses_coco_permissions # can only edit permissions if this is a brand new property, @@ -37,8 +39,8 @@ module.exports = class Handler if isBrandNew or isOwner or req.user?.isAdmin() props.push 'permissions' - if @modelClass.schema.uses_coco_versions - props.push 'commitMessage' + props.push 'commitMessage' if @modelClass.schema.uses_coco_versions + props.push 'allowPatches' if @modelClass.schema.is_patchable props @@ -48,6 +50,7 @@ module.exports = class Handler sendMethodNotAllowed: (res) -> errors.badMethod(res) sendBadInputError: (res, message) -> errors.badInput(res, message) sendDatabaseError: (res, err) -> + return @sendError(res, err.code, err.response) if err.response and err.code log.error "Database error, #{err}" errors.serverError(res, 'Database error, ' + err) @@ -82,6 +85,7 @@ module.exports = class Handler @sendSuccess(res, documents) getById: (req, res, id) -> + # return @sendNotFoundError(res) # for testing return @sendUnauthorizedError(res) unless @hasAccess(req) @getDocumentForIdOrSlug id, (err, document) => @@ -92,8 +96,63 @@ module.exports = class Handler getByRelationship: (req, res, args...) -> # this handler should be overwritten by subclasses + if @modelClass.schema.is_patchable + return @getPatchesFor(req, res, args[0]) if req.route.method is 'get' and args[1] is 'patches' + return @setWatching(req, res, args[0]) if req.route.method is 'put' and args[1] is 'watch' return @sendNotFoundError(res) + getNamesByIDs: (req, res) -> + ids = req.query.ids or req.body.ids + if @modelClass.schema.uses_coco_versions + return @getNamesByOriginals(req, res) + @getPropertiesFromMultipleDocuments res, User, "name", ids + + getNamesByOriginals: (req, res) -> + ids = req.query.ids or req.body.ids + ids = ids.split(',') if _.isString ids + ids = _.uniq ids + + project = {name:1, original:1} + sort = {'version.major':-1, 'version.minor':-1} + + makeFunc = (id) => + (callback) => + criteria = {original:mongoose.Types.ObjectId(id)} + @modelClass.findOne(criteria, project).sort(sort).exec (err, document) -> + return done(err) if err + callback(null, document?.toObject() or null) + + funcs = {} + for id in ids + return errors.badInput(res, "Given an invalid id: #{id}") unless Handler.isID(id) + funcs[id] = makeFunc(id) + + async.parallel funcs, (err, results) -> + return errors.serverError err if err + res.send (d for d in _.values(results) when d) + res.end() + + getPatchesFor: (req, res, id) -> + query = { 'target.original': mongoose.Types.ObjectId(id), status: req.query.status or 'pending' } + Patch.find(query).sort('-created').exec (err, patches) => + return @sendDatabaseError(res, err) if err + patches = (patch.toObject() for patch in patches) + @sendSuccess(res, patches) + + setWatching: (req, res, id) -> + @getDocumentForIdOrSlug id, (err, document) => + return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, document, 'get') + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless document? + watchers = document.get('watchers') or [] + me = req.user.get('_id') + watchers = (l for l in watchers when not l.equals(me)) + watchers.push me if req.body.on and req.body.on isnt 'false' + document.set 'watchers', watchers + document.save (err, document) => + return @sendDatabaseError(res, err) if err + @sendSuccess(res, @formatEntity(req, document)) + search: (req, res) -> unless @modelClass.schema.uses_coco_search return @sendNotFoundError(res) @@ -141,8 +200,6 @@ module.exports = class Handler aggregate = $match: query @modelClass.aggregate(aggregate).project(selectString).limit(FETCH_LIMIT).sort(sort).exec (err, results) => return @sendDatabaseError(res, err) if err - for doc in results - return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, doc) res.send(results) res.end() @@ -203,12 +260,14 @@ module.exports = class Handler return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendBadInputError(res, 'id should not be included.') if req.body._id return @sendUnauthorizedError(res) unless @hasAccess(req) - validation = @validateDocumentInput(req.body) - return @sendBadInputError(res, validation.errors) unless validation.valid document = @makeNewInstance(req) @saveChangesToDocument req, document, (err) => + return @sendBadInputError(res, err.errors) if err?.valid is false return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) + @onPostSuccess(req, document) + + onPostSuccess: (req, doc) -> ### TODO: think about pulling some common stuff out of postFirstVersion/postNewVersion @@ -220,13 +279,11 @@ module.exports = class Handler return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendBadInputError(res, 'id should not be included.') if req.body._id return @sendUnauthorizedError(res) unless @hasAccess(req) - validation = @validateDocumentInput(req.body) - return @sendBadInputError(res, validation.errors) unless validation.valid document = @makeNewInstance(req) document.set('original', document._id) document.set('creator', req.user._id) @saveChangesToDocument req, document, (err) => - return @sendBadInputError(res, err.response) if err?.response + return @sendBadInputError(res, err.errors) if err?.valid is false return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) @@ -245,8 +302,6 @@ module.exports = class Handler return @sendBadInputError(res, 'This entity is not versioned') unless @modelClass.schema.uses_coco_versions return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendUnauthorizedError(res) unless @hasAccess(req) - validation = @validateDocumentInput(req.body) - return @sendBadInputError(res, validation.errors) unless validation.valid @getDocumentForIdOrSlug req.body._id, (err, parentDocument) => return @sendBadInputError(res, 'Bad id.') if err and err.name is 'CastError' return @sendDatabaseError(res, err) if err @@ -261,6 +316,8 @@ module.exports = class Handler delete updatedObject[prop] delete updatedObject._id major = req.body.version?.major + validation = @validateDocumentInput(updatedObject) + return @sendBadInputError(res, validation.errors) unless validation.valid done = (err, newDocument) => return @sendDatabaseError(res, err) if err @@ -268,6 +325,7 @@ module.exports = class Handler newDocument.save (err) => return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, newDocument)) + @notifyWatchersOfChange(req.user, newDocument, req.body.editPath) if @modelClass.schema.is_patchable if major? parentDocument.makeNewMinorVersion(updatedObject, major, done) @@ -275,15 +333,38 @@ module.exports = class Handler else parentDocument.makeNewMajorVersion(updatedObject, done) + notifyWatchersOfChange: (editor, changedDocument, editPath) -> + watchers = changedDocument.get('watchers') or [] + watchers = (w for w in watchers when not w.equals(editor.get('_id'))) + return unless watchers.length + User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) => + for watcher in watchers + @notifyWatcherOfChange editor, watcher, changedDocument, editPath + + notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) -> + context = + email_id: sendwithus.templates.change_made_notify_watcher + recipient: + address: watcher.get('email') + name: watcher.get('name') + email_data: + doc_name: changedDocument.get('name') or '???' + submitter_name: editor.get('name') or '???' + doc_link: if editPath then "http://codecombat.com#{editPath}" else null + commit_message: changedDocument.get('commitMessage') + sendwithus.api.send context, (err, result) -> + makeNewInstance: (req) -> - new @modelClass({}) + model = new @modelClass({}) + model.set 'watchers', [req.user.get('_id')] if @modelClass.schema.is_patchable + model validateDocumentInput: (input) -> tv4 = require('tv4').tv4 res = tv4.validateMultiple(input, @jsonSchema) res - @isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-z0-9]/gi)?.length is 24 + @isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24 getDocumentForIdOrSlug: (idOrSlug, done) -> idOrSlug = idOrSlug+'' @@ -324,3 +405,17 @@ module.exports = class Handler return done(validation) unless validation.valid document.save (err) -> done(err) + + getPropertiesFromMultipleDocuments: (res, model, properties, ids) -> + query = model.find() + ids = ids.split(',') if _.isString ids + ids = _.uniq ids + for id in ids + return errors.badInput(res, "Given an invalid id: #{id}") unless Handler.isID(id) + query.where({'_id': { $in: ids} }) + query.select(properties).exec (err, documents) -> + dict = {} + _.each documents, (document) -> + dict[document.id] = document + res.send dict + res.end() diff --git a/server/commons/database.coffee b/server/commons/database.coffee index 6356fe7dd..d664725c0 100644 --- a/server/commons/database.coffee +++ b/server/commons/database.coffee @@ -15,7 +15,7 @@ module.exports.connect = () -> module.exports.generateMongoConnectionString = -> - if config.mongo.mongoose_replica_string + if not testing and config.mongo.mongoose_replica_string address = config.mongo.mongoose_replica_string else dbName = config.mongo.db @@ -25,4 +25,4 @@ module.exports.generateMongoConnectionString = -> address = config.mongo.username + ":" + config.mongo.password + "@" + address address = "mongodb://#{address}/#{dbName}" - return address \ No newline at end of file + return address diff --git a/server/commons/mail.coffee b/server/commons/mail.coffee index 117f10bd7..96bb56e2d 100644 --- a/server/commons/mail.coffee +++ b/server/commons/mail.coffee @@ -2,14 +2,10 @@ config = require '../../server_config' module.exports.MAILCHIMP_LIST_ID = 'e9851239eb' module.exports.MAILCHIMP_GROUP_ID = '4529' -module.exports.MAILCHIMP_GROUP_MAP = - announcement: 'Announcements' - tester: 'Adventurers' - level_creator: 'Artisans' - developer: 'Archmages' - article_editor: 'Scribes' - translator: 'Diplomats' - support: 'Ambassadors' + +# these two need to be parallel +module.exports.MAILCHIMP_GROUPS = ['Announcements', 'Adventurers', 'Artisans', 'Archmages', 'Scribes', 'Diplomats', 'Ambassadors'] +module.exports.NEWS_GROUPS = ['generalNews', 'adventurerNews', 'artisanNews', 'archmageNews', 'scribeNews', 'diplomatNews', 'ambassadorNews'] nodemailer = require 'nodemailer' module.exports.transport = nodemailer.createTransport "SMTP", diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 2f659811b..3cfcc2164 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -6,23 +6,10 @@ module.exports.handlers = 'level_feedback': 'levels/feedbacks/level_feedback_handler' 'level_session': 'levels/sessions/level_session_handler' 'level_system': 'levels/systems/level_system_handler' + 'patch': 'patches/patch_handler' 'thang_type': 'levels/thangs/thang_type_handler' 'user': 'users/user_handler' -module.exports.schemas = - 'article': 'articles/article_schema' - 'common': 'commons/schemas' - 'i18n': 'commons/i18n_schema' - 'level': 'levels/level_schema' - 'level_component': 'levels/components/level_component_schema' - 'level_feedback': 'levels/feedbacks/level_feedback_schema' - 'level_session': 'levels/sessions/level_session_schema' - 'level_system': 'levels/systems/level_system_schema' - 'metaschema': 'commons/metaschema' - 'thang_component': 'levels/thangs/thang_component_schema' - 'thang_type': 'levels/thangs/thang_type_schema' - 'user': 'users/user_schema' - module.exports.routes = [ 'routes/auth' diff --git a/server/commons/queue.coffee b/server/commons/queue.coffee index 64b71ae64..0703427d4 100644 --- a/server/commons/queue.coffee +++ b/server/commons/queue.coffee @@ -250,7 +250,7 @@ class MongoQueue extends events.EventEmitter @emit 'error',err,data else @emit 'update',err,data - log.info "The message visibility time was updated" + #log.info "The message visibility time was updated" callback? err, data diff --git a/server/levels/Level.coffee b/server/levels/Level.coffee index c61245ed5..9cadeac7b 100644 --- a/server/levels/Level.coffee +++ b/server/levels/Level.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../plugins/plugins') -jsonschema = require('./level_schema') +jsonschema = require('../../app/schemas/models/level') LevelSchema = new mongoose.Schema({ description: String @@ -10,6 +10,7 @@ LevelSchema.plugin(plugins.NamedPlugin) LevelSchema.plugin(plugins.PermissionsPlugin) LevelSchema.plugin(plugins.VersionedPlugin) LevelSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description']}) +LevelSchema.plugin(plugins.PatchablePlugin) LevelSchema.pre 'init', (next) -> return next() unless jsonschema.properties? diff --git a/server/levels/components/LevelComponent.coffee b/server/levels/components/LevelComponent.coffee index 3dc373be1..6c1a58370 100644 --- a/server/levels/components/LevelComponent.coffee +++ b/server/levels/components/LevelComponent.coffee @@ -1,16 +1,17 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_component_schema') +jsonschema = require('../../../app/schemas/models/level_component') LevelComponentSchema = new mongoose.Schema { description: String system: String }, {strict: false} -LevelComponentSchema.plugin(plugins.NamedPlugin) -LevelComponentSchema.plugin(plugins.PermissionsPlugin) -LevelComponentSchema.plugin(plugins.VersionedPlugin) -LevelComponentSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description', 'system']}) +LevelComponentSchema.plugin plugins.NamedPlugin +LevelComponentSchema.plugin plugins.PermissionsPlugin +LevelComponentSchema.plugin plugins.VersionedPlugin +LevelComponentSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description', 'system']} +LevelComponentSchema.plugin plugins.PatchablePlugin LevelComponentSchema.pre 'init', (next) -> return next() unless jsonschema.properties? diff --git a/server/levels/components/level_component_handler.coffee b/server/levels/components/level_component_handler.coffee index 89a3ea21c..3bcc572d0 100644 --- a/server/levels/components/level_component_handler.coffee +++ b/server/levels/components/level_component_handler.coffee @@ -3,6 +3,7 @@ Handler = require('../../commons/Handler') LevelComponentHandler = class LevelComponentHandler extends Handler modelClass: LevelComponent + jsonSchema: require '../../../app/schemas/models/level_component' editableProperties: [ 'system' 'description' diff --git a/server/levels/feedbacks/LevelFeedback.coffee b/server/levels/feedbacks/LevelFeedback.coffee index 0eecdec32..5fef6a567 100644 --- a/server/levels/feedbacks/LevelFeedback.coffee +++ b/server/levels/feedbacks/LevelFeedback.coffee @@ -2,7 +2,7 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_feedback_schema') +jsonschema = require('../../../app/schemas/models/level_feedback') LevelFeedbackSchema = new mongoose.Schema({ created: diff --git a/server/levels/feedbacks/level_feedback_handler.coffee b/server/levels/feedbacks/level_feedback_handler.coffee index 5cb8be50b..58d268db1 100644 --- a/server/levels/feedbacks/level_feedback_handler.coffee +++ b/server/levels/feedbacks/level_feedback_handler.coffee @@ -4,6 +4,7 @@ Handler = require('../../commons/Handler') class LevelFeedbackHandler extends Handler modelClass: LevelFeedback editableProperties: ['rating', 'review', 'level', 'levelID', 'levelName'] + jsonSchema: require '../../../app/schemas/models/level_feedback' makeNewInstance: (req) -> feedback = super(req) diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index ad26fe0e1..27c5f0042 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -5,9 +5,10 @@ SessionHandler = require('./sessions/level_session_handler') Feedback = require('./feedbacks/LevelFeedback') Handler = require('../commons/Handler') mongoose = require('mongoose') - +async = require 'async' LevelHandler = class LevelHandler extends Handler modelClass: Level + jsonSchema: require '../../app/schemas/models/level' editableProperties: [ 'description' 'documentation' @@ -37,8 +38,8 @@ LevelHandler = class LevelHandler extends Handler return @getLeaderboardFacebookFriends(req, res, args[0]) if args[1] is 'leaderboard_facebook_friends' return @getLeaderboardGPlusFriends(req, res, args[0]) if args[1] is 'leaderboard_gplus_friends' return @getHistogramData(req, res, args[0]) if args[1] is 'histogram_data' - - return @sendNotFoundError(res) + return @checkExistence(req, res, args[0]) if args[1] is 'exists' + super(arguments...) fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> @getDocumentForIdOrSlug id, (err, level) => @@ -62,7 +63,7 @@ LevelHandler = class LevelHandler extends Handler # TODO: generalize this for levels based on their teams else if level.get('type') is 'ladder' sessionQuery.team = 'humans' - + Session.findOne(sessionQuery).exec (err, doc) => return @sendDatabaseError(res, err) if err return @sendSuccess(res, doc) if doc? @@ -87,6 +88,7 @@ LevelHandler = class LevelHandler extends Handler access:'write' } ] + initVals.codeLanguage = req.user.get('aceConfig')?.language ? 'javascript' session = new Session(initVals) session.save (err) => @@ -106,7 +108,7 @@ LevelHandler = class LevelHandler extends Handler query = Level.findOne(findParameters) .select(selectString) .lean() - + query.exec (err, level) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless level? @@ -115,22 +117,38 @@ LevelHandler = class LevelHandler extends Handler original: level.original.toString() majorVersion: level.version.major creator: req.user._id+'' - + query = Session.find(sessionQuery).select('-screenshot') query.exec (err, results) => if err then @sendDatabaseError(res, err) else @sendSuccess res, results - + getHistogramData: (req, res,slug) -> query = Session.aggregate [ {$match: {"levelID":slug, "submitted": true, "team":req.query.team}} {$project: {totalScore: 1, _id: 0}} ] - + query.exec (err, data) => if err? then return @sendDatabaseError res, err valueArray = _.pluck data, "totalScore" @sendSuccess res, valueArray - + + checkExistence: (req, res, slugOrID) -> + findParameters = {} + if Handler.isID slugOrID + findParameters["_id"] = slugOrID + else + findParameters["slug"] = slugOrID + selectString = 'original version.major permissions' + query = Level.findOne(findParameters) + .select(selectString) + .lean() + + query.exec (err, level) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless level? + res.send({"exists":true}) + res.end() getLeaderboard: (req, res, id) -> sessionsQueryParameters = @makeLeaderboardQueryParameters(req, id) @@ -138,7 +156,7 @@ LevelHandler = class LevelHandler extends Handler sortParameters = "totalScore": req.query.order selectProperties = ['totalScore', 'creatorName', 'creator'] - + query = Session .find(sessionsQueryParameters) .limit(req.query.limit) @@ -203,7 +221,7 @@ LevelHandler = class LevelHandler extends Handler userMap[u._id] = u[serviceProperty] for u in userResults session[serviceProperty] = userMap[session.creator] for session in sessionResults res.send(sessionResults) - + getRandomSessionPair: (req, res, slugOrID) -> findParameters = {} if Handler.isID slugOrID @@ -218,36 +236,34 @@ LevelHandler = class LevelHandler extends Handler query.exec (err, level) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless level? - + sessionsQueryParameters = level: original: level.original.toString() majorVersion: level.version.major submitted:true - - console.log sessionsQueryParameters - - - query = Session - .find(sessionsQueryParameters) - .select('team') - .lean() - - query.exec (err, resultSessions) => - return @sendDatabaseError res, err if err? or not resultSessions - - teamSessions = _.groupBy resultSessions, 'team' - console.log teamSessions - sessions = [] - numberOfTeams = 0 - for team of teamSessions - numberOfTeams += 1 - sessions.push _.sample(teamSessions[team]) - if numberOfTeams != 2 then return @sendDatabaseError res, "There aren't sessions of 2 teams, so cannot choose random opponents!" - - @sendSuccess res, sessions - - + + query = Session.find(sessionsQueryParameters).distinct("team") + query.exec (err, teams) => + return @sendDatabaseError res, err if err? or not teams + findTop20Players = (sessionQueryParams, team, cb) -> + sessionQueryParams["team"] = team + Session.aggregate [ + {$match: sessionQueryParams} + {$project: {"totalScore":1}} + {$sort: {"totalScore":-1}} + {$limit: 20} + ], cb + + async.map teams, findTop20Players.bind(@, sessionsQueryParameters), (err, map) => + if err? then return @sendDatabaseError(res, err) + sessions = [] + for mapItem in map + sessions.push _.sample(mapItem) + if map.length != 2 then return @sendDatabaseError res, "There aren't sessions of 2 teams, so cannot choose random opponents!" + @sendSuccess res, sessions + + getFeedback: (req, res, id) -> return @sendNotFoundError(res) unless req.user @fetchLevelByIDAndHandleErrors id, req, res, (err, level) => diff --git a/server/levels/sessions/LevelSession.coffee b/server/levels/sessions/LevelSession.coffee index 952782f1b..c30519ba0 100644 --- a/server/levels/sessions/LevelSession.coffee +++ b/server/levels/sessions/LevelSession.coffee @@ -2,7 +2,7 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_session_schema') +jsonschema = require('../../../app/schemas/models/level_session') LevelSessionSchema = new mongoose.Schema({ created: diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee index ca8680a17..1c03a17f6 100644 --- a/server/levels/sessions/level_session_handler.coffee +++ b/server/levels/sessions/level_session_handler.coffee @@ -6,14 +6,22 @@ TIMEOUT = 1000 * 30 # no activity for 30 seconds means it's not active class LevelSessionHandler extends Handler modelClass: LevelSession - editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state', + editableProperties: ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state', 'levelName', 'creatorName', 'levelID', 'screenshot', - 'chat', 'teamSpells', 'submitted', 'unsubscribed'] + 'chat', 'teamSpells', 'submitted', 'unsubscribed','playtime'] + jsonSchema: require '../../../app/schemas/models/level_session' getByRelationship: (req, res, args...) -> return @getActiveSessions req, res if args.length is 2 and args[1] is 'active' - return @sendNotFoundError(res) - + super(arguments...) + + formatEntity: (req, document) -> + documentObject = super(req, document) + if req.user.isAdmin() or req.user.id is document.creator + return documentObject + else + return _.omit documentObject, ['submittedCode','code'] + getActiveSessions: (req, res) -> return @sendUnauthorizedError(res) unless req.user.isAdmin() start = new Date() diff --git a/server/levels/systems/LevelSystem.coffee b/server/levels/systems/LevelSystem.coffee index cf21f7355..f945aaa95 100644 --- a/server/levels/systems/LevelSystem.coffee +++ b/server/levels/systems/LevelSystem.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_system_schema') +jsonschema = require('../../../app/schemas/models/level_system') LevelSystemSchema = new mongoose.Schema { description: String @@ -10,6 +10,7 @@ LevelSystemSchema.plugin(plugins.NamedPlugin) LevelSystemSchema.plugin(plugins.PermissionsPlugin) LevelSystemSchema.plugin(plugins.VersionedPlugin) LevelSystemSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description']}) +LevelSystemSchema.plugin(plugins.PatchablePlugin) LevelSystemSchema.pre 'init', (next) -> return next() unless jsonschema.properties? diff --git a/server/levels/systems/level_system_handler.coffee b/server/levels/systems/level_system_handler.coffee index 1b1e511c1..bf1bb39d5 100644 --- a/server/levels/systems/level_system_handler.coffee +++ b/server/levels/systems/level_system_handler.coffee @@ -13,6 +13,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler 'configSchema' ] postEditableProperties: ['name'] + jsonSchema: require '../../../app/schemas/models/level_system' getEditableProperties: (req, document) -> props = super(req, document) diff --git a/server/levels/thangs/ThangType.coffee b/server/levels/thangs/ThangType.coffee index 92915e8d0..292597719 100644 --- a/server/levels/thangs/ThangType.coffee +++ b/server/levels/thangs/ThangType.coffee @@ -5,8 +5,9 @@ ThangTypeSchema = new mongoose.Schema({ body: String, }, {strict: false}) -ThangTypeSchema.plugin(plugins.NamedPlugin) -ThangTypeSchema.plugin(plugins.VersionedPlugin) -ThangTypeSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']}) +ThangTypeSchema.plugin plugins.NamedPlugin +ThangTypeSchema.plugin plugins.VersionedPlugin +ThangTypeSchema.plugin plugins.SearchablePlugin, {searchable: ['name']} +ThangTypeSchema.plugin plugins.PatchablePlugin module.exports = mongoose.model('thang.type', ThangTypeSchema) diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index a446b56be..a8d5c05e7 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -3,22 +3,24 @@ Handler = require('../../commons/Handler') ThangTypeHandler = class ThangTypeHandler extends Handler modelClass: ThangType + jsonSchema: require '../../../app/schemas/models/thang_type' editableProperties: [ - 'name', - 'raw', - 'actions', - 'soundTriggers', - 'rotationType', - 'matchWorldDimensions', - 'shadow', - 'layerPriority', - 'staticImage', - 'scale', - 'positions', - 'snap', - 'components', - 'colorGroups', + 'name' + 'raw' + 'actions' + 'soundTriggers' + 'rotationType' + 'matchWorldDimensions' + 'shadow' + 'layerPriority' + 'staticImage' + 'scale' + 'positions' + 'snap' + 'components' + 'colorGroups' 'kind' + 'raster' ] hasAccess: (req) -> diff --git a/server/patches/Patch.coffee b/server/patches/Patch.coffee new file mode 100644 index 000000000..3e12638cf --- /dev/null +++ b/server/patches/Patch.coffee @@ -0,0 +1,48 @@ +mongoose = require('mongoose') +{handlers} = require '../commons/mapping' + +PatchSchema = new mongoose.Schema({}, {strict: false}) + +PatchSchema.pre 'save', (next) -> + return next() unless @isNew # patch can't be altered after creation, so only need to check data once + target = @get('target') + targetID = target.id + Handler = require '../commons/Handler' + if not Handler.isID(targetID) + err = new Error('Invalid input.') + err.response = {message:"isn't a MongoDB id.", property:'target.id'} + err.code = 422 + return next(err) + + collection = target.collection + handler = require('../' + handlers[collection]) + handler.getDocumentForIdOrSlug targetID, (err, document) => + if err + err = new Error('Server error.') + err.response = {message:'', property:'target.id'} + err.code = 500 + return next(err) + + if not document + err = new Error('Target of patch not found.') + err.response = {message:'was not found.', property:'target.id'} + err.code = 404 + return next(err) + + target.id = document.get('_id') + if handler.modelClass.schema.uses_coco_versions + target.original = document.get('original') + version = document.get('version') + target.version = _.pick document.get('version'), 'major', 'minor' + @set('target', target) + else + target.original = targetID + + patches = document.get('patches') or [] + patches = _.clone patches + patches.push @_id + document.set 'patches', patches, {strict: false} + @targetLoaded = document + document.save (err) -> next(err) + +module.exports = mongoose.model('patch', PatchSchema) diff --git a/server/patches/patch_handler.coffee b/server/patches/patch_handler.coffee new file mode 100644 index 000000000..7d30ce353 --- /dev/null +++ b/server/patches/patch_handler.coffee @@ -0,0 +1,81 @@ +Patch = require('./Patch') +User = require '../users/User' +Handler = require('../commons/Handler') +schema = require '../../app/schemas/models/patch' +{handlers} = require '../commons/mapping' +mongoose = require('mongoose') +log = require 'winston' +sendwithus = require '../sendwithus' + +PatchHandler = class PatchHandler extends Handler + modelClass: Patch + editableProperties: [] + postEditableProperties: ['delta', 'target', 'commitMessage'] + jsonSchema: require '../../app/schemas/models/patch' + + makeNewInstance: (req) -> + patch = super(req) + patch.set 'creator', req.user._id + patch.set 'created', new Date().toISOString() + patch.set 'status', 'pending' + patch + + getByRelationship: (req, res, args...) -> + return @setStatus(req, res, args[0]) if req.route.method is 'put' and args[1] is 'status' + super(arguments...) + + setStatus: (req, res, id) -> + newStatus = req.body.status + unless newStatus in ['rejected', 'accepted', 'withdrawn'] + return @sendBadInputError(res, "Status must be 'rejected', 'accepted', or 'withdrawn'") + + @getDocumentForIdOrSlug id, (err, patch) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless patch? + targetInfo = patch.get('target') + targetHandler = require('../' + handlers[targetInfo.collection]) + targetModel = targetHandler.modelClass + + query = { 'original': targetInfo.original } + sort = { 'version.major': -1, 'version.minor': -1 } + targetModel.findOne(query).sort(sort).exec (err, target) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless target? + return @sendUnauthorizedError(res) unless targetHandler.hasAccessToDocument(req, target, 'get') + + if newStatus in ['rejected', 'accepted'] + return @sendUnauthorizedError(res) unless targetHandler.hasAccessToDocument(req, target, 'put') + + if newStatus is 'withdrawn' + return @sendUnauthorizedError(res) unless req.user.get('_id').equals patch.get('creator') + + # these require callbacks + patch.update {$set:{status:newStatus}}, {}, -> + target.update {$pull:{patches:patch.get('_id')}}, {}, -> + @sendSuccess(res, null) + + onPostSuccess: (req, doc) -> + log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded + return unless doc.targetLoaded + watchers = doc.targetLoaded.get('watchers') or [] + watchers = (w for w in watchers when not w.equals(req.user.get('_id'))) + return unless watchers?.length + User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) => + for watcher in watchers + @sendPatchCreatedEmail req.user, watcher, doc, doc.targetLoaded, req.body.editPath + + sendPatchCreatedEmail: (patchCreator, watcher, patch, target, editPath) -> +# return if watcher._id is patchCreator._id + context = + email_id: sendwithus.templates.patch_created + recipient: + address: watcher.get('email') + name: watcher.get('name') + email_data: + doc_name: target.get('name') or '???' + submitter_name: patchCreator.get('name') or '???' + doc_link: "http://codecombat.com#{editPath}" + commit_message: patch.get('commitMessage') + sendwithus.api.send context, (err, result) -> + +module.exports = new PatchHandler() diff --git a/server/plugins/plugins.coffee b/server/plugins/plugins.coffee index f1f224b82..4b189967e 100644 --- a/server/plugins/plugins.coffee +++ b/server/plugins/plugins.coffee @@ -2,7 +2,14 @@ mongoose = require('mongoose') User = require('../users/User') textSearch = require('mongoose-text-search') +module.exports.PatchablePlugin = (schema) -> + schema.is_patchable = true + schema.index({'target.original':1, 'status':'1', 'created':-1}) + +RESERVED_NAMES = ['search', 'names'] + module.exports.NamedPlugin = (schema) -> + schema.uses_coco_names = true schema.add({name: String, slug: String}) schema.index({'slug': 1}, {unique: true, sparse: true, name: 'slug index'}) @@ -12,6 +19,11 @@ module.exports.NamedPlugin = (schema) -> return next() unless v.isLatestMajor and v.isLatestMinor newSlug = _.str.slugify(@get('name')) + if newSlug in RESERVED_NAMES + err = new Error('Reserved name.') + err.response = {message:' is a reserved name', property: 'name'} + err.code = 422 + return next(err) if newSlug isnt @get('slug') @set('slug', newSlug) @checkSlugConflicts(next) @@ -22,13 +34,11 @@ module.exports.NamedPlugin = (schema) -> schema.methods.checkSlugConflicts = (done) -> slug = @get('slug') - try - id = mongoose.Types.ObjectId.createFromHexString(slug) + if slug.length is 24 and slug.match(/[a-f0-9]/gi)?.length is 24 err = new Error('Bad name.') - err.response = {message:'cannot be like a MondoDB id, Mr Hacker.', property:'name'} + err.response = {message: 'cannot be like a MongoDB ID, Mr. Hacker.', property: 'name'} err.code = 422 done(err) - catch e query = { slug:slug } diff --git a/server/queues/scoring.coffee b/server/queues/scoring.coffee index cd4670708..149afa959 100644 --- a/server/queues/scoring.coffee +++ b/server/queues/scoring.coffee @@ -36,7 +36,7 @@ module.exports.messagesInQueueCount = (req, res) -> module.exports.addPairwiseTaskToQueueFromRequest = (req, res) -> taskPair = req.body.sessions - addPairwiseTaskToQueue req.body.sessions (err, success) -> + addPairwiseTaskToQueue req.body.sessions, (err, success) -> if err? then return errors.serverError res, "There was an error adding pairwise tasks: #{err}" sendResponseObject req, res, {"message":"All task pairs were succesfully sent to the queue"} @@ -55,6 +55,7 @@ addPairwiseTaskToQueue = (taskPair, cb) -> if taskPairError? then return cb taskPairError cb null +# We should rip these out, probably module.exports.resimulateAllSessions = (req, res) -> unless isUserAdmin req then return errors.unauthorized res, "Unauthorized. Even if you are authorized, you shouldn't do this" @@ -99,21 +100,145 @@ resimulateSession = (originalLevelID, levelMajorVersion, session, cb) => if taskPairError? then return cb taskPairError, null cb null +selectRandomSkipIndex = (numberOfSessions) -> + numbers = [0...numberOfSessions] + numberWeights = [] + lambda = 0.025 + + for number, index in numbers + numberWeights[index] = lambda*Math.exp(-1*lambda*number) + lambda/(numberOfSessions/15) + sum = numberWeights.reduce (a, b) -> a + b + + for number,index in numberWeights + numberWeights[index] /= sum + + rand = (min, max) -> Math.random() * (max - min) + min + + totalWeight = 1 + randomNumber = Math.random() + weightSum = 0 + + for number, i in numbers + weightSum += numberWeights[i] + + if (randomNumber <= weightSum) + return numbers[i] + +module.exports.getTwoGames = (req, res) -> + #if userIsAnonymous req then return errors.unauthorized(res, "You need to be logged in to get games.") + humansGameID = req.body.humansGameID + ogresGameID = req.body.ogresGameID + + unless ogresGameID and humansGameID + #fetch random games here + queryParams = + "levelID":"greed" + "submitted":true + "team":"humans" + selection = "team totalScore transpiledCode teamSpells levelID creatorName creator submitDate" + LevelSession.count queryParams, (err, numberOfHumans) => + if err? then return errors.serverError(res, "Couldn't get the number of human games") + humanSkipCount = selectRandomSkipIndex(numberOfHumans) + ogreCountParams = + "levelID": "greed" + "submitted":true + "team":"ogres" + LevelSession.count ogreCountParams, (err, numberOfOgres) => + if err? then return errors.serverError(res, "Couldnt' get the number of ogre games") + ogresSkipCount = selectRandomSkipIndex(numberOfOgres) + + query = LevelSession + .aggregate() + .match(queryParams) + .project(selection) + .sort({"submitDate": -1}) + .skip(humanSkipCount) + .limit(1) + query.exec (err, randomSession) => + if err? then return errors.serverError(res, "Couldn't select a random session! #{err}") + randomSession = randomSession[0] + queryParams = + "levelID":"greed" + "submitted":true + "team": "ogres" + query = LevelSession + .aggregate() + .match(queryParams) + .project(selection) + .sort({"submitDate": -1}) + .skip(ogresSkipCount) + .limit(1) + query.exec (err, otherSession) => + if err? then return errors.serverError(res, "Couldnt' select the other random session!") + otherSession = otherSession[0] + taskObject = + "messageGenerated": Date.now() + "sessions": [] + for session in [randomSession, otherSession] + sessionInformation = + "sessionID": session._id + "team": session.team ? "No team" + "transpiledCode": session.transpiledCode + "teamSpells": session.teamSpells ? {} + "levelID": session.levelID + "creatorName": session.creatorName + "creator": session.creator + "totalScore": session.totalScore + taskObject.sessions.push sessionInformation + console.log "Dispatching random game between", taskObject.sessions[0].creatorName, "and", taskObject.sessions[1].creatorName + sendResponseObject req, res, taskObject + else + console.log "Directly simulating #{humansGameID} vs. #{ogresGameID}." + LevelSession.findOne(_id: humansGameID).lean().exec (err, humanSession) => + if err? then return errors.serverError(res, "Couldn't find the human game") + LevelSession.findOne(_id: ogresGameID).lean().exec (err, ogreSession) => + if err? then return errors.serverError(res, "Couldn't find the ogre game") + taskObject = + "messageGenerated": Date.now() + "sessions": [] + for session in [humanSession, ogreSession] + sessionInformation = + "sessionID": session._id + "team": session.team ? "No team" + "transpiledCode": session.transpiledCode + "teamSpells": session.teamSpells ? {} + "levelID": session.levelID + + taskObject.sessions.push sessionInformation + sendResponseObject req, res, taskObject + +module.exports.recordTwoGames = (req, res) -> + sessions = req.body.sessions + console.log "Recording non-chained result of", sessions?[0]?.name, sessions[0]?.metrics?.rank, "and", sessions?[1]?.name, sessions?[1]?.metrics?.rank + + yetiGuru = clientResponseObject: req.body, isRandomMatch: true + async.waterfall [ + fetchLevelSession.bind(yetiGuru) + updateSessions.bind(yetiGuru) + indexNewScoreArray.bind(yetiGuru) + addMatchToSessions.bind(yetiGuru) + updateUserSimulationCounts.bind(yetiGuru, req.user._id) + ], (err, successMessageObject) -> + if err? then return errors.serverError res, "There was an error recording the single game:#{err}" + sendResponseObject req, res, {"message":"The single game was submitted successfully!"} + + module.exports.createNewTask = (req, res) -> requestSessionID = req.body.session originalLevelID = req.body.originalLevelID currentLevelID = req.body.levelID + transpiledCode = req.body.transpiledCode requestLevelMajorVersion = parseInt(req.body.levelMajorVersion) + yetiGuru = {} async.waterfall [ - validatePermissions.bind(@,req,requestSessionID) - fetchAndVerifyLevelType.bind(@,currentLevelID) - fetchSessionObjectToSubmit.bind(@, requestSessionID) - updateSessionToSubmit - fetchInitialSessionsToRankAgainst.bind(@, requestLevelMajorVersion, originalLevelID) + validatePermissions.bind(yetiGuru,req,requestSessionID) + fetchAndVerifyLevelType.bind(yetiGuru,currentLevelID) + fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID) + updateSessionToSubmit.bind(yetiGuru, transpiledCode) + fetchInitialSessionsToRankAgainst.bind(yetiGuru, requestLevelMajorVersion, originalLevelID) generateAndSendTaskPairsToTheQueue - ], (err, successMessageObject) -> if err? then return errors.serverError res, "There was an error submitting the game to the queue:#{err}" sendResponseObject req, res, successMessageObject @@ -164,14 +289,15 @@ fetchSessionObjectToSubmit = (sessionID, callback) -> query.exec (err, session) -> callback err, session?.toObject() -updateSessionToSubmit = (sessionToUpdate, callback) -> +updateSessionToSubmit = (transpiledCode, sessionToUpdate, callback) -> sessionUpdateObject = submitted: true submittedCode: sessionToUpdate.code + transpiledCode: transpiledCode submitDate: new Date() - meanStrength: 25 + #meanStrength: 25 # Let's try not resetting the score on resubmission standardDeviation: 25/3 - totalScore: 10 + #totalScore: 10 # Let's try not resetting the score on resubmission numberOfWinsAndTies: 0 numberOfLosses: 0 isRanking: true @@ -193,10 +319,11 @@ fetchInitialSessionsToRankAgainst = (levelMajorVersion, levelID, submittedSessio totalScore: 1 limitNumber = 1 - - query = LevelSession.find(findParameters) - .sort(sortParameters) - .limit(limitNumber) + query = LevelSession.aggregate [ + {$match: findParameters} + {$sort: sortParameters} + {$limit: limitNumber} + ] query.exec (err, sessionToRankAgainst) -> callback err, sessionToRankAgainst, submittedSession @@ -206,17 +333,20 @@ generateAndSendTaskPairsToTheQueue = (sessionToRankAgainst,submittedSession, cal taskPairs = generateTaskPairs(sessionToRankAgainst, submittedSession) sendEachTaskPairToTheQueue taskPairs, (taskPairError) -> if taskPairError? then return callback taskPairError + #console.log "Sent task pairs to the queue!" + #console.log taskPairs callback null, {"message": "All task pairs were succesfully sent to the queue"} module.exports.dispatchTaskToConsumer = (req, res) -> + yetiGuru = {} async.waterfall [ - checkSimulationPermissions.bind(@,req) + checkSimulationPermissions.bind(yetiGuru,req) receiveMessageFromSimulationQueue changeMessageVisibilityTimeout parseTaskQueueMessage constructTaskObject - constructTaskLogObject.bind(@, getUserIDFromRequest(req)) + constructTaskLogObject.bind(yetiGuru, getUserIDFromRequest(req)) processTaskObject ], (err, taskObjectToSend) -> if err? @@ -267,11 +397,12 @@ constructTaskObject = (taskMessageBody, message, callback) -> "sessionID": session._id "submitDate": session.submitDate "team": session.team ? "No team" - "code": session.submittedCode + "transpiledCode": session.transpiledCode "teamSpells": session.teamSpells ? {} "levelID": session.levelID "creator": session.creator "creatorName":session.creatorName + "totalScore": session.totalScore taskObject.sessions.push sessionInformation callback null, taskObject, message @@ -292,7 +423,7 @@ processTaskObject = (taskObject,taskLogObject, message, cb) -> getSessionInformation = (sessionIDString, callback) -> findParameters = _id: sessionIDString - selectString = 'submitDate team submittedCode teamSpells levelID creator creatorName' + selectString = 'submitDate team submittedCode teamSpells levelID creator creatorName transpiledCode totalScore' query = LevelSession .findOne(findParameters) .select(selectString) @@ -304,70 +435,79 @@ getSessionInformation = (sessionIDString, callback) -> module.exports.processTaskResult = (req, res) -> - async.waterfall [ - verifyClientResponse.bind(@,req.body) - fetchTaskLog.bind(@) - checkTaskLog.bind(@) - deleteQueueMessage.bind(@) - fetchLevelSession.bind(@) - checkSubmissionDate.bind(@) - logTaskComputation.bind(@) - updateSessions.bind(@) - indexNewScoreArray.bind(@) - addMatchToSessions.bind(@) - updateUserSimulationCounts.bind(@, req.user._id) - determineIfSessionShouldContinueAndUpdateLog.bind(@) - findNearestBetterSessionID.bind(@) - addNewSessionsToQueue.bind(@) - ], (err, results) -> - if err is "shouldn't continue" - sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"} - else if err is "no session was found" - sendResponseObject req, res, {"message":"There were no more games to rank (game is at top)!"} - else if err? - errors.serverError res, "There was an error:#{err}" - else - sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"} + originalSessionID = req.body?.originalSessionID + yetiGuru = {} + try + async.waterfall [ + verifyClientResponse.bind(yetiGuru,req.body) + fetchTaskLog.bind(yetiGuru) + checkTaskLog.bind(yetiGuru) + deleteQueueMessage.bind(yetiGuru) + fetchLevelSession.bind(yetiGuru) + checkSubmissionDate.bind(yetiGuru) + logTaskComputation.bind(yetiGuru) + updateSessions.bind(yetiGuru) + indexNewScoreArray.bind(yetiGuru) + addMatchToSessions.bind(yetiGuru) + updateUserSimulationCounts.bind(yetiGuru, req.user._id) + determineIfSessionShouldContinueAndUpdateLog.bind(yetiGuru) + findNearestBetterSessionID.bind(yetiGuru) + addNewSessionsToQueue.bind(yetiGuru) + ], (err, results) -> + if err is "shouldn't continue" + markSessionAsDoneRanking originalSessionID, (err) -> + if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"} + sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"} + else if err is "no session was found" + markSessionAsDoneRanking originalSessionID, (err) -> + if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"} + sendResponseObject req, res, {"message":"There were no more games to rank (game is at top)!"} + else if err? + errors.serverError res, "There was an error:#{err}" + else + sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"} + catch e + errors.serverError res, "There was an error processing the task result!" verifyClientResponse = (responseObject, callback) -> #TODO: better verification - unless typeof responseObject is "object" + if typeof responseObject isnt "object" or responseObject?.originalSessionID?.length isnt 24 callback "The response to that query is required to be a JSON object." else @clientResponseObject = responseObject - log.info "Verified client response!" + + #log.info "Verified client response!" callback null, responseObject fetchTaskLog = (responseObject, callback) -> - findParameters = - _id: responseObject.taskID - query = TaskLog - .findOne(findParameters) + query = TaskLog.findOne _id: responseObject.taskID query.exec (err, taskLog) => + return callback new Error("Couldn't find TaskLog for _id #{responseObject.taskID}!") unless taskLog @taskLog = taskLog - log.info "Fetched task log!" + #log.info "Fetched task log!" callback err, taskLog.toObject() checkTaskLog = (taskLog, callback) -> if taskLog.calculationTimeMS then return callback "That computational task has already been performed" if hasTaskTimedOut taskLog.sentDate then return callback "The task has timed out" - log.info "Checked task log" + #log.info "Checked task log" callback null deleteQueueMessage = (callback) -> scoringTaskQueue.deleteMessage @clientResponseObject.receiptHandle, (err) -> - log.info "Deleted queue message" + #log.info "Deleted queue message" callback err fetchLevelSession = (callback) -> findParameters = _id: @clientResponseObject.originalSessionID + query = LevelSession .findOne(findParameters) .lean() query.exec (err, session) => @levelSession = session - log.info "Fetched level session" + #log.info "Fetched level session" callback err @@ -376,7 +516,7 @@ checkSubmissionDate = (callback) -> if Number(supposedSubmissionDate) isnt Number(@levelSession.submitDate) callback "The game has been resubmitted. Removing from queue..." else - log.info "Checked submission date" + #log.info "Checked submission date" callback null logTaskComputation = (callback) -> @@ -385,7 +525,7 @@ logTaskComputation = (callback) -> @taskLog.calculationTimeMS = @clientResponseObject.calculationTimeMS @taskLog.sessions = @clientResponseObject.sessions @taskLog.save (err, saved) -> - log.info "Logged task computation" + #log.info "Logged task computation" callback err updateSessions = (callback) -> @@ -393,15 +533,16 @@ updateSessions = (callback) -> async.map sessionIDs, retrieveOldSessionData, (err, oldScores) => if err? then callback err, {"error": "There was an error retrieving the old scores"} - - oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores - newScoreArray = bayes.updatePlayerSkills oldScoreArray - saveNewScoresToDatabase newScoreArray, callback - + try + oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores + newScoreArray = bayes.updatePlayerSkills oldScoreArray + saveNewScoresToDatabase newScoreArray, callback + catch e + callback e saveNewScoresToDatabase = (newScoreArray, callback) -> async.eachSeries newScoreArray, updateScoreInSession, (err) -> - log.info "Saved new scores to database" + #log.info "Saved new scores to database" callback err,newScoreArray @@ -419,7 +560,7 @@ updateScoreInSession = (scoreObject,callback) -> $push: {scoreHistory: {$each: [scoreHistoryAddition], $slice: -1000}} LevelSession.update {"_id": scoreObject.id}, updateObject, callback - log.info "New total score for session #{scoreObject.id} is #{updateObject.totalScore}" + #log.info "New total score for session #{scoreObject.id} is #{updateObject.totalScore}" indexNewScoreArray = (newScoreArray, callback) -> newScoresObject = _.indexBy newScoreArray, 'id' @@ -435,14 +576,17 @@ addMatchToSessions = (newScoreObject, callback) -> matchObject.opponents[sessionID] = {} matchObject.opponents[sessionID].sessionID = sessionID matchObject.opponents[sessionID].userID = session.creator + matchObject.opponents[sessionID].name = session.name + matchObject.opponents[sessionID].totalScore = session.totalScore matchObject.opponents[sessionID].metrics = {} - matchObject.opponents[sessionID].metrics.rank = Number(newScoreObject[sessionID].gameRanking) + matchObject.opponents[sessionID].metrics.rank = Number(newScoreObject[sessionID]?.gameRanking ? 0) - log.info "Match object computed, result: #{matchObject}" - log.info "Writing match object to database..." + #log.info "Match object computed, result: #{matchObject}" + #log.info "Writing match object to database..." #use bind with async to do the writes sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID' - async.each sessionIDs, updateMatchesInSession.bind(@,matchObject), (err) -> callback err + async.each sessionIDs, updateMatchesInSession.bind(@,matchObject), (err) -> + callback err updateMatchesInSession = (matchObject, sessionID, callback) -> currentMatchObject = {} @@ -452,16 +596,22 @@ updateMatchesInSession = (matchObject, sessionID, callback) -> opponentsClone = _.omit opponentsClone, sessionID opponentsArray = _.toArray opponentsClone currentMatchObject.opponents = opponentsArray - - sessionUpdateObject = - $push: {matches: {$each: [currentMatchObject], $slice: -200}} - log.info "Updating session #{sessionID}" - LevelSession.update {"_id":sessionID}, sessionUpdateObject, callback + LevelSession.findOne {"_id": sessionID}, (err, session) -> + session = session.toObject() + currentMatchObject.playtime = session.playtime ? 0 + sessionUpdateObject = + $push: {matches: {$each: [currentMatchObject], $slice: -200}} + #log.info "Updating session #{sessionID}" + LevelSession.update {"_id":sessionID}, sessionUpdateObject, callback updateUserSimulationCounts = (reqUserID,callback) -> incrementUserSimulationCount reqUserID, 'simulatedBy', (err) => if err? then return callback err - incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback + console.log "Incremented user simulation count!" + unless @isRandomMatch + incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback + else + callback null incrementUserSimulationCount = (userID, type, callback) => inc = {} @@ -491,7 +641,7 @@ determineIfSessionShouldContinueAndUpdateLog = (cb) -> totalNumberOfGamesPlayed = updatedSession.numberOfWinsAndTies + updatedSession.numberOfLosses if totalNumberOfGamesPlayed < 10 - console.log "Number of games played is less than 10, continuing..." + #console.log "Number of games played is less than 10, continuing..." cb null else ratio = (updatedSession.numberOfLosses) / (totalNumberOfGamesPlayed) @@ -499,18 +649,21 @@ determineIfSessionShouldContinueAndUpdateLog = (cb) -> cb "shouldn't continue" console.log "Ratio(#{ratio}) is bad, ending simulation" else - console.log "Ratio(#{ratio}) is good, so continuing simulations" + #console.log "Ratio(#{ratio}) is good, so continuing simulations" cb null findNearestBetterSessionID = (cb) -> - levelOriginalID = @levelSession.level.original - levelMajorVersion = @levelSession.level.majorVersion - sessionID = @clientResponseObject.originalSessionID - sessionTotalScore = @newScoresObject[sessionID].totalScore - opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID) - opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore - opposingTeam = calculateOpposingTeam(@clientResponseObject.originalSessionTeam) + try + levelOriginalID = @levelSession.level.original + levelMajorVersion = @levelSession.level.majorVersion + sessionID = @clientResponseObject.originalSessionID + sessionTotalScore = @newScoresObject[sessionID].totalScore + opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID) + opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore + opposingTeam = calculateOpposingTeam(@clientResponseObject.originalSessionTeam) + catch e + cb e retrieveAllOpponentSessionIDs sessionID, (err, opponentSessionIDs) -> if err? then return cb err, null @@ -545,11 +698,11 @@ findNearestBetterSessionID = (cb) -> .select(selectString) .lean() - console.log "Finding session with score near #{opponentSessionTotalScore}" + #console.log "Finding session with score near #{opponentSessionTotalScore}" query.exec (err, session) -> if err? then return cb err, session unless session then return cb "no session was found" - console.log "Found session with score #{session.totalScore}" + #console.log "Found session with score #{session.totalScore}" cb err, session._id @@ -580,11 +733,12 @@ sendEachTaskPairToTheQueue = (taskPairs, callback) -> async.each taskPairs, send generateTaskPairs = (submittedSessions, sessionToScore) -> taskPairs = [] for session in submittedSessions - session = session.toObject() + if session.toObject? + session = session.toObject() teams = ['ogres','humans'] opposingTeams = _.pull teams, sessionToScore.team if String(session._id) isnt String(sessionToScore._id) and session.team in opposingTeams - console.log "Adding game to taskPairs!" + #console.log "Adding game to taskPairs!" taskPairs.push [sessionToScore._id,String session._id] return taskPairs @@ -606,7 +760,7 @@ hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutI handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, "The results weren't provided within the timeout" -putRankingFromMetricsIntoScoreObject = (taskObject,scoreObject) -> +putRankingFromMetricsIntoScoreObject = (taskObject, scoreObject) -> scoreObject = _.indexBy scoreObject, 'id' scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions return scoreObject @@ -622,3 +776,7 @@ retrieveOldSessionData = (sessionID, callback) -> "totalScore":session.totalScore ? (25 - 1.8*(25/3)) "id": sessionID callback err, oldScoreObject + +markSessionAsDoneRanking = (sessionID, cb) -> + #console.log "Marking session as done ranking..." + LevelSession.update {"_id":sessionID}, {"isRanking":false}, cb diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee index 76612e2e0..eed93dfae 100644 --- a/server/routes/auth.coffee +++ b/server/routes/auth.coffee @@ -107,6 +107,7 @@ module.exports.setup = (app) -> else return res.end() else + console.log 'password is', user.get('passwordReset') res.send user.get('passwordReset') return res.end() ) @@ -131,11 +132,25 @@ module.exports.setup = (app) -> if not user return errors.notFound res, "No user found with email '#{req.query.email}'" - user.set('emailSubscriptions', []) - user.save (err) => + emails = _.clone(user.get('emails')) or {} + msg = '' + + if req.query.recruitNotes + emails.recruitNotes ?= {} + emails.recruitNotes.enabled = false + msg = "Unsubscribed #{req.query.email} from recruiting emails." + + else + msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!" + emailSettings.enabled = false for emailSettings in _.values(emails) + emails.generalNews ?= {} + emails.generalNews.enabled = false + emails.anyNotes ?= {} + emails.anyNotes.enabled = false + + user.update {$set: {emails: emails}}, {}, => return errors.serverError res, 'Database failure.' if err - - res.send "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!

Account settings

" + res.send msg + "

Account settings

" res.end() module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) -> diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 51aaa78fc..1ceb8d196 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -1,26 +1,38 @@ config = require '../../server_config' log = require 'winston' mail = require '../commons/mail' +User = require '../users/User' module.exports.setup = (app) -> app.post '/contact', (req, res) -> return res.end() unless req.user log.info "Sending mail from #{req.body.email} saying #{req.body.message}" if config.isProduction - options = createMailOptions req.body.email, req.body.message, req.user - mail.transport.sendMail options, (error, response) -> - if error - log.error "Error sending mail: #{error.message or error}" - else - log.info "Mail sent successfully. Response: #{response.message}" + createMailOptions req.body.email, req.body.message, req.user, req.body.recipientID, req.body.subject, (options) -> + mail.transport.sendMail options, (error, response) -> + if error + log.error "Error sending mail: #{error.message or error}" + else + log.info "Mail sent successfully. Response: #{response.message}" return res.end() -createMailOptions = (sender, message, user) -> +createMailOptions = (sender, message, user, recipientID, subject, done) -> # TODO: use email templates here options = from: config.mail.username to: config.mail.username replyTo: sender - subject: "[CodeCombat] Feedback - #{sender}" + subject: "[CodeCombat] #{subject ? ('Feedback - ' + sender)}" text: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}" - #html: message.replace '\n', '
\n' \ No newline at end of file + #html: message.replace '\n', '
\n' + + if recipientID and (user.isAdmin() or ('employer' in (user.permissions ? []))) + User.findById(recipientID, 'email').exec (err, document) -> + if err + log.error "Error looking up recipient to email from #{recipientID}: #{err}" if err + else + options.bcc = options.to + options.to = document.get('email') + done options + else + done options diff --git a/server/routes/db.coffee b/server/routes/db.coffee index 723e15b90..f65a56744 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -1,12 +1,12 @@ log = require 'winston' errors = require '../commons/errors' handlers = require('../commons/mapping').handlers -schemas = require('../commons/mapping').schemas mongoose = require 'mongoose' module.exports.setup = (app) -> # This is hacky and should probably get moved somewhere else, I dunno app.get '/db/cla.submissions', (req, res) -> + return errors.unauthorized(res, "You must be an admin to view that information") unless req.user?.isAdmin() res.setHeader('Content-Type', 'application/json') collection = mongoose.connection.db.collection 'cla.submissions', (err, collection) -> return log.error "Couldn't fetch CLA submissions because #{err}" if err @@ -35,6 +35,7 @@ module.exports.setup = (app) -> return handler.versions(req, res, parts[1]) if parts[2] is 'versions' return handler.files(req, res, parts[1]) if parts[2] is 'files' return handler.search(req, res) if req.route.method is 'get' and parts[1] is 'search' + return handler.getNamesByIDs(req, res) if req.route.method in ['get', 'post'] and parts[1] is 'names' return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2 return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]? return handler.patch(req, res, parts[1]) if req.route.method is 'patch' and parts[1]? @@ -42,14 +43,15 @@ module.exports.setup = (app) -> catch error log.error("Error trying db method #{req.route.method} route #{parts} from #{name}: #{error}") log.error(error) + log.error(error.stack) errors.notFound(res, "Route #{req.path} not found.") getSchema = (req, res, moduleName) -> try - name = schemas[moduleName.replace '.', '_'] - schema = require('../' + name) + name = moduleName.replace '.', '_' + schema = require('../../app/schemas/models/' + name) - res.send(schema) + res.send(JSON.stringify(schema, null, '\t')) res.end() catch error diff --git a/server/routes/file.coffee b/server/routes/file.coffee index 7a16c3709..1cc0f73d7 100644 --- a/server/routes/file.coffee +++ b/server/routes/file.coffee @@ -19,7 +19,7 @@ fileGet = (req, res) -> objectId = mongoose.Types.ObjectId(path) query = objectId catch e - path = path.split('/') + path = path.split('/') filename = path[path.length-1] path = path[...path.length-1].join('/') query = @@ -34,7 +34,7 @@ fileGet = (req, res) -> res.setHeader('Content-Type', 'text/json') res.send(results) res.end() - + else Grid.gfs.collection('media').findOne query, (err, filedata) => return errors.notFound(res) if not filedata @@ -42,7 +42,7 @@ fileGet = (req, res) -> if req.headers['if-modified-since'] is filedata.uploadDate res.status(304) return res.end() - + res.setHeader('Content-Type', filedata.contentType) res.setHeader('Last-Modified', filedata.uploadDate) res.setHeader('Cache-Control', 'public') @@ -70,7 +70,7 @@ postFileSchema = required: ['filename', 'mimetype', 'path'] filePost = (req, res) -> - return errors.forbidden(res) unless req.user?.isAdmin() + return errors.forbidden(res) unless req.user options = req.body tv4 = require('tv4').tv4 valid = tv4.validate(options, postFileSchema) @@ -83,7 +83,7 @@ filePost = (req, res) -> saveURL = (req, res) -> options = createPostOptions(req) - checkExistence options, res, req.body.force, (err) -> + checkExistence options, req, res, req.body.force, (err) -> return errors.serverError(res) if err writestream = Grid.gfs.createWriteStream(options) request(req.body.url).pipe(writestream) @@ -91,7 +91,7 @@ saveURL = (req, res) -> saveFile = (req, res) -> options = createPostOptions(req) - checkExistence options, res, req.body.force, (err) -> + checkExistence options, req, res, req.body.force, (err) -> return if err writestream = Grid.gfs.createWriteStream(options) f = req.files[req.body.postName] @@ -101,8 +101,8 @@ saveFile = (req, res) -> savePNG = (req, res) -> options = createPostOptions(req) - checkExistence options, res, req.body.force, (err) -> - return errors.serverError(res) if err + checkExistence options, req, res, req.body.force, (err) -> + return if err writestream = Grid.gfs.createWriteStream(options) img = new Buffer(req.body.b64png, 'base64') streamBuffers = require 'stream-buffers' @@ -110,21 +110,32 @@ savePNG = (req, res) -> myReadableStreamBuffer.put(img) myReadableStreamBuffer.pipe(writestream) handleStreamEnd(res, writestream) + +userCanEditFile = (user=null, file=null) -> + # no user means 'anyone'. No file means 'any file' + return false unless user + return true if user.isAdmin() + return false unless file + return true if file.metadata.creator is user.id + return false -checkExistence = (options, res, force, done) -> +checkExistence = (options, req, res, force, done) -> q = { filename: options.filename 'metadata.path': options.metadata.path } Grid.gfs.collection('media').find(q).toArray (err, files) -> - if files.length and not force - errors.conflict(res) + file = files[0] + if file and ((not userCanEditFile(req.user, file) or (not force))) + errors.conflict(res, {canForce:userCanEditFile(req.user, file)}) done(true) - else if files.length - q = { _id: files[0]._id } + else if file + q = { _id: file._id } q.root = 'media' Grid.gfs.remove q, (err) -> - return errors.serverError(res) if err + if err + errors.serverError(res) + return done(true) done() else done() @@ -143,11 +154,11 @@ createPostOptions = (req) -> unless req.body.name name = req.body.filename.split('.')[0] req.body.name = _.str.humanize(name) - + path = req.body.path or '' path = path[1...] if path and path[0] is '/' path = path[...path.length-2] if path and path[path.length-1] is '/' - + options = mode: 'w' filename: req.body.filename @@ -158,6 +169,6 @@ createPostOptions = (req) -> name: req.body.name path: path creator: ''+req.user._id - options.metadata.description = req.body.description if req.body.description? + options.metadata.description = req.body.description if req.body.description? options diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 6527e4a26..e7c2ee361 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -1,5 +1,4 @@ mail = require '../commons/mail' -map = _.invert mail.MAILCHIMP_GROUP_MAP User = require '../users/User.coffee' errors = require '../commons/errors' #request = require 'request' @@ -37,8 +36,9 @@ getTimeFromDaysAgo = (now, daysAgo) -> t = now - 86400 * 1000 * daysAgo - LADDER_PREGAME_INTERVAL isRequestFromDesignatedCronHandler = (req, res) -> - if req.ip isnt config.mail.cronHandlerPublicIP and req.ip isnt config.mail.cronHandlerPrivateIP - console.log "RECEIVED REQUEST FROM IP #{req.ip}(headers indicate #{req.headers['x-forwarded-for']}" + requestIP = req.headers['x-forwarded-for']?.replace(" ","").split(",")[0] + if requestIP isnt config.mail.cronHandlerPublicIP and requestIP isnt config.mail.cronHandlerPrivateIP + console.log "RECEIVED REQUEST FROM IP #{requestIP}(headers indicate #{req.headers['x-forwarded-for']}" console.log "UNAUTHORIZED ATTEMPT TO SEND TRANSACTIONAL LADDER EMAIL THROUGH CRON MAIL HANDLER" res.send("You aren't authorized to perform that action. Only the specified Cron handler may perform that action.") res.end() @@ -53,7 +53,7 @@ handleLadderUpdate = (req, res) -> res.send('Great work, Captain Cron! I can take it from here.') res.end() # TODO: somehow fetch the histograms - emailDays = [1, 2, 4, 7, 30] + emailDays = [1, 2, 4, 7, 14, 30] now = new Date() for daysAgo in emailDays # Get every session that was submitted in a 5-minute window after the time. @@ -76,17 +76,18 @@ handleLadderUpdate = (req, res) -> sendLadderUpdateEmail result, now, daysAgo for result in results sendLadderUpdateEmail = (session, now, daysAgo) -> - User.findOne({_id: session.creator}).select("name email firstName lastName emailSubscriptions preferredLanguage").lean().exec (err, user) -> + User.findOne({_id: session.creator}).select("name email firstName lastName emailSubscriptions emails preferredLanguage").exec (err, user) -> if err log.error "Couldn't find user for #{session.creator} from session #{session._id}" return - unless user.email and ('notification' in user.emailSubscriptions) and not session.unsubscribed - log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{user.emailSubscriptions} - session unsubscribed: #{session.unsubscribed}" + allowNotes = user.isEmailSubscriptionEnabled 'anyNotes' + unless user.get('email') and allowNotes and not session.unsubscribed + log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{user.get('emailSubscriptions')}, #{user.get('emails')} - session unsubscribed: #{session.unsubscribed}" return unless session.levelName - log.info "Not sending email to #{user.email} #{user.name} because the session had no levelName in it." + log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had no levelName in it." return - name = if user.firstName and user.lastName then "#{user.firstName}" else user.name + name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name') name = "Wizard" if not name or name is "Anoner" # Fetch the most recent defeat and victory, if there are any. @@ -107,7 +108,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> context = email_id: sendwithus.templates.ladder_update_email recipient: - address: if DEBUGGING then 'nick@codecombat.com' else user.email + address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name email_data: name: name @@ -197,13 +198,11 @@ handleMailchimpWebHook = (req, res) -> return errors.serverError(res) if err res.end('Success') +module.exports.handleProfileUpdate = handleProfileUpdate = (user, post) -> + mailchimpSubs = post.data.merges.INTERESTS.split(', ') -handleProfileUpdate = (user, post) -> - groups = post.data.merges.INTERESTS.split(', ') - groups = (map[g] for g in groups when map[g]) - otherSubscriptions = (g for g in user.get('emailSubscriptions') when not mail.MAILCHIMP_GROUP_MAP[g]) - groups = groups.concat otherSubscriptions - user.set 'emailSubscriptions', groups + for [mailchimpEmailGroup, emailGroup] in _.zip(mail.MAILCHIMP_GROUPS, mail.NEWS_GROUPS) + user.setEmailSubscription emailGroup, mailchimpEmailGroup in mailchimpSubs fname = post.data.merges.FNAME user.set('firstName', fname) if fname @@ -216,7 +215,9 @@ handleProfileUpdate = (user, post) -> # badLog("Updating user object to: #{JSON.stringify(user.toObject(), null, '\t')}") -handleUnsubscribe = (user) -> +module.exports.handleUnsubscribe = handleUnsubscribe = (user) -> user.set 'emailSubscriptions', [] + for emailGroup in mail.NEWS_GROUPS + user.setEmailSubscription emailGroup, false # badLog("Unsubscribing user object to: #{JSON.stringify(user.toObject(), null, '\t')}") diff --git a/server/routes/queue.coffee b/server/routes/queue.coffee index 18d8cc80b..388bce4e0 100644 --- a/server/routes/queue.coffee +++ b/server/routes/queue.coffee @@ -18,6 +18,13 @@ module.exports.setup = (app) -> handler = loadQueueHandler 'scoring' handler.resimulateAllSessions req, res + app.post '/queue/scoring/getTwoGames', (req, res) -> + handler = loadQueueHandler 'scoring' + handler.getTwoGames req, res + + app.put '/queue/scoring/recordTwoGames', (req, res) -> + handler = loadQueueHandler 'scoring' + handler.recordTwoGames req, res app.all '/queue/*', (req, res) -> setResponseHeaderToJSONContentType res diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index ad7a07500..1f8145eb1 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -1,14 +1,17 @@ config = require '../server_config' sendwithusAPI = require 'sendwithus' swuAPIKey = config.mail.sendwithusAPIKey -queues = require './commons/queue' module.exports.setupRoutes = (app) -> return - debug = not config.isProduction module.exports.api = new sendwithusAPI swuAPIKey, debug +if config.unittest + module.exports.api.send = -> module.exports.templates = welcome_email: 'utnGaBHuSU4Hmsi7qrAypU' ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4' + patch_created: 'tem_xhxuNosLALsizTNojBjNcL' + change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG' + one_time_recruiting_email: 'tem_mdFMgtcczHKYu94Jmq68j8' diff --git a/server/users/User.coffee b/server/users/User.coffee index 28009e610..744b8db7b 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -1,5 +1,5 @@ mongoose = require('mongoose') -jsonschema = require('./user_schema') +jsonschema = require('../../app/schemas/models/user') crypto = require('crypto') {salt, isProduction} = require('../../server_config') mail = require '../commons/mail' @@ -16,33 +16,66 @@ UserSchema = new mongoose.Schema({ UserSchema.pre('init', (next) -> return next() unless jsonschema.properties? for prop, sch of jsonschema.properties + continue if prop is 'emails' # defaults may change, so don't carry them over just yet @set(prop, sch.default) if sch.default? - @set('permissions', ['admin']) if not isProduction next() ) UserSchema.post('init', -> @set('anonymous', false) if @get('email') - @currentSubscriptions = JSON.stringify(@get('emailSubscriptions')) ) UserSchema.methods.isAdmin = -> p = @get('permissions') return p and 'admin' in p +emailNameMap = + generalNews: 'announcement' + adventurerNews: 'tester' + artisanNews: 'level_creator' + archmageNews: 'developer' + scribeNews: 'article_editor' + diplomatNews: 'translator' + ambassadorNews: 'support' + anyNotes: 'notification' + +UserSchema.methods.setEmailSubscription = (newName, enabled) -> + oldSubs = _.clone @get('emailSubscriptions') + if oldSubs and oldName = emailNameMap[newName] + oldSubs = (s for s in oldSubs when s isnt oldName) + oldSubs.push(oldName) if enabled + @set('emailSubscriptions', oldSubs) + + newSubs = _.clone(@get('emails') or _.cloneDeep(jsonschema.properties.emails.default)) + newSubs[newName] ?= {} + newSubs[newName].enabled = enabled + @set('emails', newSubs) + @newsSubsChanged = true if newName in mail.NEWS_GROUPS + +UserSchema.methods.isEmailSubscriptionEnabled = (newName) -> + emails = @get 'emails' + if not emails + oldSubs = @get('emailSubscriptions') + oldName = emailNameMap[newName] + return oldName and oldName in oldSubs if oldSubs + emails ?= {} + _.defaults emails, _.cloneDeep(jsonschema.properties.emails.default) + return emails[newName]?.enabled + UserSchema.statics.updateMailChimp = (doc, callback) -> - return callback?() unless isProduction + return callback?() unless isProduction or GLOBAL.testing return callback?() if doc.updatedMailChimp return callback?() unless doc.get('email') existingProps = doc.get('mailChimp') emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email') - emailSubs = doc.get('emailSubscriptions') - gm = mail.MAILCHIMP_GROUP_MAP - newGroups = (gm[name] for name in emailSubs when gm[name]?) + return callback?() unless emailChanged or doc.newsSubsChanged + + newGroups = [] + for [mailchimpEmailGroup, emailGroup] in _.zip(mail.MAILCHIMP_GROUPS, mail.NEWS_GROUPS) + newGroups.push(mailchimpEmailGroup) if doc.isEmailSubscriptionEnabled(emailGroup) + if (not existingProps) and newGroups.length is 0 return callback?() # don't add totally unsubscribed people to the list - subsChanged = doc.currentSubscriptions isnt JSON.stringify(emailSubs) - return callback?() unless emailChanged or subsChanged params = {} params.id = mail.MAILCHIMP_LIST_ID @@ -62,7 +95,7 @@ UserSchema.statics.updateMailChimp = (doc, callback) -> doc.updatedMailChimp = true callback?() - mc.lists.subscribe params, onSuccess, onFailure + mc?.lists.subscribe params, onSuccess, onFailure UserSchema.pre('save', (next) -> @@ -74,12 +107,13 @@ UserSchema.pre('save', (next) -> @set('password', undefined) if @get('email') and @get('anonymous') @set('anonymous', false) + @set('permissions', ['admin']) if not isProduction data = email_id: sendwithus.templates.welcome_email recipient: address: @get 'email' sendwithus.api.send data, (err, result) -> - log.error 'error', err, 'result', result if err + log.error "sendwithus post-save error: #{err}, result: #{result}" if err next() ) diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 168f10d91..f7723f859 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -1,4 +1,4 @@ -schema = require './user_schema' +schema = require '../../app/schemas/models/user' crypto = require 'crypto' request = require 'request' User = require './User' @@ -9,11 +9,15 @@ errors = require '../commons/errors' async = require 'async' log = require 'winston' LevelSession = require('../levels/sessions/LevelSession') +LevelSessionHandler = require '../levels/sessions/level_session_handler' serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset'] privateProperties = [ 'permissions', 'email', 'firstName', 'lastName', 'gender', 'facebookID', - 'gplusID', 'music', 'volume', 'aceConfig' + 'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement' +] +candidateProperties = [ + 'jobProfile', 'jobProfileApproved', 'jobProfileNotes' ] UserHandler = class UserHandler extends Handler @@ -21,9 +25,9 @@ UserHandler = class UserHandler extends Handler editableProperties: [ 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', - 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emailSubscriptions', + 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emails', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', - 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel' + 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile' ] jsonSchema: schema @@ -32,21 +36,19 @@ UserHandler = class UserHandler extends Handler super(arguments...) @editableProperties.push('permissions') unless config.isProduction + getEditableProperties: (req, document) -> + props = super req, document + props.push 'jobProfileApproved', 'jobProfileNotes' if req.user.isAdmin() + props + formatEntity: (req, document) -> return null unless document? obj = document.toObject() delete obj[prop] for prop in serverProperties - includePrivates = req.user and (req.user?.isAdmin() or req.user?._id.equals(document._id)) + includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id)) delete obj[prop] for prop in privateProperties unless includePrivates - - # emailHash is used by gravatar - hash = crypto.createHash('md5') - if document.get('email') - hash.update(_.trim(document.get('email')).toLowerCase()) - else - hash.update(@_id+'') - obj.emailHash = hash.digest('hex') - + includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.get('permissions') ? [])) and @employerCanViewCandidate req.user, obj) + delete obj[prop] for prop in candidateProperties unless includeCandidate return obj waterfallFunctions: [ @@ -103,11 +105,13 @@ UserHandler = class UserHandler extends Handler (req, user, callback) -> return callback(null, req, user) unless req.body.name nameLower = req.body.name?.toLowerCase() - return callback(null, req, user) if nameLower is user.get('nameLower') - User.findOne({nameLower:nameLower}).exec (err, otherUser) -> + return callback(null, req, user) unless nameLower + return callback(null, req, user) if nameLower is user.get('nameLower') and not user.get('anonymous') + User.findOne({nameLower:nameLower,anonymous:false}).exec (err, otherUser) -> log.error "Database error setting user name: #{err}" if err return callback(res:'Database error.', code:500) if err r = {message:'is already used by another account', property:'name'} + console.log 'Another user exists' if otherUser return callback({res:r, code:409}) if otherUser user.set('name', req.body.name) callback(null, req, user) @@ -115,43 +119,56 @@ UserHandler = class UserHandler extends Handler getById: (req, res, id) -> if req.user?._id.equals(id) - return @sendSuccess(res, @formatEntity(req, req.user)) + return @sendSuccess(res, @formatEntity(req, req.user, 256)) super(req, res, id) - getNamesByIds: (req, res) -> + getNamesByIDs: (req, res) -> ids = req.query.ids or req.body.ids - ids = ids.split(',') if _.isString ids - ids = _.uniq ids - - # TODO: Extend and repurpose this handler to return other public info about a user more flexibly, - # say by a query parameter that lists public properties to return. returnWizard = req.query.wizard or req.body.wizard - query = if returnWizard then {name:1, wizard:1} else {name:1} - - makeFunc = (id) -> - (callback) -> - User.findById(id, query).exec (err, document) -> - return done(err) if err - if document and returnWizard - callback(null, {name:document.get('name'), wizard:document.get('wizard') or {}}) - else - callback(null, document?.get('name') or '') - - funcs = {} - for id in ids - return errors.badInput(res, "Given an invalid id: #{id}") unless Handler.isID(id) - funcs[id] = makeFunc(id) - - async.parallel funcs, (err, results) -> - return errors.serverError err if err - res.send results - res.end() + properties = if returnWizard then "name wizard" else "name" + @getPropertiesFromMultipleDocuments res, User, properties, ids nameToID: (req, res, name) -> - User.findOne({nameLower:name.toLowerCase()}).exec (err, otherUser) -> + User.findOne({nameLower:name.toLowerCase(),anonymous:false}).exec (err, otherUser) -> res.send(if otherUser then otherUser._id else JSON.stringify('')) res.end() + getSimulatorLeaderboard: (req, res) -> + queryParameters = @getSimulatorLeaderboardQueryParameters(req) + leaderboardQuery = User.find(queryParameters.query).select("name simulatedBy simulatedFor").sort({"simulatedBy":queryParameters.sortOrder}).limit(queryParameters.limit) + leaderboardQuery.exec (err, otherUsers) -> + otherUsers = _.reject otherUsers, _id: req.user._id if req.query.scoreOffset isnt -1 + otherUsers ?= [] + res.send(otherUsers) + res.end() + + getMySimulatorLeaderboardRank: (req, res) -> + req.query.order = 1 + queryParameters = @getSimulatorLeaderboardQueryParameters(req) + User.count queryParameters.query, (err, count) => + return @sendDatabaseError(res, err) if err + res.send JSON.stringify(count + 1) + + getSimulatorLeaderboardQueryParameters: (req) -> + @validateSimulateLeaderboardRequestParameters(req) + + query = {} + sortOrder = -1 + limit = if req.query.limit > 30 then 30 else req.query.limit + if req.query.scoreOffset isnt -1 + simulatedByQuery = {} + simulatedByQuery[if req.query.order is 1 then "$gt" else "$lte"] = req.query.scoreOffset + query.simulatedBy = simulatedByQuery + sortOrder = 1 if req.query.order is 1 + else + query.simulatedBy = {"$exists": true} + {query: query, sortOrder: sortOrder, limit: limit} + + validateSimulateLeaderboardRequestParameters: (req) -> + req.query.order = parseInt(req.query.order) ? -1 + req.query.scoreOffset = parseFloat(req.query.scoreOffset) ? 100000 + req.query.limit = parseInt(req.query.limit) ? 20 + post: (req, res) -> return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendBadInputError(res, 'Must have an anonymous user to post with.') unless req.user @@ -167,11 +184,16 @@ UserHandler = class UserHandler extends Handler getByRelationship: (req, res, args...) -> return @agreeToCLA(req, res) if args[1] is 'agreeToCLA' + return @agreeToEmployerAgreement(req,res) if args[1] is 'agreeToEmployerAgreement' return @avatar(req, res, args[0]) if args[1] is 'avatar' - return @getNamesByIds(req, res) if args[1] is 'names' + return @getNamesByIDs(req, res) if args[1] is 'names' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' + return @getCandidates(req, res) if args[1] is 'candidates' + return @getSimulatorLeaderboard(req, res, args[0]) if args[1] is 'simulatorLeaderboard' + return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank' return @sendNotFoundError(res) + super(arguments...) agreeToCLA: (req, res) -> return @sendUnauthorizedError(res) unless req.user @@ -191,9 +213,14 @@ UserHandler = class UserHandler extends Handler @sendSuccess(res, {result:'success'}) avatar: (req, res, id) -> - @modelClass.findById(id).exec (err, document) -> + @modelClass.findById(id).exec (err, document) => return @sendDatabaseError(res, err) if err - res.redirect(document?.get('photoURL') or '/images/generic-wizard-icon.png') + photoURL = document?.get('photoURL') + if photoURL + photoURL = "/file/#{photoURL}" + else + photoURL = @buildGravatarURL document, req.query.s, req.query.fallback + res.redirect photoURL res.end() getLevelSessions: (req, res, userID) -> @@ -205,8 +232,84 @@ UserHandler = class UserHandler extends Handler projection[field] = 1 for field in req.query.project.split(',') LevelSession.find(query).select(projection).exec (err, documents) => return @sendDatabaseError(res, err) if err - documents = (@formatEntity(req, doc) for doc in documents) + documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents) @sendSuccess(res, documents) + agreeToEmployerAgreement: (req, res) -> + userIsAnonymous = req.user?.get('anonymous') + if userIsAnonymous then return errors.unauthorized(res, "You need to be logged in to agree to the employer agreeement.") + profileData = req.body + #TODO: refactor this bit to make it more elegant + if not profileData.id or not profileData.positions or not profileData.emailAddress or not profileData.firstName or not profileData.lastName + return errors.badInput(res, "You need to have a more complete profile to sign up for this service.") + @modelClass.findById(req.user.id).exec (err, user) => + if user.get('employerAt') or user.get('signedEmployerAgreement') or "employer" in user.get('permissions') + return errors.conflict(res, "You already have signed the agreement!") + #TODO: Search for the current position + employerAt = _.filter(profileData.positions.values,"isCurrent")[0]?.company.name ? "Not available" + signedEmployerAgreement = + linkedinID: profileData.id + date: new Date() + data: profileData + updateObject = + "employerAt": employerAt + "signedEmployerAgreement": signedEmployerAgreement + $push: "permissions":'employer' + + User.update {"_id": req.user.id}, updateObject, (err, result) => + if err? then return errors.serverError(res, "There was an issue updating the user object to reflect employer status: #{err}") + res.send({"message": "The agreement was successful."}) + res.end() + + getCandidates: (req, res) -> + authorized = req.user.isAdmin() or ('employer' in req.user.get('permissions')) + since = (new Date((new Date()) - 2 * 30.4 * 86400 * 1000)).toISOString() + #query = {'jobProfile.active': true, 'jobProfile.updated': {$gt: since}} + query = {'jobProfile.updated': {$gt: since}} + query.jobProfileApproved = true unless req.user.isAdmin() + query['jobProfile.active'] = true unless req.user.isAdmin() + selection = 'jobProfile' + selection += ' email' if authorized + selection += ' jobProfileApproved' if req.user.isAdmin() + User.find(query).select(selection).exec (err, documents) => + return @sendDatabaseError(res, err) if err + candidates = (candidate for candidate in documents when @employerCanViewCandidate req.user, candidate.toObject()) + candidates = (@formatCandidate(authorized, candidate) for candidate in candidates) + @sendSuccess(res, candidates) + + formatCandidate: (authorized, document) -> + fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile'] + obj = _.pick document.toObject(), fields + obj.photoURL ||= obj.jobProfile.photoURL if authorized + subfields = ['country', 'city', 'lookingFor', 'jobTitle', 'skills', 'experience', 'updated', 'active'] + if authorized + subfields = subfields.concat ['name'] + obj.jobProfile = _.pick obj.jobProfile, subfields + obj + + employerCanViewCandidate: (employer, candidate) -> + return true if employer.isAdmin() + for job in candidate.jobProfile?.work ? [] + # TODO: be smarter about different ways to write same company names to ensure privacy. + # We'll have to manually pay attention to how we set employer names for now. + if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase() + log.info "#{employer.get('name')} at #{employer.get('employerAt')} can't see #{candidate.jobProfile.name} because s/he worked there." + return false if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase() + true + + buildGravatarURL: (user, size, fallback) -> + emailHash = @buildEmailHash user + fallback ?= "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png" + fallback = "http://codecombat.com#{fallback}" unless /^http/.test fallback + "https://www.gravatar.com/avatar/#{emailHash}?s=#{size}&default=#{fallback}" + + buildEmailHash: (user) -> + # emailHash is used by gravatar + hash = crypto.createHash('md5') + if user.get('email') + hash.update(_.trim(user.get('email')).toLowerCase()) + else + hash.update(user.get('_id') + '') + hash.digest('hex') module.exports = new UserHandler() diff --git a/server/users/user_schema.coffee b/server/users/user_schema.coffee deleted file mode 100644 index 18d526de5..000000000 --- a/server/users/user_schema.coffee +++ /dev/null @@ -1,61 +0,0 @@ -c = require '../commons/schemas' -emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification'] - -UserSchema = c.object {}, - name: c.shortString({title: 'Display Name', default:''}) - email: c.shortString({title: 'Email', format: 'email'}) - firstName: c.shortString({title: 'First Name'}) - lastName: c.shortString({title: 'Last Name'}) - gender: {type: 'string', 'enum': ['male', 'female']} - password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'} - passwordReset: {type: 'string'} - photoURL: {type: 'string', format: 'url', required: false} - - facebookID: c.shortString({title: 'Facebook ID'}) - gplusID: c.shortString({title: 'G+ ID'}) - - wizardColor1: c.pct({title: 'Wizard Clothes Color'}) - volume: c.pct({title: 'Volume'}) - music: {type: 'boolean', default: true} - autocastDelay: {type: 'integer', 'default': 5000 } - lastLevel: { type: 'string' } - - emailSubscriptions: c.array {uniqueItems: true, 'default': ['announcement', 'notification']}, {'enum': emailSubscriptions} - - # server controlled - permissions: c.array {'default': []}, c.shortString() - dateCreated: c.date({title: 'Date Joined'}) - anonymous: {type: 'boolean', 'default': true} - testGroupNumber: {type: 'integer', minimum: 0, maximum: 256, exclusiveMaximum: true} - mailChimp: {type: 'object'} - hourOfCode: {type: 'boolean'} - hourOfCodeComplete: {type: 'boolean'} - - emailLower: c.shortString() - nameLower: c.shortString() - passwordHash: {type: 'string', maxLength: 256} - - # client side - #gravatarProfile: {} (should only ever be kept locally) - emailHash: {type: 'string'} - - #Internationalization stuff - preferredLanguage: {type: 'string', default: 'en', 'enum': c.getLanguageCodeArray()} - - signedCLA: c.date({title: 'Date Signed the CLA'}) - wizard: c.object {}, - colorConfig: c.object {additionalProperties: c.colorConfig()} - - aceConfig: c.object {}, - language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript']} - keyBindings: {type: 'string', 'default': 'default', 'enum': ['default', 'vim', 'emacs']} - invisibles: {type: 'boolean', 'default': false} - indentGuides: {type: 'boolean', 'default': false} - behaviors: {type: 'boolean', 'default': false} - - simulatedBy: {type: 'integer', minimum: 0, default: 0} - simulatedFor: {type: 'integer', minimum: 0, default: 0} - -c.extendBasicProperties UserSchema, 'user' - -module.exports = UserSchema diff --git a/server_setup.coffee b/server_setup.coffee index c06482a85..d214497cb 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -3,6 +3,8 @@ path = require 'path' authentication = require 'passport' useragent = require 'express-useragent' fs = require 'graceful-fs' +log = require 'winston' +compressible = require 'compressible' database = require './server/commons/database' baseRoute = require './server/routes/base' @@ -10,17 +12,7 @@ user = require './server/users/user_handler' logging = require './server/commons/logging' config = require './server_config' auth = require './server/routes/auth' -UserHandler = require('./server/users/user_handler') - -###Middleware setup functions implementation### -# 2014-03-03: Try not using this and see if it's still a problem -#setupRequestTimeoutMiddleware = (app) -> -# app.use (req, res, next) -> -# req.setTimeout 15000, -> -# console.log 'timed out!' -# req.abort() -# self.emit('pass',message) -# next() +UserHandler = require './server/users/user_handler' productionLogging = (tokens, req, res) -> status = res.statusCode @@ -30,15 +22,17 @@ productionLogging = (tokens, req, res) -> else if status >= 300 then color = 36 elapsed = (new Date()) - req._startTime elapsedColor = if elapsed < 500 then 90 else 31 - if (status isnt 200 and status isnt 304) or elapsed > 500 + if (status isnt 200 and status isnt 204 and status isnt 304 and status isnt 302) or elapsed > 500 return "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m" null setupExpressMiddleware = (app) -> - #setupRequestTimeoutMiddleware app if config.isProduction express.logger.format('prod', productionLogging) app.use(express.logger('prod')) + app.use express.compress filter: (req, res) -> + return false if req.headers.host is 'codecombat.com' # Cloudflare will gzip it for us on codecombat.com + compressible res.getHeader('Content-Type') else app.use(express.logger('dev')) app.use(express.static(path.join(__dirname, 'public'))) @@ -49,7 +43,6 @@ setupExpressMiddleware = (app) -> app.use(express.bodyParser()) app.use(express.methodOverride()) app.use(express.cookieSession({secret:'defenestrate'})) - #app.use(express.compress()) if config.isProduction # just let Cloudflare do it setupPassportMiddleware = (app) -> app.use(authentication.initialize()) @@ -96,9 +89,10 @@ setupFallbackRouteToIndex = (app) -> auth.loginUser(req, res, user, false, next) sendMain = (req, res) -> - fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err,data) -> - # insert the user object directly into the html so the application can have it immediately - data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user))) + fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err, data) -> + log.error "Error modifying main.html: #{err}" if err + # insert the user object directly into the html so the application can have it immediately. Sanitize + data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user)).replace(/\//g, '\\/')) res.send data setupFacebookCrossDomainCommunicationRoute = (app) -> diff --git a/test/app/lib/surface/camera.spec.coffee b/test/app/lib/surface/camera.spec.coffee index 48c4439bc..8019cd38f 100644 --- a/test/app/lib/surface/camera.spec.coffee +++ b/test/app/lib/surface/camera.spec.coffee @@ -98,25 +98,25 @@ describe 'Camera (Surface point of view)', -> checkCameraPos cam, wop it 'works at 90 degrees', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 100 * Camera.MPP, testLayer, 1, null, Math.PI / 2 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 100 * Camera.MPP expect(cam.x2y).toBeCloseTo 1 expect(cam.x2z).toBeGreaterThan 9001 expect(cam.z2y).toBeCloseTo 0 it 'works at 0 degrees', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 100 * Camera.MPP, testLayer, 1, null, 0 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 100 * Camera.MPP expect(cam.x2z).toBeGreaterThan 9001 expect(cam.x2y).toBeCloseTo 1 expect(cam.z2y).toBeCloseTo 0 it 'works at 45 degrees', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 100 * Camera.MPP, testLayer, 1, null, Math.PI / 4 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 100 * Camera.MPP expect(cam.x2y).toBeCloseTo 1 expect(cam.x2z).toBeGreaterThan 9001 expect(cam.z2y).toBeCloseTo 0 xit 'works at default angle of asin(0.75) ~= 48.9 degrees', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 100 * Camera.MPP, testLayer, 1 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 100 * Camera.MPP angle = 1 / Math.cos angle expect(cam.angle).toBeCloseTo angle expect(cam.x2y).toBeCloseTo 1 @@ -124,7 +124,7 @@ describe 'Camera (Surface point of view)', -> expect(cam.z2y).toBeCloseTo 0 xit 'works at 2x zoom, 90 degrees', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 100 * Camera.MPP, testLayer, 2, null, Math.PI / 2 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 100 * Camera.MPP checkCameraPos cam wop = x: 5, y: 2.5, z: 7 cap = cam.worldToCanvas wop @@ -144,7 +144,7 @@ describe 'Camera (Surface point of view)', -> expectPositionsEqual cap, {x: 0, y: 50} xit 'works at 2x zoom, 30 degrees', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 2 * 100 * Camera.MPP, testLayer, 2, null, Math.PI / 6 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 2 * 100 * Camera.MPP expect(cam.x2y).toBeCloseTo 1 expect(cam.x2z).toBeGreaterThan 9001 checkCameraPos cam @@ -165,15 +165,15 @@ describe 'Camera (Surface point of view)', -> expectPositionsEqual cap, {x: 50, y: -100} it 'works at 2x zoom, 60 degree hFOV', -> - cam = new Camera 100, 100, 100 * Camera.MPP, 100 * Camera.MPP, testLayer, 2, null, null, 0.01 + cam = new Camera { attr: (x) -> 100 }, 100 * Camera.MPP, 100 * Camera.MPP checkCameraPos cam - it 'works at 2x zoom, 60 degree hFOV, 40 degree hFOV', -> - cam = new Camera 100, 63.041494, 100 * Camera.MPP, 63.041494 * Camera.MPP, testLayer, 2, null, null, Math.PI / 3 + xit 'works at 2x zoom, 60 degree hFOV, 40 degree hFOV', -> + cam = new Camera { attr: (x) -> x is 'height' ? 63.041494 : 100 }, 100 * Camera.MPP, 63.041494 * Camera.MPP checkCameraPos cam xit 'works on a surface wider than it is tall, 30 degrees, default viewing upper left corner', -> - cam = new Camera 100, 100, 200 * Camera.MPP, 2 * 50 * Camera.MPP, testLayer, 1, {x: 0, y: 0}, Math.PI / 6 + cam = new Camera { attr: (x) -> 100 }, 200 * Camera.MPP, 2 * 50 * Camera.MPP checkCameraPos cam expect(cam.zoom).toBeCloseTo 2 wop = x: 5, y: 4, z: 6 * cam.y2z # like x: 5, y: 10 out of world width: 20, height: 10 diff --git a/test/server/common.coffee b/test/server/common.coffee index d88fa21e8..dc01d8389 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -3,13 +3,19 @@ console.log 'IT BEGINS' - +require('jasmine-spec-reporter') +jasmine.getEnv().reporter.subReporters_ = [] +jasmine.getEnv().addReporter(new jasmine.SpecReporter({ + displaySuccessfulSpec: true, + displayFailedSpec: true + })) GLOBAL._ = require('lodash') _.str = require('underscore.string') _.mixin(_.str.exports()) GLOBAL.mongoose = require 'mongoose' mongoose.connect('mongodb://localhost/coco_unittest') path = require('path') +GLOBAL.testing = true models_path = [ '../../server/articles/Article' @@ -19,6 +25,7 @@ models_path = [ '../../server/levels/sessions/LevelSession' '../../server/levels/thangs/LevelThangType' '../../server/users/User' + '../../server/patches/Patch' ] for m in models_path @@ -62,13 +69,13 @@ GLOBAL.unittest = {} unittest.users = unittest.users or {} unittest.getNormalJoe = (done, force) -> - unittest.getUser('normal@jo.com', 'food', done, force) + unittest.getUser('Joe', 'normal@jo.com', 'food', done, force) unittest.getOtherSam = (done, force) -> - unittest.getUser('other@sam.com', 'beer', done, force) + unittest.getUser('Sam', 'other@sam.com', 'beer', done, force) unittest.getAdmin = (done, force) -> - unittest.getUser('admin@afc.com', '80yqxpb38j', done, force) + unittest.getUser('Admin', 'admin@afc.com', '80yqxpb38j', done, force) -unittest.getUser = (email, password, done, force) -> +unittest.getUser = (name, email, password, done, force) -> # Creates the user if it doesn't already exist. return done(unittest.users[email]) if unittest.users[email] and not force @@ -78,18 +85,15 @@ unittest.getUser = (email, password, done, force) -> req = request.post(getURL('/db/user'), (err, response, body) -> throw err if err User.findOne({email:email}).exec((err, user) -> - if password is '80yqxpb38j' - user.set('permissions', [ 'admin' ]) - user.save (err) -> - wrapUpGetUser(email, user, done) - else + user.set('permissions', if password is '80yqxpb38j' then [ 'admin' ] else []) + user.set('name', name) + user.save (err) -> wrapUpGetUser(email, user, done) ) ) form = req.form() form.append('email', email) form.append('password', password) - wrapUpGetUser = (email, user, done) -> unittest.users[email] = user diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index 377907180..a3973007b 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -84,3 +84,12 @@ describe '/db/article', -> body = JSON.parse(body) expect(body.type).toBeDefined() done() + + it 'does not allow naming an article a reserved word', (done) -> + loginAdmin -> + new_article = {name: 'Search', body:'is a reserved word'} + request.post {uri:url, json:new_article}, (err, res, body) -> + expect(res.statusCode).toBe(422) + done() + + \ No newline at end of file diff --git a/test/server/functional/auth.spec.coffee b/test/server/functional/auth.spec.coffee index 18c3c7fc8..15ef44171 100644 --- a/test/server/functional/auth.spec.coffee +++ b/test/server/functional/auth.spec.coffee @@ -1,5 +1,6 @@ require '../common' request = require 'request' +User = require '../../../server/users/User' urlLogin = getURL('/auth/login') urlReset = getURL('/auth/reset') @@ -16,7 +17,8 @@ describe '/auth/whoami', -> describe '/auth/login', -> it 'clears Users first', (done) -> - User.remove {}, (err) -> + clearModels [User], (err) -> + throw err if err request.get getURL('/auth/whoami'), -> throw err if err done() @@ -55,7 +57,7 @@ describe '/auth/login', -> it 'rejects wrong passwords', (done) -> req = request.post(urlLogin, (error, response) -> expect(response.statusCode).toBe(401) - expect(response.body.indexOf("wrong, wrong")).toBeGreaterThan(-1) + expect(response.body.indexOf("wrong")).toBeGreaterThan(-1) done() ) form = req.form() @@ -96,7 +98,6 @@ describe '/auth/reset', -> it 'resets user password', (done) -> req = request.post(urlReset, (error, response) -> expect(response).toBeDefined() - console.log 'status code is', response.statusCode expect(response.statusCode).toBe(200) expect(response.body).toBeDefined() passwordReset = response.body @@ -135,3 +136,21 @@ describe '/auth/reset', -> form = req.form() form.append('username', 'scott@gmail.com') form.append('password', 'nada') + +describe '/auth/unsubscribe', -> + it 'clears Users first', (done) -> + clearModels [User], (err) -> + throw err if err + request.get getURL('/auth/whoami'), -> + throw err if err + done() + + it 'removes just recruitment emails if you include ?recruitNotes=1', (done) -> + loginJoe (joe) -> + url = getURL('/auth/unsubscribe?recruitNotes=1&email='+joe.get('email')) + request.get url, (error, response) -> + expect(response.statusCode).toBe(200) + user = User.findOne(joe.get('_id')).exec (err, user) -> + expect(user.get('emails').recruitNotes.enabled).toBe(false) + expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy() + done() diff --git a/test/server/functional/file.spec.coffee b/test/server/functional/file.spec.coffee index cbabfb14e..0fb58ffd8 100644 --- a/test/server/functional/file.spec.coffee +++ b/test/server/functional/file.spec.coffee @@ -27,15 +27,6 @@ describe '/file', -> dropGridFS -> done() - it 'can\'t be created by ordinary users.', (done) -> - func = (err, res, body) -> - expect(res.statusCode).toBe(403) - expect(body.metadata).toBeUndefined() - done() - - loginJoe -> - request.post(options, func) - it 'can\'t be created if invalid (property path is required)', (done) -> func = (err, res, body) -> expect(res.statusCode).toBe(422) diff --git a/test/server/functional/level.spec.coffee b/test/server/functional/level.spec.coffee index 13dc6425a..edd163d0d 100644 --- a/test/server/functional/level.spec.coffee +++ b/test/server/functional/level.spec.coffee @@ -6,6 +6,9 @@ describe 'Level', -> name: "King's Peak 3" description: 'Climb a mountain.' permissions: simplePermissions + scripts: [] + thangs: [] + documentation: {specificArticles:[], generalArticles:[]} urlLevel = '/db/level' diff --git a/test/server/functional/level_component.spec.coffee b/test/server/functional/level_component.spec.coffee index 4850d834c..9127ccefd 100644 --- a/test/server/functional/level_component.spec.coffee +++ b/test/server/functional/level_component.spec.coffee @@ -3,11 +3,14 @@ require '../common' describe 'LevelComponent', -> component = - name:'Bashes Everything' + name:'BashesEverything' description:'Makes the unit uncontrollably bash anything bashable, using the bash system.' code: 'bash();' - language: 'javascript' + language: 'coffeescript' permissions:simplePermissions + propertyDocumentation: [] + system: 'ai' + dependencies: [] components = {} @@ -45,7 +48,7 @@ describe 'LevelComponent', -> it 'have a unique name.', (done) -> loginAdmin -> request.post {uri:url, json:component}, (err, res, body) -> - expect(res.statusCode).toBe(422) + expect(res.statusCode).toBe(409) done() it 'can be read by an admin.', (done) -> diff --git a/test/server/functional/level_system.spec.coffee b/test/server/functional/level_system.spec.coffee index 32ca61df1..229c3a39d 100644 --- a/test/server/functional/level_system.spec.coffee +++ b/test/server/functional/level_system.spec.coffee @@ -11,6 +11,8 @@ describe 'LevelSystem', -> """ language: 'coffeescript' permissions:simplePermissions + dependencies: [] + propertyDocumentation: [] systems = {} @@ -48,7 +50,7 @@ describe 'LevelSystem', -> it 'have a unique name.', (done) -> loginAdmin -> request.post {uri:url, json:system}, (err, res, body) -> - expect(res.statusCode).toBe(422) + expect(res.statusCode).toBe(409) done() it 'can be read by an admin.', (done) -> diff --git a/test/server/functional/mail.spec.coffee b/test/server/functional/mail.spec.coffee new file mode 100644 index 000000000..0910aab6f --- /dev/null +++ b/test/server/functional/mail.spec.coffee @@ -0,0 +1,38 @@ +require '../common' +mail = require '../../../server/routes/mail' +User = require '../../../server/users/User' + +testPost = + data: + email: 'scott@codecombat.com' + id: '12345678' + merges: + INTERESTS: 'Announcements, Adventurers, Archmages, Scribes, Diplomats, Ambassadors, Artisans' + FNAME: 'Scott' + LNAME: 'Erickson' + +describe 'handleProfileUpdate', -> + it 'updates emails from the data passed in', (done) -> + u = new User() + mail.handleProfileUpdate(u, testPost) + expect(u.isEmailSubscriptionEnabled('generalNews')).toBeTruthy() + expect(u.isEmailSubscriptionEnabled('adventurerNews')).toBeTruthy() + expect(u.isEmailSubscriptionEnabled('archmageNews')).toBeTruthy() + expect(u.isEmailSubscriptionEnabled('scribeNews')).toBeTruthy() + expect(u.isEmailSubscriptionEnabled('diplomatNews')).toBeTruthy() + expect(u.isEmailSubscriptionEnabled('ambassadorNews')).toBeTruthy() + expect(u.isEmailSubscriptionEnabled('artisanNews')).toBeTruthy() + done() + +describe 'handleUnsubscribe', -> + it 'turns off all news and notifications', (done) -> + u = new User({generalNews: {enabled:true}, archmageNews: {enabled:true}, anyNotes: {enabled:true}}) + mail.handleUnsubscribe(u) + expect(u.isEmailSubscriptionEnabled('generalNews')).toBeFalsy() + expect(u.isEmailSubscriptionEnabled('adventurerNews')).toBeFalsy() + expect(u.isEmailSubscriptionEnabled('archmageNews')).toBeFalsy() + expect(u.isEmailSubscriptionEnabled('scribeNews')).toBeFalsy() + expect(u.isEmailSubscriptionEnabled('diplomatNews')).toBeFalsy() + expect(u.isEmailSubscriptionEnabled('ambassadorNews')).toBeFalsy() + expect(u.isEmailSubscriptionEnabled('artisanNews')).toBeFalsy() + done() diff --git a/test/server/functional/patch.spec.coffee b/test/server/functional/patch.spec.coffee new file mode 100644 index 000000000..9d0f4265d --- /dev/null +++ b/test/server/functional/patch.spec.coffee @@ -0,0 +1,119 @@ +require '../common' + +describe '/db/patch', -> + request = require 'request' + it 'clears the db first', (done) -> + clearModels [User, Article, Patch], (err) -> + throw err if err + done() + + article = {name: 'Yo', body:'yo ma'} + articleURL = getURL('/db/article') + articles = {} + + patchURL = getURL('/db/patch') + patches = {} + patch = + commitMessage: 'Accept this patch!' + delta: {name:['test']} + editPath: '/who/knows/yes' + target: + id:null + collection: 'article' + + it 'creates an Article to patch', (done) -> + loginAdmin -> + request.post {uri:articleURL, json:article}, (err, res, body) -> + articles[0] = body + patch.target.id = articles[0]._id + done() + + it "allows someone to submit a patch to something they don't control", (done) -> + loginJoe (joe) -> + request.post {uri: patchURL, json: patch}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.target.original).toBeDefined() + expect(body.target.version.major).toBeDefined() + expect(body.target.version.minor).toBeDefined() + expect(body.status).toBe('pending') + expect(body.created).toBeDefined() + expect(body.creator).toBe(joe.id) + patches[0] = body + done() + + it 'adds a patch to the target document', (done) -> + Article.findOne({}).exec (err, article) -> + expect(article.toObject().patches[0]).toBeDefined() + done() + + it 'shows up in patch requests', (done) -> + patchesURL = getURL("/db/article/#{articles[0]._id}/patches") + request.get {uri: patchesURL}, (err, res, body) -> + body = JSON.parse(body) + expect(res.statusCode).toBe(200) + expect(body.length).toBe(1) + done() + + it 'allows you to set yourself as watching', (done) -> + watchingURL = getURL("/db/article/#{articles[0]._id}/watch") + request.put {uri: watchingURL, json: {on:true}}, (err, res, body) -> + expect(body.watchers[1]).toBeDefined() + done() + + it 'added the watcher to the target document', (done) -> + Article.findOne({}).exec (err, article) -> + expect(article.toObject().watchers[1]).toBeDefined() + done() + + it 'does not add duplicate watchers', (done) -> + watchingURL = getURL("/db/article/#{articles[0]._id}/watch") + request.put {uri: watchingURL, json: {on:true}}, (err, res, body) -> + expect(body.watchers.length).toBe(2) + done() + + it 'allows removing yourself', (done) -> + watchingURL = getURL("/db/article/#{articles[0]._id}/watch") + request.put {uri: watchingURL, json: {on:false}}, (err, res, body) -> + expect(body.watchers.length).toBe(1) + done() + + it 'allows the submitter to withdraw the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + request.put {uri: statusURL, json: {status:'withdrawn'}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'withdrawn' + Article.findOne({}).exec (err, article) -> + expect(article.toObject().patches.length).toBe(0) + done() + + it 'does not allow the submitter to reject or accept the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + request.put {uri: statusURL, json: {status:'rejected'}}, (err, res, body) -> + expect(res.statusCode).toBe(403) + request.put {uri: statusURL, json: {status:'accepted'}}, (err, res, body) -> + expect(res.statusCode).toBe(403) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'withdrawn' + done() + + it 'allows the recipient to accept or reject the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + loginAdmin -> + request.put {uri: statusURL, json: {status:'rejected'}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'rejected' + request.put {uri: statusURL, json: {status:'accepted'}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'accepted' + done() + + it 'does not allow the recipient to withdraw the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + request.put {uri: statusURL, json: {status:'withdrawn'}}, (err, res, body) -> + expect(res.statusCode).toBe(403) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'accepted' + done() \ No newline at end of file diff --git a/test/server/functional/user.spec.coffee b/test/server/functional/user.spec.coffee index 29e1360a9..03a88242d 100644 --- a/test/server/functional/user.spec.coffee +++ b/test/server/functional/user.spec.coffee @@ -1,10 +1,63 @@ require '../common' request = require 'request' +User = require '../../../server/users/User' urlUser = '/db/user' +describe 'Server user object', -> + + it 'uses the schema defaults to fill in email preferences', (done) -> + user = new User() + expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy() + expect(user.isEmailSubscriptionEnabled('anyNotes')).toBeTruthy() + expect(user.isEmailSubscriptionEnabled('recruitNotes')).toBeTruthy() + expect(user.isEmailSubscriptionEnabled('archmageNews')).toBeFalsy() + done() + + it 'uses old subs if they\'re around', (done) -> + user = new User() + user.set 'emailSubscriptions', ['tester'] + expect(user.isEmailSubscriptionEnabled('adventurerNews')).toBeTruthy() + expect(user.isEmailSubscriptionEnabled('generalNews')).toBeFalsy() + done() + + it 'maintains the old subs list if it\'s around', (done) -> + user = new User() + user.set 'emailSubscriptions', ['tester'] + user.setEmailSubscription('artisanNews', true) + expect(JSON.stringify(user.get('emailSubscriptions'))).toBe(JSON.stringify(['tester','level_creator'])) + done() + +describe 'User.updateMailChimp', -> + makeMC = (callback) -> + GLOBAL.mc = + lists: + subscribe: callback + + it 'uses emails to determine what to send to MailChimp', (done) -> + makeMC (params) -> + expect(JSON.stringify(params.merge_vars.groupings[0].groups)).toBe(JSON.stringify(['Announcements'])) + done() + + user = new User({emailSubscriptions:['announcement'], email:'tester@gmail.com'}) + User.updateMailChimp(user) + describe 'POST /db/user', -> + createAnonNameUser = (done)-> + request.post getURL('/auth/logout'), -> + request.get getURL('/auth/whoami'), -> + req = request.post(getURL('/db/user'), (err, response) -> + expect(response.statusCode).toBe(200) + request.get getURL('/auth/whoami'), (request, response, body) -> + res = JSON.parse(response.body) + expect(res.anonymous).toBeTruthy() + expect(res.name).toEqual('Jim') + done() + ) + form = req.form() + form.append('name', 'Jim') + it 'preparing test : clears the db first', (done) -> clearModels [User], (err) -> throw err if err @@ -51,6 +104,36 @@ describe 'POST /db/user', -> expect(user.passwordHash).toBeUndefined() done() + it 'should allow setting anonymous user name', (done) -> + createAnonNameUser(done) + + it 'should allow multiple anonymous users with same name', (done) -> + createAnonNameUser(done) + + + it 'should not allow setting existing user name to anonymous user', (done) -> + + createAnonUser = -> + request.post getURL('/auth/logout'), -> + request.get getURL('/auth/whoami'), -> + req = request.post(getURL('/db/user'), (err, response) -> + expect(response.statusCode).toBe(409) + done() + ) + form = req.form() + form.append('name', 'Jim') + + req = request.post(getURL('/db/user'), (err,response,body) -> + expect(response.statusCode).toBe(200) + request.get getURL('/auth/whoami'), (request, response, body) -> + res = JSON.parse(response.body) + expect(res.anonymous).toBeFalsy() + createAnonUser() + ) + form = req.form() + form.append('email', 'new@user.com') + form.append('password', 'new') + describe 'PUT /db/user', -> @@ -108,11 +191,20 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl form.append('_id', String(admin._id)) form.append('email', joe.get('email').toUpperCase()) - it 'works', (done) -> + it 'does not care if you include your existing name', (done) -> + unittest.getNormalJoe (joe) -> + req = request.put getURL(urlUser+'/'+joe._id), (err, res) -> + expect(res.statusCode).toBe(200) + done() + form = req.form() + form.append('_id', String(joe._id)) + form.append('name', 'Joe') + + it 'accepts name and email changes', (done) -> unittest.getNormalJoe (joe) -> req = request.put getURL(urlUser), (err, res) -> expect(res.statusCode).toBe(200) - unittest.getUser('New@email.com', 'null', (joe) -> + unittest.getUser('Wilhelm', 'New@email.com', 'null', (joe) -> expect(joe.get('name')).toBe('Wilhelm') expect(joe.get('emailLower')).toBe('new@email.com') expect(joe.get('email')).toBe('New@email.com') @@ -122,7 +214,6 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl form.append('email', 'New@email.com') form.append('name', 'Wilhelm') - describe 'GET /db/user', -> it 'logs in as admin', (done) -> diff --git a/vendor/scripts/antiscroll.js b/vendor/scripts/antiscroll.js deleted file mode 100644 index 2de1812d6..000000000 --- a/vendor/scripts/antiscroll.js +++ /dev/null @@ -1,471 +0,0 @@ -(function ($) { - - /** - * Augment jQuery prototype. - */ - - $.fn.antiscroll = function (options) { - return this.each(function () { - if ($(this).data('antiscroll')) { - $(this).data('antiscroll').destroy(); - } - - $(this).data('antiscroll', new $.Antiscroll(this, options)); - }); - }; - - /** - * Expose constructor. - */ - - $.Antiscroll = Antiscroll; - - /** - * Antiscroll pane constructor. - * - * @param {Element|jQuery} main pane - * @parma {Object} options - * @api public - */ - - function Antiscroll (el, opts) { - this.el = $(el); - this.options = opts || {}; - - this.x = (false !== this.options.x) || this.options.forceHorizontal; - this.y = (false !== this.options.y) || this.options.forceVertical; - this.autoHide = false !== this.options.autoHide; - this.padding = undefined == this.options.padding ? 2 : this.options.padding; - - this.inner = this.el.find('.antiscroll-inner'); - this.inner.css({ - 'width': '+=' + (this.y ? scrollbarSize() : 0) - , 'height': '+=' + (this.x ? scrollbarSize() : 0) - }); - - this.refresh(); - }; - - /** - * refresh scrollbars - * - * @api public - */ - - Antiscroll.prototype.refresh = function() { - var needHScroll = this.inner.get(0).scrollWidth > this.el.width() + (this.y ? scrollbarSize() : 0), - needVScroll = this.inner.get(0).scrollHeight > this.el.height() + (this.x ? scrollbarSize() : 0); - - if (this.x) { - if (!this.horizontal && needHScroll) { - this.horizontal = new Scrollbar.Horizontal(this); - } else if (this.horizontal && !needHScroll) { - this.horizontal.destroy(); - this.horizontal = null; - } else if (this.horizontal) { - this.horizontal.update(); - } - } - - if (this.y) { - if (!this.vertical && needVScroll) { - this.vertical = new Scrollbar.Vertical(this); - } else if (this.vertical && !needVScroll) { - this.vertical.destroy(); - this.vertical = null; - } else if (this.vertical) { - this.vertical.update(); - } - } - }; - - /** - * Cleans up. - * - * @return {Antiscroll} for chaining - * @api public - */ - - Antiscroll.prototype.destroy = function () { - if (this.horizontal) { - this.horizontal.destroy(); - this.horizontal = null - } - if (this.vertical) { - this.vertical.destroy(); - this.vertical = null - } - return this; - }; - - /** - * Rebuild Antiscroll. - * - * @return {Antiscroll} for chaining - * @api public - */ - - Antiscroll.prototype.rebuild = function () { - this.destroy(); - this.inner.attr('style', ''); - Antiscroll.call(this, this.el, this.options); - return this; - }; - - /** - * Scrollbar constructor. - * - * @param {Element|jQuery} element - * @api public - */ - - function Scrollbar (pane) { - this.pane = pane; - this.pane.el.append(this.el); - this.innerEl = this.pane.inner.get(0); - - this.dragging = false; - this.enter = false; - this.shown = false; - - // hovering - this.pane.el.mouseenter($.proxy(this, 'mouseenter')); - this.pane.el.mouseleave($.proxy(this, 'mouseleave')); - - // dragging - this.el.mousedown($.proxy(this, 'mousedown')); - - // scrolling - this.innerPaneScrollListener = $.proxy(this, 'scroll'); - this.pane.inner.scroll(this.innerPaneScrollListener); - - // wheel -optional- - this.innerPaneMouseWheelListener = $.proxy(this, 'mousewheel'); - this.pane.inner.bind('mousewheel', this.innerPaneMouseWheelListener); - - // show - var initialDisplay = this.pane.options.initialDisplay; - - if (initialDisplay !== false) { - this.show(); - if (this.pane.autoHide) { - this.hiding = setTimeout($.proxy(this, 'hide'), parseInt(initialDisplay, 10) || 3000); - } - } - }; - - /** - * Cleans up. - * - * @return {Scrollbar} for chaining - * @api public - */ - - Scrollbar.prototype.destroy = function () { - this.el.remove(); - this.pane.inner.unbind('scroll', this.innerPaneScrollListener); - this.pane.inner.unbind('mousewheel', this.innerPaneMouseWheelListener); - return this; - }; - - /** - * Called upon mouseenter. - * - * @api private - */ - - Scrollbar.prototype.mouseenter = function () { - this.enter = true; - this.show(); - }; - - /** - * Called upon mouseleave. - * - * @api private - */ - - Scrollbar.prototype.mouseleave = function () { - this.enter = false; - - if (!this.dragging) { - if (this.pane.autoHide) { - this.hide(); - } - } - }; - - /** - * Called upon wrap scroll. - * - * @api private - */ - - Scrollbar.prototype.scroll = function () { - if (!this.shown) { - this.show(); - if (!this.enter && !this.dragging) { - if (this.pane.autoHide) { - this.hiding = setTimeout($.proxy(this, 'hide'), 1500); - } - } - } - - this.update(); - }; - - /** - * Called upon scrollbar mousedown. - * - * @api private - */ - - Scrollbar.prototype.mousedown = function (ev) { - ev.preventDefault(); - - this.dragging = true; - - this.startPageY = ev.pageY - parseInt(this.el.css('top'), 10); - this.startPageX = ev.pageX - parseInt(this.el.css('left'), 10); - - // prevent crazy selections on IE - this.el[0].ownerDocument.onselectstart = function () { return false; }; - - var pane = this.pane, - move = $.proxy(this, 'mousemove'), - self = this - - $(this.el[0].ownerDocument) - .mousemove(move) - .mouseup(function () { - self.dragging = false; - this.onselectstart = null; - - $(this).unbind('mousemove', move); - - if (!self.enter) { - self.hide(); - } - }); - }; - - /** - * Show scrollbar. - * - * @api private - */ - - Scrollbar.prototype.show = function (duration) { - if (!this.shown && this.update()) { - this.el.addClass('antiscroll-scrollbar-shown'); - if (this.hiding) { - clearTimeout(this.hiding); - this.hiding = null; - } - this.shown = true; - } - }; - - /** - * Hide scrollbar. - * - * @api private - */ - - Scrollbar.prototype.hide = function () { - if (this.pane.autoHide !== false && this.shown) { - // check for dragging - this.el.removeClass('antiscroll-scrollbar-shown'); - this.shown = false; - } - }; - - /** - * Horizontal scrollbar constructor - * - * @api private - */ - - Scrollbar.Horizontal = function (pane) { - this.el = $('
', pane.el); - Scrollbar.call(this, pane); - }; - - /** - * Inherits from Scrollbar. - */ - - inherits(Scrollbar.Horizontal, Scrollbar); - - /** - * Updates size/position of scrollbar. - * - * @api private - */ - - Scrollbar.Horizontal.prototype.update = function () { - var paneWidth = this.pane.el.width(), - trackWidth = paneWidth - this.pane.padding * 2, - innerEl = this.pane.inner.get(0) - - this.el - .css('width', trackWidth * paneWidth / innerEl.scrollWidth) - .css('left', trackWidth * innerEl.scrollLeft / innerEl.scrollWidth); - - return paneWidth < innerEl.scrollWidth; - }; - - /** - * Called upon drag. - * - * @api private - */ - - Scrollbar.Horizontal.prototype.mousemove = function (ev) { - var trackWidth = this.pane.el.width() - this.pane.padding * 2, - pos = ev.pageX - this.startPageX, - barWidth = this.el.width(), - innerEl = this.pane.inner.get(0) - - // minimum top is 0, maximum is the track height - var y = Math.min(Math.max(pos, 0), trackWidth - barWidth); - - innerEl.scrollLeft = (innerEl.scrollWidth - this.pane.el.width()) - * y / (trackWidth - barWidth); - }; - - /** - * Called upon container mousewheel. - * - * @api private - */ - - Scrollbar.Horizontal.prototype.mousewheel = function (ev, delta, x, y) { - if ((x < 0 && 0 == this.pane.inner.get(0).scrollLeft) || - (x > 0 && (this.innerEl.scrollLeft + Math.ceil(this.pane.el.width()) - == this.innerEl.scrollWidth))) { - ev.preventDefault(); - return false; - } - }; - - /** - * Vertical scrollbar constructor - * - * @api private - */ - - Scrollbar.Vertical = function (pane) { - this.el = $('
', pane.el); - Scrollbar.call(this, pane); - }; - - /** - * Inherits from Scrollbar. - */ - - inherits(Scrollbar.Vertical, Scrollbar); - - /** - * Updates size/position of scrollbar. - * - * @api private - */ - - Scrollbar.Vertical.prototype.update = function () { - var paneHeight = this.pane.el.height(), - trackHeight = paneHeight - this.pane.padding * 2, - innerEl = this.innerEl; - - var scrollbarHeight = trackHeight * paneHeight / innerEl.scrollHeight; - scrollbarHeight = scrollbarHeight < 20 ? 20 : scrollbarHeight; - - var topPos = trackHeight * innerEl.scrollTop / innerEl.scrollHeight; - - if((topPos + scrollbarHeight) > trackHeight) { - var diff = (topPos + scrollbarHeight) - trackHeight; - topPos = topPos - diff - 3; - } - - this.el - .css('height', scrollbarHeight) - .css('top', topPos); - - return paneHeight < innerEl.scrollHeight; - }; - - /** - * Called upon drag. - * - * @api private - */ - - Scrollbar.Vertical.prototype.mousemove = function (ev) { - var paneHeight = this.pane.el.height(), - trackHeight = paneHeight - this.pane.padding * 2, - pos = ev.pageY - this.startPageY, - barHeight = this.el.height(), - innerEl = this.innerEl - - // minimum top is 0, maximum is the track height - var y = Math.min(Math.max(pos, 0), trackHeight - barHeight); - - innerEl.scrollTop = (innerEl.scrollHeight - paneHeight) - * y / (trackHeight - barHeight); - }; - - /** - * Called upon container mousewheel. - * - * @api private - */ - - Scrollbar.Vertical.prototype.mousewheel = function (ev, delta, x, y) { - if ((y > 0 && 0 == this.innerEl.scrollTop) || - (y < 0 && (this.innerEl.scrollTop + Math.ceil(this.pane.el.height()) - == this.innerEl.scrollHeight))) { - ev.preventDefault(); - return false; - } - }; - - /** - * Cross-browser inheritance. - * - * @param {Function} constructor - * @param {Function} constructor we inherit from - * @api private - */ - - function inherits (ctorA, ctorB) { - function f() {}; - f.prototype = ctorB.prototype; - ctorA.prototype = new f; - }; - - /** - * Scrollbar size detection. - */ - - var size; - - function scrollbarSize () { - if (size === undefined) { - var div = $( - '
' - + '
' - ); - - $('body').append(div); - var w1 = $(div).innerWidth(); - var w2 = $('div', div).innerWidth(); - $(div).remove(); - - size = w1 - w2; - } - - return size; - }; - -})(jQuery); diff --git a/vendor/scripts/async.js b/vendor/scripts/async.js new file mode 100644 index 000000000..2bb5395db --- /dev/null +++ b/vendor/scripts/async.js @@ -0,0 +1,1043 @@ +/*jshint onevar: false, indent:4 */ +/*global setImmediate: false, setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root, previous_async; + + root = this; + if (root != null) { + previous_async = root.async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + function only_once(fn) { + var called = false; + return function() { + if (called) throw new Error("Callback was already called."); + called = true; + fn.apply(root, arguments); + } + } + + //// cross-browser compatiblity functions //// + + var _toString = Object.prototype.toString; + + var _isArray = Array.isArray || function (obj) { + return _toString.call(obj) === '[object Array]'; + }; + + var _each = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _each(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _each(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + async.nextTick = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + async.setImmediate = async.nextTick; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + } + else { + async.setImmediate = async.nextTick; + } + } + + async.each = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _each(arr, function (x) { + iterator(x, only_once(done) ); + }); + function done(err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(); + } + } + } + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + var remainingTasks = keys.length + if (!remainingTasks) { + return callback(); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + remainingTasks-- + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (!remainingTasks) { + var theCallback = callback; + // prevent final callback from calling itself if it errors + callback = function () {}; + + theCallback(null, results); + } + }); + + _each(keys, function (k) { + var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.retry = function(times, task, callback) { + var DEFAULT_TIMES = 5; + var attempts = []; + // Use defaults if times not passed + if (typeof times === 'function') { + callback = task; + task = times; + times = DEFAULT_TIMES; + } + // Make sure times is a number + times = parseInt(times, 10) || DEFAULT_TIMES; + var wrappedTask = function(wrappedCallback, wrappedResults) { + var retryAttempt = function(task, finalAttempt) { + return function(seriesCallback) { + task(function(err, result){ + seriesCallback(!err || finalAttempt, {err: err, result: result}); + }, wrappedResults); + }; + }; + while (times) { + attempts.push(retryAttempt(task, !(times-=1))); + } + async.series(attempts, function(done, data){ + data = data[data.length - 1]; + (wrappedCallback || callback)(data.err, data.result); + }); + } + // If a callback is passed, run this as a controll flow + return callback ? wrappedTask() : wrappedTask + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (!_isArray(tasks)) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (_isArray(tasks)) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (_isArray(tasks)) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + var args = Array.prototype.slice.call(arguments, 1); + if (test.apply(null, args)) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + var args = Array.prototype.slice.call(arguments, 1); + if (!test.apply(null, args)) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if (!q.started){ + q.started = true; + } + if (!_isArray(data)) { + data = [data]; + } + if(data.length == 0) { + // call drain immediately if there are no tasks + return async.setImmediate(function() { + if (q.drain) { + q.drain(); + } + }); + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === q.concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + started: false, + paused: false, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + kill: function () { + q.drain = null; + q.tasks = []; + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (!q.paused && workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + }, + idle: function() { + return q.tasks.length + workers === 0; + }, + pause: function () { + if (q.paused === true) { return; } + q.paused = true; + q.process(); + }, + resume: function () { + if (q.paused === false) { return; } + q.paused = false; + q.process(); + } + }; + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + drained: true, + push: function (data, callback) { + if (!_isArray(data)) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + cargo.drained = false; + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain && !cargo.drained) cargo.drain(); + cargo.drained = true; + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0, tasks.length); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + async.nextTick(function () { + callback.apply(null, memo[key]); + }); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.seq = function (/* functions... */) { + var fns = arguments; + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + async.compose = function (/* functions... */) { + return async.seq.apply(null, Array.prototype.reverse.call(arguments)); + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // Node.js + if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // AMD / RequireJS + else if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // included directly via