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) {
if (languagesImported[language]) return;
if (language === 'html') return;
importScripts("/javascripts/app/vendor/aether-" + language + ".js");
languagesImported[language] = true;
};

View file

@ -80,7 +80,7 @@ var myImportScripts = importScripts;
var languagesImported = {};
var ensureLanguageImported = function(language) {
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");
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
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'],
emitter:
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
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'],
emitter:
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
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'],
emitter:
particleCount: 200
@ -395,6 +413,12 @@ particleKinds['level-mountain-game-dev'] = particleKinds['level-mountain-game-de
colorMiddle: hsl 0.7, 0.75, 0.5
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'],
emitter:
particleCount: 200
@ -435,6 +459,12 @@ particleKinds['level-glacier-game-dev'] = particleKinds['level-glacier-game-dev-
colorMiddle: hsl 0.7, 0.75, 0.5
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'],
emitter:
particleCount: 200
@ -474,3 +504,9 @@ particleKinds['level-volcano-game-dev'] = particleKinds['level-volcano-game-dev-
colorStart: hsl 0.7, 0.75, 0.7
colorMiddle: hsl 0.7, 0.75, 0.5
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')
'multiplayer': go('MultiplayerView')
'play(/)': go('play/CampaignView') # extra slash is to get Facebook app to work
'play/ladder/:levelID/:leagueType/:leagueID': go('ladder/LadderView')
'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView')
'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/:map': go('play/CampaignView')
@ -193,7 +193,7 @@ module.exports = class CocoRouter extends Backbone.Router
@listenToOnce application.moduleLoader, 'load-complete', ->
@routeDirectly(path, args, options)
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.render()
@openView(view)

View file

@ -8,7 +8,6 @@ channelSchemas =
'errors': require 'schemas/subscriptions/errors'
'ipad': require 'schemas/subscriptions/ipad'
'misc': require 'schemas/subscriptions/misc'
'multiplayer': require 'schemas/subscriptions/multiplayer'
'play': require 'schemas/subscriptions/play'
'surface': require 'schemas/subscriptions/surface'
'tome': require 'schemas/subscriptions/tome'
@ -165,5 +164,5 @@ window.onbeforeunload = (e) ->
return leavingMessage
else
return
$ -> init()

View file

@ -2,6 +2,7 @@ CocoModel = require 'models/CocoModel'
CocoCollection = require 'collections/CocoCollection'
{me} = require('core/auth')
locale = require 'locale/locale'
utils = require 'core/utils'
initializeFilePicker = ->
require('core/services/filepicker')() unless window.application.isIPadApp
@ -234,21 +235,14 @@ class ImageFileTreema extends TreemaNode.nodeMap.string
@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
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
buildValueForEditing: (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
class CodeTreema extends TreemaNode.nodeMap.ace
@ -256,8 +250,8 @@ class CodeTreema extends TreemaNode.nodeMap.ace
super(arguments...)
@workingSchema.aceTabSize = 4
# TODO: Find a less hacky solution for this
@workingSchema.aceMode = mode if mode = codeLanguages[@keyForParent]
@workingSchema.aceMode = mode if mode = codeLanguages[@parent?.data?.language]
@workingSchema.aceMode = mode if mode = utils.aceEditModes[@keyForParent]
@workingSchema.aceMode = mode if mode = utils.aceEditModes[@parent?.data?.language]
class CoffeeTreema extends CodeTreema
constructor: ->

View file

@ -259,7 +259,7 @@ startsWithVowel = (s) -> s[0] in 'aeiouAEIOU'
module.exports.filterMarkdownCodeLanguages = (text, language) ->
return '' unless text
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.
codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
# 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
module.exports.aceEditModes = aceEditModes =
'javascript': 'ace/mode/javascript'
'coffeescript': 'ace/mode/coffee'
'python': 'ace/mode/python'
'java': 'ace/mode/java'
'lua': 'ace/mode/lua'
'java': 'ace/mode/java'
javascript: 'ace/mode/javascript'
coffeescript: 'ace/mode/coffee'
python: 'ace/mode/python'
lua: 'ace/mode/lua'
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) ->
contents = $(el).text().trim()
editor = ace.edit el

View file

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

View file

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

View file

@ -41,7 +41,6 @@ module.exports = class LevelBus extends Bus
@fireScriptsRef = @fireRef?.child('scripts')
setSession: (@session) ->
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
@timerIntervalID = setInterval(@incrementSessionPlaytime, 1000)
onIdleChanged: (e) ->
@ -53,8 +52,7 @@ module.exports = class LevelBus extends Bus
@session.set('playtime', (@session.get('playtime') ? 0) + 1)
onPoint: ->
return true unless @session?.get('multiplayer')
super()
return true
onMeSynced: =>
super()
@ -236,17 +234,11 @@ module.exports = class LevelBus extends Bus
@changedSessionProperties.chat = true
@saveSession()
onMultiplayerChanged: ->
@changedSessionProperties.multiplayer = true
@session.updatePermissions()
@changedSessionProperties.permissions = true
@saveSession()
# Debounced as saveSession
reallySaveSession: ->
return if _.isEmpty @changedSessionProperties
# 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
Backbone.Mediator.publish 'level:session-will-save', session: @session
patch = {}

View file

@ -53,6 +53,16 @@ module.exports = class LevelLoader extends CocoClass
# 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: ->
@level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID
if @level.loaded
@ -62,9 +72,18 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @level, 'sync', @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 = {}
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.
originalGet = @level.get
@level.get = ->
@ -169,7 +188,7 @@ module.exports = class LevelLoader extends CocoClass
@consolidateFlagHistory() if @opponentSession?.loaded
else if session is @opponentSession
@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
console.log "Course mode, loading custom hero: ", heroThangType if LOG
url = "/db/thang.type/#{heroThangType}/version"
@ -178,7 +197,7 @@ module.exports = class LevelLoader extends CocoClass
@worldNecessities.push heroResource
@sessionDependenciesRegistered[session.id] = true
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 ?= me.get('heroConfig') if session is @session and not @headless
heroConfig ?= {}
@ -332,8 +351,8 @@ module.exports = class LevelLoader extends CocoClass
@worldNecessities = (r for r in @worldNecessities when r?)
@onWorldNecessitiesLoaded() if @checkAllWorldNecessitiesRegisteredAndLoaded()
onWorldNecessityLoadFailed: (resource) ->
@trigger('world-necessity-load-failed', resource: resource)
onWorldNecessityLoadFailed: (event) ->
@trigger('world-necessity-load-failed', event)
checkAllWorldNecessitiesRegisteredAndLoaded: ->
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
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
# See commit c242317d9
return if not @session.id
@ -443,7 +463,7 @@ module.exports = class LevelLoader extends CocoClass
@grabTeamConfigs()
@thangTypeTeams = {}
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
for component in thang.components
if team = component.config?.team
@ -471,6 +491,7 @@ module.exports = class LevelLoader extends CocoClass
initWorld: ->
return if @initialized
@initialized = true
return if @level.isType('web-dev')
@world = new World()
@world.levelSessionIDs = if @opponentSessionID then [@sessionID, @opponentSessionID] else [@sessionID]
@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":{}}
@onInventoryModalPlayClicked()
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()
return
@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)
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 =
get: (options={}) ->
{ classroom, course, level, user } = options

View file

@ -10,7 +10,6 @@ Letterbox = require './Letterbox'
Dimmer = require './Dimmer'
CountdownScreen = require './CountdownScreen'
PlaybackOverScreen = require './PlaybackOverScreen'
WaitingScreen = require './WaitingScreen'
DebugDisplay = require './DebugDisplay'
CoordinateDisplay = require './CoordinateDisplay'
CoordinateGrid = require './CoordinateGrid'
@ -70,7 +69,6 @@ module.exports = Surface = class Surface extends CocoClass
'level:set-letterbox': 'onSetLetterbox'
'application:idle-changed': 'onIdleChanged'
'camera:zoom-updated': 'onZoomUpdated'
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'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
@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.
@waitingScreen = new WaitingScreen camera: @camera, layer: @screenLayer
@initCoordinates()
@webGLStage.enableMouseOver(10)
@webGLStage.addEventListener 'stagemousemove', @onMouseMove
@ -570,7 +567,7 @@ module.exports = Surface = class Surface extends CocoClass
scaleFactor = 1
if @options.stayVisible
availableHeight = window.innerHeight
availableHeight -= $('.ad-container').outerHeight()
availableHeight -= $('.ad-container').outerHeight()
availableHeight -= $('#game-area').outerHeight() - $('#canvas-wrapper').outerHeight()
scaleFactor = availableHeight / newHeight if availableHeight < newHeight
newWidth *= scaleFactor
@ -602,9 +599,6 @@ module.exports = Surface = class Surface extends CocoClass
#- Real-time playback
onRealTimePlaybackWaiting: (e) ->
@onRealTimePlaybackStarted e
onRealTimePlaybackStarted: (e) ->
return if @realTime
@realTimeInputEvents.reset()
@ -741,7 +735,6 @@ module.exports = Surface = class Surface extends CocoClass
@dimmer?.destroy()
@countdownScreen?.destroy()
@playbackOverScreen?.destroy()
@waitingScreen?.destroy()
@coordinateDisplay?.destroy()
@coordinateGrid?.destroy()
@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:
'god:new-world-created': 'onNewWorldCreated'
'god:new-html-goal-states': 'onNewHTMLGoalStates'
'level:restarted': 'onLevelRestarted'
backgroundSubscriptions:
@ -86,6 +87,9 @@ module.exports = class GoalManager extends CocoClass
@world = e.world
@updateGoalStates(e.goalStates) if e.goalStates?
onNewHTMLGoalStates: (e) ->
@updateGoalStates(e.goalStates) if e.goalStates?
updateGoalStates: (newGoalStates) ->
for goalID, goalState of newGoalStates
continue unless @goalStates[goalID]?
@ -114,7 +118,7 @@ module.exports = class GoalManager extends CocoClass
goalStates: @goalStates
goals: @goals
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)
checkOverallStatus: (ignoreIncomplete=false) ->
@ -264,7 +268,7 @@ module.exports = class GoalManager extends CocoClass
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
victory = 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) ->
# A thang has done something related to the goal!
@ -291,7 +295,7 @@ module.exports = class GoalManager extends CocoClass
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
victory = 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) ->
# Positive goals are completed when all conditions are true (kill all these thangs)

View file

@ -318,7 +318,7 @@
write_this_down: "Write this down:"
start_playing: "Start Playing!"
sso_connected: "Successfully connected with:"
recover:
recover_account_title: "Recover Account"
send_password: "Send Recovery Password"
@ -450,8 +450,6 @@
incomplete: "Incomplete"
timed_out: "Ran out of time"
failing: "Failing"
control_bar_multiplayer: "Multiplayer"
control_bar_join_game: "Join Game"
reload: "Reload"
reload_title: "Reload All Code?"
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_ran: "Ran"
tome_submit_button: "Submit"
tome_reload_method: "Reload original code for this method" # Title text for individual method reload button.
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_reload_method: "Reload original code to restart the level" # {change}
tome_available_spells: "Available Spells"
tome_your_skills: "Your Skills"
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_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_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_blurb: "Motivation Guru"
matt_title: "Cofounder, CTO"
@ -802,6 +797,7 @@
phoenix_title: "Software Engineer"
nolan_title: "Territory Manager"
elliot_title: "Partnership Manager"
lisa_title: "Market Development Rep"
retrostyle_title: "Illustration"
retrostyle_blurb: "RetroStyle Games"
jose_title: "Music"
@ -1477,6 +1473,29 @@
status_not_enrolled: "Not Enrolled"
status_enrolled: "Expires on {{date}}"
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:
archmage_title: "Archmage"
@ -1879,6 +1898,17 @@
vectors: "Vectors"
while_loops: "While Loops"
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:
added: "Added"
@ -1890,16 +1920,6 @@
merge_conflict_with: "MERGE CONFLICT WITH"
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:
page_title: "Legal"
opensource_intro: "CodeCombat is completely open source."

View file

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

View file

@ -5,3 +5,10 @@ module.exports = class Course extends CocoModel
@className: 'Course'
@schema: schema
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
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') 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')))
@sortThangComponents o.thangTypes, o.levelComponents, 'ThangType'
@fillInDefaultComponentConfiguration o.thangTypes, o.levelComponents
@ -59,7 +59,7 @@ module.exports = class Level extends CocoModel
denormalize: (supermodel, session, otherSession) ->
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')?)
thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization
for levelThang in o.thangs
@ -68,7 +68,7 @@ module.exports = class Level extends CocoModel
denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) ->
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 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.
@ -147,7 +147,7 @@ module.exports = class Level extends CocoModel
levelThang.components.push placeholderComponent
# 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
levelThang.thangType = heroThangType if heroThangType
@ -263,6 +263,9 @@ module.exports = class Level extends CocoModel
isLadder: ->
return @get('type')?.indexOf('ladder') > -1
isType: (types...) ->
return @get('type', true) in types
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) ->
if courseInstanceID
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/sessions/#{sessionID}/next"

View file

@ -15,8 +15,6 @@ module.exports = class LevelSession extends CocoModel
updatePermissions: ->
permissions = @get 'permissions', true
permissions = (p for p in permissions when p.target isnt 'public')
if @get('multiplayer')
permissions.push {target: 'public', access: 'write'}
@set 'permissions', permissions
getSourceFor: (spellKey) ->
@ -76,6 +74,7 @@ module.exports = class LevelSession extends CocoModel
wait
recordScores: (scores, level) ->
return unless scores
state = @get 'state'
oldTopScores = state.topScores ? []
newTopScores = []
@ -93,3 +92,17 @@ module.exports = class LevelSession extends CocoModel
newTopScores.push oldTopScore
state.topScores = newTopScores
@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

@ -247,6 +247,15 @@ module.exports = class SuperModel extends Backbone.Model
getResource: (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
constructor: (name, value=1) ->

View file

@ -61,12 +61,13 @@ _.extend CampaignSchema.properties, {
i18n: { type: 'object', format: 'hidden' }
requiresSubscription: { 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' }
original: { type: 'string', format: 'hidden' }
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
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' }
disableSpaces: { type: ['boolean','number'] }
hidesSubmitUntilRun: { type: 'boolean' }

View file

@ -25,7 +25,7 @@ _.extend ClassroomSchema.properties,
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
practice: {type: 'boolean'}
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()
original: c.objectId()
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
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.'}}
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']},
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'}
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
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
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'}
@ -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.'}
buildTime: {type: 'number', description: 'How long it has taken to build this level.'}
practice: { type: 'boolean' }
shareable: { type: 'boolean', title: 'Shareable' }
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
adventurer: { type: 'boolean' }

View file

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

View file

@ -261,4 +261,15 @@ me.concept = me.shortString enum: [
'vectors'
'while_loops'
'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: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: {type: 'object'}
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:real-time-playback-waiting': c.object {}
'playback:real-time-playback-started': c.object {}
'playback:real-time-playback-ended': c.object {}

View file

@ -39,8 +39,6 @@ module.exports =
variableChain: c.array {}, {type: 'string'}
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: []},
spell: {type: 'object'}
@ -91,10 +89,6 @@ module.exports =
problems: {type: 'array'}
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']},
language: {type: 'string'}
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']}
'tome:hide-problem-alert': c.object {title: 'Hide 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
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, #student-projects-tab
.course-overview-row
margin-top: 50px
border: thin solid gray
@ -221,7 +221,20 @@
.btn
margin-top: 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
.checkbox-flat

View file

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

View file

@ -124,37 +124,6 @@
@include rotate(-15deg)
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
position: absolute
right: 35px
@ -210,11 +179,6 @@ html.no-borderimage
background: transparent url(/images/level/control_bar_level_name_background.png)
background-size: contain
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)

View file

@ -181,3 +181,13 @@ $UNVEIL_TIME: 1.2s
left: 48px
right: 77px
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
width: 750px
@media screen and ( max-height: 625px )
margin-top: -50px
.modal-content
position: relative
margin-top: -251px
@ -55,10 +58,14 @@
top: 80px
margin-top: 80px
@media screen and ( max-height: 650px )
padding-top: 10px
.well-parchment
margin-top: 20px
@media screen and ( max-height: 675px )
margin-top: 0
html.no-borderimage

View file

@ -298,6 +298,33 @@
height: 100%
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

View file

@ -4,10 +4,18 @@
color: black
margin-bottom: 5px
p
margin-top: 30px
.next-level-description
p
margin-top: 30px
.course-title
white-space: nowrap
text-overflow: ellipsis
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: 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)
#spell-palette-view
// Make sure we have enough room for at least two columns

View file

@ -1,15 +1,7 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
.spell-list-entry-view
.method-signature
background-color: transparent
border: 0
font-size: 1.1em
display: inline-block
padding: 4px
.spell-list-entry-view.spell-tab
#spell-top-bar-view
$height: 87px
$paddingTop: 10px
$paddingBottom: 25px
@ -46,12 +38,6 @@
> *:not(.spell-tool-buttons)
@include opacity(0.5)
.thang-avatar-view
width: $childSize - 10px
margin: 5px 0.4vw
display: inline-block
float: left
.btn.btn-small
margin-top: 15px
margin-right: 1.3vw
@ -97,46 +83,8 @@
.thang-avatar-wrapper
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
// .spell-list-entry-view.spell-tab
// .spell-top-bar-view
// border-width: 0
// border-image: none
// 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
// Only those popovers which are our direct children (spell documentation)
left: auto !important
max-width: 600px
padding: 0
border-style: solid
@ -81,7 +82,6 @@ body:not(.dialogue-view-active)
@include animation(jiggle .3s infinite)
&.pinned
left: auto !important
top: 50px !important
right: 45%
// 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%
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
#level-view
#fullscreen-editor-background-screen

View file

@ -143,7 +143,7 @@ block content
img(src="/images/pages/about/lisa_small.png").img-thumbnail
.team-bio
h6.label.team-name Lisa Wu
small Marketing Development Rep
small(data-i18n="about.lisa_title")
br
// Part time / contract
@ -274,22 +274,8 @@ block content
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
.job-listing
h5 Marketing Manager
.text-center
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
h5 (No Open Roles)
p.small Check back later for updates on new positions at CodeCombat.
.col-sm-6.col-md-5.col-lg-4
.job-listing
h5(data-i18n="about.jobs_custom_title")

View file

@ -44,7 +44,7 @@ block content
a(href="/admin/classroom-levels") Classroom Levels
li
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
a(href="/admin/analytics") Dashboard
li

View file

@ -104,13 +104,23 @@ block content
tr
td
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- var i18n = level.get('type') === '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'))
- 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=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
if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')]
td #{level.get('practice') ? 'practice' : 'required'}
td #{levelNumber}. #{level.get('name').replace('Course: ', '')}
td #{levelNumber}. #{i18n(level.attributes, 'name').replace('Course: ', '')}
td
if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts')

View file

@ -122,6 +122,10 @@ block content
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab')
.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-content
@ -129,8 +133,10 @@ block content
+studentsTab
else if activeTab === '#course-progress-tab'
+courseProgressTab
else
else if activeTab === '#enrollment-status-tab'
+enrollmentStatusTab
else
+studentProjectsTab
else
.text-center.m-t-5.m-b-5
@ -150,11 +156,10 @@ mixin breadcrumbs
mixin longLevelName(data)
if data
div.level-name
span.spr Course
span= data.courseNumber
span.spr , Level
span= data.levelNumber
span.spr :
span(data-i18n="courses.course")
span= ' ' + data.courseNumber + ', '
span(data-i18n="play_level.level")
span= ' ' + data.levelNumber + ': '
span= data.levelName
else
div.level-name(data-i18n='teacher.not_applicable')
@ -223,6 +228,8 @@ mixin studentRow(student)
+longLevelName(student.latestCompleteLevel)
td
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')
- var course = view.courses.get(trimCourse._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 levelsTotal = trimCourse.levels.length
//- - var level = ???
+studentCourseProgressDot(progress, levelsTotal, level, 'CS' + (index+1))
- var label = courseLabelsArray[index];
+studentCourseProgressDot(progress, levelsTotal, level, label)
unless student.isEnrolled()
+enrollStudentButton(student)
//- td
@ -305,7 +313,7 @@ mixin courseOverview
.course-overview-row
.course-title.student-name
span= course.get('name')
span :
span= ': '
span(data-i18n='teacher.course_overview')
.course-overview-progress
each level, index in levels
@ -324,7 +332,7 @@ mixin studentLevelsRow(student)
each level, index 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)
+studentLevelProgressDot(progress, level, levelNumber, session)
+studentLevelProgressDot(progress, level, levelNumber)
mixin studentCourseProgressDot(progress, levelsTotal, level, label)
//- TODO: Refactor with TeacherClassesView jade
@ -336,7 +344,7 @@ mixin studentCourseProgressDot(progress, levelsTotal, level, label)
mixin allStudentsLevelProgressDot(progress, level, levelNumber)
- 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 })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.allStudentsLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber)
@ -344,7 +352,7 @@ mixin allStudentsLevelProgressDot(progress, level, levelNumber)
mixin studentLevelProgressDot(progress, level, levelNumber)
//- TODO: Refactor with TeacherClassesView jade
- 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 })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber)
@ -430,3 +438,37 @@ mixin enrollmentStatusTab
td.enroll-col
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")
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
+addStudentsButton(classroom)
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') || []
- var course = view.courses.get(trimCourse._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
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')
| Create a New Class
mixin progressDot(classroom, course, index)
//- TODO: Give classes abbreviations instead of using index?
mixin progressDot(classroom, course, label)
//- TODO: inefficient. Cache this in the view?
- courseInstance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
- var total = classroom.get('members').length
@ -113,14 +115,11 @@ mixin progressDot(classroom, course, index)
- dotClass = complete === total ? 'forest' : started ? 'gold' : '';
- var progressDotContext = {total: total, complete: complete};
.progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext))
+progressDotLabel(index)
+progressDotLabel(label)
mixin progressDotLabel(index)
mixin progressDotLabel(label)
.dot-label
.text-h6
| CS
span
= index + 1
.text-h6= label
mixin archivedClassRow(classroom)
.class.row

View file

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

View file

@ -5,7 +5,7 @@ block modal-header-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!
.form-group.select-group
select#tome-language(name="language")

View file

@ -7,24 +7,15 @@
.levels-link-area
a.levels-link(href=homeLink || "/")
.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
.multiplayer-area-container
.multiplayer-area
.multiplayer-label(data-i18n="play_level.control_bar_multiplayer")
if multiplayerStatus
.multiplayer-status= multiplayerStatus
else
.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
.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

View file

@ -48,7 +48,7 @@ block modal-body-content
textarea(data-i18n="[placeholder]play_level.victory_review_placeholder")
.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
- var animate = achievement.completed && !achievement.completedAWhileAgo
.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-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')
.sign-up-poke.hide
.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
if readyToRank
.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
else
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 :
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
.col-sm-5.col-sm-offset-2
// TODO: Add this and rest of campaign functionality
// button#continue-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase View Leaderboards
// TODO: Add rest of campaign functionality
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
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')

View file

@ -13,7 +13,7 @@ block modal-body-content
block modal-footer-content
if readyToRank
.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
else
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-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
.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
span.spl(data-i18n="play_level.reload") Reload
span.spl(data-i18n="play_level.restart")
if me.level() >= 15
.btn.btn-small.btn-illustrated.fullscreen-code(title=maximizeShortcutVerbose)
@ -27,4 +21,17 @@ if includeSpellList
.btn.btn-small.btn-illustrated.hints-button
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

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'
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
strong
span(data-i18n="skill_docs.current_value") Current Value

View file

@ -1,11 +1,7 @@
#spell-list-tab-entry-view
#spell-list-view
#spell-top-bar-view
#cast-button-view
#spell-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(width=924, height=589)#webgl-surface
canvas(width=924, height=589)#normal-surface
#web-surface-view
#ascii-surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient

View file

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

View file

@ -195,7 +195,7 @@ module.exports = class ClanDetailsView extends RootView
if level.concepts?
for concept in level.concepts
@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
@campaignLevelProgressions.push campaignLevelProgression
@render?()

View file

@ -496,6 +496,13 @@ module.exports = class CocoView extends Backbone.View
playSound: (trigger, volume=1) ->
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
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())
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(=>
@ -62,7 +62,7 @@ module.exports = class CourseDetailsView extends RootView
# need to figure out the next course instance
@courseComplete = true
@courseInstances.comparator = 'courseID'
# TODO: make this logic use locked course content to figure out the next course, then fetch the
# TODO: make this logic use locked course content to figure out the next course, then fetch the
# course instance for that
@supermodel.trackRequest(@courseInstances.fetchForClassroom(classroomID).then(=>
@nextCourseInstance = _.find @courseInstances.models, (ci) => ci.get('courseID') > @courseID
@ -84,9 +84,9 @@ module.exports = class CourseDetailsView extends RootView
@levelConceptMap = {}
for level in @levels.models
@levelConceptMap[level.get('original')] ?= {}
for concept in level.get('concepts')
for concept in level.get('concepts') or []
@levelConceptMap[level.get('original')][concept] = true
if level.get('type') is 'course-ladder'
if level.isType('course-ladder')
@arenaLevel = level
# console.log 'onLevelSessionsSync'
@ -124,13 +124,13 @@ module.exports = class CourseDetailsView extends RootView
for concept, state of conceptStateMap
@conceptsCompleted[concept] ?= 0
@conceptsCompleted[concept]++
onClickPlayLevel: (e) ->
levelSlug = $(e.target).closest('.btn-play-level').data('level-slug')
levelID = $(e.target).closest('.btn-play-level').data('level-id')
level = @levels.findWhere({original: levelID})
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'
viewArgs = [{supermodel: @supermodel}, levelSlug]
route = '/play/ladder/' + levelSlug

View file

@ -23,6 +23,7 @@ CourseInstances = require 'collections/CourseInstances'
module.exports = class TeacherClassView extends RootView
id: 'teacher-class-view'
template: template
helper: helper
events:
'click .nav-tabs a': 'onClickNavTabLink'
@ -43,7 +44,7 @@ module.exports = class TeacherClassView extends RootView
'click .student-checkbox': 'onClickStudentCheckbox'
'keyup #student-search': 'onKeyPressStudentSearch'
'change .course-select, .bulk-course-select': 'onChangeCourseSelect'
getInitialState: ->
{
sortAttribute: 'name'
@ -72,21 +73,21 @@ module.exports = class TeacherClassView extends RootView
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
@singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level'
@allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level'
@debouncedRender = _.debounce @render
@state = new State(@getInitialState())
@updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
@classroom = new Classroom({ _id: classroomID })
@supermodel.trackRequest @classroom.fetch()
@onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
@students = new Users()
@listenTo @classroom, 'sync', ->
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
@supermodel.trackRequests jqxhrs
@classroom.sessions = new LevelSessions()
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
@supermodel.trackRequests(requests)
@ -96,7 +97,7 @@ module.exports = class TeacherClassView extends RootView
value = @state.get('sortValue')
if value is 'name'
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
if value is 'progress'
# TODO: I would like for this to be in the Level model,
# but it doesn't know about its own courseNumber.
@ -105,7 +106,7 @@ module.exports = class TeacherClassView extends RootView
return -dir if not level1
return dir if not level2
return dir * (level1.courseNumber - level2.courseNumber or level1.levelNumber - level2.levelNumber)
if value is 'status'
statusMap = { expired: 0, 'not-enrolled': 1, enrolled: 2 }
diff = statusMap[student1.prepaidStatus()] - statusMap[student2.prepaidStatus()]
@ -114,13 +115,13 @@ module.exports = class TeacherClassView extends RootView
@courses = new Courses()
@supermodel.trackRequest @courses.fetch()
@courseInstances = new CourseInstances()
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@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()
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@ -160,11 +161,11 @@ module.exports = class TeacherClassView extends RootView
course.instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
course.members = course.instance?.get('members') or []
null
onLoaded: ->
@removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students?
@calculateProgressAndLevels()
# render callback setup
@listenTo @courseInstances, 'sync change update', @debouncedRender
@listenTo @state, 'sync change', ->
@ -174,17 +175,17 @@ module.exports = class TeacherClassView extends RootView
@debouncedRender()
@listenTo @students, 'sort', @debouncedRender
super()
afterRender: ->
super(arguments...)
$('.progress-dot').each (i, el) ->
$('.progress-dot, .btn-view-project-level').each (i, el) ->
dot = $(el)
dot.tooltip({
html: true
container: dot
}).delegate '.tooltip', 'mousemove', ->
dot.tooltip('hide')
calculateProgressAndLevels: ->
return unless @supermodel.progress is 1
# TODO: How to structure this in @state?
@ -192,14 +193,14 @@ module.exports = class TeacherClassView extends RootView
# TODO: this is a weird hack
studentsStub = new Users([ student ])
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, studentsStub)
earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
classroomsStub = new Classrooms([ @classroom ])
progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
# conceptData: helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
@state.set {
earliestIncompleteLevel
latestCompleteLevel
@ -212,7 +213,7 @@ module.exports = class TeacherClassView extends RootView
hash = $(e.target).closest('a').attr('href')
@updateHash(hash)
@state.set activeTab: hash
updateHash: (hash) ->
return if application.testing
window.location.hash = hash
@ -227,17 +228,10 @@ module.exports = class TeacherClassView extends RootView
@$('#join-url-input').val(@state.get('joinURL')).select()
@tryCopy()
tryCopy: ->
try
document.execCommand('copy')
catch err
message = 'Oops, unable to copy'
noty text: message, layout: 'topCenter', type: 'error', killer: false
onClickUnarchive: ->
window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@classroom.save { archived: false }
onClickEditClassroom: (e) ->
window.tracker?.trackEvent 'Teachers Class Edit Class Started', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
classroom = @classroom
@ -328,9 +322,11 @@ module.exports = class TeacherClassView extends RootView
window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
courseLabels = ""
courseOrder = []
for course, index in @classroom.get('courses')
courseLabels += "CS#{index + 1} Playtime,"
courseOrder.push(course._id)
courses = (@courses.get(c._id) for c in @classroom.get('courses'))
courseLabelsArray = helper.courseLabelsArray courses
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"
levelCourseMap = {}
for trimCourse in @classroom.get('courses')
@ -396,6 +392,7 @@ module.exports = class TeacherClassView extends RootView
not @students.get(userID).isEnrolled()
assigningToNobody = selectedIDs.length is 0
@state.set errors: { assigningToNobody, assigningToUnenrolled }
return if assigningToNobody
@assignCourse courseID, members
window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel']
@ -459,7 +456,7 @@ module.exports = class TeacherClassView extends RootView
enrolledUsers = @students.filter (user) -> user.isEnrolled()
stats.enrolledUsers = _.size(enrolledUsers)
return stats
studentStatusString: (student) ->

View file

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

View file

@ -46,7 +46,7 @@ module.exports = class ThangComponentConfigView extends CocoView
schema.default ?= {}
_.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 = []
treemaOptions =
supermodel: @supermodel

View file

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

View file

@ -41,7 +41,7 @@ module.exports = class LevelThangEditView extends CocoView
level: @level
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
@listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged

View file

@ -251,7 +251,7 @@ module.exports = class ThangsTabView extends CocoView
@dragged = 0
@willUnselectSprite = false
@gameUIState.set('canDragCamera', true)
if @addThangLank?.thangType.get('kind') is 'Wall'
@paintingWalls = true
@gameUIState.set('canDragCamera', false)
@ -259,7 +259,7 @@ module.exports = class ThangsTabView extends CocoView
else if @addThangLank
# We clicked on the background when we had an add Thang selected, so add it
@addThang @addThangType, @addThangLank.thang.pos
else if e.onBackground
@gameUIState.set('selected', [])
@ -331,18 +331,18 @@ module.exports = class ThangsTabView extends CocoView
@onSpriteContextMenu e
clearInterval(@movementInterval) if @movementInterval?
@movementInterval = null
return unless _.any(selected)
for singleSelected in selected
pos = singleSelected.thang.pos
thang = _.find(@level.get('thangs') ? [], {id: singleSelected.thang.id})
path = "#{@pathForThang(thang)}/components/original=#{LevelComponent.PhysicalID}"
physical = @thangsTreema.get path
continue if not physical or (physical.config.pos.x is pos.x and physical.config.pos.y is pos.y)
@thangsTreema.set path + '/config/pos', x: pos.x, y: pos.y, z: pos.z
if @willUnselectSprite
clickedSprite = _.find(selected, {sprite: e.sprite})
@gameUIState.set('selected', _.without(selected, clickedSprite))
@ -379,7 +379,7 @@ module.exports = class ThangsTabView extends CocoView
thang = selected?.thang
previousSprite?.setNameLabel?(null) unless previousSprite is sprite
if thang and not (@addThangLank and @addThangType.get('name') in overlappableThangTypeNames)
# We clicked on a Thang (or its Treema), so select the Thang
@selectAddThang(null, true)
@ -619,7 +619,7 @@ module.exports = class ThangsTabView extends CocoView
onTreemaThangSelected: (e, selectedTreemas) =>
selectedThangTreemas = _.filter(selectedTreemas, (t) -> t instanceof ThangNode)
thangIDs = (node.data.id for node in selectedThangTreemas)
lanks = (@surface.lankBoss.lanks[thangID] for thangID in thangIDs when thangID)
lanks = (@surface.lankBoss.lanks[thangID] for thangID in thangIDs when thangID)
selected = ({ thang: lank.thang, sprite: lank } for lank in lanks when lank)
@gameUIState.set('selected', selected)
@ -636,14 +636,14 @@ module.exports = class ThangsTabView extends CocoView
if batchInsert
if thangType.get('name') is '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
thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}"
else
thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID)
if @cloneSourceThang
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
else
components = _.cloneDeep thangType.get('components') ? []

View file

@ -78,7 +78,7 @@ module.exports = class VerifierTest extends CocoClass
@listenToOnce @god, 'infinite-loop', @fail
@listenToOnce @god, 'user-code-problem', @onUserCodeProblem
@listenToOnce @god, 'goals-calculated', @processSingleGameResults
@god.createWorld @generateSpellsObject()
@god.createWorld @session.generateSpellsObject()
@updateCallback? state: 'running'
processSingleGameResults: (e) ->
@ -118,18 +118,6 @@ module.exports = class VerifierTest extends CocoClass
@updateCallback? state: @state
@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: ->
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'
@levelsByCampaign[campaign.get('slug')] ?= {levels: [], checked: true}
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
filterCodeLanguages: ->

View file

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

View file

@ -21,7 +21,7 @@ module.exports = class MainLadderView extends RootView
@campaigns = campaigns
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 0).model
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@getLevelPlayCounts()
@ -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 = [
{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: ->
super()
@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()
afterRender: ->

View file

@ -397,8 +397,9 @@ module.exports = class CampaignView extends RootView
@particleMan.removeEmitters()
@particleMan.attach @$el.find('.map')
for level in @campaign.renderedLevels ? {}
particleKey = ['level', @terrain.replace('-branching-test', '')]
particleKey.push level.type if level.type and not (level.type in ['hero', 'course'])
terrain = @terrain.replace('-branching-test', '').replace(/(game|web)-dev-\d/, 'forest')
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 'premium' if level.requiresSubscription
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')
levelSlug = levelElement.data('level-slug')
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]
else
@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]
@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) ->
go = =>
@ -181,7 +178,7 @@ module.exports = class SpectateLevelView extends RootView
@insertSubView new GoldView {}
@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}
# callbacks

View file

@ -7,17 +7,13 @@ Classroom = require 'models/Classroom'
Course = require 'models/Course'
CourseInstance = require 'models/CourseInstance'
GameMenuModal = require 'views/play/menu/GameMenuModal'
RealTimeModel = require 'models/RealTimeModel'
RealTimeCollection = require 'collections/RealTimeCollection'
LevelSetupManager = require 'lib/LevelSetupManager'
GameMenuModal = require 'views/play/menu/GameMenuModal'
module.exports = class ControlBarView extends CocoView
id: 'control-bar-view'
template: template
subscriptions:
'bus:player-states-changed': 'onPlayerStatesChanged'
'level:disable-controls': 'onDisableControls'
'level:enable-controls': 'onEnableControls'
'ipad:memory-warning': 'onIPadMemoryWarning'
@ -28,7 +24,6 @@ module.exports = class ControlBarView extends CocoView
'click': -> Backbone.Mediator.publish 'tome:focus-editor', {}
'click .levels-link-area': 'onClickHome'
'click .home a': 'onClickHome'
'click .multiplayer-area': 'onClickMultiplayer'
'click #control-bar-sign-up-button': 'onClickSignupButton'
constructor: (options) ->
@ -45,7 +40,7 @@ module.exports = class ControlBarView extends CocoView
@observing = options.session.get('creator') isnt me.id
@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
if @courseInstanceID
@courseInstance = new CourseInstance(_id: @courseInstanceID)
@ -64,9 +59,6 @@ module.exports = class ControlBarView extends CocoView
@supermodel.trackRequest(@campaign.fetch())
)
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'
@listenTo @session, 'change-difficulty', @onSessionDifficultyChanged
@ -79,25 +71,10 @@ module.exports = class ControlBarView extends CocoView
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={}) ->
super c
c.worldName = @worldName
c.multiplayerEnabled = @session.get('multiplayer')
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
if c.isMultiplayerLevel = @isMultiplayerLevel
c.multiplayerStatus = @multiplayerStatusManager?.status
c.ladderGame = @level.isType('ladder', 'hero-ladder', 'course-ladder')
if @level.get 'replayable'
c.levelDifficulty = @session.get('state')?.difficulty ? 0
if @observing
@ -110,23 +87,17 @@ module.exports = class ControlBarView extends CocoView
if me.isSessionless()
@homeLink = "/teachers/courses"
@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
@homeLink = '/play/ladder/' + levelID
@homeViewClass = 'views/ladder/LadderView'
@homeViewArgs.push levelID
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 leagueID
@homeLink += "/#{leagueType}/#{leagueID}"
else if @level.get('type', true) in ['hero', 'hero-coop'] or window.serverConfig.picoCTF
@homeLink = '/play'
@homeViewClass = 'views/play/CampaignView'
campaign = @level.get 'campaign'
@homeLink += '/' + campaign
@homeViewArgs.push campaign
else if @level.get('type', true) in ['course']
else if @level.isType('course') or @courseID
@homeLink = '/courses'
@homeViewClass = 'views/courses/CoursesView'
if @courseID
@ -136,7 +107,12 @@ module.exports = class ControlBarView extends CocoView
if @courseInstanceID
@homeLink += "/#{@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
@homeLink = '/'
@homeViewClass = 'views/HomeView'
@ -153,16 +129,13 @@ module.exports = class ControlBarView extends CocoView
@setupManager.open()
onClickHome: (e) ->
if @level.get('type', true) in ['course']
if @level.isType('course')
category = if me.isTeacher() then 'Teachers' else 'Students'
window.tracker?.trackEvent 'Play Level Back To Levels', category: category, levelSlug: @levelSlug, ['Mixpanel']
e.preventDefault()
e.stopImmediatePropagation()
Backbone.Mediator.publish 'router:navigate', route: @homeLink, viewClass: @homeViewClass, viewArgs: @homeViewArgs
onClickMultiplayer: (e) ->
@showGameMenuModal e, 'multiplayer'
onClickSignupButton: (e) ->
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Control Bar', level: @levelID
@ -183,62 +156,4 @@ module.exports = class ControlBarView extends CocoView
destroy: ->
@setupManager?.destroy()
@multiplayerStatusManager?.destroy()
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) ->
@levelID = options.levelID
@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)
@sessionID = options.sessionID
@bus = LevelBus.get(@levelID, @sessionID)

View file

@ -1,9 +1,6 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/level-flags-view'
{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
id: 'level-flags-view'
@ -17,7 +14,6 @@ module.exports = class LevelFlagsView extends CocoView
'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld'
'surface:remove-flag': 'onRemoveFlag'
'real-time-multiplayer:joined-game': 'onJoinedMultiplayerGame'
events:
'click .green-flag': -> @onFlagSelected color: 'green', source: 'button'
@ -60,9 +56,8 @@ module.exports = class LevelFlagsView extends CocoView
return unless @flagColor and @realTime
@playSound 'menu-button-click' # TODO: different flag placement sound?
pos = x: e.worldPos.x, y: e.worldPos.y
delay = if @realTimeFlags then multiplayerFlagDelay else 0
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
@flagHistory.push flag
@realTimeFlags?.create flag
@ -75,9 +70,8 @@ module.exports = class LevelFlagsView extends CocoView
onRemoveFlag: (e) ->
delete @flags[e.color]
delay = if @realTimeFlags then multiplayerFlagDelay else 0
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
Backbone.Mediator.publish 'level:flag-updated', flag
#console.log e.color, 'deleted at time', flag.time
@ -85,31 +79,3 @@ module.exports = class LevelFlagsView extends CocoView
onNewWorld: (event) ->
return unless event.world.name is @world.name
@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 = []
for goal in e.goals
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
continue if goal.optional and state.status isnt 'success'
continue if not goal.optional and state.status isnt 'failure'

View file

@ -100,7 +100,7 @@ module.exports = class LevelHUDView extends CocoView
@stage?.stopTalking()
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
else if @thang.id in ['Hero Placeholder', 'Hero Placeholder 1']
name = @thangType?.getHeroShortName() or 'Hero'

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