Merge branch 'master' into production

This commit is contained in:
phoenixeliot 2016-07-21 10:21:20 -07:00
commit 6e0fde23ab
141 changed files with 2412 additions and 2035 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,164 @@
// TODO: don't serve this script from codecombat.com; serve it from a harmless extra domain we don't have yet.
window.addEventListener('message', receiveMessage, false);
var concreteDom;
var virtualDom;
var goalStates;
var allowedOrigins = [
/https:\/\/codecombat\.com/,
/http:\/\/localhost:3000/,
/http:\/\/direct\.codecombat\.com/,
/http:\/\/staging\.codecombat\.com/,
/http:\/\/next\.codecombat\.com/,
/http:\/\/.*codecombat-staging-codecombat\.runnableapp\.com/,
];
function receiveMessage(event) {
var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
var allowed = false;
allowedOrigins.forEach(function(pattern) {
allowed = allowed || pattern.test(origin);
});
if (!allowed) {
console.log('Ignoring message from bad origin:', origin);
return;
}
var data = event.data;
var source = event.source;
switch (data.type) {
case 'create':
create(_.pick(data, 'dom', 'styles', 'scripts'));
checkGoals(data.goals, source, origin);
break;
case 'update':
if (virtualDom)
update(_.pick(data, 'dom', 'styles', 'scripts'));
else
create(_.pick(data, 'dom', 'styles', 'scripts'));
checkGoals(data.goals, source, origin);
break;
case 'log':
console.log(data.text);
break;
default:
console.log('Unknown message type:', data.type);
}
}
function create({ dom, styles, scripts }) {
virtualDom = dom;
virtualStyles = styles;
virtualScripts = scripts;
concreteDom = deku.dom.create(dom);
concreteStyles = deku.dom.create(styles);
concreteScripts = deku.dom.create(scripts);
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
// TODO: :after elements don't seem to work? (:before do)
$('body').first().empty().append(concreteDom);
$('#player-styles').first().empty().append(concreteStyles);
$('#player-scripts').first().empty().append(concreteScripts);
}
function update({ dom, styles, scripts }) {
function dispatch() {} // Might want to do something here in the future
var context = {}; // Might want to use this to send shared state to every component
var domChanges = deku.diff.diffNode(virtualDom, dom);
domChanges.reduce(deku.dom.update(dispatch, context), concreteDom); // Rerender
var scriptChanges = deku.diff.diffNode(virtualScripts, scripts);
scriptChanges.reduce(deku.dom.update(dispatch, context), concreteScripts); // Rerender
var styleChanges = deku.diff.diffNode(virtualStyles, styles);
styleChanges.reduce(deku.dom.update(dispatch, context), concreteStyles); // Rerender
virtualDom = dom;
virtualStyles = styles;
virtualScripts = scripts;
}
function checkGoals(goals, source, origin) {
// Check right now and also in one second, since our 1-second CSS transition might be affecting things until it is done.
doCheckGoals(goals, source, origin);
_.delay(function() { doCheckGoals(goals, source, origin); }, 1001);
}
function doCheckGoals(goals, source, origin) {
var newGoalStates = {};
var overallSuccess = true;
goals.forEach(function(goal) {
var $result = $(goal.html.selector);
//console.log('ran selector', goal.html.selector, 'to find element(s)', $result);
var success = true;
goal.html.valueChecks.forEach(function(check) {
//console.log(' ... and should make sure that the value of', check.eventProps, 'is', _.omit(check, 'eventProps'), '?', matchesCheck($result, check))
success = success && matchesCheck($result, check);
});
overallSuccess = overallSuccess && success;
newGoalStates[goal.id] = {status: success ? 'success' : 'incomplete'}; // No 'failure' state
});
if (!_.isEqual(newGoalStates, goalStates)) {
goalStates = newGoalStates;
var overallStatus = overallSuccess ? 'success' : null; // Can't really get to 'failure', just 'incomplete', which is represented by null here
source.postMessage({type: 'goals-updated', goalStates: goalStates, overallStatus: overallStatus}, origin);
}
}
function downTheChain(obj, keyChain) {
if (!obj)
return null;
if (!_.isArray(keyChain))
return obj[keyChain];
var value = obj;
while (keyChain.length && value) {
if (keyChain[0].match(/\(.*\)$/)) {
var args, argsString = keyChain[0].match(/\((.*)\)$/)[1];
if (argsString)
args = eval(argsString).split(/, ?/g).filter(function(x) { return x !== ''; }); // TODO: can/should we avoid eval here?
else
args = [];
value = value[keyChain[0].split('(')[0]].apply(value, args); // value.text(), value.css('background-color'), etc.
}
else
value = value[keyChain[0]];
keyChain = keyChain.slice(1);
}
return value;
};
function matchesCheck(value, check) {
var v = downTheChain(value, check.eventProps);
if ((check.equalTo != null) && v !== check.equalTo) {
return false;
}
if ((check.notEqualTo != null) && v === check.notEqualTo) {
return false;
}
if ((check.greaterThan != null) && !(v > check.greaterThan)) {
return false;
}
if ((check.greaterThanOrEqualTo != null) && !(v >= check.greaterThanOrEqualTo)) {
return false;
}
if ((check.lessThan != null) && !(v < check.lessThan)) {
return false;
}
if ((check.lessThanOrEqualTo != null) && !(v <= check.lessThanOrEqualTo)) {
return false;
}
if ((check.containingString != null) && (!v || v.search(check.containingString) === -1)) {
return false;
}
if ((check.notContainingString != null) && (v != null ? v.search(check.notContainingString) : void 0) !== -1) {
return false;
}
if ((check.containingRegexp != null) && (!v || v.search(new RegExp(check.containingRegexp)) === -1)) {
return false;
}
if ((check.notContainingRegexp != null) && (v != null ? v.search(new RegExp(check.notContainingRegexp)) : void 0) !== -1) {
return false;
}
return true;
}

View file

@ -19,6 +19,7 @@ var languagesImported = {};
var ensureLanguageImported = function(language) { var ensureLanguageImported = function(language) {
if (languagesImported[language]) return; if (languagesImported[language]) return;
if (language === 'html') return;
importScripts("/javascripts/app/vendor/aether-" + language + ".js"); importScripts("/javascripts/app/vendor/aether-" + language + ".js");
languagesImported[language] = true; languagesImported[language] = true;
}; };

View file

@ -80,7 +80,7 @@ var myImportScripts = importScripts;
var languagesImported = {}; var languagesImported = {};
var ensureLanguageImported = function(language) { var ensureLanguageImported = function(language) {
if (languagesImported[language]) return; if (languagesImported[language]) return;
if (language === 'javascript') return; // Only has JSHint, but we don't need to lint here. if (language === 'javascript' || language === 'html') return; // Only has JSHint, but we don't need to lint here.
myImportScripts("/javascripts/app/vendor/aether-" + language + ".js"); myImportScripts("/javascripts/app/vendor/aether-" + language + ".js");
languagesImported[language] = true; languagesImported[language] = true;
}; };

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>My CodeCombat Website</title>
<script src="https://cdn.jsdelivr.net/lodash/4.13.1/lodash.min.js" integrity="sha256-8SeyqJ7ZAZx8WnIgP/bgK6LGIjKjhojNPHSMV/fo29Y=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="/javascripts/web-dev-listener.js"></script>
<script src="/javascripts/app/vendor/aether-html.js"></script>
<style>
* {
transition: 1s ease-in-out;
}
</style>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
<!-- Extracted player/level styles and scripts -->
<style id="player-styles">
</style>
<script id="player-scripts">
</script>
</head>
<body>
<h1>Loading...</h1>
</body>
</html>

View file

@ -1,6 +0,0 @@
module.exports = class RealTimeCollection extends Backbone.Firebase.Collection
constructor: (savePath) ->
# TODO: Don't hard code this here
# TODO: Use prod path in prod
@firebase = 'https://codecombat.firebaseio.com/test/db/' + savePath
super()

View file

@ -245,6 +245,12 @@ particleKinds['level-dungeon-game-dev'] = particleKinds['level-dungeon-game-dev-
colorMiddle: hsl 0.7, 0.75, 0.5 colorMiddle: hsl 0.7, 0.75, 0.5
colorEnd: hsl 0.7, 0.75, 0.3 colorEnd: hsl 0.7, 0.75, 0.3
particleKinds['level-dungeon-web-dev'] = particleKinds['level-dungeon-web-dev-premium'] = ext particleKinds['level-dungeon-hero-ladder'],
emitter:
colorStart: hsl 0.7, 0.25, 0.7
colorMiddle: hsl 0.7, 0.25, 0.5
colorEnd: hsl 0.7, 0.25, 0.3
particleKinds['level-dungeon-premium-item'] = ext particleKinds['level-dungeon-gate'], particleKinds['level-dungeon-premium-item'] = ext particleKinds['level-dungeon-gate'],
emitter: emitter:
particleCount: 2000 particleCount: 2000
@ -300,6 +306,12 @@ particleKinds['level-forest-game-dev'] = particleKinds['level-forest-game-dev-pr
colorMiddle: hsl 0.7, 0.75, 0.5 colorMiddle: hsl 0.7, 0.75, 0.5
colorEnd: hsl 0.7, 0.75, 0.3 colorEnd: hsl 0.7, 0.75, 0.3
particleKinds['level-forest-web-dev'] = particleKinds['level-forest-web-dev-premium'] = ext particleKinds['level-forest-hero-ladder'],
emitter:
colorStart: hsl 0.7, 0.25, 0.7
colorMiddle: hsl 0.7, 0.25, 0.5
colorEnd: hsl 0.7, 0.25, 0.3
particleKinds['level-forest-premium-item'] = ext particleKinds['level-forest-gate'], particleKinds['level-forest-premium-item'] = ext particleKinds['level-forest-gate'],
emitter: emitter:
particleCount: 2000 particleCount: 2000
@ -355,6 +367,12 @@ particleKinds['level-desert-game-dev'] = particleKinds['level-desert-game-dev-pr
colorMiddle: hsl 0.7, 0.75, 0.5 colorMiddle: hsl 0.7, 0.75, 0.5
colorEnd: hsl 0.7, 0.75, 0.3 colorEnd: hsl 0.7, 0.75, 0.3
particleKinds['level-desert-web-dev'] = particleKinds['level-desert-web-dev-premium'] = ext particleKinds['level-desert-hero-ladder'],
emitter:
colorStart: hsl 0.7, 0.25, 0.7
colorMiddle: hsl 0.7, 0.25, 0.5
colorEnd: hsl 0.7, 0.25, 0.3
particleKinds['level-mountain-premium-hero'] = ext particleKinds['level-mountain-premium'], particleKinds['level-mountain-premium-hero'] = ext particleKinds['level-mountain-premium'],
emitter: emitter:
particleCount: 200 particleCount: 200
@ -395,6 +413,12 @@ particleKinds['level-mountain-game-dev'] = particleKinds['level-mountain-game-de
colorMiddle: hsl 0.7, 0.75, 0.5 colorMiddle: hsl 0.7, 0.75, 0.5
colorEnd: hsl 0.7, 0.75, 0.3 colorEnd: hsl 0.7, 0.75, 0.3
particleKinds['level-mountain-web-dev'] = particleKinds['level-mountain-web-dev-premium'] = ext particleKinds['level-mountain-hero-ladder'],
emitter:
colorStart: hsl 0.7, 0.25, 0.7
colorMiddle: hsl 0.7, 0.25, 0.5
colorEnd: hsl 0.7, 0.25, 0.3
particleKinds['level-glacier-premium-hero'] = ext particleKinds['level-glacier-premium'], particleKinds['level-glacier-premium-hero'] = ext particleKinds['level-glacier-premium'],
emitter: emitter:
particleCount: 200 particleCount: 200
@ -435,6 +459,12 @@ particleKinds['level-glacier-game-dev'] = particleKinds['level-glacier-game-dev-
colorMiddle: hsl 0.7, 0.75, 0.5 colorMiddle: hsl 0.7, 0.75, 0.5
colorEnd: hsl 0.7, 0.75, 0.3 colorEnd: hsl 0.7, 0.75, 0.3
particleKinds['level-glacier-web-dev'] = particleKinds['level-glacier-web-dev-premium'] = ext particleKinds['level-glacier-hero-ladder'],
emitter:
colorStart: hsl 0.7, 0.25, 0.7
colorMiddle: hsl 0.7, 0.25, 0.5
colorEnd: hsl 0.7, 0.25, 0.3
particleKinds['level-volcano-premium-hero'] = ext particleKinds['level-volcano-premium'], particleKinds['level-volcano-premium-hero'] = ext particleKinds['level-volcano-premium'],
emitter: emitter:
particleCount: 200 particleCount: 200
@ -474,3 +504,9 @@ particleKinds['level-volcano-game-dev'] = particleKinds['level-volcano-game-dev-
colorStart: hsl 0.7, 0.75, 0.7 colorStart: hsl 0.7, 0.75, 0.7
colorMiddle: hsl 0.7, 0.75, 0.5 colorMiddle: hsl 0.7, 0.75, 0.5
colorEnd: hsl 0.7, 0.75, 0.3 colorEnd: hsl 0.7, 0.75, 0.3
particleKinds['level-volcano-web-dev'] = particleKinds['level-volcano-web-dev-premium'] = ext particleKinds['level-volcano-hero-ladder'],
emitter:
colorStart: hsl 0.7, 0.25, 0.7
colorMiddle: hsl 0.7, 0.25, 0.5
colorEnd: hsl 0.7, 0.25, 0.3

View file

@ -126,13 +126,13 @@ module.exports = class CocoRouter extends Backbone.Router
'legal': go('LegalView') 'legal': go('LegalView')
'multiplayer': go('MultiplayerView')
'play(/)': go('play/CampaignView') # extra slash is to get Facebook app to work 'play(/)': go('play/CampaignView') # extra slash is to get Facebook app to work
'play/ladder/:levelID/:leagueType/:leagueID': go('ladder/LadderView') 'play/ladder/:levelID/:leagueType/:leagueID': go('ladder/LadderView')
'play/ladder/:levelID': go('ladder/LadderView') 'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView') 'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView') 'play/level/:levelID': go('play/level/PlayLevelView')
'play/game-dev-level/:levelID/:sessionID': go('play/level/PlayGameDevLevelView')
'play/web-dev-level/:levelID/:sessionID': go('play/level/PlayWebDevLevelView')
'play/spectate/:levelID': go('play/SpectateView') 'play/spectate/:levelID': go('play/SpectateView')
'play/:map': go('play/CampaignView') 'play/:map': go('play/CampaignView')
@ -193,7 +193,7 @@ module.exports = class CocoRouter extends Backbone.Router
@listenToOnce application.moduleLoader, 'load-complete', -> @listenToOnce application.moduleLoader, 'load-complete', ->
@routeDirectly(path, args, options) @routeDirectly(path, args, options)
return return
return @openView @notFoundView() if not ViewClass return go('NotFoundView') if not ViewClass
view = new ViewClass(options, args...) # options, then any path fragment args view = new ViewClass(options, args...) # options, then any path fragment args
view.render() view.render()
@openView(view) @openView(view)

View file

@ -8,7 +8,6 @@ channelSchemas =
'errors': require 'schemas/subscriptions/errors' 'errors': require 'schemas/subscriptions/errors'
'ipad': require 'schemas/subscriptions/ipad' 'ipad': require 'schemas/subscriptions/ipad'
'misc': require 'schemas/subscriptions/misc' 'misc': require 'schemas/subscriptions/misc'
'multiplayer': require 'schemas/subscriptions/multiplayer'
'play': require 'schemas/subscriptions/play' 'play': require 'schemas/subscriptions/play'
'surface': require 'schemas/subscriptions/surface' 'surface': require 'schemas/subscriptions/surface'
'tome': require 'schemas/subscriptions/tome' 'tome': require 'schemas/subscriptions/tome'

View file

@ -2,6 +2,7 @@ CocoModel = require 'models/CocoModel'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
{me} = require('core/auth') {me} = require('core/auth')
locale = require 'locale/locale' locale = require 'locale/locale'
utils = require 'core/utils'
initializeFilePicker = -> initializeFilePicker = ->
require('core/services/filepicker')() unless window.application.isIPadApp require('core/services/filepicker')() unless window.application.isIPadApp
@ -234,21 +235,14 @@ class ImageFileTreema extends TreemaNode.nodeMap.string
@refreshDisplay() @refreshDisplay()
codeLanguages =
javascript: 'ace/mode/javascript'
coffeescript: 'ace/mode/coffee'
python: 'ace/mode/python'
lua: 'ace/mode/lua'
java: 'ace/mode/java'
class CodeLanguagesObjectTreema extends TreemaNode.nodeMap.object class CodeLanguagesObjectTreema extends TreemaNode.nodeMap.object
childPropertiesAvailable: -> childPropertiesAvailable: ->
(key for key in _.keys(codeLanguages) when not @data[key]? and not (key is 'javascript' and @workingSchema.skipJavaScript)) (key for key in _.keys(utils.aceEditModes) when not @data[key]? and not (key is 'javascript' and @workingSchema.skipJavaScript))
class CodeLanguageTreema extends TreemaNode.nodeMap.string class CodeLanguageTreema extends TreemaNode.nodeMap.string
buildValueForEditing: (valEl, data) -> buildValueForEditing: (valEl, data) ->
super(valEl, data) super(valEl, data)
valEl.find('input').autocomplete(source: _.keys(codeLanguages), minLength: 0, delay: 0, autoFocus: true) valEl.find('input').autocomplete(source: _.keys(utils.aceEditModes), minLength: 0, delay: 0, autoFocus: true)
valEl valEl
class CodeTreema extends TreemaNode.nodeMap.ace class CodeTreema extends TreemaNode.nodeMap.ace
@ -256,8 +250,8 @@ class CodeTreema extends TreemaNode.nodeMap.ace
super(arguments...) super(arguments...)
@workingSchema.aceTabSize = 4 @workingSchema.aceTabSize = 4
# TODO: Find a less hacky solution for this # TODO: Find a less hacky solution for this
@workingSchema.aceMode = mode if mode = codeLanguages[@keyForParent] @workingSchema.aceMode = mode if mode = utils.aceEditModes[@keyForParent]
@workingSchema.aceMode = mode if mode = codeLanguages[@parent?.data?.language] @workingSchema.aceMode = mode if mode = utils.aceEditModes[@parent?.data?.language]
class CoffeeTreema extends CodeTreema class CoffeeTreema extends CodeTreema
constructor: -> constructor: ->

View file

@ -259,7 +259,7 @@ startsWithVowel = (s) -> s[0] in 'aeiouAEIOU'
module.exports.filterMarkdownCodeLanguages = (text, language) -> module.exports.filterMarkdownCodeLanguages = (text, language) ->
return '' unless text return '' unless text
currentLanguage = language or me.get('aceConfig')?.language or 'python' currentLanguage = language or me.get('aceConfig')?.language or 'python'
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io'], currentLanguage excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io', 'html'], currentLanguage
# Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language. # Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language.
codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm' codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
# Exclude language-specific images like ![python - image description](image url) for each non-target language. # Exclude language-specific images like ![python - image description](image url) for each non-target language.
@ -290,13 +290,15 @@ module.exports.filterMarkdownCodeLanguages = (text, language) ->
return text return text
module.exports.aceEditModes = aceEditModes = module.exports.aceEditModes = aceEditModes =
'javascript': 'ace/mode/javascript' javascript: 'ace/mode/javascript'
'coffeescript': 'ace/mode/coffee' coffeescript: 'ace/mode/coffee'
'python': 'ace/mode/python' python: 'ace/mode/python'
'java': 'ace/mode/java' lua: 'ace/mode/lua'
'lua': 'ace/mode/lua' java: 'ace/mode/java'
'java': 'ace/mode/java' html: 'ace/mode/html'
# These ACEs are used for displaying code snippets statically, like in SpellPaletteEntryView popovers
# and have short lifespans
module.exports.initializeACE = (el, codeLanguage) -> module.exports.initializeACE = (el, codeLanguage) ->
contents = $(el).text().trim() contents = $(el).text().trim()
editor = ace.edit el editor = ace.edit el

View file

@ -22,6 +22,7 @@ module.exports = Bus = class Bus extends CocoClass
'auth:me-synced': 'onMeSynced' 'auth:me-synced': 'onMeSynced'
connect: -> connect: ->
# Put Firebase back in bower if you want to use this
Backbone.Mediator.publish 'bus:connecting', {bus: @} Backbone.Mediator.publish 'bus:connecting', {bus: @}
Firebase.goOnline() Firebase.goOnline()
@fireRef = new Firebase(Bus.fireHost + '/' + @docName) @fireRef = new Firebase(Bus.fireHost + '/' + @docName)

View file

@ -94,9 +94,9 @@ module.exports = class God extends CocoClass
return if hadPreloader return if hadPreloader
@angelsShare.workQueue = [] @angelsShare.workQueue = []
work = work = {
userCodeMap: userCodeMap userCodeMap: userCodeMap
level: @level @level
levelSessionIDs: @levelSessionIDs levelSessionIDs: @levelSessionIDs
submissionCount: @lastSubmissionCount submissionCount: @lastSubmissionCount
fixedSeed: @lastFixedSeed fixedSeed: @lastFixedSeed
@ -104,9 +104,10 @@ module.exports = class God extends CocoClass
difficulty: @lastDifficulty difficulty: @lastDifficulty
goals: @angelsShare.goalManager?.getGoals() goals: @angelsShare.goalManager?.getGoals()
headless: @angelsShare.headless headless: @angelsShare.headless
preload: preload preload
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9. synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
realTime: realTime realTime
}
@angelsShare.workQueue.push work @angelsShare.workQueue.push work
angel.workIfIdle() for angel in @angelsShare.angels angel.workIfIdle() for angel in @angelsShare.angels
work work
@ -114,9 +115,7 @@ module.exports = class God extends CocoClass
getUserCodeMap: (spells) -> getUserCodeMap: (spells) ->
userCodeMap = {} userCodeMap = {}
for spellKey, spell of spells for spellKey, spell of spells
for thangID, spellThang of spell.thangs (userCodeMap[spell.thang.thang.id] ?= {})[spell.name] = spell.thang.aether.serialize()
continue if spellThang.thang?.programmableMethods[spell.name].cloneOf
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
userCodeMap userCodeMap

View file

@ -41,7 +41,6 @@ module.exports = class LevelBus extends Bus
@fireScriptsRef = @fireRef?.child('scripts') @fireScriptsRef = @fireRef?.child('scripts')
setSession: (@session) -> setSession: (@session) ->
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
@timerIntervalID = setInterval(@incrementSessionPlaytime, 1000) @timerIntervalID = setInterval(@incrementSessionPlaytime, 1000)
onIdleChanged: (e) -> onIdleChanged: (e) ->
@ -53,8 +52,7 @@ module.exports = class LevelBus extends Bus
@session.set('playtime', (@session.get('playtime') ? 0) + 1) @session.set('playtime', (@session.get('playtime') ? 0) + 1)
onPoint: -> onPoint: ->
return true unless @session?.get('multiplayer') return true
super()
onMeSynced: => onMeSynced: =>
super() super()
@ -236,17 +234,11 @@ module.exports = class LevelBus extends Bus
@changedSessionProperties.chat = true @changedSessionProperties.chat = true
@saveSession() @saveSession()
onMultiplayerChanged: ->
@changedSessionProperties.multiplayer = true
@session.updatePermissions()
@changedSessionProperties.permissions = true
@saveSession()
# Debounced as saveSession # Debounced as saveSession
reallySaveSession: -> reallySaveSession: ->
return if _.isEmpty @changedSessionProperties return if _.isEmpty @changedSessionProperties
# don't let peeking admins mess with the session accidentally # don't let peeking admins mess with the session accidentally
return unless @session.get('multiplayer') or @session.get('creator') is me.id return unless @session.get('creator') is me.id
return if @session.fake return if @session.fake
Backbone.Mediator.publish 'level:session-will-save', session: @session Backbone.Mediator.publish 'level:session-will-save', session: @session
patch = {} patch = {}

View file

@ -53,6 +53,16 @@ module.exports = class LevelLoader extends CocoClass
# Supermodel (Level) Loading # Supermodel (Level) Loading
loadWorldNecessities: ->
# TODO: Actually trigger loading, instead of in the constructor
new Promise((resolve, reject) =>
return resolve(@) if @world
@once 'world-necessities-loaded', => resolve(@)
@once 'world-necessity-load-failed', ({resource}) ->
{ jqxhr } = resource
reject({message: jqxhr.responseJSON?.message or jqxhr.responseText or 'Unknown Error'})
)
loadLevel: -> loadLevel: ->
@level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID @level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID
if @level.loaded if @level.loaded
@ -62,9 +72,18 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @level, 'sync', @onLevelLoaded @listenToOnce @level, 'sync', @onLevelLoaded
onLevelLoaded: -> onLevelLoaded: ->
if not @sessionless and @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course'] if not @sessionless and @level.isType('hero', 'hero-ladder', 'hero-coop', 'course')
@sessionDependenciesRegistered = {} @sessionDependenciesRegistered = {}
if (@courseID and @level.get('type', true) not in ['course', 'course-ladder']) or window.serverConfig.picoCTF if @level.isType('web-dev')
@headless = true
if @sessionless
# When loading a web-dev level in the level editor, pretend it's a normal hero level so we can put down our placeholder Thang.
# TODO: avoid this whole roundabout Thang-based way of doing web-dev levels
originalGet = @level.get
@level.get = ->
return 'hero' if arguments[0] is 'type'
originalGet.apply @, arguments
if (@courseID and not @level.isType('course', 'course-ladder', 'game-dev', 'web-dev')) or window.serverConfig.picoCTF
# Because we now use original hero levels for both hero and course levels, we fake being a course level in this context. # Because we now use original hero levels for both hero and course levels, we fake being a course level in this context.
originalGet = @level.get originalGet = @level.get
@level.get = -> @level.get = ->
@ -169,7 +188,7 @@ module.exports = class LevelLoader extends CocoClass
@consolidateFlagHistory() if @opponentSession?.loaded @consolidateFlagHistory() if @opponentSession?.loaded
else if session is @opponentSession else if session is @opponentSession
@consolidateFlagHistory() if @session.loaded @consolidateFlagHistory() if @session.loaded
if @level.get('type', true) in ['course'] # course-ladder is hard to handle because there's 2 sessions if @level.isType('course') # course-ladder is hard to handle because there's 2 sessions
heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
console.log "Course mode, loading custom hero: ", heroThangType if LOG console.log "Course mode, loading custom hero: ", heroThangType if LOG
url = "/db/thang.type/#{heroThangType}/version" url = "/db/thang.type/#{heroThangType}/version"
@ -178,7 +197,7 @@ module.exports = class LevelLoader extends CocoClass
@worldNecessities.push heroResource @worldNecessities.push heroResource
@sessionDependenciesRegistered[session.id] = true @sessionDependenciesRegistered[session.id] = true
return return
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] return unless @level.isType('hero', 'hero-ladder', 'hero-coop')
heroConfig = session.get('heroConfig') heroConfig = session.get('heroConfig')
heroConfig ?= me.get('heroConfig') if session is @session and not @headless heroConfig ?= me.get('heroConfig') if session is @session and not @headless
heroConfig ?= {} heroConfig ?= {}
@ -332,8 +351,8 @@ module.exports = class LevelLoader extends CocoClass
@worldNecessities = (r for r in @worldNecessities when r?) @worldNecessities = (r for r in @worldNecessities when r?)
@onWorldNecessitiesLoaded() if @checkAllWorldNecessitiesRegisteredAndLoaded() @onWorldNecessitiesLoaded() if @checkAllWorldNecessitiesRegisteredAndLoaded()
onWorldNecessityLoadFailed: (resource) -> onWorldNecessityLoadFailed: (event) ->
@trigger('world-necessity-load-failed', resource: resource) @trigger('world-necessity-load-failed', event)
checkAllWorldNecessitiesRegisteredAndLoaded: -> checkAllWorldNecessitiesRegisteredAndLoaded: ->
return false unless _.filter(@worldNecessities).length is 0 return false unless _.filter(@worldNecessities).length is 0
@ -401,7 +420,8 @@ module.exports = class LevelLoader extends CocoClass
resource.markLoaded() if resource.spriteSheetKeys.length is 0 resource.markLoaded() if resource.spriteSheetKeys.length is 0
denormalizeSession: -> denormalizeSession: ->
return if @headless or @sessionDenormalized or @spectateMode or @sessionless or me.isSessionless() return if @sessionDenormalized or @spectateMode or @sessionless or me.isSessionless()
return if @headless and not @level.isType('web-dev')
# This is a way (the way?) PUT /db/level.sessions/undefined was happening # This is a way (the way?) PUT /db/level.sessions/undefined was happening
# See commit c242317d9 # See commit c242317d9
return if not @session.id return if not @session.id
@ -443,7 +463,7 @@ module.exports = class LevelLoader extends CocoClass
@grabTeamConfigs() @grabTeamConfigs()
@thangTypeTeams = {} @thangTypeTeams = {}
for thang in @level.get('thangs') for thang in @level.get('thangs')
if @level.get('type', true) in ['hero', 'course'] and thang.id is 'Hero Placeholder' if @level.isType('hero', 'course') and thang.id is 'Hero Placeholder'
continue # No team colors for heroes on single-player levels continue # No team colors for heroes on single-player levels
for component in thang.components for component in thang.components
if team = component.config?.team if team = component.config?.team
@ -471,6 +491,7 @@ module.exports = class LevelLoader extends CocoClass
initWorld: -> initWorld: ->
return if @initialized return if @initialized
@initialized = true @initialized = true
return if @level.isType('web-dev')
@world = new World() @world = new World()
@world.levelSessionIDs = if @opponentSessionID then [@sessionID, @opponentSessionID] else [@sessionID] @world.levelSessionIDs = if @opponentSessionID then [@sessionID, @opponentSessionID] else [@sessionID]
@world.submissionCount = @session?.get('state')?.submissionCount ? 0 @world.submissionCount = @session?.get('state')?.submissionCount ? 0

View file

@ -74,7 +74,7 @@ module.exports = class LevelSetupManager extends CocoClass
@session.set 'heroConfig', {"thangType":raider,"inventory":{}} @session.set 'heroConfig', {"thangType":raider,"inventory":{}}
@onInventoryModalPlayClicked() @onInventoryModalPlayClicked()
return return
if @level.get('type', true) in ['course', 'course-ladder'] or window.serverConfig.picoCTF if @level.isType('course', 'course-ladder', 'game-dev', 'web-dev') or window.serverConfig.picoCTF
@onInventoryModalPlayClicked() @onInventoryModalPlayClicked()
return return
@heroesModal = new PlayHeroesModal({supermodel: @supermodel, session: @session, confirmButtonI18N: 'play.next', level: @level, hadEverChosenHero: @options.hadEverChosenHero}) @heroesModal = new PlayHeroesModal({supermodel: @supermodel, session: @session, confirmButtonI18N: 'play.next', level: @level, hadEverChosenHero: @options.hadEverChosenHero})

View file

@ -195,6 +195,17 @@ module.exports =
_.assign(progressData, progressMixin) _.assign(progressData, progressMixin)
return progressData return progressData
courseLabelsArray: (courses) ->
labels = []
courseLabelIndexes = CS: 0, GD: 0, WD: 0
for course in courses
acronym = switch
when /game-dev/.test(course.get('slug')) then 'GD'
when /web-dev/.test(course.get('slug')) then 'WD'
else 'CS'
labels.push acronym + ++courseLabelIndexes[acronym]
labels
progressMixin = progressMixin =
get: (options={}) -> get: (options={}) ->
{ classroom, course, level, user } = options { classroom, course, level, user } = options

View file

@ -10,7 +10,6 @@ Letterbox = require './Letterbox'
Dimmer = require './Dimmer' Dimmer = require './Dimmer'
CountdownScreen = require './CountdownScreen' CountdownScreen = require './CountdownScreen'
PlaybackOverScreen = require './PlaybackOverScreen' PlaybackOverScreen = require './PlaybackOverScreen'
WaitingScreen = require './WaitingScreen'
DebugDisplay = require './DebugDisplay' DebugDisplay = require './DebugDisplay'
CoordinateDisplay = require './CoordinateDisplay' CoordinateDisplay = require './CoordinateDisplay'
CoordinateGrid = require './CoordinateGrid' CoordinateGrid = require './CoordinateGrid'
@ -70,7 +69,6 @@ module.exports = Surface = class Surface extends CocoClass
'level:set-letterbox': 'onSetLetterbox' 'level:set-letterbox': 'onSetLetterbox'
'application:idle-changed': 'onIdleChanged' 'application:idle-changed': 'onIdleChanged'
'camera:zoom-updated': 'onZoomUpdated' 'camera:zoom-updated': 'onZoomUpdated'
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted' 'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded' 'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'level:flag-color-selected': 'onFlagColorSelected' 'level:flag-color-selected': 'onFlagColorSelected'
@ -135,7 +133,6 @@ module.exports = Surface = class Surface extends CocoClass
@countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer, showsCountdown: @world.showsCountdown @countdownScreen = new CountdownScreen camera: @camera, layer: @screenLayer, showsCountdown: @world.showsCountdown
@playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer, playerNames: @options.playerNames @playbackOverScreen = new PlaybackOverScreen camera: @camera, layer: @screenLayer, playerNames: @options.playerNames
@normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen. @normalStage.addChildAt @playbackOverScreen.dimLayer, 0 # Put this below the other layers, actually, so we can more easily read text on the screen.
@waitingScreen = new WaitingScreen camera: @camera, layer: @screenLayer
@initCoordinates() @initCoordinates()
@webGLStage.enableMouseOver(10) @webGLStage.enableMouseOver(10)
@webGLStage.addEventListener 'stagemousemove', @onMouseMove @webGLStage.addEventListener 'stagemousemove', @onMouseMove
@ -602,9 +599,6 @@ module.exports = Surface = class Surface extends CocoClass
#- Real-time playback #- Real-time playback
onRealTimePlaybackWaiting: (e) ->
@onRealTimePlaybackStarted e
onRealTimePlaybackStarted: (e) -> onRealTimePlaybackStarted: (e) ->
return if @realTime return if @realTime
@realTimeInputEvents.reset() @realTimeInputEvents.reset()
@ -741,7 +735,6 @@ module.exports = Surface = class Surface extends CocoClass
@dimmer?.destroy() @dimmer?.destroy()
@countdownScreen?.destroy() @countdownScreen?.destroy()
@playbackOverScreen?.destroy() @playbackOverScreen?.destroy()
@waitingScreen?.destroy()
@coordinateDisplay?.destroy() @coordinateDisplay?.destroy()
@coordinateGrid?.destroy() @coordinateGrid?.destroy()
@normalStage.clear() @normalStage.clear()

View file

@ -1,65 +0,0 @@
CocoClass = require 'core/CocoClass'
RealTimeCollection = require 'collections/RealTimeCollection'
module.exports = class WaitingScreen extends CocoClass
subscriptions:
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'real-time-multiplayer:player-status': 'onRealTimeMultiplayerPlayerStatus'
constructor: (options) ->
super()
options ?= {}
@camera = options.camera
@layer = options.layer
@waitingText = options.text or 'Waiting...'
console.error @toString(), 'needs a camera.' unless @camera
console.error @toString(), 'needs a layer.' unless @layer
@build()
onCastingBegins: (e) -> @show() unless e.preload
onCastingEnds: (e) -> @hide()
toString: -> '<WaitingScreen>'
build: ->
@dimLayer = new createjs.Container()
@dimLayer.mouseEnabled = @dimLayer.mouseChildren = false
@dimLayer.addChild @dimScreen = new createjs.Shape()
@dimScreen.graphics.beginFill('rgba(0,0,0,0.5)').rect 0, 0, @camera.canvasWidth, @camera.canvasHeight
@dimLayer.alpha = 0
@dimLayer.addChild @makeWaitingText()
makeWaitingText: ->
size = Math.ceil @camera.canvasHeight / 8
text = new createjs.Text @waitingText, "#{size}px Open Sans Condensed", '#F7B42C'
text.shadow = new createjs.Shadow '#000', Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 120)
text.textAlign = 'center'
text.textBaseline = 'middle'
text.x = @camera.canvasWidth / 2
text.y = @camera.canvasHeight / 2
@text = text
return text
show: ->
return if @showing
@showing = true
@dimLayer.alpha = 0
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 1}, 500)
@layer.addChild @dimLayer
hide: ->
return unless @showing
@showing = false
createjs.Tween.removeTweens @dimLayer
createjs.Tween.get(@dimLayer).to({alpha: 0}, 500).call => @layer.removeChild @dimLayer unless @destroyed
onRealTimeMultiplayerPlayerStatus: (e) -> @text.text = e.status
onRealTimePlaybackWaiting: (e) -> @show()
onRealTimePlaybackStarted: (e) -> @hide()
onRealTimePlaybackEnded: (e) -> @hide()

View file

@ -38,6 +38,7 @@ module.exports = class GoalManager extends CocoClass
subscriptions: subscriptions:
'god:new-world-created': 'onNewWorldCreated' 'god:new-world-created': 'onNewWorldCreated'
'god:new-html-goal-states': 'onNewHTMLGoalStates'
'level:restarted': 'onLevelRestarted' 'level:restarted': 'onLevelRestarted'
backgroundSubscriptions: backgroundSubscriptions:
@ -86,6 +87,9 @@ module.exports = class GoalManager extends CocoClass
@world = e.world @world = e.world
@updateGoalStates(e.goalStates) if e.goalStates? @updateGoalStates(e.goalStates) if e.goalStates?
onNewHTMLGoalStates: (e) ->
@updateGoalStates(e.goalStates) if e.goalStates?
updateGoalStates: (newGoalStates) -> updateGoalStates: (newGoalStates) ->
for goalID, goalState of newGoalStates for goalID, goalState of newGoalStates
continue unless @goalStates[goalID]? continue unless @goalStates[goalID]?
@ -114,7 +118,7 @@ module.exports = class GoalManager extends CocoClass
goalStates: @goalStates goalStates: @goalStates
goals: @goals goals: @goals
overallStatus: overallStatus overallStatus: overallStatus
timedOut: @world.totalFrames is @world.maxTotalFrames and overallStatus not in ['success', 'failure'] timedOut: @world? and (@world.totalFrames is @world.maxTotalFrames and overallStatus not in ['success', 'failure'])
Backbone.Mediator.publish('goal-manager:new-goal-states', event) Backbone.Mediator.publish('goal-manager:new-goal-states', event)
checkOverallStatus: (ignoreIncomplete=false) -> checkOverallStatus: (ignoreIncomplete=false) ->
@ -264,7 +268,7 @@ module.exports = class GoalManager extends CocoClass
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter' mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
victory = overallStatus is 'success' victory = overallStatus is 'success'
tentative = overallStatus is 'success' tentative = overallStatus is 'success'
@world.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity @world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity
updateGoalState: (goalID, thangID, progressObjectName, frameNumber) -> updateGoalState: (goalID, thangID, progressObjectName, frameNumber) ->
# A thang has done something related to the goal! # A thang has done something related to the goal!
@ -291,7 +295,7 @@ module.exports = class GoalManager extends CocoClass
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter' mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
victory = overallStatus is 'success' victory = overallStatus is 'success'
tentative = overallStatus is 'success' tentative = overallStatus is 'success'
@world.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity @world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity
goalIsPositive: (goalID) -> goalIsPositive: (goalID) ->
# Positive goals are completed when all conditions are true (kill all these thangs) # Positive goals are completed when all conditions are true (kill all these thangs)

View file

@ -450,8 +450,6 @@
incomplete: "Incomplete" incomplete: "Incomplete"
timed_out: "Ran out of time" timed_out: "Ran out of time"
failing: "Failing" failing: "Failing"
control_bar_multiplayer: "Multiplayer"
control_bar_join_game: "Join Game"
reload: "Reload" reload: "Reload"
reload_title: "Reload All Code?" reload_title: "Reload All Code?"
reload_really: "Are you sure you want to reload this level back to the beginning?" reload_really: "Are you sure you want to reload this level back to the beginning?"
@ -480,10 +478,7 @@
tome_cast_button_running: "Running" tome_cast_button_running: "Running"
tome_cast_button_ran: "Ran" tome_cast_button_ran: "Ran"
tome_submit_button: "Submit" tome_submit_button: "Submit"
tome_reload_method: "Reload original code for this method" # Title text for individual method reload button. tome_reload_method: "Reload original code to restart the level" # {change}
tome_select_method: "Select a Method"
tome_see_all_methods: "See all methods you can edit" # Title text for method list selector (shown when there are multiple programmable methods).
tome_select_a_thang: "Select Someone for "
tome_available_spells: "Available Spells" tome_available_spells: "Available Spells"
tome_your_skills: "Your Skills" tome_your_skills: "Your Skills"
tome_current_method: "Current Method" tome_current_method: "Current Method"
@ -781,7 +776,7 @@
mission_description_1: "<strong>Programming is magic</strong>. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using <strong>typed code</strong>." mission_description_1: "<strong>Programming is magic</strong>. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using <strong>typed code</strong>."
mission_description_2: "As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to <strong>every student</strong>, because everyone should have the chance to learn the magic of programming." mission_description_2: "As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to <strong>every student</strong>, because everyone should have the chance to learn the magic of programming."
team_title: "Meet the CodeCombat team" team_title: "Meet the CodeCombat team"
team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our Github contributors, because we value growth and learning in our team." team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our GitHub contributors, because we value growth and learning in our team."
nick_title: "Cofounder, CEO" nick_title: "Cofounder, CEO"
nick_blurb: "Motivation Guru" nick_blurb: "Motivation Guru"
matt_title: "Cofounder, CTO" matt_title: "Cofounder, CTO"
@ -802,6 +797,7 @@
phoenix_title: "Software Engineer" phoenix_title: "Software Engineer"
nolan_title: "Territory Manager" nolan_title: "Territory Manager"
elliot_title: "Partnership Manager" elliot_title: "Partnership Manager"
lisa_title: "Market Development Rep"
retrostyle_title: "Illustration" retrostyle_title: "Illustration"
retrostyle_blurb: "RetroStyle Games" retrostyle_blurb: "RetroStyle Games"
jose_title: "Music" jose_title: "Music"
@ -1477,6 +1473,29 @@
status_not_enrolled: "Not Enrolled" status_not_enrolled: "Not Enrolled"
status_enrolled: "Expires on {{date}}" status_enrolled: "Expires on {{date}}"
select_all: "Select All" select_all: "Select All"
projects: "Projects"
sharing:
game: "Game"
webpage: "Webpage"
share_game: "Share This Game"
share_web: "Share This Webpage"
victory_share_prefix: "Share this link to invite your friends & family to"
victory_share_game: "play your game level"
victory_share_web: "view your webpage"
victory_share_suffix: "."
victory_course_share_prefix: "This link will let your friends & family"
victory_course_share_game: "play the game"
victory_course_share_web: "view the webpage"
victory_course_share_suffix: "you just created."
copy_url: "Copy URL"
game_dev:
creator: "Creator"
web_dev:
image_gallery_title: "Image Gallery"
image_gallery_description: "Copy these images into your webpage, or find your own image URLs online."
classes: classes:
archmage_title: "Archmage" archmage_title: "Archmage"
@ -1879,6 +1898,17 @@
vectors: "Vectors" vectors: "Vectors"
while_loops: "While Loops" while_loops: "While Loops"
recursion: "Recursion" recursion: "Recursion"
basic_html: "Basic HTML" # TODO: these web-dev concepts will change, don't need to translate
basic_css: "Basic CSS"
basic_web_scripting: "Basic Web Scripting"
intermediate_html: "Intermediate HTML"
intermediate_css: "Intermediate CSS"
intermediate_web_scripting: "Intermediate Web Scripting"
advanced_html: "Advanced HTML"
advanced_css: "Advanced CSS"
advanced_web_scripting: "Advanced Web Scripting"
jquery: "jQuery"
bootstrap: "Bootstrap"
delta: delta:
added: "Added" added: "Added"
@ -1890,16 +1920,6 @@
merge_conflict_with: "MERGE CONFLICT WITH" merge_conflict_with: "MERGE CONFLICT WITH"
no_changes: "No Changes" no_changes: "No Changes"
multiplayer:
multiplayer_title: "Multiplayer Settings" # We'll be changing this around significantly soon. Until then, it's not important to translate.
multiplayer_toggle: "Enable multiplayer"
multiplayer_toggle_description: "Allow others to join your game."
multiplayer_link_description: "Give this link to anyone to have them join you."
multiplayer_hint_label: "Hint:"
multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link."
multiplayer_coming_soon: "More multiplayer features to come!"
multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard."
legal: legal:
page_title: "Legal" page_title: "Legal"
opensource_intro: "CodeCombat is completely open source." opensource_intro: "CodeCombat is completely open source."

View file

@ -74,7 +74,7 @@ module.exports = class Classroom extends CocoModel
} }
getLevels: (options={}) -> getLevels: (options={}) ->
# options: courseID, withoutLadderLevels # options: courseID, withoutLadderLevels, projectLevels
Levels = require 'collections/Levels' Levels = require 'collections/Levels'
courses = @get('courses') courses = @get('courses')
return new Levels() unless courses return new Levels() unless courses
@ -86,6 +86,8 @@ module.exports = class Classroom extends CocoModel
levels = new Levels(_.flatten(levelObjects)) levels = new Levels(_.flatten(levelObjects))
if options.withoutLadderLevels if options.withoutLadderLevels
levels.remove(levels.filter((level) -> level.isLadder())) levels.remove(levels.filter((level) -> level.isLadder()))
if options.projectLevels
levels.remove(levels.filter((level) -> level.get('shareable') isnt 'project'))
return levels return levels
getLadderLevel: (courseID) -> getLadderLevel: (courseID) ->

View file

@ -5,3 +5,10 @@ module.exports = class Course extends CocoModel
@className: 'Course' @className: 'Course'
@schema: schema @schema: schema
urlRoot: '/db/course' urlRoot: '/db/course'
fetchForCourseInstance: (courseInstanceID, opts) ->
options = {
url: "/db/course_instance/#{courseInstanceID}/course"
}
_.extend options, opts
@fetch options

View file

@ -34,7 +34,7 @@ module.exports = class Level extends CocoModel
for tt in supermodel.getModels ThangType for tt in supermodel.getModels ThangType
if tmap[tt.get('original')] or if tmap[tt.get('original')] or
(tt.get('kind') isnt 'Hero' and tt.get('kind')? and tt.get('components') and not tt.notInLevel) or (tt.get('kind') isnt 'Hero' and tt.get('kind')? and tt.get('components') and not tt.notInLevel) or
(tt.get('kind') is 'Hero' and ((@get('type', true) in ['course', 'course-ladder']) or tt.get('original') in sessionHeroes)) (tt.get('kind') is 'Hero' and (@isType('course', 'course-ladder', 'game-dev') or tt.get('original') in sessionHeroes))
o.thangTypes.push (original: tt.get('original'), name: tt.get('name'), components: $.extend(true, [], tt.get('components'))) o.thangTypes.push (original: tt.get('original'), name: tt.get('name'), components: $.extend(true, [], tt.get('components')))
@sortThangComponents o.thangTypes, o.levelComponents, 'ThangType' @sortThangComponents o.thangTypes, o.levelComponents, 'ThangType'
@fillInDefaultComponentConfiguration o.thangTypes, o.levelComponents @fillInDefaultComponentConfiguration o.thangTypes, o.levelComponents
@ -59,7 +59,7 @@ module.exports = class Level extends CocoModel
denormalize: (supermodel, session, otherSession) -> denormalize: (supermodel, session, otherSession) ->
o = $.extend true, {}, @attributes o = $.extend true, {}, @attributes
if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] if o.thangs and @isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?) thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?)
thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization
for levelThang in o.thangs for levelThang in o.thangs
@ -68,7 +68,7 @@ module.exports = class Level extends CocoModel
denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) -> denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) ->
levelThang.components ?= [] levelThang.components ?= []
isHero = /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] isHero = /Hero Placeholder/.test(levelThang.id) and @isType('hero', 'hero-ladder', 'hero-coop')
if isHero and otherSession if isHero and otherSession
# If it's a hero and there's another session, find the right session for it. # If it's a hero and there's another session, find the right session for it.
# If there is no other session (playing against default code, or on single player), clone all placeholders. # If there is no other session (playing against default code, or on single player), clone all placeholders.
@ -147,7 +147,7 @@ module.exports = class Level extends CocoModel
levelThang.components.push placeholderComponent levelThang.components.push placeholderComponent
# Load the user's chosen hero AFTER getting stats from default char # Load the user's chosen hero AFTER getting stats from default char
if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course'] and not @headless and not @sessionless if /Hero Placeholder/.test(levelThang.id) and @isType('course') and not @headless and not @sessionless
heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain heroThangType = me.get('heroConfig')?.thangType or ThangType.heroes.captain
levelThang.thangType = heroThangType if heroThangType levelThang.thangType = heroThangType if heroThangType
@ -263,6 +263,9 @@ module.exports = class Level extends CocoModel
isLadder: -> isLadder: ->
return @get('type')?.indexOf('ladder') > -1 return @get('type')?.indexOf('ladder') > -1
isType: (types...) ->
return @get('type', true) in types
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) -> fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) ->
if courseInstanceID if courseInstanceID
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/sessions/#{sessionID}/next" options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/sessions/#{sessionID}/next"

View file

@ -15,8 +15,6 @@ module.exports = class LevelSession extends CocoModel
updatePermissions: -> updatePermissions: ->
permissions = @get 'permissions', true permissions = @get 'permissions', true
permissions = (p for p in permissions when p.target isnt 'public') permissions = (p for p in permissions when p.target isnt 'public')
if @get('multiplayer')
permissions.push {target: 'public', access: 'write'}
@set 'permissions', permissions @set 'permissions', permissions
getSourceFor: (spellKey) -> getSourceFor: (spellKey) ->
@ -76,6 +74,7 @@ module.exports = class LevelSession extends CocoModel
wait wait
recordScores: (scores, level) -> recordScores: (scores, level) ->
return unless scores
state = @get 'state' state = @get 'state'
oldTopScores = state.topScores ? [] oldTopScores = state.topScores ? []
newTopScores = [] newTopScores = []
@ -93,3 +92,17 @@ module.exports = class LevelSession extends CocoModel
newTopScores.push oldTopScore newTopScores.push oldTopScore
state.topScores = newTopScores state.topScores = newTopScores
@set 'state', state @set 'state', state
generateSpellsObject: (options={}) ->
{level} = options
{createAetherOptions} = require 'lib/aether_utils'
aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @get('codeLanguage'), skipProtectAPI: options.level?.isType('game-dev')
spellThang = thang: {id: 'Hero Placeholder'}, aether: new Aether aetherOptions
spells = "hero-placeholder/plan": thang: spellThang, name: 'plan'
source = @get('code')?['hero-placeholder']?.plan ? ''
try
spellThang.aether.transpile source
catch e
console.log "Couldn't transpile!\n#{source}\n", e
spellThang.aether.transpile ''
spells

View file

@ -1,6 +0,0 @@
module.exports = class RealTimeModel extends Backbone.Firebase.Model
constructor: (savePath) ->
# TODO: Don't hard code this here
# TODO: Use prod path in prod
@firebase = 'https://codecombat.firebaseio.com/test/db/' + savePath
super()

View file

@ -248,6 +248,15 @@ module.exports = class SuperModel extends Backbone.Model
getResource: (rid) -> getResource: (rid) ->
return @resources[rid] return @resources[rid]
# Promises
finishLoading: ->
new Promise (resolve, reject) =>
return resolve(@) if @finished()
@once 'failed', ({resource}) ->
jqxhr = resource.jqxhr
reject({message: jqxhr.responseJSON?.message or jqxhr.responseText or 'Unknown Error'})
@once 'loaded-all', => resolve(@)
class Resource extends Backbone.Model class Resource extends Backbone.Model
constructor: (name, value=1) -> constructor: (name, value=1) ->
@name = name @name = name

View file

@ -61,12 +61,13 @@ _.extend CampaignSchema.properties, {
i18n: { type: 'object', format: 'hidden' } i18n: { type: 'object', format: 'hidden' }
requiresSubscription: { type: 'boolean' } requiresSubscription: { type: 'boolean' }
replayable: { type: 'boolean' } replayable: { type: 'boolean' }
type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']} type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev']}
slug: { type: 'string', format: 'hidden' } slug: { type: 'string', format: 'hidden' }
original: { type: 'string', format: 'hidden' } original: { type: 'string', format: 'hidden' }
adventurer: { type: 'boolean' } adventurer: { type: 'boolean' }
practice: { type: 'boolean' } practice: { type: 'boolean' }
practiceThresholdMinutes: {type: 'number'} practiceThresholdMinutes: {type: 'number'}
shareable: { title: 'Shareable', type: ['string', 'boolean'], enum: [false, true, 'project'], description: 'Whether the level is not shareable, shareable, or a sharing-encouraged project level.' }
adminOnly: { type: 'boolean' } adminOnly: { type: 'boolean' }
disableSpaces: { type: ['boolean','number'] } disableSpaces: { type: ['boolean','number'] }
hidesSubmitUntilRun: { type: 'boolean' } hidesSubmitUntilRun: { type: 'boolean' }

View file

@ -25,7 +25,7 @@ _.extend ClassroomSchema.properties,
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, { levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
practice: {type: 'boolean'} practice: {type: 'boolean'}
practiceThresholdMinutes: {type: 'number'} practiceThresholdMinutes: {type: 'number'}
shareable: {type: 'boolean'} shareable: { title: 'Shareable', type: ['string', 'boolean'], enum: [false, true, 'project'], description: 'Whether the level is not shareable, shareable, or a sharing-encouraged project level.' }
type: c.shortString() type: c.shortString()
original: c.objectId() original: c.objectId()
name: {type: 'string'} name: {type: 'string'}

View file

@ -114,6 +114,9 @@ GoalSchema = c.object {title: 'Goal', description: 'A goal that the player can a
targets: c.array {title: 'Targets', description: 'The target items which the Thangs must not collect.', minItems: 1}, thang targets: c.array {title: 'Targets', description: 'The target items which the Thangs must not collect.', minItems: 1}, thang
codeProblems: c.array {title: 'Code Problems', description: 'A list of Thang IDs that should not have any code problems, or team names.', uniqueItems: true, minItems: 1, 'default': ['humans']}, thang codeProblems: c.array {title: 'Code Problems', description: 'A list of Thang IDs that should not have any code problems, or team names.', uniqueItems: true, minItems: 1, 'default': ['humans']}, thang
linesOfCode: {title: 'Lines of Code', description: 'A mapping of Thang IDs or teams to how many many lines of code should be allowed (well, statements).', type: 'object', default: {humans: 10}, additionalProperties: {type: 'integer', description: 'How many lines to allow for this Thang.'}} linesOfCode: {title: 'Lines of Code', description: 'A mapping of Thang IDs or teams to how many many lines of code should be allowed (well, statements).', type: 'object', default: {humans: 10}, additionalProperties: {type: 'integer', description: 'How many lines to allow for this Thang.'}}
html: c.object {title: 'HTML', description: 'A jQuery selector and what its result should be'},
selector: {type: 'string', description: 'jQuery selector to run on the user HTML, like "h1:first-child"'}
valueChecks: c.array {title: 'Value checks', description: 'Logical checks on the resulting value for this goal to pass.', format: 'event-prereqs'}, EventPrereqSchema
ResponseSchema = c.object {title: 'Dialogue Button', description: 'A button to be shown to the user with the dialogue.', required: ['text']}, ResponseSchema = c.object {title: 'Dialogue Button', description: 'A button to be shown to the user with the dialogue.', required: ['text']},
text: {title: 'Title', description: 'The text that will be on the button', 'default': 'Okay', type: 'string', maxLength: 30} text: {title: 'Title', description: 'The text that will be on the button', 'default': 'Okay', type: 'string', maxLength: 30}
@ -313,7 +316,7 @@ _.extend LevelSchema.properties,
icon: {type: 'string', format: 'image-file', title: 'Icon'} icon: {type: 'string', format: 'image-file', title: 'Icon'}
banner: {type: 'string', format: 'image-file', title: 'Banner'} banner: {type: 'string', format: 'image-file', title: 'Banner'}
goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema
type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev'])
terrain: c.terrainString terrain: c.terrainString
showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always']) showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always'])
requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'} requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'}
@ -325,8 +328,8 @@ _.extend LevelSchema.properties,
replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'} replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'}
buildTime: {type: 'number', description: 'How long it has taken to build this level.'} buildTime: {type: 'number', description: 'How long it has taken to build this level.'}
practice: { type: 'boolean' } practice: { type: 'boolean' }
shareable: { type: 'boolean', title: 'Shareable' }
practiceThresholdMinutes: {type: 'number', description: 'Players with larger playtimes may be directed to a practice level.'} practiceThresholdMinutes: {type: 'number', description: 'Players with larger playtimes may be directed to a practice level.'}
shareable: { title: 'Shareable', type: ['string', 'boolean'], enum: [false, true, 'project'], description: 'Whether the level is not shareable, shareable, or a sharing-encouraged project level.' }
# Admin flags # Admin flags
adventurer: { type: 'boolean' } adventurer: { type: 'boolean' }

View file

@ -37,8 +37,6 @@ _.extend LevelSessionSchema.properties,
type: 'string' type: 'string'
levelID: levelID:
type: 'string' type: 'string'
multiplayer:
type: 'boolean'
creator: c.objectId creator: c.objectId
links: links:
[ [

View file

@ -261,4 +261,15 @@ me.concept = me.shortString enum: [
'vectors' 'vectors'
'while_loops' 'while_loops'
'recursion' 'recursion'
'basic_html'
'basic_css'
'basic_web_scripting'
'intermediate_html'
'intermediate_css'
'intermediate_web_scripting'
'advanced_html'
'advanced_css'
'advanced_web_scripting'
'jquery'
'bootstrap'
] ]

View file

@ -45,6 +45,10 @@ module.exports =
'god:streaming-world-updated': worldUpdatedEventSchema 'god:streaming-world-updated': worldUpdatedEventSchema
'god:new-html-goal-states': c.object {required: ['goalStates', 'overallStatus']},
goalStates: goalStatesSchema
overallStatus: {type: ['string', 'null'], enum: ['success', 'failure', 'incomplete', null]}
'god:goals-calculated': c.object {required: ['goalStates', 'god']}, 'god:goals-calculated': c.object {required: ['goalStates', 'god']},
god: {type: 'object'} god: {type: 'object'}
goalStates: goalStatesSchema goalStates: goalStatesSchema

View file

@ -1,21 +0,0 @@
c = require 'schemas/schemas'
module.exports =
'real-time-multiplayer:created-game': c.object {title: 'Multiplayer created game', required: ['realTimeSessionID']},
realTimeSessionID: {type: 'string'}
'real-time-multiplayer:joined-game': c.object {title: 'Multiplayer joined game', required: ['realTimeSessionID']},
realTimeSessionID: {type: 'string'}
'real-time-multiplayer:left-game': c.object {title: 'Multiplayer left game'},
userID: {type: 'string'}
'real-time-multiplayer:manual-cast': c.object {title: 'Multiplayer manual cast'}
'real-time-multiplayer:new-opponent-code': c.object {title: 'Multiplayer new opponent code', required: ['code', 'codeLanguage']},
code: {type: 'object'}
codeLanguage: {type: 'string'}
team: {type: 'string'}
'real-time-multiplayer:player-status': c.object {title: 'Multiplayer player status', required: ['status']},
status: {type: 'string'}

View file

@ -96,8 +96,6 @@ module.exports =
'playback:stop-real-time-playback': c.object {} 'playback:stop-real-time-playback': c.object {}
'playback:real-time-playback-waiting': c.object {}
'playback:real-time-playback-started': c.object {} 'playback:real-time-playback-started': c.object {}
'playback:real-time-playback-ended': c.object {} 'playback:real-time-playback-ended': c.object {}

View file

@ -39,8 +39,6 @@ module.exports =
variableChain: c.array {}, {type: 'string'} variableChain: c.array {}, {type: 'string'}
frame: {type: 'integer', minimum: 0} frame: {type: 'integer', minimum: 0}
'tome:toggle-spell-list': c.object {title: 'Toggle Spell List', description: 'Published when you toggle the dropdown for a thang\'s spells'}
'tome:reload-code': c.object {title: 'Reload Code', description: 'Published when you reset a spell to its original source', required: []}, 'tome:reload-code': c.object {title: 'Reload Code', description: 'Published when you reset a spell to its original source', required: []},
spell: {type: 'object'} spell: {type: 'object'}
@ -91,10 +89,6 @@ module.exports =
problems: {type: 'array'} problems: {type: 'array'}
isCast: {type: 'boolean'} isCast: {type: 'boolean'}
'tome:spell-shown': c.object {title: 'Spell Shown', description: 'Published when we show a spell', required: ['thang', 'spell']},
thang: {type: 'object'}
spell: {type: 'object'}
'tome:change-language': c.object {title: 'Tome Change Language', description: 'Published when the Tome should update its programming language', required: ['language']}, 'tome:change-language': c.object {title: 'Tome Change Language', description: 'Published when the Tome should update its programming language', required: ['language']},
language: {type: 'string'} language: {type: 'string'}
reload: {type: 'boolean', description: 'Whether player code should reload to the default when the language changes.'} reload: {type: 'boolean', description: 'Whether player code should reload to the default when the language changes.'}
@ -146,3 +140,7 @@ module.exports =
lineOffsetPx: {type: ['number', 'undefined']} lineOffsetPx: {type: ['number', 'undefined']}
'tome:hide-problem-alert': c.object {title: 'Hide Problem Alert'} 'tome:hide-problem-alert': c.object {title: 'Hide Problem Alert'}
'tome:jiggle-problem-alert': c.object {title: 'Jiggle Problem Alert'} 'tome:jiggle-problem-alert': c.object {title: 'Jiggle Problem Alert'}
'tome:html-updated': c.object {title: 'HTML Updated', required: ['html', 'create']},
html: {type: 'string', description: 'The full HTML to display'}
create: {type: 'boolean', description: 'Whether we should (re)create the DOM (as opposed to updating it)'}

View file

@ -35,3 +35,6 @@
h1 h1
font-size: 48px font-size: 48px
.btn-view-project-level
margin-left: 10px;

View file

@ -177,7 +177,7 @@
// Course Progress tab // Course Progress tab
#course-progress-tab #course-progress-tab, #student-projects-tab
.course-overview-row .course-overview-row
margin-top: 50px margin-top: 50px
border: thin solid gray border: thin solid gray
@ -222,6 +222,19 @@
margin-top: 6.5px margin-top: 6.5px
margin-bottom: 6.5px margin-bottom: 6.5px
#student-projects-tab
.student-levels-table
margin-top: 0px
.student-info
margin-top: 5px
.student-levels-row
padding-top: 10px
padding-bottom: 15px
.btn-view-project-level
margin-left: 15px
// Checkboxes // Checkboxes
.checkbox-flat .checkbox-flat

View file

@ -6,10 +6,10 @@ $mapWidth: 2350
$levelDotWidth: 2% $levelDotWidth: 2%
$levelDotHeight: $levelDotWidth * $mapWidth / $mapHeight $levelDotHeight: $levelDotWidth * $mapWidth / $mapHeight
$levelDotZ: $levelDotHeight * 0.25 $levelDotZ: $levelDotHeight * 0.25
$levelDotHoverZ: $levelDotZ * 2 $levelDotHoverZ: $levelDotZ * 1.5
$levelDotShadowWidth: 0.8 * $levelDotWidth $levelDotShadowWidth: 0.8 * $levelDotWidth
$levelDotShadowHeight: 0.8 * $levelDotHeight $levelDotShadowHeight: 0.8 * $levelDotHeight
$levelClickRadius: 40px $levelClickRadius: 20px
$gameControlSize: 80px $gameControlSize: 80px
$gameControlMargin: 30px $gameControlMargin: 30px
@ -25,8 +25,10 @@ $gameControlMargin: 30px
margin-bottom: -$levelDotHeight / 3 + $levelDotZ margin-bottom: -$levelDotHeight / 3 + $levelDotZ
#campaign-view #campaign-view
width: 100% top: 0
height: 100% right: 0
bottom: 0
left: 0
position: absolute position: absolute
.gradient .gradient
@ -105,7 +107,7 @@ $gameControlMargin: 30px
.level-difficulty-banner-text .level-difficulty-banner-text
position: absolute position: absolute
bottom: 170% bottom: 80%
pointer-events: none pointer-events: none
color: rgb(246, 208, 2) color: rgb(246, 208, 2)
text-shadow: 0px 1px 0px black text-shadow: 0px 1px 0px black
@ -117,8 +119,7 @@ $gameControlMargin: 30px
img.banner img.banner
position: absolute position: absolute
bottom: 38% bottom: 38%
left: -50% width: 100%
width: 200%
pointer-events: none pointer-events: none
img.star img.star
@ -182,7 +183,7 @@ $gameControlMargin: 30px
border-radius: $levelClickRadius border-radius: $levelClickRadius
.tooltip .tooltip
z-index: 2 z-index: 3
pointer-events: none pointer-events: none
.tooltip-arrow .tooltip-arrow
@ -616,6 +617,8 @@ $gameControlMargin: 30px
.gameplay-container .gameplay-container
position: absolute position: absolute
height: 100%
width: 100%
body.ipad #campaign-view body.ipad #campaign-view
// iPad only supports up to Kithgard Gates for now. // iPad only supports up to Kithgard Gates for now.
@ -624,4 +627,3 @@ body.ipad #campaign-view
body[lang='ru'] .portals h2 body[lang='ru'] .portals h2
font-size: 26px font-size: 26px

View file

@ -124,37 +124,6 @@
@include rotate(-15deg) @include rotate(-15deg)
vertical-align: middle vertical-align: middle
.multiplayer-area-container
position: relative
width: 100%
height: 50px
pointer-events: none
.multiplayer-area
min-width: 200px
max-width: 293px
height: 60px
margin: 0 auto
padding: 8px
border-style: solid
border-image: url(/images/level/control_bar_level_name_background.png) 30 fill round
border-width: 0 15px 15px 15px
text-align: center
position: absolute
left: 50%
cursor: pointer
pointer-events: all
@include translate(-50%, 0)
.multiplayer-label
font-size: 12px
color: $control-yellow-highlight
margin-bottom: -5px
.multiplayer-status
color: white
font-size: 18px
.buttons-area .buttons-area
position: absolute position: absolute
right: 35px right: 35px
@ -210,11 +179,6 @@ html.no-borderimage
background: transparent url(/images/level/control_bar_level_name_background.png) background: transparent url(/images/level/control_bar_level_name_background.png)
background-size: contain background-size: contain
background-repeat: no-repeat background-repeat: no-repeat
#control-bar-view .multiplayer-area
border: 0
background: transparent url(/images/level/control_bar_level_name_background.png)
background-size: contain
background-repeat: no-repeat
body:not(.ipad) body:not(.ipad)

View file

@ -181,3 +181,13 @@ $UNVEIL_TIME: 1.2s
left: 48px left: 48px
right: 77px right: 77px
width: auto width: auto
#level-view.web-dev
#loading-details.preview
@media screen and ( min-height: 900px )
background: transparent
border: 1px solid transparent
border-width: 124px 76px 64px 40px
border-image: url(/images/level/code_editor_background.png) 124 76 64 40 fill round
padding: 0 35px 0 15px

View file

@ -9,6 +9,9 @@
padding-top: 0 padding-top: 0
width: 750px width: 750px
@media screen and ( max-height: 625px )
margin-top: -50px
.modal-content .modal-content
position: relative position: relative
margin-top: -251px margin-top: -251px
@ -55,10 +58,14 @@
top: 80px top: 80px
margin-top: 80px margin-top: 80px
@media screen and ( max-height: 650px )
padding-top: 10px
.well-parchment .well-parchment
margin-top: 20px margin-top: 20px
@media screen and ( max-height: 675px )
margin-top: 0
html.no-borderimage html.no-borderimage

View file

@ -298,6 +298,33 @@
height: 100% height: 100%
position: absolute position: absolute
#share-level-container
width: 709px
height: 96px
background: transparent url(/images/pages/play/level/modal/share_level_parchment.png)
position: relative
text-align: left
padding: 12px 20px 0 20px
text-align: center
.share-level-label
color: rgb(103, 92, 76)
text-transform: uppercase
font-weight: bold
font-family: $headings-font-family
font-size: 18px
margin-top: 13px
line-height: 18px
text-align: center
#share-level-input
font-size: 12px
margin-top: 8px
#share-level-btn
width: 100%
margin-top: 7px
//- Footer - other stuff //- Footer - other stuff

View file

@ -4,10 +4,18 @@
color: black color: black
margin-bottom: 5px margin-bottom: 5px
p .next-level-description
margin-top: 30px p
margin-top: 30px
.course-title .course-title
white-space: nowrap white-space: nowrap
text-overflow: ellipsis text-overflow: ellipsis
overflow: hidden overflow: hidden
#share-level-input
font-size: 12px
margin-top: 5px
#share-level-btn
width: 100%

View file

@ -0,0 +1,22 @@
#play-game-dev-level-view
#canvas-wrapper
width: 100%
position: relative
overflow: hidden
z-index: 0
#webgl-surface
background-color: #333
#normal-surface
position: absolute
top: 0
left: 0
pointer-events: none
canvas#webgl-surface, canvas#normal-surface
display: block
z-index: 2
#play-btn
text-transform: uppercase

View file

@ -0,0 +1,18 @@
#play-web-dev-level-view
#web-surface-view
position: absolute
top: 0
right: 0
bottom: 0
left: 0
z-index: 0
#info-bar
position: absolute
right: 0
bottom: 0
left: 0
height: 100px
z-index: 1
background-color: transparent
text-align: center

View file

@ -113,6 +113,14 @@
width: -webkit-calc(100% - 38px) width: -webkit-calc(100% - 38px)
width: calc(100% - 38px) width: calc(100% - 38px)
&.web-dev.hero .properties
.property-entry-item-group
width: 100px
.spell-palette-entry-view
margin-left: 0
width: 100px
@media only screen and (max-width: 1100px) @media only screen and (max-width: 1100px)
#spell-palette-view #spell-palette-view
// Make sure we have enough room for at least two columns // Make sure we have enough room for at least two columns

View file

@ -1,15 +1,7 @@
@import "app/styles/mixins" @import "app/styles/mixins"
@import "app/styles/bootstrap/variables" @import "app/styles/bootstrap/variables"
.spell-list-entry-view #spell-top-bar-view
.method-signature
background-color: transparent
border: 0
font-size: 1.1em
display: inline-block
padding: 4px
.spell-list-entry-view.spell-tab
$height: 87px $height: 87px
$paddingTop: 10px $paddingTop: 10px
$paddingBottom: 25px $paddingBottom: 25px
@ -46,12 +38,6 @@
> *:not(.spell-tool-buttons) > *:not(.spell-tool-buttons)
@include opacity(0.5) @include opacity(0.5)
.thang-avatar-view
width: $childSize - 10px
margin: 5px 0.4vw
display: inline-block
float: left
.btn.btn-small .btn.btn-small
margin-top: 15px margin-top: 15px
margin-right: 1.3vw margin-right: 1.3vw
@ -97,46 +83,8 @@
.thang-avatar-wrapper .thang-avatar-wrapper
border-width: 0 border-width: 0
.spell-list-entry-view:not(.spell-tab)
cursor: pointer
@include opacity(0.90)
clear: both
padding: 5px
position: relative
&:hover
@include opacity(1)
background-color: hsla(240, 40, 80, 0.25)
&.shows-top-divider:not(:first-child)
border-top: 1px dashed #ccc
.method-signature
margin-top: 5px
.thang-names
float: right
margin: 8px
font-variant: small-caps
color: darken(#ca8, 50%)
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
font-size: 13px
max-width: 35%
text-align: right
.thang-avatar-view
width: 40px
float: right
.thang-avatar-wrapper
margin: 0 5px 0 0
//margin: 2px 10px 2px 5px
//html.no-borderimage //html.no-borderimage
// .spell-list-entry-view.spell-tab // .spell-top-bar-view
// border-width: 0 // border-width: 0
// border-image: none // border-image: none
// background: transparent url(/images/level/code_editor_tab_background.png) no-repeat // background: transparent url(/images/level/code_editor_tab_background.png) no-repeat

View file

@ -1,20 +0,0 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
#spell-list-view
display: none
position: absolute
z-index: 10
top: 50px
left: 0%
right: 10%
padding: 4%
border-style: solid
border-image: url(/images/level/popover_border_background.png) 16 12 fill round
border-width: 16px 12px
html.no-borderimage
#spell-list-view
background: transparent url(/images/level/popover_background.png)
background-size: 100% 100%
border: 0

View file

@ -1,30 +0,0 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
.spell-list-entry-view
.spell-list-entry-thangs-view
position: absolute
z-index: 11
top: 50px
right: -10%
max-width: 70%
max-height: 500px
overflow: scroll
padding: 4%
border-style: solid
border-image: url(/images/level/popover_border_background.png) 16 12 fill round
border-width: 16px 12px
.thang-avatar-view
cursor: pointer
max-width: 100px
width: 20%
display: inline-block
html.no-borderimage
.spell-list-entry-view
.spell-list-entry-thangs-view
background: transparent url(/images/level/popover_background.png)
background-size: 100% 100%
border: 0

View file

@ -54,6 +54,7 @@ body:not(.dialogue-view-active)
.spell-palette-popover.popover .spell-palette-popover.popover
// Only those popovers which are our direct children (spell documentation) // Only those popovers which are our direct children (spell documentation)
left: auto !important
max-width: 600px max-width: 600px
padding: 0 padding: 0
border-style: solid border-style: solid
@ -81,7 +82,6 @@ body:not(.dialogue-view-active)
@include animation(jiggle .3s infinite) @include animation(jiggle .3s infinite)
&.pinned &.pinned
left: auto !important
top: 50px !important top: 50px !important
right: 45% right: 45%
// bottom: 151px // bottom: 151px

View file

@ -0,0 +1,6 @@
#web-surface-view
background-color: white
iframe
width: 100%
height: 100%

View file

@ -1,8 +0,0 @@
#multiplayer-view
textarea
width: 100%
box-sizing: border-box
padding: 5px
text-align: center
height: 30px
font-size: 11px

View file

@ -0,0 +1,11 @@
@import "app/styles/mixins"
#image-gallery-modal
.modal-dialog
width: 800px
li
font-size: 12px
.no-select
@include user-select(none)

View file

@ -272,6 +272,29 @@ $level-resize-transition-time: 0.5s
right: 45% right: 45%
z-index: 1000000 z-index: 1000000
&.web-dev
position: absolute
top: 0
bottom: 0
left: 0
right: 0
#playback-view, #thang-hud, #level-dialogue-view, #play-footer, #level-footer-background, #level-footer-shadow
display: none
.game-container, .level-content, #game-area, #canvas-wrapper
height: 100%
#canvas-wrapper canvas
display: none
#web-surface-view
position: absolute
top: 0
right: 0
left: 0
bottom: 0
html.fullscreen-editor html.fullscreen-editor
#level-view #level-view
#fullscreen-editor-background-screen #fullscreen-editor-background-screen

View file

@ -143,7 +143,7 @@ block content
img(src="/images/pages/about/lisa_small.png").img-thumbnail img(src="/images/pages/about/lisa_small.png").img-thumbnail
.team-bio .team-bio
h6.label.team-name Lisa Wu h6.label.team-name Lisa Wu
small Marketing Development Rep small(data-i18n="about.lisa_title")
br br
// Part time / contract // Part time / contract
@ -274,22 +274,8 @@ block content
li.small(data-i18n="about.jobs_benefit_7") li.small(data-i18n="about.jobs_benefit_7")
.col-sm-6.col-md-5.col-md-offset-1.col-lg-4.col-lg-offset-0 .col-sm-6.col-md-5.col-md-offset-1.col-lg-4.col-lg-offset-0
.job-listing .job-listing
h5 Marketing Manager h5 (No Open Roles)
.text-center p.small Check back later for updates on new positions at CodeCombat.
small.label
| San Francisco • Fulltime
p.small We're looking for an amazing marketer to help us fill our funnel and keep the pipeline growing. As our Marketing Manager, you'll be responsible for driving traffic to our website through content creation and converting those visitors into leads and customers using both automated and personalized content.
a.job-link.btn.btn-lg.btn-navy(href="https://jobs.lever.co/codecombat/1033ec13-d4a0-498d-99e0-628afdb56fb5" rel="external")
span(data-i18n="about.learn_more")
.col-sm-6.col-md-5.col-md-offset-1.col-lg-4.col-lg-offset-0
.job-listing
h5 Sales Representative
.text-center
small.label
| San Francisco • Fulltime
p.small School districts are scrambling to offer computer science classes to all their students as a core subject. They have had no solution, because they can't afford to hire enough programming teachers until now.
a.job-link.btn.btn-lg.btn-navy(href="https://jobs.lever.co/codecombat/3f6ff123-16ce-4ecb-aba3-dcf4e8927c47" rel="external")
| Learn More
.col-sm-6.col-md-5.col-lg-4 .col-sm-6.col-md-5.col-lg-4
.job-listing .job-listing
h5(data-i18n="about.jobs_custom_title") h5(data-i18n="about.jobs_custom_title")

View file

@ -44,7 +44,7 @@ block content
a(href="/admin/classroom-levels") Classroom Levels a(href="/admin/classroom-levels") Classroom Levels
li li
button.classroom-progress-csv.btn.btn-sm.btn-success Classroom Progress CSV button.classroom-progress-csv.btn.btn-sm.btn-success Classroom Progress CSV
input.classroom-progress-class-code(type=text value="<class code>") input.classroom-progress-class-code(type=text placeholder="<class code>")
li li
a(href="/admin/analytics") Dashboard a(href="/admin/analytics") Dashboard
li li

View file

@ -104,13 +104,23 @@ block content
tr tr
td td
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- var i18n = level.get('type') === 'course-ladder' ? 'play.compete' : 'home.play'; - var i18nTag = level.isType('course-ladder') ? 'play.compete' : 'home.play';
button.btn.btn-success.btn-play-level(data-level-slug=level.get('slug'), data-i18n=i18n, data-level-id=level.get('original')) button.btn.btn-success.btn-play-level(data-level-slug=level.get('slug'), data-i18n=i18nTag, data-level-id=level.get('original'))
if level.get('shareable')
- var levelOriginal = level.get('original');
- var session = view.levelSessions.find(function(session) { return session.get('level').original === levelOriginal });
if session
- var url = '/play/' + level.get('type') + '-level/' + level.get('slug') + '/' + session.id + '?course=' + view.courseID;
a.btn.btn-warning.btn-view-project-level(href=url)
if level.isType('game-dev')
span(data-i18n='sharing.game')
else
span(data-i18n='sharing.webpage')
td td
if view.userLevelStateMap[me.id] if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')] div= view.userLevelStateMap[me.id][level.get('original')]
td #{level.get('practice') ? 'practice' : 'required'} td #{level.get('practice') ? 'practice' : 'required'}
td #{levelNumber}. #{level.get('name').replace('Course: ', '')} td #{levelNumber}. #{i18n(level.attributes, 'name').replace('Course: ', '')}
td td
if view.levelConceptMap[level.get('original')] if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts') each concept in view.course.get('concepts')

View file

@ -122,6 +122,10 @@ block content
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : '')) li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab') a.course-progress-tab-btn(href='#enrollment-status-tab')
.small-details.text-center(data-i18n='teacher.enrollment_status') .small-details.text-center(data-i18n='teacher.enrollment_status')
.tab-spacer
li(class=(activeTab === "#student-projects-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#student-projects-tab')
.small-details.text-center(data-i18n='teacher.projects')
.tab-filler .tab-filler
.tab-content .tab-content
@ -129,8 +133,10 @@ block content
+studentsTab +studentsTab
else if activeTab === '#course-progress-tab' else if activeTab === '#course-progress-tab'
+courseProgressTab +courseProgressTab
else else if activeTab === '#enrollment-status-tab'
+enrollmentStatusTab +enrollmentStatusTab
else
+studentProjectsTab
else else
.text-center.m-t-5.m-b-5 .text-center.m-t-5.m-b-5
@ -150,11 +156,10 @@ mixin breadcrumbs
mixin longLevelName(data) mixin longLevelName(data)
if data if data
div.level-name div.level-name
span.spr Course span(data-i18n="courses.course")
span= data.courseNumber span= ' ' + data.courseNumber + ', '
span.spr , Level span(data-i18n="play_level.level")
span= data.levelNumber span= ' ' + data.levelNumber + ': '
span.spr :
span= data.levelName span= data.levelName
else else
div.level-name(data-i18n='teacher.not_applicable') div.level-name(data-i18n='teacher.not_applicable')
@ -223,6 +228,8 @@ mixin studentRow(student)
+longLevelName(student.latestCompleteLevel) +longLevelName(student.latestCompleteLevel)
td td
if state.get('progressData') if state.get('progressData')
- var courses = view.classroom.get('courses').map(function(c) { return view.courses.get(c._id); });
- var courseLabelsArray = view.helper.courseLabelsArray(courses);
each trimCourse, index in view.classroom.get('courses') each trimCourse, index in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id); - var course = view.courses.get(trimCourse._id);
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) - var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
@ -230,7 +237,8 @@ mixin studentRow(student)
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student }) - var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student })
- var levelsTotal = trimCourse.levels.length - var levelsTotal = trimCourse.levels.length
//- - var level = ??? //- - var level = ???
+studentCourseProgressDot(progress, levelsTotal, level, 'CS' + (index+1)) - var label = courseLabelsArray[index];
+studentCourseProgressDot(progress, levelsTotal, level, label)
unless student.isEnrolled() unless student.isEnrolled()
+enrollStudentButton(student) +enrollStudentButton(student)
//- td //- td
@ -305,7 +313,7 @@ mixin courseOverview
.course-overview-row .course-overview-row
.course-title.student-name .course-title.student-name
span= course.get('name') span= course.get('name')
span : span= ': '
span(data-i18n='teacher.course_overview') span(data-i18n='teacher.course_overview')
.course-overview-progress .course-overview-progress
each level, index in levels each level, index in levels
@ -324,7 +332,7 @@ mixin studentLevelsRow(student)
each level, index in levels each level, index in levels
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student }) - var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1) - var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1)
+studentLevelProgressDot(progress, level, levelNumber, session) +studentLevelProgressDot(progress, level, levelNumber)
mixin studentCourseProgressDot(progress, levelsTotal, level, label) mixin studentCourseProgressDot(progress, levelsTotal, level, label)
//- TODO: Refactor with TeacherClassesView jade //- TODO: Refactor with TeacherClassesView jade
@ -336,7 +344,7 @@ mixin studentCourseProgressDot(progress, levelsTotal, level, label)
mixin allStudentsLevelProgressDot(progress, level, levelNumber) mixin allStudentsLevelProgressDot(progress, level, levelNumber)
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : ''); - dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- levelName = level.get('name') - levelName = i18n(level.attributes, 'name')
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, numStudents: view.students.length }) - context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, numStudents: view.students.length })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.allStudentsLevelProgressDotTemplate(context)) .progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.allStudentsLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber) +progressDotLabel(levelNumber)
@ -344,7 +352,7 @@ mixin allStudentsLevelProgressDot(progress, level, levelNumber)
mixin studentLevelProgressDot(progress, level, levelNumber) mixin studentLevelProgressDot(progress, level, levelNumber)
//- TODO: Refactor with TeacherClassesView jade //- TODO: Refactor with TeacherClassesView jade
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : ''); - dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- levelName = level.get('name') - levelName = i18n(level.attributes, 'name')
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment }) - context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context)) .progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber) +progressDotLabel(levelNumber)
@ -430,3 +438,37 @@ mixin enrollmentStatusTab
td.enroll-col td.enroll-col
if status !== 'enrolled' if status !== 'enrolled'
button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student") button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student")
mixin studentProjectsTab
#student-projects-tab.m-t-3
if state.get('progressData')
.render-on-course-sync
.student-levels-table
+sortButtons
each student in state.get('students').models
+studentProjectsRow(student)
mixin studentProjectsRow(student)
.row.student-levels-row.alternating-background
div.student-info.col-sm-3
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
div.student-levels-progress.col-sm-9
each trimCourse in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
- var levels = view.classroom.getLevels({courseID: course.id, projectLevels: true}).models
each level in levels
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1)
+studentProjectLink(progress, level, levelNumber, course)
mixin studentProjectLink(progress, level, levelNumber, course)
- var colorClass = progress.completed ? 'btn-primary' : (progress.started ? 'btn-warning' : 'btn-primary');
- var levelName = i18n(level.attributes, 'name')
- var context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment })
- var title = view.singleStudentLevelProgressDotTemplate(context);
if context.session
- var url = '/play/' + level.get('type') + '-level/' + level.get('slug') + '/' + context.session.id + '?course=' + course.id;
a(class="btn btn-lg btn-view-project-level " + colorClass, href=url, data-title=title)= levelName
else
btn(class="btn btn-lg btn-view-project-level " + colorClass, data-title=title, disabled=true)= levelName

View file

@ -76,10 +76,13 @@ mixin classRow(classroom)
if classroom.get('members').length == 0 if classroom.get('members').length == 0
+addStudentsButton(classroom) +addStudentsButton(classroom)
else else
- var courses = classroom.get('courses').map(function(c) { return view.courses.get(c._id); });
- var courseLabelsArray = view.helper.courseLabelsArray(courses);
each trimCourse, index in classroom.get('courses') || [] each trimCourse, index in classroom.get('courses') || []
- var course = view.courses.get(trimCourse._id); - var course = view.courses.get(trimCourse._id);
if view.courseInstances.findWhere({ classroomID: classroom.id, courseID: course.id }) if view.courseInstances.findWhere({ classroomID: classroom.id, courseID: course.id })
+progressDot(classroom, course, index) - var label = courseLabelsArray[index];
+progressDot(classroom, course, label)
.view-class-arrow.col-xs-1 .view-class-arrow.col-xs-1
a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right.view-class-btn(data-classroom-id=classroom.id data-event-action="Teachers Classes View Class Chevron") a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right.view-class-btn(data-classroom-id=classroom.id data-event-action="Teachers Classes View Class Chevron")
@ -99,8 +102,7 @@ mixin createClassButton
a.create-classroom-btn.btn.btn-lg.btn-primary(data-i18n='teacher.create_new_class') a.create-classroom-btn.btn.btn-lg.btn-primary(data-i18n='teacher.create_new_class')
| Create a New Class | Create a New Class
mixin progressDot(classroom, course, index) mixin progressDot(classroom, course, label)
//- TODO: Give classes abbreviations instead of using index?
//- TODO: inefficient. Cache this in the view? //- TODO: inefficient. Cache this in the view?
- courseInstance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) - courseInstance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
- var total = classroom.get('members').length - var total = classroom.get('members').length
@ -113,14 +115,11 @@ mixin progressDot(classroom, course, index)
- dotClass = complete === total ? 'forest' : started ? 'gold' : ''; - dotClass = complete === total ? 'forest' : started ? 'gold' : '';
- var progressDotContext = {total: total, complete: complete}; - var progressDotContext = {total: total, complete: complete};
.progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext)) .progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext))
+progressDotLabel(index) +progressDotLabel(label)
mixin progressDotLabel(index) mixin progressDotLabel(label)
.dot-label .dot-label
.text-h6 .text-h6= label
| CS
span
= index + 1
mixin archivedClassRow(classroom) mixin archivedClassRow(classroom)
.class.row .class.row

View file

@ -64,7 +64,7 @@ block header
a a
span.glyphicon-floppy-disk.glyphicon span.glyphicon-floppy-disk.glyphicon
if level.get('type') === 'ladder' if level.isType('ladder')
li.dropdown li.dropdown
a(data-toggle='dropdown').play-with-team-parent a(data-toggle='dropdown').play-with-team-parent
span.glyphicon-play.glyphicon span.glyphicon-play.glyphicon

View file

@ -5,7 +5,7 @@ block modal-header-content
block modal-body-content block modal-body-content
if view.level.get('type') != 'course-ladder' if !view.level.isType('course-ladder')
h4.language-selection(data-i18n="ladder.select_your_language") Select your language! h4.language-selection(data-i18n="ladder.select_your_language") Select your language!
.form-group.select-group .form-group.select-group
select#tome-language(name="language") select#tome-language(name="language")

View file

@ -7,24 +7,15 @@
.levels-link-area .levels-link-area
a.levels-link(href=homeLink || "/") a.levels-link(href=homeLink || "/")
.glyphicon.glyphicon-play .glyphicon.glyphicon-play
span(data-i18n=me.isSessionless() ? "nav.courses" : (ladderGame ? "general.ladder" : "nav.play")).home-text Levels span(data-i18n=me.isSessionless() ? "nav.courses" : (ladderGame ? "general.ladder" : "nav.play")).home-text
if isMultiplayerLevel && !observing .level-name-area-container
.multiplayer-area-container .level-name-area
.multiplayer-area .level-label(data-i18n="play_level.level")
.multiplayer-label(data-i18n="play_level.control_bar_multiplayer") .level-name(title=difficultyTitle || "")
if multiplayerStatus span #{view.levelNumber ? view.levelNumber + '. ' : ''}#{worldName.replace('Course: ', '')}
.multiplayer-status= multiplayerStatus if levelDifficulty
else sup.level-difficulty= levelDifficulty
.multiplayer-status(data-i18n="play_level.control_bar_join_game")
else
.level-name-area-container
.level-name-area
.level-label(data-i18n="play_level.level")
.level-name(title=difficultyTitle || "")
span #{view.levelNumber ? view.levelNumber + '. ' : ''}#{worldName.replace('Course: ', '')}
if levelDifficulty
sup.level-difficulty= levelDifficulty
.buttons-area .buttons-area

View file

@ -48,7 +48,7 @@ block modal-body-content
textarea(data-i18n="[placeholder]play_level.victory_review_placeholder") textarea(data-i18n="[placeholder]play_level.victory_review_placeholder")
.clearfix .clearfix
if level.get('type', true) === 'hero' || level.get('type') == 'hero-ladder' if level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev')
for achievement in achievements for achievement in achievements
- var animate = achievement.completed && !achievement.completedAWhileAgo - var animate = achievement.completed && !achievement.completedAWhileAgo
.achievement-panel(class=achievement.completedAWhileAgo ? 'earned' : '' data-achievement-id=achievement.id data-animate=animate) .achievement-panel(class=achievement.completedAWhileAgo ? 'earned' : '' data-achievement-id=achievement.id data-animate=animate)
@ -108,6 +108,24 @@ block modal-footer-content
.total-count#gem-total 0 .total-count#gem-total 0
.total-label(data-i18n="play_level.victory_gems_gained") Gems Gained .total-label(data-i18n="play_level.victory_gems_gained") Gems Gained
if view.shareURL
#share-level-container
span.share-level-label
span(data-i18n='sharing.victory_share_prefix') Share this link to invite your friends & family to
span= ' '
a(href=view.shareURL, target='_blank')
if view.level.isType('game-dev')
span(data-i18n='sharing.victory_share_game') play your game level
else
span(data-i18n='sharing.victory_share_web') view your webpage
span(data-i18n='sharing.victory_share_suffix') .
.row
.col-sm-9
input.text-h4.semibold.form-control.input-md#share-level-input(value=view.shareURL)
.col-sm-3
button#share-level-btn.btn.btn-md.btn-success.btn-illustrated
span(data-i18n='sharing.copy_url') Copy URL
if me.get('anonymous') if me.get('anonymous')
.sign-up-poke.hide .sign-up-poke.hide
.sign-up-blurb(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account! .sign-up-blurb(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account!
@ -118,7 +136,7 @@ block modal-footer-content
.next-level-buttons .next-level-buttons
if readyToRank if readyToRank
.ladder-submission-view .ladder-submission-view
else if level.get('type') === 'hero-ladder' else if level.isType('hero-ladder')
button.btn.btn-illustrated.btn-primary.btn-lg.return-to-ladder-button(data-href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder button.btn.btn-illustrated.btn-primary.btn-lg.return-to-ladder-button(data-href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder
else else
button.btn.btn-illustrated.btn-success.btn-lg.world-map-button.next-level-button.hide#continue-button(data-i18n="common.continue") Continue button.btn.btn-illustrated.btn-success.btn-lg.world-map-button.next-level-button.hide#continue-button(data-i18n="common.continue") Continue

View file

@ -0,0 +1,23 @@
extends /templates/core/modal-base-flat
block modal-header-content
h3(data-i18n="web_dev.image_gallery_title")
span(data-i18n="web_dev.image_gallery_description")
block modal-body-content
dl.dl-horizontal
for image in view.images
dt
img(src=image.portraitURL)
dd
ul.list-unstyled
li
span.no-select= 'URL: '
kbd= image.portraitURL
br
li
span.no-select= '<img>: '
kbd= '<img src="' + image.portraitURL + '">'
block modal-footer-content
a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="modal.close").btn.btn-primary Close

View file

@ -38,12 +38,37 @@
span : span :
h2.text-uppercase= i18n(view.nextLevel.attributes, 'name').replace('Course: ', '') h2.text-uppercase= i18n(view.nextLevel.attributes, 'name').replace('Course: ', '')
div!= view.nextLevelDescription div.next-level-description!= view.nextLevelDescription
if view.shareURL
.well.well-sm.well-parchment
h3.text-uppercase
if view.level.isType('game-dev')
span(data-i18n='sharing.share_game')
else
span(data-i18n='sharing.share_web')
p
span(data-i18n='sharing.victory_course_share_prefix')
span= ' '
a(href=view.shareURL, target='_blank')
if view.level.isType('game-dev')
span(data-i18n='sharing.victory_course_share_game')
else
span(data-i18n='sharing.victory_course_share_web')
span= ' '
span(data-i18n='sharing.victory_course_share_suffix')
.row
.col-sm-9
input.text-h4.semibold.form-control.input-lg#share-level-input(value=view.shareURL)
.col-sm-3
button#share-level-btn.btn.btn-lg.btn-success.btn-illustrated
span(data-i18n='sharing.copy_url')
.row .row
.col-sm-5.col-sm-offset-2 .col-sm-5.col-sm-offset-2
// TODO: Add this and rest of campaign functionality // TODO: Add rest of campaign functionality
// button#continue-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase View Leaderboards if view.level.get('type') === 'course-ladder'
button#ladder-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase Ladder
.col-sm-5 .col-sm-5
if !view.nextLevel.isNew() if !view.nextLevel.isNew()
button#next-level-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.next_level') button#next-level-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.next_level')

View file

@ -13,7 +13,7 @@ block modal-body-content
block modal-footer-content block modal-footer-content
if readyToRank if readyToRank
.ladder-submission-view .ladder-submission-view
else if level.get('type') === 'ladder' else if level.isType('ladder')
a.btn.btn-primary(href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder a.btn.btn-primary(href="/play/ladder/#{level.get('slug')}#my-matches", data-dismiss="modal", data-i18n="play_level.victory_return_to_ladder") Return to Ladder
else else
a.btn.btn-primary(href="/", data-dismiss="modal", data-i18n="play_level.victory_go_home") Go Home a.btn.btn-primary(href="/", data-dismiss="modal", data-i18n="play_level.victory_go_home") Go Home

View file

@ -0,0 +1,38 @@
.container-fluid
.row
.col-xs-9
#canvas-wrapper
canvas(width=924, height=589)#webgl-surface
canvas(width=924, height=589)#normal-surface
.col-xs-3#info-col.style-flat
if view.state.get('errorMessage')
.alert.alert-danger= view.state.get('errorMessage')
else if view.state.get('loading')
h1.m-y-1(data-i18n="common.loading")
.progress
.progress-bar(style="width: #{view.state.get('progress')}")
else
h1.m-y-1 Info
ul
li
b
span(data-i18n="play_level.level")
span= ': '
| #{view.level.get('name')}
li
b
span(data-i18n="game_dev.creator")
span= ': '
| #{view.session.get('creatorName')}
- var playing = view.state.get('playing')
.m-y-3
if playing
button#play-btn.btn.btn-lg.btn-burgandy(data-i18n="play_level.restart")
else
button#play-btn.btn.btn-lg.btn-navy(data-i18n="common.play")

View file

@ -0,0 +1,11 @@
#web-surface-view
#info-bar.style-flat
if !view.supermodel.finished()
h1(data-i18n="common.loading")
else
h1
span(data-i18n="game_dev.creator")
span= ': '
| #{view.session.get('creatorName')}

View file

@ -3,16 +3,10 @@
.hinge.hinge-2 .hinge.hinge-2
.hinge.hinge-3 .hinge.hinge-3
if includeSpellList
.btn.btn-small.btn-illustrated.spell-list-button(data-i18n="[title]play_level.tome_see_all_methods", title="See all methods you can edit")
.glyphicon.glyphicon-chevron-down
.thang-avatar-placeholder
.spell-tool-buttons .spell-tool-buttons
.btn.btn-small.btn-illustrated.btn-warning.reload-code(data-i18n="[title]play_level.tome_reload_method", title="Reload original code for this method") .btn.btn-small.btn-illustrated.btn-warning.reload-code(data-i18n="[title]play_level.tome_reload_method")
.glyphicon.glyphicon-repeat .glyphicon.glyphicon-repeat
span.spl(data-i18n="play_level.reload") Reload span.spl(data-i18n="play_level.restart")
if me.level() >= 15 if me.level() >= 15
.btn.btn-small.btn-illustrated.fullscreen-code(title=maximizeShortcutVerbose) .btn.btn-small.btn-illustrated.fullscreen-code(title=maximizeShortcutVerbose)
@ -27,4 +21,17 @@ if includeSpellList
.btn.btn-small.btn-illustrated.hints-button .btn.btn-small.btn-illustrated.hints-button
span(data-i18n="play_level.hints") span(data-i18n="play_level.hints")
if view.options.level.isType('web-dev')
.btn.btn-small.btn-illustrated.image-gallery-button
span(data-i18n='web_dev.image_gallery_title')
if view.options.level.get('shareable')
- var url = '/play/' + view.options.level.get('type') + '-level/' + view.options.level.get('slug') + '/' + view.options.session.id;
- if (view.options.courseID) url += '?course=' + view.options.courseID;
a.btn.btn-small.btn-illustrated(href=url)
if view.options.level.isType('game-dev')
span(data-i18n='sharing.game')
else
span(data-i18n='sharing.webpage')
.clearfix .clearfix

View file

@ -1 +0,0 @@
h5(data-i18n="play_level.tome_select_method") Select a Method

View file

@ -1,7 +0,0 @@
if showTopDivider
// Don't repeat Thang names when not changed from previous entry
.thang-names(title=thangNames)= thangNames
code #{spell.name}(#{parameters})

View file

@ -1,3 +0,0 @@
h4
span(data-i18n="play_level.tome_select_a_thang") Select Someone for
code #{view.spell.name}(#{(view.spell.parameters || []).join(", ")})

View file

@ -95,7 +95,7 @@ if !selectedMethod
else if language == 'io' else if language == 'io'
span= (doc.ownerName == 'this' ? '' : doc.ownerName + ' ') + docName + '(' + argumentExamples.join(', ') + ')' span= (doc.ownerName == 'this' ? '' : doc.ownerName + ' ') + docName + '(' + argumentExamples.join(', ') + ')'
if (doc.type != 'function' && doc.type != 'snippet') || doc.name == 'now' if (doc.type != 'function' && doc.type != 'snippet' && doc.owner != 'HTML' && doc.owner != 'CSS') || doc.name == 'now'
p.value p.value
strong strong
span(data-i18n="skill_docs.current_value") Current Value span(data-i18n="skill_docs.current_value") Current Value

View file

@ -1,11 +1,7 @@
#spell-list-tab-entry-view #spell-top-bar-view
#spell-list-view
#cast-button-view #cast-button-view
#spell-view #spell-view
#spell-palette-view #spell-palette-view

View file

@ -0,0 +1 @@
iframe(src="/web-dev-iframe.html")

View file

@ -1,90 +0,0 @@
if !ladderGame
.form
.form-group.checkbox
label(for="multiplayer")
input#multiplayer(name="multiplayer", type="checkbox", checked=multiplayer)
span(data-i18n="multiplayer.multiplayer_toggle") Enable multiplayer
span.help-block(data-i18n="multiplayer.multiplayer_toggle_description") Allow others to join your game.
hr
div#link-area
p(data-i18n="multiplayer.multiplayer_link_description") Give this link to anyone to have them join you.
textarea.well#multiplayer-join-link(readonly=true)= joinLink
p
strong(data-i18n="multiplayer.multiplayer_hint_label") Hint:
span(data-i18n="multiplayer.multiplayer_hint") Click the link to select all, then press ⌘-C or Ctrl-C to copy the link.
p(data-i18n="multiplayer.multiplayer_coming_soon") More multiplayer features to come!
if ladderGame
if me.get('anonymous')
p(data-i18n="multiplayer.multiplayer_sign_in_leaderboard") Sign in or create an account and get your solution on the leaderboard.
else if realTimeSessions && realTimeSessionsPlayers
button#create-game-button Create Game
hr
div#created-multiplayer-session
h3 Your Game
if currentRealTimeSession
div
span(style="margin:10px")= currentRealTimeSession.get('levelID')
span(style="margin:10px")= currentRealTimeSession.get('creatorName')
span(style="margin:10px")= currentRealTimeSession.get('state')
span(style="margin:10px")= currentRealTimeSession.id
button#leave-game-button(data-item=item) Leave Game
div
- var players = realTimeSessionsPlayers[currentRealTimeSession.id]
if players
span(style="margin:10px") Players:
- for (var i=0; i < players.length; i++) {
span(style="margin:10px")= players.at(i).get('name')
span(style="margin:10px")= players.at(i).get('team')
span(style="margin:10px")= players.at(i).get('state')
- }
else
span No Players?
else
div Click something above to create a game.
hr
div#open-games
h3 Open Games
//- TODO: do not let you join ones with same-team opponent
- var noOpenGames = true
- for (var i=0; i < realTimeSessions.length; i++) {
if (currentRealTimeSession && realTimeSessions.at(i).id == currentRealTimeSession.id)
- continue
if levelID === realTimeSessions.at(i).get('levelID') && realTimeSessions.at(i).get('state') === 'creating'
- var id = realTimeSessions.at(i).get('id')
- var players = realTimeSessionsPlayers[id]
if players && players.length === 1
- noOpenGames = false
- var creatorName = realTimeSessions.at(i).get('creatorName')
- var creator = realTimeSessions.at(i).get('creator')
- var state = realTimeSessions.at(i).get('state')
- var item = realTimeSessions.at(i)
div
button#join-game-button(data-item=item) Join Game
span(style="margin:10px")= levelID
span(style="margin:10px")= creatorName
span(style="margin:10px")= state
span(style="margin:10px")= id
div
span(style="margin:10px") Players:
span(style="margin:10px")= players.at(0).get('name')
span(style="margin:10px")= players.at(0).get('team')
span(style="margin:10px")= players.at(0).get('state')
- }
if noOpenGames
div No games available.
hr
.ladder-submission-view
else
a.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches", data-i18n="multiplayer.victory_go_ladder") Return to Ladder

View file

@ -24,6 +24,8 @@ if view.showAds()
#canvas-wrapper #canvas-wrapper
canvas(width=924, height=589)#webgl-surface canvas(width=924, height=589)#webgl-surface
canvas(width=924, height=589)#normal-surface canvas(width=924, height=589)#normal-surface
#web-surface-view
#ascii-surface #ascii-surface
#canvas-left-gradient.gradient #canvas-left-gradient.gradient
#canvas-top-gradient.gradient #canvas-top-gradient.gradient

View file

@ -9,6 +9,7 @@ Campaigns = require 'collections/Campaigns'
Classroom = require 'models/Classroom' Classroom = require 'models/Classroom'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course' Course = require 'models/Course'
Courses = require 'collections/Courses'
LevelSessions = require 'collections/LevelSessions' LevelSessions = require 'collections/LevelSessions'
User = require 'models/User' User = require 'models/User'
Users = require 'collections/Users' Users = require 'collections/Users'
@ -152,6 +153,7 @@ module.exports = class MainAdminView extends RootView
$('.classroom-progress-csv').prop('disabled', true) $('.classroom-progress-csv').prop('disabled', true)
classCode = $('.classroom-progress-class-code').val() classCode = $('.classroom-progress-class-code').val()
classroom = null classroom = null
courses = null
courseLevels = [] courseLevels = []
sessions = null sessions = null
users = null users = null
@ -161,12 +163,16 @@ module.exports = class MainAdminView extends RootView
classroom = new Classroom({ _id: model.data._id }) classroom = new Classroom({ _id: model.data._id })
Promise.resolve(classroom.fetch()) Promise.resolve(classroom.fetch())
.then (model) => .then (model) =>
courses = new Courses()
Promise.resolve(courses.fetch())
.then (models) =>
for course, index in classroom.get('courses') for course, index in classroom.get('courses')
for level in course.levels for level in course.levels
courseLevels.push courseLevels.push
courseIndex: index + 1 courseIndex: index + 1
levelID: level.original levelID: level.original
slug: level.slug slug: level.slug
courseSlug: courses.get(course._id).get('slug')
users = new Users() users = new Users()
Promise.resolve($.when(users.fetchForClassroom(classroom)...)) Promise.resolve($.when(users.fetchForClassroom(classroom)...))
.then (models) => .then (models) =>
@ -202,12 +208,19 @@ module.exports = class MainAdminView extends RootView
columnLabels = "Username" columnLabels = "Username"
currentLevel = 1 currentLevel = 1
courseLabelIndexes = CS: 1, GD: 0, WD: 0
lastCourseIndex = 1 lastCourseIndex = 1
lastCourseLabel = 'CS1'
for level in courseLevels for level in courseLevels
unless level.courseIndex is lastCourseIndex unless level.courseIndex is lastCourseIndex
currentLevel = 1 currentLevel = 1
lastCourseIndex = level.courseIndex lastCourseIndex = level.courseIndex
columnLabels += ",CS#{level.courseIndex}.#{currentLevel++} #{level.slug}" acronym = switch
when /game-dev/.test(level.courseSlug) then 'GD'
when /web-dev/.test(level.courseSlug) then 'WD'
else 'CS'
lastCourseLabel = acronym + ++courseLabelIndexes[acronym]
columnLabels += ",#{lastCourseLabel}.#{currentLevel++} #{level.slug}"
csvContent = "data:text/csv;charset=utf-8,#{columnLabels}\n" csvContent = "data:text/csv;charset=utf-8,#{columnLabels}\n"
for studentRow in userPlaytimes for studentRow in userPlaytimes
csvContent += studentRow.join(',') + "\n" csvContent += studentRow.join(',') + "\n"

View file

@ -195,7 +195,7 @@ module.exports = class ClanDetailsView extends RootView
if level.concepts? if level.concepts?
for concept in level.concepts for concept in level.concepts
@conceptsProgression.push concept unless concept in @conceptsProgression @conceptsProgression.push concept unless concept in @conceptsProgression
if level.type is 'hero-ladder' and level.slug not in ['capture-their-flag'] if level.type is 'hero-ladder' and level.slug not in ['capture-their-flag'] # Would use isType, but it's not a Level model
@arenas.push level @arenas.push level
@campaignLevelProgressions.push campaignLevelProgression @campaignLevelProgressions.push campaignLevelProgression
@render?() @render?()

View file

@ -496,6 +496,13 @@ module.exports = class CocoView extends Backbone.View
playSound: (trigger, volume=1) -> playSound: (trigger, volume=1) ->
Backbone.Mediator.publish 'audio-player:play-sound', trigger: trigger, volume: volume Backbone.Mediator.publish 'audio-player:play-sound', trigger: trigger, volume: volume
tryCopy: ->
try
document.execCommand('copy')
catch err
message = 'Oops, unable to copy'
noty text: message, layout: 'topCenter', type: 'error', killer: false
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|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|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 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

View file

@ -52,7 +52,7 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.trackRequest(@classroom.fetch()) @supermodel.trackRequest(@classroom.fetch())
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, { levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
data: { project: 'concepts,practice,type,slug,name,original,description' } data: { project: 'concepts,practice,type,slug,name,original,description,shareable,i18n' }
})) }))
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=> @supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>
@ -84,9 +84,9 @@ module.exports = class CourseDetailsView extends RootView
@levelConceptMap = {} @levelConceptMap = {}
for level in @levels.models for level in @levels.models
@levelConceptMap[level.get('original')] ?= {} @levelConceptMap[level.get('original')] ?= {}
for concept in level.get('concepts') for concept in level.get('concepts') or []
@levelConceptMap[level.get('original')][concept] = true @levelConceptMap[level.get('original')][concept] = true
if level.get('type') is 'course-ladder' if level.isType('course-ladder')
@arenaLevel = level @arenaLevel = level
# console.log 'onLevelSessionsSync' # console.log 'onLevelSessionsSync'
@ -130,7 +130,7 @@ module.exports = class CourseDetailsView extends RootView
levelID = $(e.target).closest('.btn-play-level').data('level-id') levelID = $(e.target).closest('.btn-play-level').data('level-id')
level = @levels.findWhere({original: levelID}) level = @levels.findWhere({original: levelID})
window.tracker?.trackEvent 'Students Class Course Play Level', category: 'Students', courseID: @courseID, courseInstanceID: @courseInstanceID, levelSlug: levelSlug, ['Mixpanel'] window.tracker?.trackEvent 'Students Class Course Play Level', category: 'Students', courseID: @courseID, courseInstanceID: @courseInstanceID, levelSlug: levelSlug, ['Mixpanel']
if level.get('type') is 'course-ladder' if level.isType('course-ladder')
viewClass = 'views/ladder/LadderView' viewClass = 'views/ladder/LadderView'
viewArgs = [{supermodel: @supermodel}, levelSlug] viewArgs = [{supermodel: @supermodel}, levelSlug]
route = '/play/ladder/' + levelSlug route = '/play/ladder/' + levelSlug

View file

@ -23,6 +23,7 @@ CourseInstances = require 'collections/CourseInstances'
module.exports = class TeacherClassView extends RootView module.exports = class TeacherClassView extends RootView
id: 'teacher-class-view' id: 'teacher-class-view'
template: template template: template
helper: helper
events: events:
'click .nav-tabs a': 'onClickNavTabLink' 'click .nav-tabs a': 'onClickNavTabLink'
@ -119,7 +120,7 @@ module.exports = class TeacherClassView extends RootView
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID) @supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@levels = new Levels() @levels = new Levels()
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice'}}) @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice,shareable,i18n'}})
@attachMediatorEvents() @attachMediatorEvents()
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@ -177,7 +178,7 @@ module.exports = class TeacherClassView extends RootView
afterRender: -> afterRender: ->
super(arguments...) super(arguments...)
$('.progress-dot').each (i, el) -> $('.progress-dot, .btn-view-project-level').each (i, el) ->
dot = $(el) dot = $(el)
dot.tooltip({ dot.tooltip({
html: true html: true
@ -227,13 +228,6 @@ module.exports = class TeacherClassView extends RootView
@$('#join-url-input').val(@state.get('joinURL')).select() @$('#join-url-input').val(@state.get('joinURL')).select()
@tryCopy() @tryCopy()
tryCopy: ->
try
document.execCommand('copy')
catch err
message = 'Oops, unable to copy'
noty text: message, layout: 'topCenter', type: 'error', killer: false
onClickUnarchive: -> onClickUnarchive: ->
window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@classroom.save { archived: false } @classroom.save { archived: false }
@ -328,9 +322,11 @@ module.exports = class TeacherClassView extends RootView
window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
courseLabels = "" courseLabels = ""
courseOrder = [] courseOrder = []
for course, index in @classroom.get('courses') courses = (@courses.get(c._id) for c in @classroom.get('courses'))
courseLabels += "CS#{index + 1} Playtime," courseLabelsArray = helper.courseLabelsArray courses
courseOrder.push(course._id) for course, index in courses
courseLabels += "#{courseLabelsArray[index]} Playtime,"
courseOrder.push(course.id)
csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n" csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n"
levelCourseMap = {} levelCourseMap = {}
for trimCourse in @classroom.get('courses') for trimCourse in @classroom.get('courses')
@ -396,6 +392,7 @@ module.exports = class TeacherClassView extends RootView
not @students.get(userID).isEnrolled() not @students.get(userID).isEnrolled()
assigningToNobody = selectedIDs.length is 0 assigningToNobody = selectedIDs.length is 0
@state.set errors: { assigningToNobody, assigningToUnenrolled } @state.set errors: { assigningToNobody, assigningToUnenrolled }
return if assigningToNobody
@assignCourse courseID, members @assignCourse courseID, members
window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel'] window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel']

View file

@ -17,6 +17,7 @@ helper = require 'lib/coursesHelper'
module.exports = class TeacherClassesView extends RootView module.exports = class TeacherClassesView extends RootView
id: 'teacher-classes-view' id: 'teacher-classes-view'
template: template template: template
helper: helper
events: events:
'click .edit-classroom': 'onClickEditClassroom' 'click .edit-classroom': 'onClickEditClassroom'

View file

@ -46,7 +46,7 @@ module.exports = class ThangComponentConfigView extends CocoView
schema.default ?= {} schema.default ?= {}
_.merge schema.default, @additionalDefaults if @additionalDefaults _.merge schema.default, @additionalDefaults if @additionalDefaults
if @level?.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] if @level?.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
schema.required = [] schema.required = []
treemaOptions = treemaOptions =
supermodel: @supermodel supermodel: @supermodel

View file

@ -37,6 +37,7 @@ require 'vendor/aether-python'
require 'vendor/aether-coffeescript' require 'vendor/aether-coffeescript'
require 'vendor/aether-lua' require 'vendor/aether-lua'
require 'vendor/aether-java' require 'vendor/aether-java'
require 'vendor/aether-html'
module.exports = class LevelEditView extends RootView module.exports = class LevelEditView extends RootView
id: 'editor-level-view' id: 'editor-level-view'

View file

@ -41,7 +41,7 @@ module.exports = class LevelThangEditView extends CocoView
level: @level level: @level
world: @world world: @world
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then options.thangType = thangType if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') then options.thangType = thangType
@thangComponentEditView = new ThangComponentsEditView options @thangComponentEditView = new ThangComponentsEditView options
@listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged @listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged

View file

@ -636,14 +636,14 @@ module.exports = class ThangsTabView extends CocoView
if batchInsert if batchInsert
if thangType.get('name') is 'Hero Placeholder' if thangType.get('name') is 'Hero Placeholder'
thangID = 'Hero Placeholder' thangID = 'Hero Placeholder'
return if not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) or @getThangByID(thangID) return if not @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') or @getThangByID(thangID)
else else
thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}" thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}"
else else
thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID) thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID)
if @cloneSourceThang if @cloneSourceThang
components = _.cloneDeep @getThangByID(@cloneSourceThang.id).components components = _.cloneDeep @getThangByID(@cloneSourceThang.id).components
else if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] else if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
components = [] # Load them all from default ThangType Components components = [] # Load them all from default ThangType Components
else else
components = _.cloneDeep thangType.get('components') ? [] components = _.cloneDeep thangType.get('components') ? []

View file

@ -78,7 +78,7 @@ module.exports = class VerifierTest extends CocoClass
@listenToOnce @god, 'infinite-loop', @fail @listenToOnce @god, 'infinite-loop', @fail
@listenToOnce @god, 'user-code-problem', @onUserCodeProblem @listenToOnce @god, 'user-code-problem', @onUserCodeProblem
@listenToOnce @god, 'goals-calculated', @processSingleGameResults @listenToOnce @god, 'goals-calculated', @processSingleGameResults
@god.createWorld @generateSpellsObject() @god.createWorld @session.generateSpellsObject()
@updateCallback? state: 'running' @updateCallback? state: 'running'
processSingleGameResults: (e) -> processSingleGameResults: (e) ->
@ -118,18 +118,6 @@ module.exports = class VerifierTest extends CocoClass
@updateCallback? state: @state @updateCallback? state: @state
@scheduleCleanup() @scheduleCleanup()
generateSpellsObject: ->
aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @session.get('codeLanguage')
spellThang = aether: new Aether aetherOptions
spells = "hero-placeholder/plan": thangs: {'Hero Placeholder': spellThang}, name: 'plan'
source = @session.get('code')['hero-placeholder'].plan
try
spellThang.aether.transpile source
catch e
console.log "Couldn't transpile!\n#{source}\n", e
spellThang.aether.transpile ''
spells
scheduleCleanup: -> scheduleCleanup: ->
setTimeout @cleanup, 100 setTimeout @cleanup, 100

View file

@ -51,7 +51,7 @@ module.exports = class VerifierView extends RootView
for campaign in @campaigns.models when campaign.get('type') in ['course', 'hero'] and campaign.get('slug') isnt 'picoctf' for campaign in @campaigns.models when campaign.get('type') in ['course', 'hero'] and campaign.get('slug') isnt 'picoctf'
@levelsByCampaign[campaign.get('slug')] ?= {levels: [], checked: true} @levelsByCampaign[campaign.get('slug')] ?= {levels: [], checked: true}
campaignInfo = @levelsByCampaign[campaign.get('slug')] campaignInfo = @levelsByCampaign[campaign.get('slug')]
for levelID, level of campaign.get('levels') when level.type not in ['hero-ladder', 'course-ladder', 'game-dev'] for levelID, level of campaign.get('levels') when level.type not in ['hero-ladder', 'course-ladder', 'game-dev', 'web-dev'] # Would use isType, but it's not a Level model
campaignInfo.levels.push level.slug campaignInfo.levels.push level.slug
filterCodeLanguages: -> filterCodeLanguages: ->

View file

@ -26,8 +26,8 @@ module.exports = class LadderPlayModal extends ModalView
initialize: (options, @level, @session, @team) -> initialize: (options, @level, @session, @team) ->
@otherTeam = if @team is 'ogres' then 'humans' else 'ogres' @otherTeam = if @team is 'ogres' then 'humans' else 'ogres'
@startLoadingChallengersMaybe()
@wizardType = ThangType.loadUniversalWizard() @wizardType = ThangType.loadUniversalWizard()
@startLoadingChallengersMaybe()
@levelID = @level.get('slug') or @level.id @levelID = @level.get('slug') or @level.id
@language = @session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python' @language = @session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python'
@languages = [ @languages = [

View file

@ -94,52 +94,6 @@ heroArenas = [
} }
] ]
oldArenas = [
{
name: 'Criss-Cross'
difficulty: 5
id: 'criss-cross'
image: '/file/db/level/5391f3d519dc22b8082159b2/banner2.png'
description: 'Participate in a bidding war with opponents to reach the other side!'
}
{
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: 'Sky Span (Testing)'
difficulty: 3
id: 'sky-span'
image: '/file/db/level/53c80fce0ddbef000084c667/sky-Span-banner.jpg'
description: 'Preview version of an upgraded Dungeon Arena. Help us with hero balance before release!'
}
{
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.)'
}
]
campaigns = [ campaigns = [
{id: 'multiplayer', name: 'Multiplayer Arenas', description: '... in which you code head-to-head against other players.', levels: heroArenas} {id: 'multiplayer', name: 'Multiplayer Arenas', description: '... in which you code head-to-head against other players.', levels: heroArenas}
#{id: 'old_multiplayer', name: '(Deprecated) Old Multiplayer Arenas', description: 'Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas.', levels: oldArenas}
] ]

View file

@ -24,7 +24,7 @@ module.exports = class SimulateTabView extends CocoView
onLoaded: -> onLoaded: ->
super() super()
@render() @render()
if (document.location.hash is '#simulate' or @options.level.get('type') is 'course-ladder') and not @simulator if (document.location.hash is '#simulate' or @options.level.isType('course-ladder')) and not @simulator
@startSimulating() @startSimulating()
afterRender: -> afterRender: ->

View file

@ -397,8 +397,9 @@ module.exports = class CampaignView extends RootView
@particleMan.removeEmitters() @particleMan.removeEmitters()
@particleMan.attach @$el.find('.map') @particleMan.attach @$el.find('.map')
for level in @campaign.renderedLevels ? {} for level in @campaign.renderedLevels ? {}
particleKey = ['level', @terrain.replace('-branching-test', '')] terrain = @terrain.replace('-branching-test', '').replace(/(game|web)-dev-\d/, 'forest')
particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) particleKey = ['level', terrain]
particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) # Would use isType, but it's not a Level model
particleKey.push 'replayable' if level.replayable particleKey.push 'replayable' if level.replayable
particleKey.push 'premium' if level.requiresSubscription particleKey.push 'premium' if level.requiresSubscription
particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones', 'summits-gate'] particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones', 'summits-gate']
@ -532,7 +533,7 @@ module.exports = class CampaignView extends RootView
levelElement = $(e.target).parents('.level-info-container') levelElement = $(e.target).parents('.level-info-container')
levelSlug = levelElement.data('level-slug') levelSlug = levelElement.data('level-slug')
level = _.find _.values(@campaign.get('levels')), slug: levelSlug level = _.find _.values(@campaign.get('levels')), slug: levelSlug
if level.type in ['hero-ladder', 'course-ladder'] if level.type in ['hero-ladder', 'course-ladder'] # Would use isType, but it's not a Level model
Backbone.Mediator.publish 'router:navigate', route: "/play/ladder/#{levelSlug}", viewClass: 'views/ladder/LadderView', viewArgs: [{supermodel: @supermodel}, levelSlug] Backbone.Mediator.publish 'router:navigate', route: "/play/ladder/#{levelSlug}", viewClass: 'views/ladder/LadderView', viewArgs: [{supermodel: @supermodel}, levelSlug]
else else
@showLeaderboard levelSlug @showLeaderboard levelSlug

View file

@ -144,9 +144,6 @@ module.exports = class SpectateLevelView extends RootView
if c then myCode[thang][spell] = c else delete myCode[thang][spell] if c then myCode[thang][spell] = c else delete myCode[thang][spell]
@session.set('code', myCode) @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.
@session.set 'multiplayer', false
onLevelStarted: (e) -> onLevelStarted: (e) ->
go = => go = =>
@ -181,7 +178,7 @@ module.exports = class SpectateLevelView extends RootView
@insertSubView new GoldView {} @insertSubView new GoldView {}
@insertSubView new HUDView {level: @level} @insertSubView new HUDView {level: @level}
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder'] @insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.isType('hero-ladder', 'course-ladder')
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, spectateGame: true} @insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, spectateGame: true}
# callbacks # callbacks

View file

@ -7,17 +7,13 @@ Classroom = require 'models/Classroom'
Course = require 'models/Course' Course = require 'models/Course'
CourseInstance = require 'models/CourseInstance' CourseInstance = require 'models/CourseInstance'
GameMenuModal = require 'views/play/menu/GameMenuModal' GameMenuModal = require 'views/play/menu/GameMenuModal'
RealTimeModel = require 'models/RealTimeModel'
RealTimeCollection = require 'collections/RealTimeCollection'
LevelSetupManager = require 'lib/LevelSetupManager' LevelSetupManager = require 'lib/LevelSetupManager'
GameMenuModal = require 'views/play/menu/GameMenuModal'
module.exports = class ControlBarView extends CocoView module.exports = class ControlBarView extends CocoView
id: 'control-bar-view' id: 'control-bar-view'
template: template template: template
subscriptions: subscriptions:
'bus:player-states-changed': 'onPlayerStatesChanged'
'level:disable-controls': 'onDisableControls' 'level:disable-controls': 'onDisableControls'
'level:enable-controls': 'onEnableControls' 'level:enable-controls': 'onEnableControls'
'ipad:memory-warning': 'onIPadMemoryWarning' 'ipad:memory-warning': 'onIPadMemoryWarning'
@ -28,7 +24,6 @@ module.exports = class ControlBarView extends CocoView
'click': -> Backbone.Mediator.publish 'tome:focus-editor', {} 'click': -> Backbone.Mediator.publish 'tome:focus-editor', {}
'click .levels-link-area': 'onClickHome' 'click .levels-link-area': 'onClickHome'
'click .home a': 'onClickHome' 'click .home a': 'onClickHome'
'click .multiplayer-area': 'onClickMultiplayer'
'click #control-bar-sign-up-button': 'onClickSignupButton' 'click #control-bar-sign-up-button': 'onClickSignupButton'
constructor: (options) -> constructor: (options) ->
@ -45,7 +40,7 @@ module.exports = class ControlBarView extends CocoView
@observing = options.session.get('creator') isnt me.id @observing = options.session.get('creator') isnt me.id
@levelNumber = '' @levelNumber = ''
if @level.get('type') is 'course' and @level.get('campaignIndex')? if @level.isType('course', 'game-dev', 'web-dev') and @level.get('campaignIndex')?
@levelNumber = @level.get('campaignIndex') + 1 @levelNumber = @level.get('campaignIndex') + 1
if @courseInstanceID if @courseInstanceID
@courseInstance = new CourseInstance(_id: @courseInstanceID) @courseInstance = new CourseInstance(_id: @courseInstanceID)
@ -64,9 +59,6 @@ module.exports = class ControlBarView extends CocoView
@supermodel.trackRequest(@campaign.fetch()) @supermodel.trackRequest(@campaign.fetch())
) )
super options super options
if @level.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
@isMultiplayerLevel = true
@multiplayerStatusManager = new MultiplayerStatusManager @levelID, @onMultiplayerStateChanged
if @level.get 'replayable' if @level.get 'replayable'
@listenTo @session, 'change-difficulty', @onSessionDifficultyChanged @listenTo @session, 'change-difficulty', @onSessionDifficultyChanged
@ -79,25 +71,10 @@ module.exports = class ControlBarView extends CocoView
setBus: (@bus) -> setBus: (@bus) ->
onPlayerStatesChanged: (e) ->
# TODO: this doesn't fire any more. Replacement?
return unless @bus is e.bus
numPlayers = _.keys(e.players).length
return if numPlayers is @numPlayers
@numPlayers = numPlayers
text = 'Multiplayer'
text += " (#{numPlayers})" if numPlayers > 1
$('#multiplayer-button', @$el).text(text)
onMultiplayerStateChanged: => @render?()
getRenderData: (c={}) -> getRenderData: (c={}) ->
super c super c
c.worldName = @worldName c.worldName = @worldName
c.multiplayerEnabled = @session.get('multiplayer') c.ladderGame = @level.isType('ladder', 'hero-ladder', 'course-ladder')
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
if c.isMultiplayerLevel = @isMultiplayerLevel
c.multiplayerStatus = @multiplayerStatusManager?.status
if @level.get 'replayable' if @level.get 'replayable'
c.levelDifficulty = @session.get('state')?.difficulty ? 0 c.levelDifficulty = @session.get('state')?.difficulty ? 0
if @observing if @observing
@ -110,23 +87,17 @@ module.exports = class ControlBarView extends CocoView
if me.isSessionless() if me.isSessionless()
@homeLink = "/teachers/courses" @homeLink = "/teachers/courses"
@homeViewClass = "views/courses/TeacherCoursesView" @homeViewClass = "views/courses/TeacherCoursesView"
else if @level.get('type', true) in ['ladder', 'ladder-tutorial', 'hero-ladder', 'course-ladder'] else if @level.isType('ladder', 'ladder-tutorial', 'hero-ladder', 'course-ladder')
levelID = @level.get('slug')?.replace(/\-tutorial$/, '') or @level.id levelID = @level.get('slug')?.replace(/\-tutorial$/, '') or @level.id
@homeLink = '/play/ladder/' + levelID @homeLink = '/play/ladder/' + levelID
@homeViewClass = 'views/ladder/LadderView' @homeViewClass = 'views/ladder/LadderView'
@homeViewArgs.push levelID @homeViewArgs.push levelID
if leagueID = @getQueryVariable 'league' if leagueID = @getQueryVariable 'league'
leagueType = if @level.get('type') is 'course-ladder' then 'course' else 'clan' leagueType = if @level.isType('course-ladder') then 'course' else 'clan'
@homeViewArgs.push leagueType @homeViewArgs.push leagueType
@homeViewArgs.push leagueID @homeViewArgs.push leagueID
@homeLink += "/#{leagueType}/#{leagueID}" @homeLink += "/#{leagueType}/#{leagueID}"
else if @level.get('type', true) in ['hero', 'hero-coop'] or window.serverConfig.picoCTF else if @level.isType('course') or @courseID
@homeLink = '/play'
@homeViewClass = 'views/play/CampaignView'
campaign = @level.get 'campaign'
@homeLink += '/' + campaign
@homeViewArgs.push campaign
else if @level.get('type', true) in ['course']
@homeLink = '/courses' @homeLink = '/courses'
@homeViewClass = 'views/courses/CoursesView' @homeViewClass = 'views/courses/CoursesView'
if @courseID if @courseID
@ -136,7 +107,12 @@ module.exports = class ControlBarView extends CocoView
if @courseInstanceID if @courseInstanceID
@homeLink += "/#{@courseInstanceID}" @homeLink += "/#{@courseInstanceID}"
@homeViewArgs.push @courseInstanceID @homeViewArgs.push @courseInstanceID
#else if @level.get('type', true) is 'game-dev' # TODO else if @level.isType('hero', 'hero-coop', 'game-dev', 'web-dev') or window.serverConfig.picoCTF
@homeLink = '/play'
@homeViewClass = 'views/play/CampaignView'
campaign = @level.get 'campaign'
@homeLink += '/' + campaign
@homeViewArgs.push campaign
else else
@homeLink = '/' @homeLink = '/'
@homeViewClass = 'views/HomeView' @homeViewClass = 'views/HomeView'
@ -153,16 +129,13 @@ module.exports = class ControlBarView extends CocoView
@setupManager.open() @setupManager.open()
onClickHome: (e) -> onClickHome: (e) ->
if @level.get('type', true) in ['course'] if @level.isType('course')
category = if me.isTeacher() then 'Teachers' else 'Students' category = if me.isTeacher() then 'Teachers' else 'Students'
window.tracker?.trackEvent 'Play Level Back To Levels', category: category, levelSlug: @levelSlug, ['Mixpanel'] window.tracker?.trackEvent 'Play Level Back To Levels', category: category, levelSlug: @levelSlug, ['Mixpanel']
e.preventDefault() e.preventDefault()
e.stopImmediatePropagation() e.stopImmediatePropagation()
Backbone.Mediator.publish 'router:navigate', route: @homeLink, viewClass: @homeViewClass, viewArgs: @homeViewArgs Backbone.Mediator.publish 'router:navigate', route: @homeLink, viewClass: @homeViewClass, viewArgs: @homeViewArgs
onClickMultiplayer: (e) ->
@showGameMenuModal e, 'multiplayer'
onClickSignupButton: (e) -> onClickSignupButton: (e) ->
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Control Bar', level: @levelID window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Control Bar', level: @levelID
@ -183,62 +156,4 @@ module.exports = class ControlBarView extends CocoView
destroy: -> destroy: ->
@setupManager?.destroy() @setupManager?.destroy()
@multiplayerStatusManager?.destroy()
super() super()
# MultiplayerStatusManager ######################################################
#
# Manages the multiplayer status, and calls @statusChangedCallback when it changes.
#
# It monitors these:
# Real-time multiplayer players
# Internal multiplayer status
#
# Real-time state variables:
# @playersCollection - Real-time multiplayer players
#
# TODO: Not currently using player counts. Should remove if we keep simple design.
#
class MultiplayerStatusManager
constructor: (@levelID, @statusChangedCallback) ->
@status = ''
# @players = {}
# @playersCollection = new RealTimeCollection('multiplayer_players/' + @levelID)
# @playersCollection.on 'add', @onPlayerAdded
# @playersCollection.each (player) => @onPlayerAdded player
Backbone.Mediator.subscribe 'real-time-multiplayer:player-status', @onMultiplayerPlayerStatus
destroy: ->
Backbone.Mediator.unsubscribe 'real-time-multiplayer:player-status', @onMultiplayerPlayerStatus
# @playersCollection?.off 'add', @onPlayerAdded
# player.off 'change', @onPlayerChanged for id, player of @players
onMultiplayerPlayerStatus: (e) =>
@status = e.status
@statusChangedCallback()
# onPlayerAdded: (player) =>
# unless player.id is me.id
# @players[player.id] = new RealTimeModel('multiplayer_players/' + @levelID + '/' + player.id)
# @players[player.id].on 'change', @onPlayerChanged
# @countPlayers player
#
# onPlayerChanged: (player) =>
# @countPlayers player
#
# countPlayers: (changedPlayer) =>
# # TODO: save this stale hearbeat threshold setting somewhere
# staleHeartbeat = new Date()
# staleHeartbeat.setMinutes staleHeartbeat.getMinutes() - 3
# @playerCount = 0
# @playersCollectionAvailable = 0
# @playersCollectionUnavailable = 0
# @playersCollection.each (player) =>
# # Assume changedPlayer is fresher than entry in @playersCollection collection
# player = changedPlayer if changedPlayer? and player.id is changedPlayer.id
# unless staleHeartbeat >= new Date(player.get('heartbeat'))
# @playerCount++
# @playersCollectionAvailable++ if player.get('state') is 'available'
# @playersCollectionUnavailable++ if player.get('state') is 'unavailable'
# @statusChangedCallback()

View file

@ -18,6 +18,7 @@ module.exports = class LevelChatView extends CocoView
constructor: (options) -> constructor: (options) ->
@levelID = options.levelID @levelID = options.levelID
@session = options.session @session = options.session
# TODO: we took out session.multiplayer, so this will not fire. If we want to resurrect it, we'll of course need a new way of activating chat.
@listenTo(@session, 'change:multiplayer', @updateMultiplayerVisibility) @listenTo(@session, 'change:multiplayer', @updateMultiplayerVisibility)
@sessionID = options.sessionID @sessionID = options.sessionID
@bus = LevelBus.get(@levelID, @sessionID) @bus = LevelBus.get(@levelID, @sessionID)

View file

@ -1,9 +1,6 @@
CocoView = require 'views/core/CocoView' CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/level-flags-view' template = require 'templates/play/level/level-flags-view'
{me} = require 'core/auth' {me} = require 'core/auth'
RealTimeCollection = require 'collections/RealTimeCollection'
multiplayerFlagDelay = 0.5 # Long, static second delay for now; should be more than enough.
module.exports = class LevelFlagsView extends CocoView module.exports = class LevelFlagsView extends CocoView
id: 'level-flags-view' id: 'level-flags-view'
@ -17,7 +14,6 @@ module.exports = class LevelFlagsView extends CocoView
'god:new-world-created': 'onNewWorld' 'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld' 'god:streaming-world-updated': 'onNewWorld'
'surface:remove-flag': 'onRemoveFlag' 'surface:remove-flag': 'onRemoveFlag'
'real-time-multiplayer:joined-game': 'onJoinedMultiplayerGame'
events: events:
'click .green-flag': -> @onFlagSelected color: 'green', source: 'button' 'click .green-flag': -> @onFlagSelected color: 'green', source: 'button'
@ -60,9 +56,8 @@ module.exports = class LevelFlagsView extends CocoView
return unless @flagColor and @realTime return unless @flagColor and @realTime
@playSound 'menu-button-click' # TODO: different flag placement sound? @playSound 'menu-button-click' # TODO: different flag placement sound?
pos = x: e.worldPos.x, y: e.worldPos.y pos = x: e.worldPos.x, y: e.worldPos.y
delay = if @realTimeFlags then multiplayerFlagDelay else 0
now = @world.dt * @world.frames.length now = @world.dt * @world.frames.length
flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: now + delay, active: true, source: 'click' flag = player: me.id, team: me.team, color: @flagColor, pos: pos, time: now, active: true, source: 'click'
@flags[@flagColor] = flag @flags[@flagColor] = flag
@flagHistory.push flag @flagHistory.push flag
@realTimeFlags?.create flag @realTimeFlags?.create flag
@ -75,9 +70,8 @@ module.exports = class LevelFlagsView extends CocoView
onRemoveFlag: (e) -> onRemoveFlag: (e) ->
delete @flags[e.color] delete @flags[e.color]
delay = if @realTimeFlags then multiplayerFlagDelay else 0
now = @world.dt * @world.frames.length now = @world.dt * @world.frames.length
flag = player: me.id, team: me.team, color: e.color, time: now + delay, active: false, source: 'click' flag = player: me.id, team: me.team, color: e.color, time: now, active: false, source: 'click'
@flagHistory.push flag @flagHistory.push flag
Backbone.Mediator.publish 'level:flag-updated', flag Backbone.Mediator.publish 'level:flag-updated', flag
#console.log e.color, 'deleted at time', flag.time #console.log e.color, 'deleted at time', flag.time
@ -85,31 +79,3 @@ module.exports = class LevelFlagsView extends CocoView
onNewWorld: (event) -> onNewWorld: (event) ->
return unless event.world.name is @world.name return unless event.world.name is @world.name
@world = @options.world = event.world @world = @options.world = event.world
onJoinedMultiplayerGame: (e) ->
@realTimeFlags = new RealTimeCollection("multiplayer_level_sessions/#{@levelID}/#{e.realTimeSessionID}/flagHistory")
@realTimeFlags.on 'add', @onRealTimeMultiplayerFlagAdded
@realTimeFlags.on 'remove', @onRealTimeMultiplayerFlagRemoved
onLeftMultiplayerGame: (e) ->
if @realTimeFlags
@realTimeFlags.off 'add', @onRealTimeMultiplayerFlagAdded
@realTimeFlags.off 'remove', @onRealTimeMultiplayerFlagRemoved
@realTimeFlags = null
onRealTimeMultiplayerFlagAdded: (e) =>
if e.get('player') != me.id
# TODO: what is @flags used for?
# Build local flag from Backbone.Model flag
flag =
player: e.get('player')
team: e.get('team')
color: e.get('color')
pos: e.get('pos')
time: e.get('time')
active: e.get('active')
#source: 'click'? e.get('source')? nothing?
@flagHistory.push flag
Backbone.Mediator.publish 'level:flag-updated', flag
onRealTimeMultiplayerFlagRemoved: (e) =>

View file

@ -49,7 +49,7 @@ module.exports = class LevelGoalsView extends CocoView
goals = [] goals = []
for goal in e.goals for goal in e.goals
state = e.goalStates[goal.id] state = e.goalStates[goal.id]
continue if goal.optional and @level.get('type', true) is 'course' and state.status isnt 'success' continue if goal.optional and @level.isType('course') and state.status isnt 'success'
if goal.hiddenGoal if goal.hiddenGoal
continue if goal.optional and state.status isnt 'success' continue if goal.optional and state.status isnt 'success'
continue if not goal.optional and state.status isnt 'failure' continue if not goal.optional and state.status isnt 'failure'

View file

@ -100,7 +100,7 @@ module.exports = class LevelHUDView extends CocoView
@stage?.stopTalking() @stage?.stopTalking()
createProperties: -> createProperties: ->
if @options.level.get('type') in ['game-dev'] if @options.level.isType('game-dev')
name = 'Game' # TODO: we don't need the HUD at all name = 'Game' # TODO: we don't need the HUD at all
else if @thang.id in ['Hero Placeholder', 'Hero Placeholder 1'] else if @thang.id in ['Hero Placeholder', 'Hero Placeholder 1']
name = @thangType?.getHeroShortName() or 'Hero' name = @thangType?.getHeroShortName() or 'Hero'

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