mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
Merge remote-tracking branch 'origin/web-dev-levels' into game-dev-levels
# Conflicts: # app/views/play/level/PlayLevelView.coffee
This commit is contained in:
commit
ab704a1cab
100 changed files with 754 additions and 1903 deletions
139
app/assets/javascripts/web-dev-listener.js
Normal file
139
app/assets/javascripts/web-dev-listener.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
// 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:\/\/.*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;
|
||||
}
|
||||
//console.log(event);
|
||||
switch (event.data.type) {
|
||||
case 'create':
|
||||
create(event.data.dom);
|
||||
checkGoals(event.data.goals, event.source);
|
||||
break;
|
||||
case 'update':
|
||||
if (virtualDOM)
|
||||
update(event.data.dom);
|
||||
else
|
||||
create(event.data.dom);
|
||||
checkGoals(event.data.goals, event.source);
|
||||
break;
|
||||
case 'log':
|
||||
console.log(event.data.text);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', event.data.type);
|
||||
}
|
||||
}
|
||||
|
||||
function create(dom) {
|
||||
concreteDOM = deku.dom.create(event.data.dom);
|
||||
virtualDOM = event.data.dom;
|
||||
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
|
||||
$('body').empty().append(concreteDOM);
|
||||
}
|
||||
|
||||
function update(dom) {
|
||||
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 changes = deku.diff.diffNode(virtualDOM, event.data.dom);
|
||||
changes.reduce(deku.dom.update(dispatch, context), concreteDOM); // Rerender
|
||||
virtualDOM = event.data.dom;
|
||||
}
|
||||
|
||||
function checkGoals(goals) {
|
||||
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
|
||||
event.source.postMessage({type: 'goals-updated', goalStates: goalStates, overallStatus: overallStatus}, event.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;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
31
app/assets/web-dev-iframe.html
Normal file
31
app/assets/web-dev-iframe.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!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>
|
||||
|
||||
<!-- 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>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Loading...</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -125,8 +125,6 @@ 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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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,12 +290,12 @@ 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'
|
||||
|
||||
module.exports.initializeACE = (el, codeLanguage) ->
|
||||
contents = $(el).text().trim()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -115,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
|
||||
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -52,7 +52,7 @@ module.exports = class LevelLoader extends CocoClass
|
|||
@listenToOnce @supermodel, 'loaded-all', @onSupermodelLoaded
|
||||
|
||||
# Supermodel (Level) Loading
|
||||
|
||||
|
||||
loadWorldNecessities: ->
|
||||
# TODO: Actually trigger loading, instead of in the constructor
|
||||
new Promise((resolve, reject) =>
|
||||
|
@ -72,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')) 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 = ->
|
||||
|
@ -179,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"
|
||||
|
@ -188,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 ?= {}
|
||||
|
@ -453,7 +462,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
|
||||
|
@ -481,6 +490,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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
@ -481,9 +481,7 @@
|
|||
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_available_spells: "Available Spells"
|
||||
tome_your_skills: "Your Skills"
|
||||
tome_current_method: "Current Method"
|
||||
|
@ -1879,6 +1877,9 @@
|
|||
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"
|
||||
|
||||
delta:
|
||||
added: "Added"
|
||||
|
@ -1890,16 +1891,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."
|
||||
|
|
|
@ -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', 'game-dev']) 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"
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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()
|
|
@ -61,7 +61,7 @@ _.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' }
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -37,8 +37,6 @@ _.extend LevelSessionSchema.properties,
|
|||
type: 'string'
|
||||
levelID:
|
||||
type: 'string'
|
||||
multiplayer:
|
||||
type: 'boolean'
|
||||
creator: c.objectId
|
||||
links:
|
||||
[
|
||||
|
|
|
@ -261,4 +261,7 @@ me.concept = me.shortString enum: [
|
|||
'vectors'
|
||||
'while_loops'
|
||||
'recursion'
|
||||
'basic_html'
|
||||
'basic_css'
|
||||
'basic_web_scripting'
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
|
@ -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 {}
|
||||
|
|
|
@ -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)'}
|
||||
|
|
|
@ -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
|
||||
|
@ -616,6 +618,8 @@ $gameControlMargin: 30px
|
|||
|
||||
.gameplay-container
|
||||
position: absolute
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
body.ipad #campaign-view
|
||||
// iPad only supports up to Kithgard Gates for now.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -74,6 +74,13 @@ $UNVEIL_TIME: 1.2s
|
|||
.progress-or-start-container.intro-footer
|
||||
bottom: 30px
|
||||
|
||||
@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
|
||||
|
||||
.level-loading-goals
|
||||
text-align: left
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
53
app/styles/play/level/web-surface-view.sass
Normal file
53
app/styles/play/level/web-surface-view.sass
Normal file
|
@ -0,0 +1,53 @@
|
|||
#web-surface-view
|
||||
background-color: white
|
||||
|
||||
iframe
|
||||
width: 100%
|
||||
height: 100%
|
||||
//
|
||||
// body
|
||||
// background-color: initial
|
||||
// color: initial
|
||||
//
|
||||
// html, body, div, span, applet, object, iframe,
|
||||
// h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
// a, abbr, acronym, address, big, cite, code,
|
||||
// del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
// small, strike, strong, sub, sup, tt, var,
|
||||
// b, u, i, center,
|
||||
// dl, dt, dd, ol, ul, li,
|
||||
// fieldset, form, label, legend,
|
||||
// table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
// article, aside, canvas, details, embed,
|
||||
// figure, figcaption, footer, header, hgroup,
|
||||
// menu, nav, output, ruby, section, summary,
|
||||
// time, mark, audio, video
|
||||
// margin: initial
|
||||
// padding: initial
|
||||
// border: initial
|
||||
// font-size: innitial
|
||||
// font: initial
|
||||
// vertical-align: baseline
|
||||
//
|
||||
// // HTML5 display-role reset for older browsers
|
||||
// article, aside, details, figcaption, figure,
|
||||
// footer, header, hgroup, menu, nav, section
|
||||
// display: block
|
||||
//
|
||||
// body
|
||||
// line-height: 1
|
||||
//
|
||||
// ol, ul
|
||||
// list-style: none
|
||||
//
|
||||
// blockquote, q
|
||||
// quotes: none
|
||||
//
|
||||
// blockquote:before, blockquote:after, q:before, q:after
|
||||
// content: ''
|
||||
// content: none
|
||||
//
|
||||
// table
|
||||
// border-collapse: collapse
|
||||
// border-spacing: 0
|
||||
//
|
|
@ -1,8 +0,0 @@
|
|||
#multiplayer-view
|
||||
textarea
|
||||
width: 100%
|
||||
box-sizing: border-box
|
||||
padding: 5px
|
||||
text-align: center
|
||||
height: 30px
|
||||
font-size: 11px
|
|
@ -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
|
||||
|
|
|
@ -104,7 +104,7 @@ block content
|
|||
tr
|
||||
td
|
||||
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
|
||||
- var i18n = level.get('type') === 'course-ladder' ? 'play.compete' : 'home.play';
|
||||
- var i18n = level.isType('course-ladder') ? 'play.compete' : 'home.play';
|
||||
button.btn.btn-success.btn-play-level(data-level-slug=level.get('slug'), data-i18n=i18n, data-level-id=level.get('original'))
|
||||
if view.showGameDevButtons
|
||||
- var levelOriginal = level.get('original');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -9,22 +9,13 @@
|
|||
.glyphicon.glyphicon-play
|
||||
span(data-i18n=me.isSessionless() ? "nav.courses" : (ladderGame ? "general.ladder" : "nav.play")).home-text Levels
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -118,7 +118,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,12 +3,6 @@
|
|||
.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")
|
||||
.glyphicon.glyphicon-repeat
|
|
@ -1 +0,0 @@
|
|||
h5(data-i18n="play_level.tome_select_method") Select a Method
|
|
@ -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})
|
||||
|
||||
|
|
@ -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(", ")})
|
|
@ -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.name == 'now'
|
||||
p.value
|
||||
strong
|
||||
span(data-i18n="skill_docs.current_value") Current Value
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
#spell-list-tab-entry-view
|
||||
|
||||
#spell-list-view
|
||||
#spell-top-bar-view
|
||||
|
||||
#cast-button-view
|
||||
|
||||
#spell-view
|
||||
|
||||
#spell-palette-view
|
||||
|
||||
|
||||
|
|
1
app/templates/play/level/web-surface-view.jade
Normal file
1
app/templates/play/level/web-surface-view.jade
Normal file
|
@ -0,0 +1 @@
|
|||
iframe(src="/web-dev-iframe.html")
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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?()
|
||||
|
|
|
@ -63,7 +63,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
|
||||
|
@ -87,7 +87,7 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@levelConceptMap[level.get('original')] ?= {}
|
||||
for concept in level.get('concepts')
|
||||
@levelConceptMap[level.get('original')][concept] = true
|
||||
if level.get('type') is 'course-ladder'
|
||||
if level.isType('course-ladder')
|
||||
@arenaLevel = level
|
||||
|
||||
# console.log 'onLevelSessionsSync'
|
||||
|
@ -125,13 +125,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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') ? []
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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}
|
||||
]
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -398,7 +398,7 @@ module.exports = class CampaignView extends RootView
|
|||
@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'])
|
||||
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 +532,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,23 @@ 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
|
||||
else if @level.isType('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')
|
||||
@homeLink = '/courses'
|
||||
@homeViewClass = 'views/courses/CoursesView'
|
||||
if @courseID
|
||||
|
@ -136,7 +113,8 @@ 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('game-dev') # TODO
|
||||
#else if @level.isType('web-dev') # TODO
|
||||
else
|
||||
@homeLink = '/'
|
||||
@homeViewClass = 'views/HomeView'
|
||||
|
@ -153,16 +131,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 +158,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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -59,7 +59,7 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
goalList = goalContainer.find('ul')
|
||||
goalCount = 0
|
||||
for goalID, goal of @level.get('goals') when (not goal.team or goal.team is (e.team or 'humans')) and not goal.hiddenGoal
|
||||
continue if goal.optional and @level.get('type', true) is 'course'
|
||||
continue if goal.optional and @level.isType('course')
|
||||
name = utils.i18n goal, 'name'
|
||||
goalList.append $('<li>' + name + '</li>')
|
||||
++goalCount
|
||||
|
@ -169,7 +169,7 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
@playSound 'loading-view-unveil', 0.5
|
||||
@$el.find('.left-wing').css left: '-100%', backgroundPosition: 'right -400px top 0'
|
||||
@$el.find('.right-wing').css right: '-100%', backgroundPosition: 'left -400px top 0'
|
||||
$('#level-footer-background').detach().appendTo('#page-container').slideDown(duration)
|
||||
$('#level-footer-background').detach().appendTo('#page-container').slideDown(duration) unless @level.isType('web-dev')
|
||||
|
||||
unveilIntro: =>
|
||||
return if @destroyed or not @intro or @unveiled
|
||||
|
|
|
@ -21,7 +21,6 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
'tome:cast-spells': 'onTomeCast'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
'playback:stop-real-time-playback': 'onStopRealTimePlayback'
|
||||
'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
|
||||
|
||||
events:
|
||||
'click #music-button': 'onToggleMusic'
|
||||
|
@ -110,11 +109,6 @@ module.exports = class LevelPlaybackView extends CocoView
|
|||
Backbone.Mediator.publish 'playback:real-time-playback-started', {}
|
||||
@playSound 'real-time-playback-start'
|
||||
|
||||
onRealTimeMultiplayerCast: (e) ->
|
||||
@realTime = true
|
||||
@togglePlaybackControls false
|
||||
Backbone.Mediator.publish 'playback:real-time-playback-waiting', {}
|
||||
|
||||
onWindowResize: (s...) =>
|
||||
@barWidth = $('.progress', @$el).width()
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ LevelSetupManager = require 'lib/LevelSetupManager'
|
|||
ContactModal = require 'views/core/ContactModal'
|
||||
HintsView = require './HintsView'
|
||||
HintsState = require './HintsState'
|
||||
WebSurfaceView = require './WebSurfaceView'
|
||||
|
||||
PROFILE_ME = false
|
||||
|
||||
|
@ -72,14 +73,10 @@ module.exports = class PlayLevelView extends RootView
|
|||
'level:started': 'onLevelStarted'
|
||||
'level:loading-view-unveiling': 'onLoadingViewUnveiling'
|
||||
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
|
||||
'level:loaded': 'onLevelLoaded'
|
||||
'level:session-loaded': 'onSessionLoaded'
|
||||
'playback:real-time-playback-waiting': 'onRealTimePlaybackWaiting'
|
||||
'playback:real-time-playback-started': 'onRealTimePlaybackStarted'
|
||||
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
|
||||
'real-time-multiplayer:created-game': 'onRealTimeMultiplayerCreatedGame'
|
||||
'real-time-multiplayer:joined-game': 'onRealTimeMultiplayerJoinedGame'
|
||||
'real-time-multiplayer:left-game': 'onRealTimeMultiplayerLeftGame'
|
||||
'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
|
||||
'ipad:memory-warning': 'onIPadMemoryWarning'
|
||||
'store:item-purchased': 'onItemPurchased'
|
||||
|
||||
|
@ -137,7 +134,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
|
||||
load: ->
|
||||
@loadStartTime = new Date()
|
||||
@god = new God({@gameUIState})
|
||||
levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID
|
||||
if me.isSessionless()
|
||||
levelLoaderOptions.fakeSessionConfig = {}
|
||||
|
@ -145,6 +141,9 @@ module.exports = class PlayLevelView extends RootView
|
|||
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
|
||||
@listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed
|
||||
|
||||
onLevelLoaded: (e) ->
|
||||
@god = new God({@gameUIState}) unless e.level.isType('web-dev')
|
||||
|
||||
trackLevelLoadEnd: ->
|
||||
return if @isEditorPreview
|
||||
@loadEndTime = new Date()
|
||||
|
@ -192,8 +191,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
@initGoalManager()
|
||||
@insertSubviews()
|
||||
@initVolume()
|
||||
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
|
||||
|
||||
@register()
|
||||
@controlBar.setBus(@bus)
|
||||
@initScriptManager()
|
||||
|
@ -203,9 +200,12 @@ module.exports = class PlayLevelView extends RootView
|
|||
|
||||
grabLevelLoaderData: ->
|
||||
@session = @levelLoader.session
|
||||
@world = @levelLoader.world
|
||||
@level = @levelLoader.level
|
||||
@$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||
if @level.isType('web-dev')
|
||||
@$el.addClass 'web-dev' # Hide some of the elements we won't be using
|
||||
return
|
||||
@world = @levelLoader.world
|
||||
@$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') # TODO: figure out what this does and comment it
|
||||
@$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span'
|
||||
# TODO: Update terminology to always be opponentSession or otherSession
|
||||
# TODO: E.g. if it's always opponent right now, then variable names should be opponentSession until we have coop play
|
||||
|
@ -239,11 +239,9 @@ module.exports = class PlayLevelView extends RootView
|
|||
myCode[thang] ?= {}
|
||||
if c then myCode[thang][spell] = c else delete myCode[thang][spell]
|
||||
@session.set('code', myCode)
|
||||
if @session.get('multiplayer') and @otherSession?
|
||||
# For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet.
|
||||
@session.set 'multiplayer', false
|
||||
|
||||
setupGod: ->
|
||||
return if @level.isType('web-dev')
|
||||
@god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
|
||||
@god.setLevelSessionIDs if @otherSession then [@session.id, @otherSession.id] else [@session.id]
|
||||
@god.setWorldClassMap @world.classMap
|
||||
|
@ -258,22 +256,23 @@ module.exports = class PlayLevelView extends RootView
|
|||
|
||||
initGoalManager: ->
|
||||
@goalManager = new GoalManager(@world, @level.get('goals'), @team)
|
||||
@god.setGoalManager @goalManager
|
||||
@god?.setGoalManager @goalManager
|
||||
|
||||
insertSubviews: ->
|
||||
@hintsState = new HintsState({ hidden: true }, { @session, @level })
|
||||
@insertSubView @tome = new TomeView { @levelID, @session, @otherSession, thangs: @world.thangs, @supermodel, @level, @observing, @courseID, @courseInstanceID, @god, @hintsState }
|
||||
@insertSubView new LevelPlaybackView session: @session, level: @level
|
||||
@insertSubView @tome = new TomeView { @levelID, @session, @otherSession, thangs: @world?.thangs ? [], @supermodel, @level, @observing, @courseID, @courseInstanceID, @god, @hintsState }
|
||||
@insertSubView new LevelPlaybackView session: @session, level: @level unless @level.isType('web-dev')
|
||||
@insertSubView new GoalsView {level: @level}
|
||||
@insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags'
|
||||
@insertSubView new GoldView {} unless @level.get('slug') in ['wakka-maul']
|
||||
@insertSubView new HUDView {level: @level}
|
||||
@insertSubView new GoldView {} unless @level.get('slug') in ['wakka-maul'] unless @level.isType('web-dev')
|
||||
@insertSubView new HUDView {level: @level} unless @level.isType('web-dev')
|
||||
@insertSubView new LevelDialogueView {level: @level, sessionID: @session.id}
|
||||
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
|
||||
@insertSubView new ProblemAlertView session: @session, level: @level, supermodel: @supermodel
|
||||
@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, courseID: @courseID, courseInstanceID: @courseInstanceID}
|
||||
@insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view')
|
||||
@insertSubView @webSurface = new WebSurfaceView {level: @level, @goalManager} if @level.isType('web-dev')
|
||||
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
|
||||
|
||||
initVolume: ->
|
||||
|
@ -282,6 +281,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
Backbone.Mediator.publish 'level:set-volume', volume: volume
|
||||
|
||||
initScriptManager: ->
|
||||
return if @level.isType('web-dev')
|
||||
@scriptManager = new ScriptManager({scripts: @world.scripts or [], view: @, session: @session, levelID: @level.get('slug')})
|
||||
@scriptManager.loadFromSession()
|
||||
|
||||
|
@ -289,9 +289,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@bus = LevelBus.get(@levelID, @session.id)
|
||||
@bus.setSession(@session)
|
||||
@bus.setSpells @tome.spells
|
||||
if @session.get('multiplayer') and not me.isAdmin()
|
||||
@session.set 'multiplayer', false # Temp: multiplayer has bugged out some sessions, so ignoring it.
|
||||
@bus.connect() if @session.get('multiplayer')
|
||||
#@bus.connect() if @session.get('multiplayer') # TODO: session's multiplayer flag removed; connect bus another way if we care about it
|
||||
|
||||
# Load Completed Setup ######################################################
|
||||
|
||||
|
@ -310,13 +308,11 @@ module.exports = class PlayLevelView extends RootView
|
|||
else if e.level.get('slug') is 'assembly-speed'
|
||||
raider = '55527eb0b8abf4ba1fe9a107'
|
||||
e.session.set 'heroConfig', {"thangType":raider,"inventory":{}}
|
||||
else if e.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] and not _.size e.session.get('heroConfig')?.inventory ? {}
|
||||
else if e.level.isType('hero', 'hero-ladder', 'hero-coop') and not _.size e.session.get('heroConfig')?.inventory ? {}
|
||||
@setupManager?.destroy()
|
||||
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: e.level, levelID: @levelID, parent: @, session: e.session, courseID: @courseID, courseInstanceID: @courseInstanceID})
|
||||
@setupManager.open()
|
||||
|
||||
@onRealTimeMultiplayerLevelLoaded e.session if e.level.get('type') in ['hero-ladder', 'course-ladder']
|
||||
|
||||
onLoaded: ->
|
||||
_.defer => @onLevelLoaderLoaded()
|
||||
|
||||
|
@ -325,14 +321,17 @@ module.exports = class PlayLevelView extends RootView
|
|||
return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
|
||||
|
||||
# Save latest level played.
|
||||
if not @observing and not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
|
||||
if not @observing and not (@levelLoader.level.isType('ladder', 'ladder-tutorial'))
|
||||
me.set('lastLevel', @levelID)
|
||||
me.save()
|
||||
application.tracker?.identify()
|
||||
@saveRecentMatch() if @otherSession
|
||||
@levelLoader.destroy()
|
||||
@levelLoader = null
|
||||
@initSurface()
|
||||
if @level.isType('web-dev')
|
||||
Backbone.Mediator.publish 'level:started', {}
|
||||
else
|
||||
@initSurface()
|
||||
|
||||
saveRecentMatch: ->
|
||||
allRecentlyPlayedMatches = storage.load('recently-played-matches') ? {}
|
||||
|
@ -360,7 +359,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
|
||||
|
||||
findPlayerNames: ->
|
||||
return {} unless @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
||||
return {} unless @level.isType('ladder', 'hero-ladder', 'course-ladder')
|
||||
playerNames = {}
|
||||
for session in [@session, @otherSession] when session?.get('team')
|
||||
playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous'
|
||||
|
@ -369,33 +368,30 @@ module.exports = class PlayLevelView extends RootView
|
|||
# Once Surface is Loaded ####################################################
|
||||
|
||||
onLevelStarted: ->
|
||||
return unless @surface?
|
||||
return unless @surface? or @webSurface?
|
||||
@loadingView.showReady()
|
||||
@trackLevelLoadEnd()
|
||||
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
|
||||
return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @
|
||||
@surface.showLevel()
|
||||
@surface?.showLevel()
|
||||
Backbone.Mediator.publish 'level:set-time', time: 0
|
||||
if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
|
||||
@loadingView.startUnveiling()
|
||||
@loadingView.unveil true
|
||||
else
|
||||
@scriptManager.initializeCamera()
|
||||
@scriptManager?.initializeCamera()
|
||||
|
||||
onLoadingViewUnveiling: (e) ->
|
||||
@selectHero()
|
||||
|
||||
onLoadingViewUnveiled: (e) ->
|
||||
if @level.get('type') in ['course-ladder', 'hero-ladder'] or @observing
|
||||
if @level.isType('course-ladder', 'hero-ladder') or @observing
|
||||
# We used to autoplay by default, but now we only do it if the level says to in the introduction script.
|
||||
Backbone.Mediator.publish 'level:set-playing', playing: true
|
||||
@loadingView.$el.remove()
|
||||
@removeSubView @loadingView
|
||||
@loadingView = null
|
||||
@playAmbientSound()
|
||||
if @options.realTimeMultiplayerSessionID?
|
||||
Backbone.Mediator.publish 'playback:real-time-playback-waiting', {}
|
||||
@realTimeMultiplayerContinueGame @options.realTimeMultiplayerSessionID
|
||||
# TODO: Is it possible to create a Mongoose ObjectId for 'ls', instead of the string returned from get()?
|
||||
application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id') unless @observing
|
||||
$(window).trigger 'resize'
|
||||
|
@ -423,7 +419,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
|
||||
Backbone.Mediator.publish 'tome:select-primary-sprite', {}
|
||||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
|
||||
@surface.focusOnHero()
|
||||
@surface?.focusOnHero()
|
||||
|
||||
perhapsStartSimulating: ->
|
||||
return unless @shouldSimulate()
|
||||
|
@ -440,7 +436,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
simulateNextGame: ->
|
||||
return @simulator.fetchAndSimulateOneGame() if @simulator
|
||||
simulatorOptions = background: true, leagueID: @courseInstanceID
|
||||
simulatorOptions.levelID = @level.get('slug') if @level.get('type', true) in ['course-ladder', 'hero-ladder']
|
||||
simulatorOptions.levelID = @level.get('slug') if @level.isType('course-ladder', 'hero-ladder')
|
||||
@simulator = new Simulator simulatorOptions
|
||||
# Crude method of mitigating Simulator memory leak issues
|
||||
fetchAndSimulateOneGameOriginal = @simulator.fetchAndSimulateOneGame
|
||||
|
@ -462,31 +458,30 @@ module.exports = class PlayLevelView extends RootView
|
|||
cores = window.navigator.hardwareConcurrency or defaultCores # Available on Chrome/Opera, soon Safari
|
||||
defaultHeapLimit = 793000000
|
||||
heapLimit = window.performance?.memory?.jsHeapSizeLimit or defaultHeapLimit # Only available on Chrome, basically just says 32- vs. 64-bit
|
||||
levelType = @level.get 'type', true
|
||||
gamesSimulated = me.get('simulatedBy')
|
||||
console.debug "Should we start simulating? Cores:", window.navigator.hardwareConcurrency, "Heap limit:", window.performance?.memory?.jsHeapSizeLimit, "Load duration:", @loadDuration
|
||||
return false unless $.browser?.desktop
|
||||
return false if $.browser?.msie or $.browser?.msedge
|
||||
return false if $.browser.linux
|
||||
return false if me.level() < 8
|
||||
if levelType in ['course', 'game-dev']
|
||||
if @level.isType('course', 'game-dev', 'web-dev')
|
||||
return false
|
||||
else if levelType is 'hero' and gamesSimulated
|
||||
else if @level.isType('hero') and gamesSimulated
|
||||
return false if stillBuggy
|
||||
return false if cores < 8
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 10000
|
||||
else if levelType is 'hero-ladder' and gamesSimulated
|
||||
else if @level.isType('hero-ladder') and gamesSimulated
|
||||
return false if stillBuggy
|
||||
return false if cores < 4
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 15000
|
||||
else if levelType is 'hero-ladder' and not gamesSimulated
|
||||
else if @level.isType('hero-ladder') and not gamesSimulated
|
||||
return false if stillBuggy
|
||||
return false if cores < 8
|
||||
return false if heapLimit <= defaultHeapLimit
|
||||
return false if @loadDuration > 20000
|
||||
else if levelType is 'course-ladder'
|
||||
else if @level.isType('course-ladder')
|
||||
return false if cores <= defaultCores
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 18000
|
||||
|
@ -542,7 +537,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
onDonePressed: -> @showVictory()
|
||||
|
||||
onShowVictory: (e) ->
|
||||
$('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||
$('#level-done-button').show() unless @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
|
||||
@showVictory() if e.showModal
|
||||
return if @victorySeen
|
||||
@victorySeen = true
|
||||
|
@ -560,12 +555,11 @@ module.exports = class PlayLevelView extends RootView
|
|||
return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor
|
||||
@endHighlight()
|
||||
options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world}
|
||||
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then HeroVictoryModal else VictoryModal
|
||||
ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
|
||||
if @level.get('type', true) is 'course-ladder'
|
||||
ModalClass = if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') then HeroVictoryModal else VictoryModal
|
||||
ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
|
||||
if @level.isType('course-ladder')
|
||||
ModalClass = CourseVictoryModal
|
||||
options.courseInstanceID = @getQueryVariable 'league'
|
||||
# TODO: Figure out how the course victory modal can get the course
|
||||
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
|
||||
victoryModal = new ModalClass(options)
|
||||
@openModalView(victoryModal)
|
||||
|
@ -589,13 +583,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
|
||||
onFocusDom: (e) -> $(e.selector).focus()
|
||||
|
||||
onMultiplayerChanged: (e) ->
|
||||
if @session.get('multiplayer')
|
||||
@bus.connect()
|
||||
else
|
||||
@bus.removeFirebaseData =>
|
||||
@bus.disconnect()
|
||||
|
||||
onContactClicked: (e) ->
|
||||
Backbone.Mediator.publish 'level:contact-button-pressed', {}
|
||||
@openModalView contactModal = new ContactModal levelID: @level.get('slug') or @level.id, courseID: @courseID, courseInstanceID: @courseInstanceID
|
||||
|
@ -634,10 +621,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
AudioPlayer.preloadSoundReference sound
|
||||
|
||||
# Real-time playback
|
||||
onRealTimePlaybackWaiting: (e) ->
|
||||
@$el.addClass('real-time').focus()
|
||||
@onWindowResize()
|
||||
|
||||
onRealTimePlaybackStarted: (e) ->
|
||||
@$el.addClass('real-time').focus()
|
||||
@onWindowResize()
|
||||
|
@ -650,14 +633,12 @@ module.exports = class PlayLevelView extends RootView
|
|||
_.delay @onSubmissionComplete, 750 # Wait for transition to end.
|
||||
else
|
||||
@waitingForSubmissionComplete = true
|
||||
@onRealTimeMultiplayerPlaybackEnded()
|
||||
|
||||
onSubmissionComplete: =>
|
||||
return if @destroyed
|
||||
Backbone.Mediator.publish 'level:set-time', ratio: 1
|
||||
return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor
|
||||
# TODO: Show a victory dialog specific to hero-ladder level
|
||||
if @goalManager.checkOverallStatus() is 'success' and not @options.realTimeMultiplayerSessionID?
|
||||
if @goalManager.checkOverallStatus() is 'success'
|
||||
showModalFn = -> Backbone.Mediator.publish 'level:show-victory', showModal: true
|
||||
@session.recordScores @world.scores, @level
|
||||
if @level.get 'replayable'
|
||||
|
@ -682,7 +663,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
#@instance.save() unless @instance.loading
|
||||
delete window.nextURL
|
||||
console.profileEnd?() if PROFILE_ME
|
||||
@onRealTimeMultiplayerLevelUnloaded()
|
||||
application.tracker?.disableInspectletJS()
|
||||
super()
|
||||
|
||||
|
@ -698,358 +678,3 @@ module.exports = class PlayLevelView extends RootView
|
|||
@setupManager?.destroy()
|
||||
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true})
|
||||
@setupManager.open()
|
||||
|
||||
# Start Real-time Multiplayer ######################################################
|
||||
#
|
||||
# This view acts as a hub for the real-time multiplayer session for the current level.
|
||||
#
|
||||
# It performs these actions:
|
||||
# Player heartbeat
|
||||
# Publishes player status
|
||||
# Updates real-time multiplayer session state
|
||||
# Updates real-time multiplayer player state
|
||||
# Cleans up old sessions (sets state to 'finished')
|
||||
# Real-time multiplayer cast handshake
|
||||
# Swap teams on game joined, if necessary
|
||||
# Reload PlayLevelView on real-time submit, automatically continue game and real-time playback
|
||||
#
|
||||
# It monitors these:
|
||||
# Real-time multiplayer sessions
|
||||
# Current real-time multiplayer session
|
||||
# Internal multiplayer create/joined/left events
|
||||
#
|
||||
# Real-time state variables.
|
||||
# Each Ref is Firebase reference, and may have a matching Data suffixed variable with the latest data received.
|
||||
# @realTimePlayerRef - User's real-time multiplayer player for this level
|
||||
# @realTimePlayerGameRef - User's current real-time multiplayer player game session
|
||||
# @realTimeSessionRef - Current real-time multiplayer game session
|
||||
# @realTimeOpponentRef - Current real-time multiplayer opponent
|
||||
# @realTimePlayersRef - Real-time players for current real-time multiplayer game session
|
||||
# @options.realTimeMultiplayerSessionID - Need to continue an existing real-time multiplayer session
|
||||
#
|
||||
# TODO: Move this code to it's own file, or possibly the LevelBus
|
||||
# TODO: Save settings somewhere reasonable
|
||||
multiplayerFireHost: 'https://codecombat.firebaseio.com/test/db/'
|
||||
|
||||
onRealTimeMultiplayerLevelLoaded: (session) ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerLevelLoaded'
|
||||
return if @realTimePlayerRef?
|
||||
return if me.get('anonymous')
|
||||
@realTimePlayerRef = new Firebase "#{@multiplayerFireHost}multiplayer_players/#{@levelID}/#{me.id}"
|
||||
unless @options.realTimeMultiplayerSessionID?
|
||||
# TODO: Wait for name instead of using 'Anon', or try and update it later?
|
||||
name = me.get('name') ? session.get('creatorName') ? 'Anon'
|
||||
@realTimePlayerRef.set
|
||||
id: me.id # TODO: is this redundant info necessary?
|
||||
name: name
|
||||
state: 'playing'
|
||||
created: new Date().toISOString()
|
||||
heartbeat: new Date().toISOString()
|
||||
@timerMultiplayerHeartbeatID = setInterval @onRealTimeMultiplayerHeartbeat, 60 * 1000
|
||||
@cleanupRealTimeSessions()
|
||||
|
||||
cleanupRealTimeSessions: ->
|
||||
# console.log 'PlayLevelView cleanupRealTimeSessions'
|
||||
# TODO: Reduce this call, possibly by username and dates
|
||||
realTimeSessionCollection = new Firebase "#{@multiplayerFireHost}multiplayer_level_sessions/#{@levelID}"
|
||||
realTimeSessionCollection.once 'value', (collectionSnapshot) =>
|
||||
for multiplayerSessionID, multiplayerSession of collectionSnapshot.val()
|
||||
continue if @options.realTimeMultiplayerSessionID? and @options.realTimeMultiplayerSessionID is multiplayerSessionID
|
||||
continue unless multiplayerSession.state isnt 'finished'
|
||||
player = realTimeSessionCollection.child "#{multiplayerSession.id}/players/#{me.id}"
|
||||
player.once 'value', (playerSnapshot) =>
|
||||
if playerSnapshot.val()
|
||||
console.info 'Cleaning up previous real-time multiplayer session', multiplayerSessionID
|
||||
player.update 'state': 'left'
|
||||
multiplayerSessionRef = realTimeSessionCollection.child "#{multiplayerSessionID}"
|
||||
multiplayerSessionRef.update 'state': 'finished'
|
||||
|
||||
onRealTimeMultiplayerLevelUnloaded: ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerLevelUnloaded'
|
||||
if @timerMultiplayerHeartbeatID?
|
||||
clearInterval @timerMultiplayerHeartbeatID
|
||||
@timerMultiplayerHeartbeatID = null
|
||||
|
||||
# TODO: similar to game ending cleanup
|
||||
if @realTimeOpponentRef?
|
||||
@realTimeOpponentRef.off 'value', @onRealTimeOpponentChanged
|
||||
@realTimeOpponentRef = null
|
||||
if @realTimePlayersRef?
|
||||
@realTimePlayersRef.off 'child_added', @onRealTimePlayerAdded
|
||||
@realTimePlayersRef = null
|
||||
if @realTimeSessionRef?
|
||||
@realTimeSessionRef.off 'value', @onRealTimeSessionChanged
|
||||
@realTimeSessionRef = null
|
||||
if @realTimePlayerGameRef?
|
||||
@realTimePlayerGameRef = null
|
||||
if @realTimePlayerRef?
|
||||
@realTimePlayerRef = null
|
||||
|
||||
onRealTimeMultiplayerHeartbeat: =>
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerHeartbeat', @realTimePlayerRef
|
||||
@realTimePlayerRef.update 'heartbeat': new Date().toISOString() if @realTimePlayerRef?
|
||||
|
||||
onRealTimeMultiplayerCreatedGame: (e) ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerCreatedGame'
|
||||
@joinRealTimeMultiplayerGame e
|
||||
@realTimePlayerGameRef.update 'state': 'coding'
|
||||
@realTimePlayerRef.update 'state': 'available'
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Waiting for opponent..'
|
||||
|
||||
onRealTimeSessionChanged: (snapshot) =>
|
||||
# console.log 'PlayLevelView onRealTimeSessionChanged', snapshot.val()
|
||||
@realTimeSessionData = snapshot.val()
|
||||
if @realTimeSessionData?.state is 'finished'
|
||||
@realTimeGameEnded()
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:left-game', {}
|
||||
|
||||
onRealTimePlayerAdded: (snapshot) =>
|
||||
# console.log 'PlayLevelView onRealTimePlayerAdded', snapshot.val()
|
||||
# Assume game is full, game on
|
||||
data = snapshot.val()
|
||||
if data? and data.id isnt me.id
|
||||
@realTimeOpponentData = data
|
||||
# console.log 'PlayLevelView onRealTimePlayerAdded opponent', @realTimeOpponentData, @realTimePlayersData
|
||||
@realTimePlayersData[@realTimeOpponentData.id] = @realTimeOpponentData
|
||||
if @realTimeSessionData?.state is 'creating'
|
||||
@realTimeSessionRef.update 'state': 'coding'
|
||||
@realTimePlayerRef.update 'state': 'unavailable'
|
||||
@realTimeOpponentRef = @realTimeSessionRef.child "players/#{@realTimeOpponentData.id}"
|
||||
@realTimeOpponentRef.on 'value', @onRealTimeOpponentChanged
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: "Playing against #{@realTimeOpponentData.name}"
|
||||
|
||||
onRealTimeOpponentChanged: (snapshot) =>
|
||||
# console.log 'PlayLevelView onRealTimeOpponentChanged', snapshot.val()
|
||||
@realTimeOpponentData = snapshot.val()
|
||||
switch @realTimeOpponentData?.state
|
||||
when 'left'
|
||||
console.info 'Real-time multiplayer opponent left the game'
|
||||
opponentID = @realTimeOpponentData.id
|
||||
@realTimeGameEnded()
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:left-game', userID: opponentID
|
||||
when 'submitted'
|
||||
# TODO: What should this message say?
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: "#{@realTimeOpponentData.name} waiting for your code"
|
||||
|
||||
joinRealTimeMultiplayerGame: (e) ->
|
||||
# console.log 'PlayLevelView joinRealTimeMultiplayerGame', e
|
||||
unless @realTimeSessionRef?
|
||||
@session.set('submittedCodeLanguage', @session.get('codeLanguage'))
|
||||
@session.save()
|
||||
|
||||
@realTimeSessionRef = new Firebase "#{@multiplayerFireHost}multiplayer_level_sessions/#{@levelID}/#{e.realTimeSessionID}"
|
||||
@realTimePlayersRef = @realTimeSessionRef.child 'players'
|
||||
|
||||
# Look for opponent
|
||||
@realTimeSessionRef.once 'value', (multiplayerSessionSnapshot) =>
|
||||
if @realTimeSessionData = multiplayerSessionSnapshot.val()
|
||||
@realTimePlayersRef.once 'value', (playsSnapshot) =>
|
||||
if @realTimePlayersData = playsSnapshot.val()
|
||||
for id, player of @realTimePlayersData
|
||||
if id isnt me.id
|
||||
@realTimeOpponentRef = @realTimeSessionRef.child "players/#{id}"
|
||||
@realTimeOpponentRef.once 'value', (opponentSnapshot) =>
|
||||
if @realTimeOpponentData = opponentSnapshot.val()
|
||||
@updateTeam()
|
||||
else
|
||||
console.error 'Could not lookup multiplayer opponent data.'
|
||||
@realTimeOpponentRef.on 'value', @onRealTimeOpponentChanged
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Playing against ' + player.name
|
||||
else
|
||||
console.error 'Could not lookup multiplayer session players data.'
|
||||
# TODO: need child_removed too?
|
||||
@realTimePlayersRef.on 'child_added', @onRealTimePlayerAdded
|
||||
else
|
||||
console.error 'Could not lookup multiplayer session data.'
|
||||
@realTimeSessionRef.on 'value', @onRealTimeSessionChanged
|
||||
|
||||
@realTimePlayerGameRef = @realTimeSessionRef.child "players/#{me.id}"
|
||||
|
||||
# TODO: Follow up in MultiplayerView to see if double joins can be avoided
|
||||
# else
|
||||
# console.error 'Joining real-time multiplayer game with an existing @realTimeSessionRef.'
|
||||
|
||||
onRealTimeMultiplayerJoinedGame: (e) ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerJoinedGame', e
|
||||
@joinRealTimeMultiplayerGame e
|
||||
@realTimePlayerGameRef.update 'state': 'coding'
|
||||
@realTimePlayerRef.update 'state': 'unavailable'
|
||||
|
||||
onRealTimeMultiplayerLeftGame: (e) ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerLeftGame', e
|
||||
if e.userID? and e.userID is me.id
|
||||
@realTimePlayerGameRef.update 'state': 'left'
|
||||
@realTimeGameEnded()
|
||||
|
||||
realTimeMultiplayerContinueGame: (realTimeSessionID) ->
|
||||
# console.log 'PlayLevelView realTimeMultiplayerContinueGame', realTimeSessionID, me.id
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', realTimeSessionID: realTimeSessionID
|
||||
|
||||
console.info 'Setting my game status to ready'
|
||||
@realTimePlayerGameRef.update 'state': 'ready'
|
||||
|
||||
if @realTimeOpponentData.state is 'ready'
|
||||
@realTimeOpponentIsReady()
|
||||
else
|
||||
console.info 'Waiting for opponent to be ready'
|
||||
@realTimeOpponentRef.on 'value', @realTimeOpponentMaybeReady
|
||||
|
||||
realTimeOpponentMaybeReady: (snapshot) =>
|
||||
# console.log 'PlayLevelView realTimeOpponentMaybeReady'
|
||||
if @realTimeOpponentData = snapshot.val()
|
||||
if @realTimeOpponentData.state is 'ready'
|
||||
@realTimeOpponentRef.off 'value', @realTimeOpponentMaybeReady
|
||||
@realTimeOpponentIsReady()
|
||||
|
||||
realTimeOpponentIsReady: =>
|
||||
console.info 'All real-time multiplayer players are ready!'
|
||||
@realTimeSessionRef.update 'state': 'running'
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: 'Battling ' + @realTimeOpponentData.name
|
||||
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
|
||||
|
||||
realTimeGameEnded: ->
|
||||
if @realTimeOpponentRef?
|
||||
@realTimeOpponentRef.off 'value', @onRealTimeOpponentChanged
|
||||
@realTimeOpponentRef = null
|
||||
if @realTimePlayersRef?
|
||||
@realTimePlayersRef.off 'child_added', @onRealTimePlayerAdded
|
||||
@realTimePlayersRef = null
|
||||
if @realTimeSessionRef?
|
||||
@realTimeSessionRef.off 'value', @onRealTimeSessionChanged
|
||||
@realTimeSessionRef.update 'state': 'finished'
|
||||
@realTimeSessionRef = null
|
||||
if @realTimePlayerGameRef?
|
||||
@realTimePlayerGameRef = null
|
||||
if @realTimePlayerRef?
|
||||
@realTimePlayerRef.update 'state': 'playing'
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: ''
|
||||
|
||||
onRealTimeMultiplayerCast: (e) ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerCast', @realTimeSessionData, @realTimePlayersData
|
||||
unless @realTimeSessionRef?
|
||||
console.error 'Real-time multiplayer cast without multiplayer session.'
|
||||
return
|
||||
unless @realTimeSessionData?
|
||||
console.error 'Real-time multiplayer cast without multiplayer data.'
|
||||
return
|
||||
unless @realTimePlayersData?
|
||||
console.error 'Real-time multiplayer cast without multiplayer players data.'
|
||||
return
|
||||
|
||||
# Set submissionCount for created real-time multiplayer session
|
||||
if me.id is @realTimeSessionData.creator
|
||||
sessionState = @session.get('state')
|
||||
if sessionState?
|
||||
submissionCount = sessionState.submissionCount ? 0
|
||||
console.info 'Setting multiplayer submissionCount to', submissionCount
|
||||
@realTimeSessionRef.update 'submissionCount': submissionCount
|
||||
else
|
||||
console.error 'Failed to read sessionState in onRealTimeMultiplayerCast'
|
||||
|
||||
console.info 'Submitting my code'
|
||||
permissions = @session.get 'permissions' ? []
|
||||
unless _.find(permissions, (p) -> p.target is 'public' and p.access is 'read')
|
||||
permissions.push target:'public', access:'read'
|
||||
@session.set 'permissions', permissions
|
||||
@session.patch()
|
||||
@realTimePlayerGameRef.update 'state': 'submitted'
|
||||
|
||||
console.info 'Other player is', @realTimeOpponentData.state
|
||||
if @realTimeOpponentData.state in ['submitted', 'ready']
|
||||
@realTimeOpponentSubmittedCode @realTimeOpponentData, @realTimePlayerGameData
|
||||
else
|
||||
# Wait for opponent to submit their code
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: "Waiting for code from #{@realTimeOpponentData.name}"
|
||||
@realTimeOpponentRef.on 'value', @realTimeOpponentMaybeSubmitted
|
||||
|
||||
realTimeOpponentMaybeSubmitted: (snapshot) =>
|
||||
if @realTimeOpponentData = snapshot.val()
|
||||
if @realTimeOpponentData.state in ['submitted', 'ready']
|
||||
@realTimeOpponentRef.off 'value', @realTimeOpponentMaybeSubmitted
|
||||
@realTimeOpponentSubmittedCode @realTimeOpponentData, @realTimePlayerGameData
|
||||
|
||||
onRealTimeMultiplayerPlaybackEnded: ->
|
||||
# console.log 'PlayLevelView onRealTimeMultiplayerPlaybackEnded'
|
||||
if @realTimeSessionRef?
|
||||
@realTimeSessionRef.update 'state': 'coding'
|
||||
@realTimePlayerGameRef.update 'state': 'coding'
|
||||
if @realTimeOpponentData?
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:player-status', status: "Playing against #{@realTimeOpponentData.name}"
|
||||
|
||||
realTimeOpponentSubmittedCode: (opponentPlayer, myPlayer) =>
|
||||
# console.log 'PlayLevelView realTimeOpponentSubmittedCode', @realTimeSessionData.id, opponentPlayer.level_session
|
||||
# Read submissionCount for joined real-time multiplayer session
|
||||
if me.id isnt @realTimeSessionData.creator
|
||||
sessionState = @session.get('state') ? {}
|
||||
newSubmissionCount = @realTimeSessionData.submissionCount
|
||||
if newSubmissionCount?
|
||||
# TODO: This isn't always getting updated where the random seed generation uses it.
|
||||
sessionState.submissionCount = parseInt newSubmissionCount
|
||||
console.info 'Got multiplayer submissionCount', sessionState.submissionCount
|
||||
@session.set 'state', sessionState
|
||||
@session.patch()
|
||||
|
||||
# Reload this level so the opponent session can easily be wired up
|
||||
Backbone.Mediator.publish 'router:navigate',
|
||||
route: "/play/level/#{@levelID}"
|
||||
viewClass: PlayLevelView
|
||||
viewArgs: [{supermodel: @supermodel, autoUnveil: true, realTimeMultiplayerSessionID: @realTimeSessionData.id, opponent: opponentPlayer.level_session, team: @team}, @levelID]
|
||||
|
||||
updateTeam: ->
|
||||
# If not creator, and same team as creator, then switch teams
|
||||
# TODO: Assumes there are only 'humans' and 'ogres'
|
||||
|
||||
unless @realTimeOpponentData?
|
||||
console.error 'Tried to switch teams without real-time multiplayer opponent data.'
|
||||
return
|
||||
unless @realTimeSessionData?
|
||||
console.error 'Tried to switch teams without real-time multiplayer session data.'
|
||||
return
|
||||
return if me.id is @realTimeSessionData.creator
|
||||
|
||||
oldTeam = @realTimeOpponentData.team
|
||||
return unless oldTeam is @session.get('team')
|
||||
|
||||
# Need to switch to other team
|
||||
newTeam = if oldTeam is 'humans' then 'ogres' else 'humans'
|
||||
console.info "Switching from team #{oldTeam} to #{newTeam}"
|
||||
|
||||
# Move code from old team to new team
|
||||
# Assumes teamSpells has matching spells for each team
|
||||
# TODO: Similar to code in loadOpponentTeam, consolidate?
|
||||
code = @session.get 'code'
|
||||
teamSpells = @session.get 'teamSpells'
|
||||
for oldSpellKey in teamSpells[oldTeam]
|
||||
[oldThang, oldSpell] = oldSpellKey.split '/'
|
||||
oldCode = code[oldThang]?[oldSpell]
|
||||
continue unless oldCode?
|
||||
# Move oldCode to new team under same spell
|
||||
for newSpellKey in teamSpells[newTeam]
|
||||
[newThang, newSpell] = newSpellKey.split '/'
|
||||
if newSpell is oldSpell
|
||||
# Found spell location under new team
|
||||
# console.log "Swapping spell=#{oldSpell} from #{oldThang} to #{newThang}"
|
||||
if code[newThang]?[oldSpell]?
|
||||
# Option 1: have a new spell to swap
|
||||
code[oldThang][oldSpell] = code[newThang][oldSpell]
|
||||
else
|
||||
# Option 2: no new spell to swap
|
||||
delete code[oldThang][oldSpell]
|
||||
code[newThang] = {} unless code[newThang]?
|
||||
code[newThang][oldSpell] = oldCode
|
||||
break
|
||||
|
||||
@setTeam newTeam # Sets @session 'team'
|
||||
sessionState = @session.get('state')
|
||||
if sessionState?
|
||||
# TODO: Don't hard code thangID
|
||||
sessionState.selected = if newTeam is 'humans' then 'Hero Placeholder' else 'Hero Placeholder 1'
|
||||
@session.set 'state', sessionState
|
||||
@session.set 'code', code
|
||||
@session.patch()
|
||||
|
||||
if sessionState?
|
||||
# TODO: Don't hardcode spellName
|
||||
Backbone.Mediator.publish 'level:select-sprite', thangID: sessionState.selected, spellName: 'plan'
|
||||
|
||||
# End Real-time Multiplayer ######################################################
|
||||
|
|
|
@ -57,12 +57,9 @@ module.exports = class ThangAvatarView extends CocoView
|
|||
@$el.toggleClass 'selected', Boolean(selected)
|
||||
|
||||
onProblemsUpdated: (e) ->
|
||||
return unless @thang?.id of e.spell.thangs
|
||||
myProblems = []
|
||||
for thangID, spellThang of e.spell.thangs when thangID is @thang.id
|
||||
#aether = if e.isCast and spellThang.castAether then spellThang.castAether else spellThang.aether
|
||||
aether = spellThang.castAether # try only paying attention to the actually cast ones
|
||||
myProblems = myProblems.concat aether.getAllProblems() if aether
|
||||
return unless @thang?.id is e.spell.thang?.thang.id
|
||||
aether = e.spell.thang.castAether
|
||||
myProblems = aether?.getAllProblems() ? []
|
||||
worstLevel = null
|
||||
for level in ['error', 'warning', 'info'] when _.some myProblems, {level: level}
|
||||
worstLevel = level
|
||||
|
|
65
app/views/play/level/WebSurfaceView.coffee
Normal file
65
app/views/play/level/WebSurfaceView.coffee
Normal file
|
@ -0,0 +1,65 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
State = require 'models/State'
|
||||
template = require 'templates/play/level/web-surface-view'
|
||||
|
||||
module.exports = class WebSurfaceView extends CocoView
|
||||
id: 'web-surface-view'
|
||||
template: template
|
||||
|
||||
subscriptions:
|
||||
'tome:html-updated': 'onHTMLUpdated'
|
||||
|
||||
initialize: (options) ->
|
||||
@state = new State
|
||||
blah: 'blah'
|
||||
@goals = (goal for goal in options.goalManager.goals when goal.html)
|
||||
# Consider https://www.npmjs.com/package/css-select to do this on virtualDOM instead of in iframe on concreteDOM
|
||||
super(options)
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@iframe = @$('iframe')[0]
|
||||
$(@iframe).on 'load', (e) =>
|
||||
window.addEventListener 'message', @onIframeMessage
|
||||
#@iframe.contentWindow.postMessage {type: 'log', text: 'Player HTML iframe is ready.'}, "*"
|
||||
@iframeLoaded = true
|
||||
@onIframeLoaded?()
|
||||
@onIframeLoaded = null
|
||||
|
||||
# TODO: make clicking Run actually trigger a 'create' update here (for resetting scripts)
|
||||
|
||||
onHTMLUpdated: (e) ->
|
||||
unless @iframeLoaded
|
||||
return @onIframeLoaded = => @onHTMLUpdated e unless @destroyed
|
||||
dom = htmlparser2.parseDOM e.html, {}
|
||||
body = _.find(dom, name: 'body') ? {name: 'body', attribs: null, children: dom}
|
||||
html = _.find(dom, name: 'html') ? {name: 'html', attribs: null, children: [body]}
|
||||
# TODO: pull out the actual scripts, styles, and body/elements they are doing so we can merge them with our initial structure on the other side
|
||||
virtualDOM = @dekuify html
|
||||
messageType = if e.create or not @virtualDOM then 'create' else 'update'
|
||||
@iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM, goals: @goals}, '*'
|
||||
@virtualDOM = virtualDOM
|
||||
|
||||
dekuify: (elem) ->
|
||||
return elem.data if elem.type is 'text'
|
||||
return null if elem.type is 'comment' # TODO: figure out how to make a comment in virtual dom
|
||||
unless elem.name
|
||||
console.log("Failed to dekuify", elem)
|
||||
return elem.type
|
||||
deku.element(elem.name, elem.attribs, (@dekuify(c) for c in elem.children ? []))
|
||||
|
||||
onIframeMessage: (e) =>
|
||||
origin = e.origin or e.originalEvent.origin
|
||||
unless origin is window.location.origin
|
||||
return console.log 'Ignoring message from bad origin:', origin
|
||||
unless event.source is @iframe.contentWindow
|
||||
return console.log 'Ignoring message from somewhere other than our iframe:', event.source
|
||||
switch event.data.type
|
||||
when 'goals-updated'
|
||||
Backbone.Mediator.publish 'god:new-html-goal-states', goalStates: event.data.goalStates, overallStatus: event.data.overallStatus
|
||||
else
|
||||
console.warn 'Unknown message type', event.data.type, 'for message', e, 'from origin', origin
|
||||
|
||||
destroy: ->
|
||||
window.removeEventListener 'message', @onIframeMessage
|
||||
super()
|
|
@ -49,7 +49,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
@session = options.session
|
||||
@level = options.level
|
||||
@thangTypes = {}
|
||||
if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder', 'game-dev']
|
||||
if @level.isType('hero', 'hero-ladder', 'course', 'course-ladder', 'game-dev', 'web-dev')
|
||||
achievements = new CocoCollection([], {
|
||||
url: "/db/achievement?related=#{@session.get('level').original}"
|
||||
model: Achievement
|
||||
|
@ -63,17 +63,17 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
else
|
||||
@readyToContinue = true
|
||||
@playSound 'victory'
|
||||
if @level.get('type', true) is 'course'
|
||||
if @level.isType('course', 'game-dev', 'web-dev')
|
||||
if nextLevel = @level.get('nextLevel')
|
||||
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
|
||||
@nextLevel = @supermodel.loadModel(@nextLevel).model
|
||||
if @courseID
|
||||
@course = new Course().setURL "/db/course/#{@courseID}"
|
||||
@course = @supermodel.loadModel(@course).model
|
||||
if @level.get('type', true) in ['course', 'course-ladder']
|
||||
if @level.isType('course', 'course-ladder')
|
||||
@saveReviewEventually = _.debounce(@saveReviewEventually, 2000)
|
||||
@loadExistingFeedback()
|
||||
# TODO: support game-dev
|
||||
# TODO: support 'game-dev' and 'web-dev' (not the same as 'course' since can be played outside of courses)
|
||||
|
||||
destroy: ->
|
||||
clearInterval @sequentialAnimationInterval
|
||||
|
@ -154,8 +154,8 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
getRenderData: ->
|
||||
c = super()
|
||||
c.levelName = utils.i18n @level.attributes, 'name'
|
||||
# TODO: support 'game-dev'
|
||||
if @level.get('type', true) not in ['hero', 'game-dev']
|
||||
# TODO: support 'game-dev', 'web-dev'
|
||||
if @level.isType('hero', 'game-dev', 'web-dev')
|
||||
c.victoryText = utils.i18n @level.get('victory') ? {}, 'body'
|
||||
earnedAchievementMap = _.indexBy(@newEarnedAchievements or [], (ea) -> ea.get('achievement'))
|
||||
for achievement in (@achievements?.models or [])
|
||||
|
@ -192,7 +192,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
|
||||
c.thangTypes = @thangTypes
|
||||
c.me = me
|
||||
c.readyToRank = @level.get('type', true) in ['hero-ladder', 'course-ladder'] and @session.readyToRank()
|
||||
c.readyToRank = @level.isType('hero-ladder', 'course-ladder') and @session.readyToRank()
|
||||
c.level = @level
|
||||
c.i18n = utils.i18n
|
||||
|
||||
|
@ -211,10 +211,10 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
# Show the "I'm done" button between 30 - 120 minutes if they definitely came from Hour of Code
|
||||
c.showHourOfCodeDoneButton = showDone
|
||||
|
||||
c.showLeaderboard = @level.get('scoreTypes')?.length > 0 and @level.get('type', true) isnt 'course'
|
||||
c.showLeaderboard = @level.get('scoreTypes')?.length > 0 and not @level.isType('course')
|
||||
|
||||
c.showReturnToCourse = not c.showLeaderboard and not me.get('anonymous') and @level.get('type', true) in ['course', 'course-ladder']
|
||||
c.isCourseLevel = @level.get('type', true) in ['course']
|
||||
c.showReturnToCourse = not c.showLeaderboard and not me.get('anonymous') and @level.isType('course', 'course-ladder')
|
||||
c.isCourseLevel = @level.isType('course')
|
||||
c.currentCourseName = @course?.get('name')
|
||||
c.currentLevelName = @level?.get('name')
|
||||
c.nextLevelName = @nextLevel?.get('name')
|
||||
|
@ -223,17 +223,17 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
|
||||
afterRender: ->
|
||||
super()
|
||||
@$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
|
||||
@$el.toggleClass 'with-achievements', @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev') # TODO: support game-dev, web-dev
|
||||
return unless @supermodel.finished()
|
||||
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them
|
||||
@updateSavingProgressStatus()
|
||||
@initializeAnimations()
|
||||
if @level.get('type', true) in ['hero-ladder', 'course-ladder']
|
||||
if @level.isType('hero-ladder', 'course-ladder')
|
||||
@ladderSubmissionView = new LadderSubmissionView session: @session, level: @level
|
||||
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
|
||||
|
||||
initializeAnimations: ->
|
||||
return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
|
||||
return @endSequentialAnimations() unless @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev') # TODO: support game-dev, web-dev
|
||||
@updateXPBars 0
|
||||
#playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this
|
||||
@$el.find('#victory-header').delay(250).queue(->
|
||||
|
@ -264,7 +264,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
|
||||
beginSequentialAnimations: ->
|
||||
return if @destroyed
|
||||
return unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev
|
||||
return unless @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev') # TODO: support game-dev, web-dev
|
||||
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
|
||||
number: $(panel).data('number')
|
||||
previousNumber: $(panel).data('previous-number')
|
||||
|
@ -379,7 +379,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
clearInterval @sequentialAnimationInterval
|
||||
@animationComplete = true
|
||||
@updateSavingProgressStatus()
|
||||
Backbone.Mediator.publish 'music-player:enter-menu', terrain: @level.get('terrain', true)
|
||||
Backbone.Mediator.publish 'music-player:enter-menu', terrain: @level.get('terrain', true) or 'forest'
|
||||
|
||||
updateSavingProgressStatus: ->
|
||||
@$el.find('#saving-progress-label').toggleClass('hide', @readyToContinue)
|
||||
|
@ -394,7 +394,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
viewArgs = [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @level.get('slug')]
|
||||
ladderURL = "/play/ladder/#{@level.get('slug') || @level.id}"
|
||||
if leagueID = (@courseInstanceID or @getQueryVariable 'league')
|
||||
leagueType = if @level.get('type') is 'course-ladder' then 'course' else 'clan'
|
||||
leagueType = if @level.isType('course-ladder') then 'course' else 'clan'
|
||||
viewArgs.push leagueType
|
||||
viewArgs.push leagueID
|
||||
ladderURL += "/#{leagueType}/#{leagueID}"
|
||||
|
@ -414,14 +414,14 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
{'kithgard-gates': 'forest', 'kithgard-mastery': 'forest', 'siege-of-stonehold': 'desert', 'clash-of-clones': 'mountain', 'summits-gate': 'glacier'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out.
|
||||
|
||||
getNextLevelLink: (returnToCourse=false) ->
|
||||
if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel') and not returnToCourse
|
||||
if @level.isType('course', 'game-dev', 'web-dev') and nextLevel = @level.get('nextLevel') and not returnToCourse # TODO: support game-dev and web-dev
|
||||
# need to do something more complicated to load its slug
|
||||
console.log 'have @nextLevel', @nextLevel, 'from nextLevel', nextLevel
|
||||
link = "/play/level/#{@nextLevel.get('slug')}"
|
||||
if @courseID
|
||||
link += "?course=#{@courseID}"
|
||||
link += "&course-instance=#{@courseInstanceID}" if @courseInstanceID
|
||||
else if @level.get('type', true) is 'course'
|
||||
else if @level.isType('course')
|
||||
link = "/courses"
|
||||
if @courseID
|
||||
link += "/#{@courseID}"
|
||||
|
@ -440,12 +440,12 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
justBeatLevel: @level
|
||||
supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel
|
||||
_.merge options, extraOptions if extraOptions
|
||||
if @level.get('type', true) is 'course' and @nextLevel and not options.returnToCourse
|
||||
if @level.isType('course') and @nextLevel and not options.returnToCourse
|
||||
viewClass = require 'views/play/level/PlayLevelView'
|
||||
options.courseID = @courseID
|
||||
options.courseInstanceID = @courseInstanceID
|
||||
viewArgs = [options, @nextLevel.get('slug')]
|
||||
else if @level.get('type', true) is 'course'
|
||||
else if @level.isType('course')
|
||||
# TODO: shouldn't set viewClass and route in different places
|
||||
viewClass = require 'views/courses/CoursesView'
|
||||
viewArgs = [options]
|
||||
|
@ -453,7 +453,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
viewClass = require 'views/courses/CourseDetailsView'
|
||||
viewArgs.push @courseID
|
||||
viewArgs.push @courseInstanceID if @courseInstanceID
|
||||
else if @level.get('type', true) is 'course-ladder'
|
||||
else if @level.isType('course-ladder')
|
||||
leagueID = @courseInstanceID or @getQueryVariable 'league'
|
||||
nextLevelLink = "/play/ladder/#{@level.get('slug')}"
|
||||
nextLevelLink += "/course/#{leagueID}" if leagueID
|
||||
|
|
|
@ -71,7 +71,7 @@ module.exports = class VictoryModal extends ModalView
|
|||
c.me = me
|
||||
c.levelName = utils.i18n @level.attributes, 'name'
|
||||
c.level = @level
|
||||
if c.level.get('type') is 'ladder'
|
||||
if c.level.isType('ladder')
|
||||
c.readyToRank = @session.readyToRank()
|
||||
c
|
||||
|
||||
|
|
|
@ -18,9 +18,6 @@ module.exports = class CastButtonView extends CocoView
|
|||
'tome:cast-spells': 'onCastSpells'
|
||||
'tome:manual-cast-denied': 'onManualCastDenied'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'real-time-multiplayer:created-game': 'onJoinedRealTimeMultiplayerGame'
|
||||
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame'
|
||||
'real-time-multiplayer:left-game': 'onLeftRealTimeMultiplayerGame'
|
||||
'goal-manager:new-goal-states': 'onNewGoalStates'
|
||||
'god:goals-calculated': 'onGoalsCalculated'
|
||||
'playback:ended-changed': 'onPlaybackEndedChanged'
|
||||
|
@ -43,9 +40,9 @@ module.exports = class CastButtonView extends CocoView
|
|||
super()
|
||||
@castButton = $('.cast-button', @$el)
|
||||
spell.view?.createOnCodeChangeHandlers() for spellKey, spell of @spells
|
||||
if @options.level.get('hidesSubmitUntilRun') or @options.level.get 'hidesRealTimePlayback'
|
||||
if @options.level.get('hidesSubmitUntilRun') or @options.level.get('hidesRealTimePlayback') or @options.level.isType('web-dev')
|
||||
@$el.find('.submit-button').hide() # Hide Submit for the first few until they run it once.
|
||||
if @options.session.get('state')?.complete and @options.level.get 'hidesRealTimePlayback'
|
||||
if @options.session.get('state')?.complete and (@options.level.get('hidesRealTimePlayback') or @options.level.isType('web-dev'))
|
||||
@$el.find('.done-button').show()
|
||||
if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm']
|
||||
@$el.find('.submit-button').hide() # Hide submit until first win so that script can explain it.
|
||||
|
@ -71,9 +68,7 @@ module.exports = class CastButtonView extends CocoView
|
|||
Backbone.Mediator.publish 'tome:manual-cast', {}
|
||||
|
||||
onCastRealTimeButtonClick: (e) ->
|
||||
if @inRealTimeMultiplayerSession
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:manual-cast', {}
|
||||
else if @options.level.get('replayable') and (timeUntilResubmit = @options.session.timeUntilResubmit()) > 0
|
||||
if @options.level.get('replayable') and (timeUntilResubmit = @options.session.timeUntilResubmit()) > 0
|
||||
Backbone.Mediator.publish 'tome:manual-cast-denied', timeUntilResubmit: timeUntilResubmit
|
||||
else
|
||||
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
|
||||
|
@ -81,7 +76,7 @@ module.exports = class CastButtonView extends CocoView
|
|||
|
||||
onDoneButtonClick: (e) ->
|
||||
return if @options.level.hasLocalChanges() # Don't award achievements when beating level changed in level editor
|
||||
@options.session.recordScores @world.scores, @options.level
|
||||
@options.session.recordScores @world?.scores, @options.level
|
||||
Backbone.Mediator.publish 'level:show-victory', showModal: true
|
||||
|
||||
onSpellChanged: (e) ->
|
||||
|
@ -118,7 +113,7 @@ module.exports = class CastButtonView extends CocoView
|
|||
@winnable = winnable
|
||||
@$el.toggleClass 'winnable', @winnable
|
||||
Backbone.Mediator.publish 'tome:winnability-updated', winnable: @winnable, level: @options.level
|
||||
if @options.level.get 'hidesRealTimePlayback'
|
||||
if @options.level.get('hidesRealTimePlayback') or @options.level.isType('web-dev')
|
||||
@$el.find('.done-button').toggle @winnable
|
||||
else if @winnable and @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm']
|
||||
@$el.find('.submit-button').show() # Hide submit until first win so that script can explain it.
|
||||
|
@ -178,9 +173,3 @@ module.exports = class CastButtonView extends CocoView
|
|||
return unless placeholder.length
|
||||
@ladderSubmissionView = new LadderSubmissionView session: @options.session, level: @options.level, mirrorSession: @mirrorSession
|
||||
@insertSubView @ladderSubmissionView, placeholder
|
||||
|
||||
onJoinedRealTimeMultiplayerGame: (e) ->
|
||||
@inRealTimeMultiplayerSession = true
|
||||
|
||||
onLeftRealTimeMultiplayerGame: (e) ->
|
||||
@inRealTimeMultiplayerSession = false
|
||||
|
|
|
@ -42,12 +42,15 @@ module.exports = class DocFormatter
|
|||
@fillOutDoc()
|
||||
|
||||
fillOutDoc: ->
|
||||
# TODO: figure out how to do html docs for web-dev levels
|
||||
if _.isString @doc
|
||||
@doc = name: @doc, type: typeof @options.thang[@doc]
|
||||
if @options.isSnippet
|
||||
@doc.type = 'snippet'
|
||||
@doc.owner = 'snippets'
|
||||
@doc.shortName = @doc.shorterName = @doc.title = @doc.name
|
||||
else if @doc.owner is 'HTML'
|
||||
@doc.shortName = @doc.shorterName = @doc.title = @doc.name
|
||||
else
|
||||
@doc.owner ?= 'this'
|
||||
ownerName = @doc.ownerName = if @doc.owner isnt 'this' then @doc.owner else switch @options.language
|
||||
|
@ -139,7 +142,7 @@ module.exports = class DocFormatter
|
|||
if @doc.args
|
||||
arg.example = arg.example.replace thisToken[@options.language], 'hero' for arg in @doc.args when arg.example
|
||||
|
||||
if @doc.shortName is 'loop' and @options.level.get('type', true) in ['course', 'course-ladder']
|
||||
if @doc.shortName is 'loop' and @options.level.isType('course', 'course-ladder')
|
||||
@replaceSimpleLoops()
|
||||
|
||||
replaceSimpleLoops: ->
|
||||
|
@ -185,6 +188,7 @@ module.exports = class DocFormatter
|
|||
[docName, args]
|
||||
|
||||
formatValue: (v) ->
|
||||
return null if @options.level.isType('web-dev')
|
||||
return null if @doc.type is 'snippet'
|
||||
return @options.thang.now() if @doc.name is 'now'
|
||||
return '[Function]' if not v and @doc.type is 'function'
|
||||
|
|
|
@ -14,7 +14,6 @@ module.exports = class ProblemAlertView extends CocoView
|
|||
'level:restart': 'onHideProblemAlert'
|
||||
'tome:jiggle-problem-alert': 'onJiggleProblemAlert'
|
||||
'tome:manual-cast': 'onHideProblemAlert'
|
||||
'real-time-multiplayer:manual-cast': 'onHideProblemAlert'
|
||||
|
||||
events:
|
||||
'click .close': 'onRemoveClicked'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
SpellView = require './SpellView'
|
||||
SpellListTabEntryView = require './SpellListTabEntryView'
|
||||
SpellTopBarView = require './SpellTopBarView'
|
||||
{me} = require 'core/auth'
|
||||
{createAetherOptions} = require 'lib/aether_utils'
|
||||
utils = require 'core/utils'
|
||||
|
@ -7,7 +7,7 @@ utils = require 'core/utils'
|
|||
module.exports = class Spell
|
||||
loaded: false
|
||||
view: null
|
||||
entryView: null
|
||||
topBarView: null
|
||||
|
||||
constructor: (options) ->
|
||||
@spellKey = options.spellKey
|
||||
|
@ -20,8 +20,6 @@ module.exports = class Spell
|
|||
@supermodel = options.supermodel
|
||||
@skipProtectAPI = options.skipProtectAPI
|
||||
@worker = options.worker
|
||||
@levelID = options.levelID
|
||||
@levelType = options.level.get('type', true)
|
||||
@level = options.level
|
||||
|
||||
p = options.programmableMethod
|
||||
|
@ -47,30 +45,38 @@ module.exports = class Spell
|
|||
if p.aiSource and not @otherSession and not @canWrite()
|
||||
@source = @originalSource = p.aiSource
|
||||
@isAISource = true
|
||||
@thangs = {}
|
||||
if @canRead() # We can avoid creating these views if we'll never use them.
|
||||
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel}
|
||||
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel, levelID: options.levelID}
|
||||
@view.render() # Get it ready and code loaded in advance
|
||||
@tabView = new SpellListTabEntryView
|
||||
@topBarView = new SpellTopBarView
|
||||
hintsState: options.hintsState
|
||||
spell: @
|
||||
supermodel: @supermodel
|
||||
codeLanguage: @language
|
||||
level: options.level
|
||||
@tabView.render()
|
||||
@topBarView.render()
|
||||
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
||||
|
||||
destroy: ->
|
||||
@view?.destroy()
|
||||
@tabView?.destroy()
|
||||
@thangs = null
|
||||
@topBarView?.destroy()
|
||||
@thang = null
|
||||
@worker = null
|
||||
|
||||
setLanguage: (@language) ->
|
||||
@language = 'html' if @level.isType('web-dev')
|
||||
#console.log 'setting language to', @language, 'so using original source', @languages[language] ? @languages.javascript
|
||||
@originalSource = @languages[@language] ? @languages.javascript
|
||||
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
|
||||
|
||||
if @level.isType('web-dev')
|
||||
playerCode = @originalSource.match(/<playercode>\n([\s\S]*)\n *<\/playercode>/)[1]
|
||||
playerCodeLines = playerCode.split('\n')
|
||||
indentation = playerCodeLines[0].length - playerCodeLines[0].trim().length
|
||||
playerCode = (line.substr(indentation) for line in playerCodeLines).join('\n')
|
||||
@wrapperCode = @originalSource.replace /<playercode>[\s\S]*<\/playercode>/, '☃'
|
||||
@originalSource = playerCode
|
||||
|
||||
# Translate comments chosen spoken language.
|
||||
return unless @commentContext
|
||||
context = $.extend true, {}, @commentContext
|
||||
|
@ -87,7 +93,7 @@ module.exports = class Spell
|
|||
catch e
|
||||
console.error "Couldn't create example code template of", @originalSource, "\nwith context", context, "\nError:", e
|
||||
|
||||
if /loop/.test(@originalSource) and @levelType in ['course', 'course-ladder']
|
||||
if /loop/.test(@originalSource) and @level.isType('course', 'course-ladder')
|
||||
# Temporary hackery to make it look like we meant while True: in our sample code until we can update everything
|
||||
@originalSource = switch @language
|
||||
when 'python' then @originalSource.replace /loop:/, 'while True:'
|
||||
|
@ -96,6 +102,9 @@ module.exports = class Spell
|
|||
when 'coffeescript' then @originalSource
|
||||
else @originalSource
|
||||
|
||||
constructHTML: (source) ->
|
||||
@wrapperCode.replace '☃', source
|
||||
|
||||
addPicoCTFProblem: ->
|
||||
return @originalSource unless problem = @level.picoCTFProblem
|
||||
description = """
|
||||
|
@ -105,13 +114,13 @@ module.exports = class Spell
|
|||
("// #{line}" for line in description.split('\n')).join('\n') + '\n' + @originalSource
|
||||
|
||||
addThang: (thang) ->
|
||||
if @thangs[thang.id]
|
||||
@thangs[thang.id].thang = thang
|
||||
if @thang?.thang.id is thang.id
|
||||
@thang.thang = thang
|
||||
else
|
||||
@thangs[thang.id] = {thang: thang, aether: @createAether(thang), castAether: null}
|
||||
@thang = {thang: thang, aether: @createAether(thang), castAether: null}
|
||||
|
||||
removeThangID: (thangID) ->
|
||||
delete @thangs[thangID]
|
||||
@thang = null if @thang?.thang.id is thangID
|
||||
|
||||
canRead: (team) ->
|
||||
(team ? me.team) in @permissions.read or (team ? me.team) in @permissions.readwrite
|
||||
|
@ -127,28 +136,16 @@ module.exports = class Spell
|
|||
@source = source
|
||||
else
|
||||
source = @getSource()
|
||||
[pure, problems] = [null, null]
|
||||
for thangID, spellThang of @thangs
|
||||
unless pure
|
||||
pure = spellThang.aether.transpile source
|
||||
problems = spellThang.aether.problems
|
||||
#console.log 'aether transpiled', source.length, 'to', spellThang.aether.pure.length, 'for', thangID, @spellKey
|
||||
else
|
||||
spellThang.aether.raw = source
|
||||
spellThang.aether.pure = pure
|
||||
spellThang.aether.problems = problems
|
||||
#console.log 'aether reused transpilation for', thangID, @spellKey
|
||||
unless @language is 'html'
|
||||
@thang?.aether.transpile source
|
||||
null
|
||||
|
||||
hasChanged: (newSource=null, currentSource=null) ->
|
||||
(newSource ? @originalSource) isnt (currentSource ? @source)
|
||||
|
||||
hasChangedSignificantly: (newSource=null, currentSource=null, cb) ->
|
||||
for thangID, spellThang of @thangs
|
||||
aether = spellThang.aether
|
||||
break
|
||||
unless aether
|
||||
console.error @toString(), 'couldn\'t find a spellThang with aether of', @thangs
|
||||
unless aether = @thang?.aether
|
||||
console.error @toString(), 'couldn\'t find a spellThang with aether', @thang
|
||||
cb false
|
||||
if @worker
|
||||
workerMessage =
|
||||
|
@ -169,9 +166,9 @@ module.exports = class Spell
|
|||
|
||||
createAether: (thang) ->
|
||||
writable = @permissions.readwrite.length > 0 and not @isAISource
|
||||
skipProtectAPI = @skipProtectAPI or not writable or @levelType in ['game-dev']
|
||||
skipProtectAPI = @skipProtectAPI or not writable or @level.isType('game-dev')
|
||||
problemContext = @createProblemContext thang
|
||||
includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) and not skipProtectAPI
|
||||
includeFlow = @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') and not skipProtectAPI
|
||||
aetherOptions = createAetherOptions
|
||||
functionName: @name
|
||||
codeLanguage: @language
|
||||
|
@ -190,10 +187,9 @@ module.exports = class Spell
|
|||
aether
|
||||
|
||||
updateLanguageAether: (@language) ->
|
||||
for thangId, spellThang of @thangs
|
||||
spellThang.aether?.setLanguage @language
|
||||
spellThang.castAether = null
|
||||
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @, language: @language
|
||||
@thang?.aether?.setLanguage @language
|
||||
@thang?.castAether = null
|
||||
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @, language: @language
|
||||
if @worker
|
||||
workerMessage =
|
||||
function: 'updateLanguageAether'
|
||||
|
|
|
@ -17,7 +17,6 @@ module.exports = class SpellDebugView extends CocoView
|
|||
'god:new-world-created': 'onNewWorld'
|
||||
'god:debug-value-return': 'handleDebugValue'
|
||||
'god:debug-world-load-progress-changed': 'handleWorldLoadProgressChanged'
|
||||
'tome:spell-shown': 'changeCurrentThangAndSpell'
|
||||
'tome:cast-spells': 'onTomeCast'
|
||||
'surface:frame-changed': 'onFrameChanged'
|
||||
'tome:spell-has-changed-significantly-calculation': 'onSpellChangedCalculation'
|
||||
|
@ -98,10 +97,6 @@ module.exports = class SpellDebugView extends CocoView
|
|||
currentObject = currentObject[key]
|
||||
currentObject[keys[keys.length - 1]] = value
|
||||
|
||||
changeCurrentThangAndSpell: (thangAndSpellObject) ->
|
||||
@thang = thangAndSpellObject.thang
|
||||
@spell = thangAndSpellObject.spell
|
||||
|
||||
handleDebugValue: (e) ->
|
||||
{key, value} = e
|
||||
@workerIsSimulating = false
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
ThangAvatarView = require 'views/play/level/ThangAvatarView'
|
||||
template = require 'templates/play/level/tome/spell_list_entry_thangs'
|
||||
|
||||
module.exports = class SpellListEntryThangsView extends CocoView
|
||||
className: 'spell-list-entry-thangs-view'
|
||||
template: template
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@thangs = options.thangs
|
||||
@thang = options.thang
|
||||
@spell = options.spell
|
||||
@avatars = []
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
avatar.destroy() for avatar in @avatars if @avatars
|
||||
@avatars = []
|
||||
spellName = @spell.name
|
||||
for thang in @thangs
|
||||
avatar = new ThangAvatarView thang: thang, includeName: true, supermodel: @supermodel, creator: @
|
||||
@$el.append avatar.el
|
||||
avatar.render()
|
||||
avatar.setSelected thang is @thang
|
||||
avatar.$el.data('thang-id', thang.id).click (e) ->
|
||||
Backbone.Mediator.publish 'level:select-sprite', thangID: $(@).data('thang-id'), spellName: spellName
|
||||
avatar.onProblemsUpdated spell: @spell
|
||||
@avatars.push avatar
|
||||
|
||||
destroy: ->
|
||||
avatar.destroy() for avatar in @avatars
|
||||
super()
|
|
@ -1,115 +0,0 @@
|
|||
# TODO: This still needs a way to send problem states to its Thang
|
||||
|
||||
CocoView = require 'views/core/CocoView'
|
||||
ThangAvatarView = require 'views/play/level/ThangAvatarView'
|
||||
SpellListEntryThangsView = require 'views/play/level/tome/SpellListEntryThangsView'
|
||||
template = require 'templates/play/level/tome/spell_list_entry'
|
||||
|
||||
module.exports = class SpellListEntryView extends CocoView
|
||||
tagName: 'div' #'li'
|
||||
className: 'spell-list-entry-view'
|
||||
template: template
|
||||
controlsEnabled: true
|
||||
|
||||
subscriptions:
|
||||
'tome:problems-updated': 'onProblemsUpdated'
|
||||
'tome:spell-changed-language': 'onSpellChangedLanguage'
|
||||
'level:disable-controls': 'onDisableControls'
|
||||
'level:enable-controls': 'onEnableControls'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
|
||||
events:
|
||||
'click': 'onClick'
|
||||
'mouseenter .thang-avatar-view': 'onMouseEnterAvatar'
|
||||
'mouseleave .thang-avatar-view': 'onMouseLeaveAvatar'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@spell = options.spell
|
||||
@showTopDivider = options.showTopDivider
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super context
|
||||
context.spell = @spell
|
||||
context.thangNames = (thangID for thangID, spellThang of @spell.thangs when spellThang.thang.exists).join(', ') # + ', Marcus, Robert, Phoebe, Will Smith, Zap Brannigan, You, Gandaaaaalf'
|
||||
context.showTopDivider = @showTopDivider
|
||||
context
|
||||
|
||||
getPrimarySpellThang: ->
|
||||
if @lastSelectedThang
|
||||
spellThang = _.find @spell.thangs, (spellThang) => spellThang.thang.id is @lastSelectedThang.id
|
||||
return spellThang if spellThang
|
||||
for thangID, spellThang of @spell.thangs
|
||||
continue unless spellThang.thang.exists
|
||||
return spellThang # Just do the first one else
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
return unless @options.showTopDivider # Don't repeat Thang avatars when not changed from previous entry
|
||||
return @$el.hide() unless spellThang = @getPrimarySpellThang()
|
||||
@$el.show()
|
||||
@avatar?.destroy()
|
||||
@avatar = new ThangAvatarView thang: spellThang.thang, includeName: false, supermodel: @supermodel
|
||||
@$el.prepend @avatar.el # Before rendering, so render can use parent for popover
|
||||
@avatar.render()
|
||||
@avatar.setSharedThangs _.size @spell.thangs
|
||||
@$el.addClass 'shows-top-divider' if @options.showTopDivider
|
||||
|
||||
setSelected: (selected, @lastSelectedThang) ->
|
||||
@avatar?.setSelected selected
|
||||
|
||||
onClick: (e) ->
|
||||
spellThang = @getPrimarySpellThang()
|
||||
Backbone.Mediator.publish 'level:select-sprite', thangID: spellThang.thang.id, spellName: @spell.name
|
||||
|
||||
onMouseEnterAvatar: (e) ->
|
||||
return unless @controlsEnabled and _.size(@spell.thangs) > 1
|
||||
@showThangs()
|
||||
|
||||
onMouseLeaveAvatar: (e) ->
|
||||
return unless @controlsEnabled and _.size(@spell.thangs) > 1
|
||||
@hideThangsTimeout = _.delay @hideThangs, 100
|
||||
|
||||
showThangs: ->
|
||||
clearTimeout @hideThangsTimeout if @hideThangsTimeout
|
||||
return if @thangsView
|
||||
spellThang = @getPrimarySpellThang()
|
||||
return unless spellThang
|
||||
@thangsView = new SpellListEntryThangsView thangs: (spellThang.thang for thangID, spellThang of @spell.thangs), thang: spellThang.thang, spell: @spell, supermodel: @supermodel
|
||||
@thangsView.render()
|
||||
@$el.append @thangsView.el
|
||||
@thangsView.$el.mouseenter (e) => @onMouseEnterAvatar()
|
||||
@thangsView.$el.mouseleave (e) => @onMouseLeaveAvatar()
|
||||
|
||||
hideThangs: =>
|
||||
return unless @thangsView
|
||||
@thangsView.off 'mouseenter mouseleave'
|
||||
@thangsView.$el.remove()
|
||||
@thangsView.destroy()
|
||||
@thangsView = null
|
||||
|
||||
onProblemsUpdated: (e) ->
|
||||
return unless e.spell is @spell
|
||||
@$el.toggleClass 'user-code-problem', e.problems.length
|
||||
|
||||
onSpellChangedLanguage: (e) ->
|
||||
return unless e.spell is @spell
|
||||
@render() # So that we can update parameters if needed
|
||||
|
||||
onDisableControls: (e) -> @toggleControls e, false
|
||||
onEnableControls: (e) -> @toggleControls e, true
|
||||
toggleControls: (e, enabled) ->
|
||||
return if e.controls and not ('editor' in e.controls)
|
||||
return if enabled is @controlsEnabled
|
||||
@controlsEnabled = enabled
|
||||
disabled = not enabled
|
||||
# Should refactor the disabling list so we can target the spell list separately?
|
||||
# Should not call it 'editor' any more?
|
||||
@$el.toggleClass('disabled', disabled).find('*').prop('disabled', disabled)
|
||||
|
||||
onNewWorld: (e) ->
|
||||
@lastSelectedThang = e.world.thangMap[@lastSelectedThang.id] if @lastSelectedThang
|
||||
|
||||
destroy: ->
|
||||
@avatar?.destroy()
|
||||
super()
|
|
@ -1,96 +0,0 @@
|
|||
# The SpellListView has SpellListEntryViews, which have ThangAvatarViews.
|
||||
# The SpellListView serves as a dropdown triggered from a SpellListTabEntryView, which actually isn't in a list, just had a lot of similar parts.
|
||||
# There is only one SpellListView, and it belongs to the TomeView.
|
||||
|
||||
# TODO: showTopDivider should change when we reorder
|
||||
|
||||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/play/level/tome/spell_list'
|
||||
{me} = require 'core/auth'
|
||||
SpellListEntryView = require './SpellListEntryView'
|
||||
|
||||
module.exports = class SpellListView extends CocoView
|
||||
className: 'spell-list-view'
|
||||
id: 'spell-list-view'
|
||||
template: template
|
||||
|
||||
subscriptions:
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@entries = []
|
||||
@sortSpells()
|
||||
|
||||
sortSpells: ->
|
||||
# Keep only spells for which we have permissions
|
||||
spells = _.filter @options.spells, (s) -> s.canRead()
|
||||
@spells = _.sortBy spells, @sortScoreForSpell
|
||||
#console.log 'Kept sorted spells', @spells
|
||||
|
||||
sortScoreForSpell: (s) =>
|
||||
# Sort by most spells per fewest Thangs
|
||||
# Lower comes first
|
||||
score = 0
|
||||
# Selected spell at the top
|
||||
score -= 9001900190019001 if s is @spell
|
||||
# Spells for selected thang at the top
|
||||
score -= 900190019001 if @thang and @thang.id of s.thangs
|
||||
# Read-only spells at the bottom
|
||||
score += 90019001 unless s.canWrite()
|
||||
# The more Thangs sharing a spell, the lower
|
||||
score += 9001 * _.size(s.thangs)
|
||||
# The more spells per Thang, the higher
|
||||
score -= _.filter(@spells, (s2) -> thangID of s2.thangs).length for thangID of s.thangs
|
||||
score
|
||||
|
||||
sortEntries: ->
|
||||
# Call sortSpells before this
|
||||
@entries = _.sortBy @entries, (entry) => _.indexOf @spells, entry.spell
|
||||
@$el.append entry.$el for entry in @entries
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@addSpellListEntries()
|
||||
|
||||
addSpellListEntries: ->
|
||||
newEntries = []
|
||||
lastThangs = null
|
||||
for spell, index in @spells
|
||||
continue if _.find @entries, spell: spell
|
||||
theseThangs = _.keys(spell.thangs)
|
||||
changedThangs = not lastThangs or not _.isEqual theseThangs, lastThangs
|
||||
lastThangs = theseThangs
|
||||
newEntries.push entry = new SpellListEntryView spell: spell, showTopDivider: changedThangs, supermodel: @supermodel, level: @options.level
|
||||
@entries.push entry
|
||||
for entry in newEntries
|
||||
@$el.append entry.el
|
||||
entry.render() # Render after appending so that we can access parent container for popover
|
||||
|
||||
rerenderEntries: ->
|
||||
entry.render() for entry in @entries
|
||||
|
||||
onNewWorld: (e) ->
|
||||
@thang = e.world.thangMap[@thang.id] if @thang
|
||||
|
||||
setThangAndSpell: (@thang, @spell) ->
|
||||
@entries[0]?.setSelected false
|
||||
@sortSpells()
|
||||
@sortEntries()
|
||||
@entries[0].setSelected true, @thang
|
||||
|
||||
addThang: (thang) ->
|
||||
@sortSpells()
|
||||
@addSpellListEntries()
|
||||
|
||||
adjustSpells: (spells) ->
|
||||
for entry in @entries when _.isEmpty entry.spell.thangs
|
||||
entry.$el.remove()
|
||||
entry.destroy()
|
||||
@spells = @options.spells = spells
|
||||
@sortSpells()
|
||||
@addSpellListEntries()
|
||||
|
||||
destroy: ->
|
||||
entry.destroy() for entry in @entries
|
||||
super()
|
|
@ -33,7 +33,7 @@ module.exports = class SpellPaletteEntryView extends CocoView
|
|||
|
||||
afterRender: ->
|
||||
super()
|
||||
@$el.addClass(@doc.type)
|
||||
@$el.addClass _.string.slugify @doc.type
|
||||
placement = -> if $('body').hasClass('dialogue-view-active') then 'top' else 'left'
|
||||
@$el.popover(
|
||||
animation: false
|
||||
|
@ -84,7 +84,7 @@ module.exports = class SpellPaletteEntryView extends CocoView
|
|||
Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned
|
||||
|
||||
onClick: (e) =>
|
||||
if true or @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||
if true or @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev')
|
||||
# Jiggle instead of pin for hero levels
|
||||
# Actually, do it all the time, because we recently busted the pin CSS. TODO: restore pinning
|
||||
jigglyPopover = $('.spell-palette-popover.popover')
|
||||
|
|
|
@ -87,6 +87,7 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
entry.$el.addClass 'first-entry'
|
||||
@$el.addClass 'hero'
|
||||
@$el.toggleClass 'shortenize', Boolean @shortenize
|
||||
@$el.toggleClass 'web-dev', @options.level.isType('web-dev')
|
||||
@updateMaxHeight() unless application.isIPadApp
|
||||
|
||||
afterInsert: ->
|
||||
|
@ -100,7 +101,9 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
return unless @isHero
|
||||
# We figure out how many columns we can fit, width-wise, and then guess how many rows will be needed.
|
||||
# We can then assign a height based on the number of rows, and the flex layout will do the rest.
|
||||
columnWidth = if @shortenize then 175 else 212
|
||||
columnWidth = 212
|
||||
columnWidth = 175 if @shortenize
|
||||
columnWidth = 100 if @options.level.isType('web-dev')
|
||||
nColumns = Math.floor @$el.find('.properties-this').innerWidth() / columnWidth # will always have 2 columns, since at 1024px screen we have 424px .properties
|
||||
columns = ({items: [], nEntries: 0} for i in [0 ... nColumns])
|
||||
orderedColumns = []
|
||||
|
@ -153,11 +156,12 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
JSON: 'programmableJSONProperties'
|
||||
LoDash: 'programmableLoDashProperties'
|
||||
Vector: 'programmableVectorProperties'
|
||||
HTML: 'programmableHTMLProperties'
|
||||
snippets: 'programmableSnippets'
|
||||
else
|
||||
propStorage =
|
||||
'this': ['apiProperties', 'apiMethods']
|
||||
if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) or not @options.programmable
|
||||
if not @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') or not @options.programmable
|
||||
@organizePalette propStorage, allDocs, excludedDocs
|
||||
else
|
||||
@organizePaletteHero propStorage, allDocs, excludedDocs
|
||||
|
@ -192,14 +196,14 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
return 'more' if entry.doc.owner is 'this' and entry.doc.name in (propGroups.more ? [])
|
||||
entry.doc.owner
|
||||
@entries = _.sortBy @entries, (entry) ->
|
||||
order = ['this', 'more', 'Math', 'Vector', 'String', 'Object', 'Array', 'Function', 'snippets']
|
||||
order = ['this', 'more', 'Math', 'Vector', 'String', 'Object', 'Array', 'Function', 'HTML', 'snippets']
|
||||
index = order.indexOf groupForEntry entry
|
||||
index = String.fromCharCode if index is -1 then order.length else index
|
||||
index += entry.doc.name
|
||||
if tabbify and _.find @entries, ((entry) -> entry.doc.owner isnt 'this')
|
||||
@entryGroups = _.groupBy @entries, groupForEntry
|
||||
else
|
||||
i18nKey = if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
|
||||
i18nKey = if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
|
||||
defaultGroup = $.i18n.t i18nKey
|
||||
@entryGroups = {}
|
||||
@entryGroups[defaultGroup] = @entries
|
||||
|
@ -243,7 +247,7 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
console.log @thang.id, "couldn't find item ThangType for", slot, thangTypeName
|
||||
|
||||
# Get any Math-, Vector-, etc.-owned properties into their own tabs
|
||||
for owner, storage of propStorage when not (owner in ['this', 'more', 'snippets'])
|
||||
for owner, storage of propStorage when not (owner in ['this', 'more', 'snippets', 'HTML'])
|
||||
continue unless @thang[storage]?.length
|
||||
@tabs ?= {}
|
||||
@tabs[owner] = []
|
||||
|
@ -257,7 +261,7 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
|
||||
# Assign any unassigned properties to the hero itself.
|
||||
for owner, storage of propStorage
|
||||
continue unless owner in ['this', 'more', 'snippets']
|
||||
continue unless owner in ['this', 'more', 'snippets', 'HTML']
|
||||
for prop in _.reject(@thang[storage] ? [], (prop) -> itemsByProp[prop] or prop[0] is '_') # no private properties
|
||||
continue if prop is 'say' and @options.level.get 'hidesSay' # Hide for Dungeon Campaign
|
||||
continue if prop is 'moveXY' and @options.level.get('slug') is 'slalom' # Hide for Slalom
|
||||
|
@ -282,7 +286,10 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
doc ?= prop
|
||||
if doc
|
||||
@entries.push @addEntry(doc, @shortenize, owner is 'snippets', item, propIndex > 0)
|
||||
@entryGroups = _.groupBy @entries, (entry) -> itemsByProp[entry.doc.name]?.get('name') ? 'Hero'
|
||||
if @options.level.isType('web-dev')
|
||||
@entryGroups = _.groupBy @entries, (entry) -> entry.doc.type
|
||||
else
|
||||
@entryGroups = _.groupBy @entries, (entry) -> itemsByProp[entry.doc.name]?.get('name') ? 'Hero'
|
||||
iOSEntryGroups = {}
|
||||
for group, entries of @entryGroups
|
||||
iOSEntryGroups[group] =
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
SpellListEntryView = require './SpellListEntryView'
|
||||
ThangAvatarView = require 'views/play/level/ThangAvatarView'
|
||||
template = require 'templates/play/level/tome/spell_list_tab_entry'
|
||||
LevelComponent = require 'models/LevelComponent'
|
||||
DocFormatter = require './DocFormatter'
|
||||
template = require 'templates/play/level/tome/spell-top-bar-view'
|
||||
ReloadLevelModal = require 'views/play/level/modal/ReloadLevelModal'
|
||||
CocoView = require 'views/core/CocoView'
|
||||
|
||||
module.exports = class SpellListTabEntryView extends SpellListEntryView
|
||||
module.exports = class SpellTopBarView extends CocoView
|
||||
template: template
|
||||
id: 'spell-list-tab-entry-view'
|
||||
id: 'spell-top-bar-view'
|
||||
controlsEnabled: true
|
||||
|
||||
subscriptions:
|
||||
'level:disable-controls': 'onDisableControls'
|
||||
'level:enable-controls': 'onEnableControls'
|
||||
'tome:spell-loaded': 'onSpellLoaded'
|
||||
'tome:spell-changed': 'onSpellChanged'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
'tome:spell-changed-language': 'onSpellChangedLanguage'
|
||||
'tome:toggle-maximize': 'onToggleMaximize'
|
||||
|
||||
events:
|
||||
'click .spell-list-button': 'onDropdownClick'
|
||||
'click .reload-code': 'onCodeReload'
|
||||
'click .beautify-code': 'onBeautifyClick'
|
||||
'click .fullscreen-code': 'onToggleMaximize'
|
||||
|
@ -27,6 +23,7 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
|
||||
constructor: (options) ->
|
||||
@hintsState = options.hintsState
|
||||
@spell = options.spell
|
||||
super(options)
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
|
@ -35,9 +32,7 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
shift = $.i18n.t 'keyboard_shortcuts.shift'
|
||||
context.beautifyShortcutVerbose = "#{ctrl}+#{shift}+B: #{$.i18n.t 'keyboard_shortcuts.beautify'}"
|
||||
context.maximizeShortcutVerbose = "#{ctrl}+#{shift}+M: #{$.i18n.t 'keyboard_shortcuts.maximize_editor'}"
|
||||
context.includeSpellList = @options.level.get('slug') in ['break-the-prison', 'zone-of-danger', 'k-means-cluster-wars', 'brawlwood', 'dungeon-arena', 'sky-span', 'minimax-tic-tac-toe']
|
||||
context.codeLanguage = @options.codeLanguage
|
||||
context.levelType = @options.level.get 'type', true
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
@ -45,51 +40,6 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
@$el.addClass 'spell-tab'
|
||||
@attachTransitionEventListener()
|
||||
|
||||
onNewWorld: (e) ->
|
||||
@thang = e.world.thangMap[@thang.id] if @thang
|
||||
|
||||
setThang: (thang) ->
|
||||
return if thang.id is @thang?.id
|
||||
@thang = thang
|
||||
@spellThang = @spell.thangs[@thang.id]
|
||||
@buildAvatar()
|
||||
@buildDocs() unless @docsBuilt
|
||||
|
||||
buildAvatar: ->
|
||||
avatar = new ThangAvatarView thang: @thang, includeName: false, supermodel: @supermodel
|
||||
if @avatar
|
||||
@avatar.$el.replaceWith avatar.$el
|
||||
@avatar.destroy()
|
||||
else
|
||||
@$el.find('.thang-avatar-placeholder').replaceWith avatar.$el
|
||||
@avatar = avatar
|
||||
@avatar.render()
|
||||
|
||||
buildDocs: ->
|
||||
return if @spell.name is 'plan' # Too confusing for beginners
|
||||
@docsBuilt = true
|
||||
lcs = @supermodel.getModels LevelComponent
|
||||
found = false
|
||||
for lc in lcs when not found
|
||||
for doc in lc.get('propertyDocumentation') ? []
|
||||
if doc.name is @spell.name
|
||||
found = true
|
||||
break
|
||||
return unless found
|
||||
docFormatter = new DocFormatter doc: doc, thang: @thang, language: @options.codeLanguage, selectedMethod: true
|
||||
@$el.find('.method-signature').popover(
|
||||
animation: true
|
||||
html: true
|
||||
placement: 'bottom'
|
||||
trigger: 'hover'
|
||||
content: docFormatter.formatPopover()
|
||||
container: @$el.parent()
|
||||
).on 'show.bs.popover', =>
|
||||
@playSound 'spell-tab-entry-open', 0.75
|
||||
|
||||
onMouseEnterAvatar: (e) -> # Don't call super
|
||||
onMouseLeaveAvatar: (e) -> # Don't call super
|
||||
onClick: (e) -> # Don't call super
|
||||
onDisableControls: (e) -> @toggleControls e, false
|
||||
onEnableControls: (e) -> @toggleControls e, true
|
||||
|
||||
|
@ -98,15 +48,8 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
@hintsState.set('hidden', not @hintsState.get('hidden'))
|
||||
window.tracker?.trackEvent 'Hints Clicked', category: 'Students', levelSlug: @options.level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, ['Mixpanel']
|
||||
|
||||
onDropdownClick: (e) ->
|
||||
return unless @controlsEnabled
|
||||
Backbone.Mediator.publish 'tome:toggle-spell-list', {}
|
||||
@playSound 'spell-list-open'
|
||||
|
||||
onCodeReload: (e) ->
|
||||
#return unless @controlsEnabled
|
||||
#Backbone.Mediator.publish 'tome:reload-code', spell: @spell # Old: just reload the current code
|
||||
@openModalView new ReloadLevelModal() # New: prompt them to restart the level
|
||||
@openModalView new ReloadLevelModal()
|
||||
|
||||
onBeautifyClick: (e) ->
|
||||
return unless @controlsEnabled
|
||||
|
@ -134,14 +77,10 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
onSpellChangedLanguage: (e) ->
|
||||
return unless e.spell is @spell
|
||||
@options.codeLanguage = e.language
|
||||
@$el.find('.method-signature').popover 'destroy'
|
||||
@render()
|
||||
@docsBuilt = false
|
||||
@buildDocs() if @thang
|
||||
@updateReloadButton()
|
||||
|
||||
toggleControls: (e, enabled) ->
|
||||
# Don't call super; do it differently
|
||||
return if e.controls and not ('editor' in e.controls)
|
||||
return if enabled is @controlsEnabled
|
||||
@controlsEnabled = enabled
|
||||
|
@ -163,8 +102,5 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
$codearea.on transitionListener, =>
|
||||
$codearea.css 'z-index', 2 unless $('html').hasClass 'fullscreen-editor'
|
||||
|
||||
|
||||
destroy: ->
|
||||
@avatar?.destroy()
|
||||
@$el.find('.method-signature').popover 'destroy'
|
||||
super()
|
|
@ -61,7 +61,6 @@ module.exports = class SpellView extends CocoView
|
|||
@supermodel = options.supermodel
|
||||
@worker = options.worker
|
||||
@session = options.session
|
||||
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
|
||||
@spell = options.spell
|
||||
@problems = []
|
||||
@savedProblems = {} # Cache saved user code problems to prevent duplicates
|
||||
|
@ -77,11 +76,7 @@ module.exports = class SpellView extends CocoView
|
|||
@fillACE()
|
||||
@createOnCodeChangeHandlers()
|
||||
@lockDefaultCode()
|
||||
if @session.get('multiplayer')
|
||||
@createFirepad()
|
||||
else
|
||||
# needs to happen after the code generating this view is complete
|
||||
_.defer @onAllLoaded
|
||||
_.defer @onAllLoaded # Needs to happen after the code generating this view is complete
|
||||
|
||||
createACE: ->
|
||||
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
|
||||
|
@ -231,7 +226,7 @@ module.exports = class SpellView extends CocoView
|
|||
disableSpaces = @options.level.get('disableSpaces') or false
|
||||
aceConfig = me.get('aceConfig') ? {}
|
||||
disableSpaces = false if aceConfig.keyBindings and aceConfig.keyBindings isnt 'default' # Not in vim/emacs mode
|
||||
disableSpaces = false if @spell.language in ['lua', 'java', 'coffeescript'] # Don't disable for more advanced/experimental languages
|
||||
disableSpaces = false if @spell.language in ['lua', 'java', 'coffeescript', 'html'] # Don't disable for more advanced/experimental languages
|
||||
if not disableSpaces or (_.isNumber(disableSpaces) and disableSpaces < me.level())
|
||||
return @ace.execCommand 'insertstring', ' '
|
||||
line = @aceDoc.getLine @ace.getCursorPosition().row
|
||||
|
@ -402,7 +397,6 @@ module.exports = class SpellView extends CocoView
|
|||
wrapper => orig.apply obj, args
|
||||
obj[method]
|
||||
|
||||
|
||||
finishRange = (row, startRow, startColumn) =>
|
||||
range = new Range startRow, startColumn, row, @aceSession.getLine(row).length - 1
|
||||
range.start = @aceDoc.createAnchor range.start
|
||||
|
@ -476,6 +470,7 @@ module.exports = class SpellView extends CocoView
|
|||
# TODO: Turn on more autocompletion based on level sophistication
|
||||
# TODO: E.g. using the language default snippets yields a bunch of crazy non-beginner suggestions
|
||||
# TODO: Options logic shouldn't exist both here and in updateAutocomplete()
|
||||
return if @spell.language is 'html'
|
||||
popupFontSizePx = @options.level.get('autocompleteFontSizePx') ? 16
|
||||
@zatanna = new Zatanna @ace,
|
||||
basic: false
|
||||
|
@ -502,8 +497,6 @@ module.exports = class SpellView extends CocoView
|
|||
return unless @zatanna and @autocomplete
|
||||
@zatanna.addCodeCombatSnippets @options.level, @, e
|
||||
|
||||
|
||||
|
||||
translateFindNearest: ->
|
||||
# If they have advanced glasses but are playing a level which assumes earlier glasses, we'll adjust the sample code to use the more advanced APIs instead.
|
||||
oldSource = @getSource()
|
||||
|
@ -514,14 +507,9 @@ module.exports = class SpellView extends CocoView
|
|||
@updateACEText newSource
|
||||
_.delay (=> @recompile?()), 1000
|
||||
|
||||
onMultiplayerChanged: ->
|
||||
if @session.get('multiplayer')
|
||||
@createFirepad()
|
||||
else
|
||||
@firepad?.dispose()
|
||||
|
||||
createFirepad: ->
|
||||
# load from firebase or the original source if there's nothing there
|
||||
# Currently not called; could be brought back for future multiplayer modes.
|
||||
# Load from firebase or the original source if there's nothing there.
|
||||
return if @firepadLoading
|
||||
@eventsSuppressed = true
|
||||
@loaded = false
|
||||
|
@ -532,19 +520,18 @@ module.exports = class SpellView extends CocoView
|
|||
@fireRef = new Firebase fireURL
|
||||
firepadOptions = userId: me.id
|
||||
@firepad = Firepad.fromACE @fireRef, @ace, firepadOptions
|
||||
@firepad.on 'ready', @onFirepadLoaded
|
||||
@firepadLoading = true
|
||||
|
||||
onFirepadLoaded: =>
|
||||
@firepadLoading = false
|
||||
firepadSource = @ace.getValue()
|
||||
if firepadSource
|
||||
@spell.source = firepadSource
|
||||
else
|
||||
@ace.setValue @previousSource
|
||||
@aceSession.setUndoManager(new UndoManager())
|
||||
@ace.clearSelection()
|
||||
@onAllLoaded()
|
||||
@firepad.on 'ready', =>
|
||||
return if @destroyed
|
||||
@firepadLoading = false
|
||||
firepadSource = @ace.getValue()
|
||||
if firepadSource
|
||||
@spell.source = firepadSource
|
||||
else
|
||||
@ace.setValue @previousSource
|
||||
@aceSession.setUndoManager(new UndoManager())
|
||||
@ace.clearSelection()
|
||||
@onAllLoaded()
|
||||
|
||||
onAllLoaded: =>
|
||||
@spell.transpile @spell.source
|
||||
|
@ -552,9 +539,10 @@ module.exports = class SpellView extends CocoView
|
|||
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell
|
||||
@eventsSuppressed = false # Now that the initial change is in, we can start running any changed code
|
||||
@createToolbarView()
|
||||
@updateHTML create: true if @options.level.isType('web-dev')
|
||||
|
||||
createDebugView: ->
|
||||
return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # We'll turn this on later, maybe, but not yet.
|
||||
return if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') # We'll turn this on later, maybe, but not yet.
|
||||
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
|
||||
@$el.append @debugView.render().$el.hide()
|
||||
|
||||
|
@ -573,7 +561,7 @@ module.exports = class SpellView extends CocoView
|
|||
@saveSpade()
|
||||
|
||||
getSource: ->
|
||||
@ace.getValue() # could also do @firepad.getText()
|
||||
@ace.getValue()
|
||||
|
||||
setThang: (thang) ->
|
||||
@focus()
|
||||
|
@ -581,7 +569,7 @@ module.exports = class SpellView extends CocoView
|
|||
@updateLines()
|
||||
return if thang.id is @thang?.id
|
||||
@thang = thang
|
||||
@spellThang = @spell.thangs[@thang.id]
|
||||
@spellThang = @spell.thang
|
||||
@createDebugView() unless @debugView
|
||||
@debugView?.thang = @thang
|
||||
@createTranslationView() unless @translationView
|
||||
|
@ -624,11 +612,11 @@ module.exports = class SpellView extends CocoView
|
|||
lineHeight = @ace.renderer.lineHeight or 20
|
||||
tomeHeight = $('#tome-view').innerHeight()
|
||||
spellPaletteView = $('#spell-palette-view')
|
||||
spellListTabEntryHeight = $('#spell-list-tab-entry-view').outerHeight()
|
||||
spellTopBarHeight = $('#spell-top-bar-view').outerHeight()
|
||||
spellToolbarHeight = $('.spell-toolbar-view').outerHeight()
|
||||
@spellPaletteHeight ?= spellPaletteView.outerHeight() # Remember this until resize, since we change it afterward
|
||||
spellPaletteAllowedHeight = Math.min @spellPaletteHeight, tomeHeight / 3
|
||||
maxHeight = tomeHeight - spellListTabEntryHeight - spellToolbarHeight - spellPaletteAllowedHeight
|
||||
maxHeight = tomeHeight - spellTopBarHeight - spellToolbarHeight - spellPaletteAllowedHeight
|
||||
linesAtMaxHeight = Math.floor(maxHeight / lineHeight)
|
||||
lines = Math.max 8, Math.min(screenLineCount + 2, linesAtMaxHeight)
|
||||
# 2 lines buffer is nice
|
||||
|
@ -675,6 +663,7 @@ module.exports = class SpellView extends CocoView
|
|||
cast = @$el.parent().length
|
||||
@recompile cast, e.realTime
|
||||
@focus() if cast
|
||||
@updateHTML create: true if @options.level.isType('web-dev')
|
||||
|
||||
onCodeReload: (e) ->
|
||||
return unless e.spell is @spell or not e.spell
|
||||
|
@ -725,6 +714,8 @@ module.exports = class SpellView extends CocoView
|
|||
]
|
||||
onSignificantChange.push _.debounce @checkRequiredCode, 750 if @options.level.get 'requiredCode'
|
||||
onSignificantChange.push _.debounce @checkSuspectCode, 750 if @options.level.get 'suspectCode'
|
||||
onAnyChange.push _.throttle @updateHTML, 10 if @options.level.isType('web-dev')
|
||||
|
||||
@onCodeChangeMetaHandler = =>
|
||||
return if @eventsSuppressed
|
||||
#@playSound 'code-change', volume: 0.5 # Currently not using this sound.
|
||||
|
@ -737,6 +728,9 @@ module.exports = class SpellView extends CocoView
|
|||
|
||||
onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment.
|
||||
|
||||
updateHTML: (options={}) =>
|
||||
Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML(@getSource()), create: Boolean(options.create)
|
||||
|
||||
# Design for a simpler system?
|
||||
# * Keep Aether linting, debounced, on any significant change
|
||||
# - All problems just vanish when you make any change to the code
|
||||
|
@ -811,7 +805,7 @@ module.exports = class SpellView extends CocoView
|
|||
for aetherProblem, problemIndex in aether.getAllProblems()
|
||||
continue if key = aetherProblem.userInfo?.key and key of seenProblemKeys
|
||||
seenProblemKeys[key] = true if key
|
||||
@problems.push problem = new Problem aether, aetherProblem, @ace, isCast, @spell.levelID
|
||||
@problems.push problem = new Problem aether, aetherProblem, @ace, isCast, @options.levelID
|
||||
if isCast and problemIndex is 0
|
||||
if problem.aetherProblem.range?
|
||||
lineOffsetPx = 0
|
||||
|
@ -859,7 +853,7 @@ module.exports = class SpellView extends CocoView
|
|||
@userCodeProblem.set 'errRange', aetherProblem.range if aetherProblem.range
|
||||
@userCodeProblem.set 'errType', aetherProblem.type if aetherProblem.type
|
||||
@userCodeProblem.set 'language', aether.language.id if aether.language?.id
|
||||
@userCodeProblem.set 'levelID', @spell.levelID if @spell.levelID
|
||||
@userCodeProblem.set 'levelID', @options.levelID if @options.levelID
|
||||
@userCodeProblem.save()
|
||||
null
|
||||
|
||||
|
@ -907,16 +901,14 @@ module.exports = class SpellView extends CocoView
|
|||
return if @spell.source.indexOf('while') isnt -1 # If they're working with while-loops, it's more likely to be an incomplete infinite loop, so don't preload.
|
||||
return if @spell.source.length > 500 # Only preload on really short methods
|
||||
return if @spellThang?.castAether?.metrics?.statementsExecuted > 2000 # Don't preload if they are running significant amounts of user code
|
||||
return if @options.level.isType('web-dev')
|
||||
oldSource = @spell.source
|
||||
oldSpellThangAethers = {}
|
||||
for thangID, spellThang of @spell.thangs
|
||||
oldSpellThangAethers[thangID] = spellThang.aether.serialize() # Get raw, pure, and problems
|
||||
oldSpellThangAether = @spell.thang?.aether.serialize()
|
||||
@spell.transpile @getSource()
|
||||
@cast true
|
||||
@spell.source = oldSource
|
||||
for thangID, spellThang of @spell.thangs
|
||||
for key, value of oldSpellThangAethers[thangID]
|
||||
spellThang.aether[key] = value
|
||||
for key, value of oldSpellThangAether
|
||||
@spell.thang.aether[key] = value
|
||||
|
||||
onSpellChanged: (e) ->
|
||||
@spellHasChanged = true
|
||||
|
@ -933,10 +925,10 @@ module.exports = class SpellView extends CocoView
|
|||
return unless e.god is @options.god
|
||||
return @onInfiniteLoop e if e.problem.id is 'runtime_InfiniteLoop'
|
||||
return unless e.problem.userInfo.methodName is @spell.name
|
||||
return unless spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID
|
||||
return unless @spell.thang?.thang.id is e.problem.userInfo.thangID
|
||||
@spell.hasChangedSignificantly @getSource(), null, (hasChanged) =>
|
||||
return if hasChanged
|
||||
spellThang.aether.addProblem e.problem
|
||||
@spell.thang.aether.addProblem e.problem
|
||||
@lastUpdatedAetherSpellThang = null # force a refresh without a re-transpile
|
||||
@updateAether false, false
|
||||
|
||||
|
@ -957,13 +949,14 @@ module.exports = class SpellView extends CocoView
|
|||
@updateAether false, false
|
||||
|
||||
onNewWorld: (e) ->
|
||||
@spell.removeThangID thangID for thangID of @spell.thangs when not e.world.getThangByID thangID
|
||||
for thangID, spellThang of @spell.thangs
|
||||
thang = e.world.getThangByID(thangID)
|
||||
aether = e.world.userCodeMap[thangID]?[@spell.name] # Might not be there if this is a new Programmable Thang.
|
||||
spellThang.castAether = aether
|
||||
spellThang.aether = @spell.createAether thang
|
||||
#console.log thangID, @spell.spellKey, 'ran', aether.metrics.callsExecuted, 'times over', aether.metrics.statementsExecuted, 'statements, with max recursion depth', aether.metrics.maxDepth, 'and full flow/metrics', aether.metrics, aether.flow
|
||||
if thang = e.world.getThangByID @spell.thang?.thang.id
|
||||
aether = e.world.userCodeMap[thang.id]?[@spell.name]
|
||||
@spell.thang.castAether = aether
|
||||
@spell.thang.aether = @spell.createAether thang
|
||||
#console.log thang.id, @spell.spellKey, 'ran', aether.metrics.callsExecuted, 'times over', aether.metrics.statementsExecuted, 'statements, with max recursion depth', aether.metrics.maxDepth, 'and full flow/metrics', aether.metrics, aether.flow
|
||||
else
|
||||
@spell.thang = null
|
||||
|
||||
@spell.transpile() # TODO: is there any way we can avoid doing this if it hasn't changed? Causes a slight hang.
|
||||
@updateAether false, false
|
||||
|
||||
|
@ -990,8 +983,6 @@ module.exports = class SpellView extends CocoView
|
|||
@ace.insert "{x=#{e.x}, y=#{e.y}}"
|
||||
else
|
||||
@ace.insert "{x: #{e.x}, y: #{e.y}}"
|
||||
|
||||
|
||||
@highlightCurrentLine()
|
||||
|
||||
onStatementIndexUpdated: (e) ->
|
||||
|
@ -1156,7 +1147,7 @@ module.exports = class SpellView extends CocoView
|
|||
|
||||
toggleBackground: =>
|
||||
# TODO: make the background an actual background and do the CSS trick
|
||||
# used in spell_list_entry.sass for disabling
|
||||
# used in spell-top-bar-view.sass for disabling
|
||||
background = @$el.find('img.code-background')[0]
|
||||
if background.naturalWidth is 0 # not loaded yet
|
||||
return _.delay @toggleBackground, 100
|
||||
|
@ -1260,7 +1251,7 @@ module.exports = class SpellView extends CocoView
|
|||
@debugView?.destroy()
|
||||
@translationView?.destroy()
|
||||
@toolbarView?.destroy()
|
||||
@zatanna.addSnippets [], @editorLang if @editorLang?
|
||||
@zatanna?.addSnippets [], @editorLang if @editorLang?
|
||||
$(window).off 'resize', @onWindowResize
|
||||
window.clearTimeout @saveSpadeTimeout
|
||||
@saveSpadeTimeout = null
|
||||
|
|
|
@ -2,36 +2,26 @@
|
|||
# - a CastButtonView, which has
|
||||
# - a cast button
|
||||
# - a submit/done button
|
||||
# - for each spell (programmableMethod):
|
||||
# - for each spell (programmableMethod) (which is now just always only 'plan')
|
||||
# - a Spell, which has
|
||||
# - a list of Thangs that share that Spell, with one aether per Thang per Spell
|
||||
# - a Thang that uses that Spell, with an aether and a castAether
|
||||
# - a SpellView, which has
|
||||
# - tons of stuff; the meat
|
||||
# - a SpellListView, which has
|
||||
# - for each spell:
|
||||
# - a SpellListEntryView, which has
|
||||
# - icons for each Thang
|
||||
# - the spell name
|
||||
# - a reload button
|
||||
# - documentation for that method (in a popover)
|
||||
# - a SpellTopBarView, which has some controls
|
||||
# - a SpellPaletteView, which has
|
||||
# - for each programmableProperty:
|
||||
# - a SpellPaletteEntryView
|
||||
#
|
||||
# The CastButtonView and SpellListView always show.
|
||||
# The CastButtonView always shows.
|
||||
# The SpellPaletteView shows the entries for the currently selected Programmable Thang.
|
||||
# The SpellView shows the code and runtime state for the currently selected Spell and, specifically, Thang.
|
||||
# The SpellView obscures most of the SpellListView when present. We might mess with this.
|
||||
# You can switch a SpellView to showing the runtime state of another Thang sharing that Spell.
|
||||
# SpellPaletteViews are destroyed and recreated whenever you switch Thangs.
|
||||
# The SpellListView shows spells to which your team has read or readwrite access.
|
||||
# It doubles as a Thang selector, since it's there when nothing is selected.
|
||||
|
||||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/play/level/tome/tome'
|
||||
{me} = require 'core/auth'
|
||||
Spell = require './Spell'
|
||||
SpellListView = require './SpellListView'
|
||||
SpellPaletteView = require './SpellPaletteView'
|
||||
CastButtonView = require './CastButtonView'
|
||||
|
||||
|
@ -44,7 +34,6 @@ module.exports = class TomeView extends CocoView
|
|||
subscriptions:
|
||||
'tome:spell-loaded': 'onSpellLoaded'
|
||||
'tome:cast-spell': 'onCastSpell'
|
||||
'tome:toggle-spell-list': 'onToggleSpellList'
|
||||
'tome:change-language': 'updateLanguageForAllSpells'
|
||||
'surface:sprite-selected': 'onSpriteSelected'
|
||||
'god:new-world-created': 'onNewWorld'
|
||||
|
@ -52,16 +41,16 @@ module.exports = class TomeView extends CocoView
|
|||
'tome:select-primary-sprite': 'onSelectPrimarySprite'
|
||||
|
||||
events:
|
||||
'click #spell-view': 'onSpellViewClick'
|
||||
'click': 'onClick'
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@worker = @createWorker()
|
||||
programmableThangs = _.filter @options.thangs, (t) -> t.isProgrammable and t.programmableMethods
|
||||
@createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
|
||||
unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
|
||||
if @options.level.isType('web-dev')
|
||||
if @fakeProgrammableThang = @createFakeProgrammableThang()
|
||||
programmableThangs = [@fakeProgrammableThang]
|
||||
@createSpells programmableThangs, programmableThangs[0]?.world # Do before castButton
|
||||
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god
|
||||
@teamSpellMap = @generateTeamSpellMap(@spells)
|
||||
unless programmableThangs.length
|
||||
|
@ -72,10 +61,8 @@ module.exports = class TomeView extends CocoView
|
|||
delete @options.thangs
|
||||
|
||||
onNewWorld: (e) ->
|
||||
thangs = _.filter e.world.thangs, 'inThangList'
|
||||
programmableThangs = _.filter thangs, (t) -> t.isProgrammable and t.programmableMethods
|
||||
programmableThangs = _.filter e.thangs, (t) -> t.isProgrammable and t.programmableMethods and t.inThangList
|
||||
@createSpells programmableThangs, e.world
|
||||
@spellList?.adjustSpells @spells
|
||||
|
||||
onCommentMyCode: (e) ->
|
||||
for spellKey, spell of @spells when spell.canWrite()
|
||||
|
@ -114,33 +101,30 @@ module.exports = class TomeView extends CocoView
|
|||
@thangSpells[thang.id] = []
|
||||
for methodName, method of thang.programmableMethods
|
||||
pathComponents = [thang.id, methodName]
|
||||
if method.cloneOf
|
||||
pathComponents[0] = method.cloneOf # referencing another Thang's method
|
||||
pathComponents[0] = _.string.slugify pathComponents[0]
|
||||
spellKey = pathComponents.join '/'
|
||||
@thangSpells[thang.id].push spellKey
|
||||
unless method.cloneOf
|
||||
skipProtectAPI = @getQueryVariable 'skip_protect_api', (@options.levelID in ['gridmancer', 'minimax-tic-tac-toe'])
|
||||
spell = @spells[spellKey] = new Spell
|
||||
hintsState: @options.hintsState
|
||||
programmableMethod: method
|
||||
spellKey: spellKey
|
||||
pathComponents: pathPrefixComponents.concat(pathComponents)
|
||||
session: @options.session
|
||||
otherSession: @options.otherSession
|
||||
supermodel: @supermodel
|
||||
skipProtectAPI: skipProtectAPI
|
||||
worker: @worker
|
||||
language: language
|
||||
spectateView: @options.spectateView
|
||||
spectateOpponentCodeLanguage: @options.spectateOpponentCodeLanguage
|
||||
observing: @options.observing
|
||||
levelID: @options.levelID
|
||||
level: @options.level
|
||||
god: @options.god
|
||||
skipProtectAPI = @getQueryVariable 'skip_protect_api', false
|
||||
spell = @spells[spellKey] = new Spell
|
||||
hintsState: @options.hintsState
|
||||
programmableMethod: method
|
||||
spellKey: spellKey
|
||||
pathComponents: pathPrefixComponents.concat(pathComponents)
|
||||
session: @options.session
|
||||
otherSession: @options.otherSession
|
||||
supermodel: @supermodel
|
||||
skipProtectAPI: skipProtectAPI
|
||||
worker: @worker
|
||||
language: language
|
||||
spectateView: @options.spectateView
|
||||
spectateOpponentCodeLanguage: @options.spectateOpponentCodeLanguage
|
||||
observing: @options.observing
|
||||
levelID: @options.levelID
|
||||
level: @options.level
|
||||
god: @options.god
|
||||
|
||||
for thangID, spellKeys of @thangSpells
|
||||
thang = world.getThangByID thangID
|
||||
thang = @fakeProgrammableThang ? world.getThangByID thangID
|
||||
if thang
|
||||
@spells[spellKey].addThang thang for spellKey in spellKeys
|
||||
else
|
||||
|
@ -161,6 +145,7 @@ module.exports = class TomeView extends CocoView
|
|||
@cast e?.preload, e?.realTime
|
||||
|
||||
cast: (preload=false, realTime=false) ->
|
||||
return if @options.level.isType('web-dev')
|
||||
sessionState = @options.session.get('state') ? {}
|
||||
if realTime
|
||||
sessionState.submissionCount = (sessionState.submissionCount ? 0) + 1
|
||||
|
@ -172,53 +157,27 @@ module.exports = class TomeView extends CocoView
|
|||
difficulty = Math.max 0, difficulty - 1 # Show the difficulty they won, not the next one.
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty, god: @options.god, fixedSeed: @options.fixedSeed
|
||||
|
||||
onToggleSpellList: (e) ->
|
||||
@spellList?.rerenderEntries()
|
||||
@spellList?.$el.toggle()
|
||||
|
||||
onSpellViewClick: (e) ->
|
||||
@spellList?.$el.hide()
|
||||
|
||||
onClick: (e) ->
|
||||
Backbone.Mediator.publish 'tome:focus-editor', {} unless $(e.target).parents('.popover').length
|
||||
|
||||
clearSpellView: ->
|
||||
@spellView?.dismiss()
|
||||
@spellView?.$el.after('<div id="' + @spellView.id + '"></div>').detach()
|
||||
@spellView = null
|
||||
@spellTabView?.$el.after('<div id="' + @spellTabView.id + '"></div>').detach()
|
||||
@spellTabView = null
|
||||
@removeSubView @spellPaletteView if @spellPaletteView
|
||||
@spellPaletteView = null
|
||||
@$el.find('#spell-palette-view').hide()
|
||||
@castButton?.$el.hide()
|
||||
|
||||
onSpriteSelected: (e) ->
|
||||
return if @spellView and @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # Never deselect the hero in the Tome.
|
||||
thang = e.thang
|
||||
spellName = e.spellName
|
||||
@spellList?.$el.hide()
|
||||
return @clearSpellView() unless thang
|
||||
spell = @spellFor thang, spellName
|
||||
unless spell?.canRead()
|
||||
@clearSpellView()
|
||||
@updateSpellPalette thang, spell if spell
|
||||
return
|
||||
return if @spellView and @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev'] # Never deselect the hero in the Tome.
|
||||
spell = @spellFor e.thang, e.spellName
|
||||
if spell?.canRead()
|
||||
@setSpellView spell, e.thang
|
||||
|
||||
setSpellView: (spell, thang) ->
|
||||
unless spell.view is @spellView
|
||||
@clearSpellView()
|
||||
@spellView = spell.view
|
||||
@spellTabView = spell.tabView
|
||||
@spellTopBarView = spell.topBarView
|
||||
@$el.find('#' + @spellView.id).after(@spellView.el).remove()
|
||||
@$el.find('#' + @spellTabView.id).after(@spellTabView.el).remove()
|
||||
@$el.find('#' + @spellTopBarView.id).after(@spellTopBarView.el).remove()
|
||||
@castButton?.attachTo @spellView
|
||||
Backbone.Mediator.publish 'tome:spell-shown', thang: thang, spell: spell
|
||||
@updateSpellPalette thang, spell
|
||||
@spellList?.setThangAndSpell thang, spell
|
||||
@spellView?.setThang thang
|
||||
@spellTabView?.setThang thang
|
||||
|
||||
updateSpellPalette: (thang, spell) ->
|
||||
return unless thang and @spellPaletteView?.thang isnt thang and thang.programmableProperties or thang.apiProperties
|
||||
return unless thang and @spellPaletteView?.thang isnt thang and (thang.programmableProperties or thang.apiProperties or thang.programmableHTMLProperties)
|
||||
useHero = /hero/.test(spell.getSource()) or not /(self[\.\:]|this\.|\@)/.test(spell.getSource())
|
||||
@spellPaletteView = @insertSubView new SpellPaletteView { thang, @supermodel, programmable: spell?.canRead(), language: spell?.language ? @options.session.get('codeLanguage'), session: @options.session, level: @options.level, courseID: @options.courseID, courseInstanceID: @options.courseInstanceID, useHero }
|
||||
@spellPaletteView.toggleControls {}, spell.view.controlsEnabled if spell?.view # TODO: know when palette should have been disabled but didn't exist
|
||||
|
@ -246,13 +205,27 @@ module.exports = class TomeView extends CocoView
|
|||
@cast()
|
||||
|
||||
onSelectPrimarySprite: (e) ->
|
||||
# This is only fired by PlayLevelView for hero levels currently
|
||||
if @options.level.isType('web-dev')
|
||||
@setSpellView @spells['hero-placeholder/plan'], @fakeProgrammableThang
|
||||
return
|
||||
# This is fired by PlayLevelView
|
||||
# TODO: Don't hard code these hero names
|
||||
if @options.session.get('team') is 'ogres'
|
||||
Backbone.Mediator.publish 'level:select-sprite', thangID: 'Hero Placeholder 1'
|
||||
else
|
||||
Backbone.Mediator.publish 'level:select-sprite', thangID: 'Hero Placeholder'
|
||||
|
||||
createFakeProgrammableThang: ->
|
||||
return null unless hero = _.find @options.level.get('thangs'), id: 'Hero Placeholder'
|
||||
return null unless programmableConfig = _.find(hero.components, (component) -> component.config?.programmableMethods).config
|
||||
usesHTMLConfig = _.find(hero.components, (component) -> component.config?.programmableHTMLProperties).config
|
||||
console.warn "Couldn't find usesHTML config; is it presented and not defaulted on the Hero Placeholder?" unless usesHTMLConfig
|
||||
thang =
|
||||
id: 'Hero Placeholder'
|
||||
isProgrammable: true
|
||||
thang = _.merge thang, programmableConfig, usesHTMLConfig
|
||||
thang
|
||||
|
||||
destroy: ->
|
||||
spell.destroy() for spellKey, spell of @spells
|
||||
@worker?.terminate()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
utils = require 'core/utils'
|
||||
|
||||
defaults =
|
||||
defaults =
|
||||
autoLineEndings:
|
||||
# Mapping ace mode language to line endings to automatically insert
|
||||
# E.g. javascript: ";"
|
||||
|
@ -69,7 +69,7 @@ module.exports = class Zatanna
|
|||
@editor.commands.on 'afterExec', @doLiveCompletion
|
||||
|
||||
setAceOptions: () ->
|
||||
aceOptions =
|
||||
aceOptions =
|
||||
'enableLiveAutocompletion': @options.liveCompletion
|
||||
'enableBasicAutocompletion': @options.basic
|
||||
'enableSnippets': @options.completers.snippets
|
||||
|
@ -92,20 +92,20 @@ module.exports = class Zatanna
|
|||
else if typeof comp is 'string'
|
||||
if @completers[comp]? and @editor.completers[@completers[comp].pos] isnt @completers[comp].comp
|
||||
@editor.completers.splice(@completers[comp].pos, 0, @completers[comp].comp)
|
||||
else
|
||||
else
|
||||
@editor.completers = []
|
||||
for type, comparator of @completers
|
||||
if @options.completers[type] is true
|
||||
@activateCompleter type
|
||||
@activateCompleter type
|
||||
|
||||
addSnippets: (snippets, language) ->
|
||||
@options.language = language
|
||||
ace.config.loadModule 'ace/ext/language_tools', () =>
|
||||
@snippetManager = ace.require('ace/snippets').snippetManager
|
||||
snippetModulePath = 'ace/snippets/' + language
|
||||
ace.config.loadModule snippetModulePath, (m) =>
|
||||
ace.config.loadModule snippetModulePath, (m) =>
|
||||
if m?
|
||||
@snippetManager.files[language] = m
|
||||
@snippetManager.files[language] = m
|
||||
@snippetManager.unregister m.snippets if m.snippets?.length > 0
|
||||
@snippetManager.unregister @oldSnippets if @oldSnippets?
|
||||
m.snippets = if @options.snippetsLangDefaults then @snippetManager.parseSnippetFile m.snippetText else []
|
||||
|
@ -265,7 +265,7 @@ module.exports = class Zatanna
|
|||
when 'python' then 'loop:\n self.moveRight()\n ${1:}'
|
||||
when 'javascript' then 'loop {\n this.moveRight();\n ${1:}\n}'
|
||||
else content
|
||||
if /loop/.test(content) and level.get('type') in ['course', 'course-ladder']
|
||||
if /loop/.test(content) and level.isType('course', 'course-ladder')
|
||||
# Temporary hackery to make it look like we meant while True: in our loop snippets until we can update everything
|
||||
content = switch e.language
|
||||
when 'python' then content.replace /loop:/, 'while True:'
|
||||
|
|
|
@ -5,7 +5,6 @@ submenuViews = [
|
|||
require 'views/play/menu/SaveLoadView'
|
||||
require 'views/play/menu/OptionsView'
|
||||
require 'views/play/menu/GuideView'
|
||||
require 'views/play/menu/MultiplayerView'
|
||||
]
|
||||
|
||||
module.exports = class GameMenuModal extends ModalView
|
||||
|
@ -31,11 +30,10 @@ module.exports = class GameMenuModal extends ModalView
|
|||
getRenderData: (context={}) ->
|
||||
context = super(context)
|
||||
docs = @options.level.get('documentation') ? {}
|
||||
submenus = ['guide', 'options', 'save-load', 'multiplayer']
|
||||
submenus = ['guide', 'options', 'save-load']
|
||||
submenus = _.without submenus, 'options' if window.serverConfig.picoCTF
|
||||
submenus = _.without submenus, 'guide' unless docs.specificArticles?.length or docs.generalArticles?.length or window.serverConfig.picoCTF
|
||||
submenus = _.without submenus, 'save-load' unless me.isAdmin() or /https?:\/\/localhost/.test(window.location.href)
|
||||
submenus = _.without submenus, 'multiplayer' unless me.isAdmin() or (@level?.get('type') in ['ladder', 'hero-ladder', 'course-ladder'] and @level.get('slug') not in ['ace-of-coders', 'elemental-wars'])
|
||||
@includedSubmenus = submenus
|
||||
context.showTab = @options.showTab ? submenus[0]
|
||||
context.submenus = submenus
|
||||
|
@ -43,11 +41,10 @@ module.exports = class GameMenuModal extends ModalView
|
|||
'options': 'cog'
|
||||
'guide': 'list'
|
||||
'save-load': 'floppy-disk'
|
||||
'multiplayer': 'globe'
|
||||
context
|
||||
|
||||
showsChooseHero: ->
|
||||
return false if @level?.get('type') in ['course', 'course-ladder']
|
||||
return false if @level?.isType('course', 'course-ladder')
|
||||
return false if @options.levelID in ['zero-sum', 'ace-of-coders', 'elemental-wars']
|
||||
return true
|
||||
|
||||
|
@ -55,7 +52,6 @@ module.exports = class GameMenuModal extends ModalView
|
|||
super()
|
||||
@insertSubView new submenuView @options for submenuView in submenuViews
|
||||
firstView = switch @options.showTab
|
||||
when 'multiplayer' then @subviews.multiplayer_view
|
||||
when 'guide' then @subviews.guide_view
|
||||
else
|
||||
if 'guide' in @includedSubmenus then @subviews.guide_view else @subviews.options_view
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = class LevelGuideView extends CocoView
|
|||
@levelSlug = options.level.get('slug')
|
||||
@sessionID = options.session.get('_id')
|
||||
@requiresSubscription = not me.isPremium()
|
||||
@isCourseLevel = options.level.get('type', true) in ['course', 'course-ladder']
|
||||
@isCourseLevel = options.level.isType('course', 'course-ladder') # TODO: figure this out for game-dev, web-dev levels
|
||||
@helpVideos = if @isCourseLevel then [] else options.level.get('helpVideos') ? []
|
||||
@trackedHelpVideoStart = @trackedHelpVideoFinish = false
|
||||
# A/B Testing video tutorial styles
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/play/menu/multiplayer-view'
|
||||
{me} = require 'core/auth'
|
||||
ThangType = require 'models/ThangType'
|
||||
LadderSubmissionView = require 'views/play/common/LadderSubmissionView'
|
||||
RealTimeModel = require 'models/RealTimeModel'
|
||||
RealTimeCollection = require 'collections/RealTimeCollection'
|
||||
|
||||
module.exports = class MultiplayerView extends CocoView
|
||||
id: 'multiplayer-view'
|
||||
className: 'tab-pane'
|
||||
template: template
|
||||
|
||||
subscriptions:
|
||||
'ladder:game-submitted': 'onGameSubmitted'
|
||||
|
||||
events:
|
||||
'click textarea': 'onClickLink'
|
||||
'change #multiplayer': 'updateLinkSection'
|
||||
'click #create-game-button': 'onCreateRealTimeGame'
|
||||
'click #join-game-button': 'onJoinRealTimeGame'
|
||||
'click #leave-game-button': 'onLeaveRealTimeGame'
|
||||
|
||||
constructor: (options) ->
|
||||
super(options)
|
||||
@level = options.level
|
||||
@levelID = @level?.get 'slug'
|
||||
@session = options.session
|
||||
@listenTo @session, 'change:multiplayer', @updateLinkSection
|
||||
@watchRealTimeSessions() if @level?.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
|
||||
|
||||
destroy: ->
|
||||
@realTimeSessions?.off 'add', @onRealTimeSessionAdded
|
||||
@currentRealTimeSession?.off 'change', @onCurrentRealTimeSessionChanged
|
||||
collection.off() for id, collection of @realTimeSessionsPlayers
|
||||
super()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.joinLink = "#{document.location.href.replace(/\?.*/, '').replace('#', '')}?session=#{@session.id}"
|
||||
c.multiplayer = @session.get 'multiplayer'
|
||||
c.team = @session.get 'team'
|
||||
c.levelSlug = @levelID
|
||||
# For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet.
|
||||
if @level?.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
||||
c.ladderGame = true
|
||||
c.readyToRank = @session?.readyToRank()
|
||||
|
||||
# Real-time multiplayer stuff
|
||||
if @level?.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
|
||||
c.levelID = @session.get('levelID')
|
||||
c.realTimeSessions = @realTimeSessions
|
||||
c.currentRealTimeSession = @currentRealTimeSession if @currentRealTimeSession
|
||||
c.realTimeSessionsPlayers = @realTimeSessionsPlayers if @realTimeSessionsPlayers
|
||||
# console.log 'MultiplayerView getRenderData', c.levelID
|
||||
# console.log 'realTimeSessions', c.realTimeSessions
|
||||
# console.log c.realTimeSessions.at(c.realTimeSessions.length - 1).get('state') if c.realTimeSessions.length > 0
|
||||
# console.log 'currentRealTimeSession', c.currentRealTimeSession
|
||||
# console.log 'realTimeSessionPlayers', c.realTimeSessionsPlayers
|
||||
|
||||
c
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@updateLinkSection()
|
||||
@ladderSubmissionView = new LadderSubmissionView session: @session, level: @level
|
||||
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
|
||||
@$el.find('#created-multiplayer-session').toggle Boolean(@currentRealTimeSession?)
|
||||
@$el.find('#create-game-button').toggle Boolean(not (@currentRealTimeSession?))
|
||||
|
||||
onClickLink: (e) ->
|
||||
e.target.select()
|
||||
|
||||
onGameSubmitted: (e) ->
|
||||
# Preserve the supermodel as we navigate back to the ladder.
|
||||
viewArgs = [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @levelID]
|
||||
ladderURL = "/play/ladder/#{@levelID}"
|
||||
if leagueID = @getQueryVariable 'league'
|
||||
leagueType = if @level?.get('type') is 'course-ladder' then 'course' else 'clan'
|
||||
viewArgs.push leagueType
|
||||
viewArgs.push leagueID
|
||||
ladderURL += "/#{leagueType}/#{leagueID}"
|
||||
ladderURL += '#my-matches'
|
||||
Backbone.Mediator.publish 'router:navigate', route: ladderURL, viewClass: 'views/ladder/LadderView', viewArgs: viewArgs
|
||||
|
||||
updateLinkSection: ->
|
||||
multiplayer = @$el.find('#multiplayer').prop('checked')
|
||||
la = @$el.find('#link-area')
|
||||
la.toggle if @level?.get('type') in ['ladder', 'hero-ladder', 'course-ladder'] then false else Boolean(multiplayer)
|
||||
true
|
||||
|
||||
onHidden: ->
|
||||
multiplayer = Boolean(@$el.find('#multiplayer').prop('checked'))
|
||||
@session.set('multiplayer', multiplayer)
|
||||
|
||||
# Real-time Multiplayer ######################################################
|
||||
#
|
||||
# This view is responsible for joining and leaving real-time multiplayer games.
|
||||
#
|
||||
# It performs these actions:
|
||||
# Display your current game (level, players)
|
||||
# Display open games
|
||||
# Create game button, if not in a game
|
||||
# Join game button
|
||||
# Leave game button, if in a game
|
||||
#
|
||||
# It monitors these:
|
||||
# Real-time multiplayer sessions (for open games, player states)
|
||||
# Current real-time multiplayer game session for changes
|
||||
# Players for real-time multiplayer game session
|
||||
#
|
||||
# Real-time state variables:
|
||||
# @realTimeSessionsPlayers - Collection of player lists for active real-time multiplayer sessions
|
||||
# @realTimeSessions - Active real-time multiplayer sessions
|
||||
# @currentRealTimeSession - Our current real-time multiplayer session
|
||||
#
|
||||
# TODO: Ditch backfire and just use Firebase directly. Easier to debug, richer APIs (E.g. presence stuff).
|
||||
|
||||
watchRealTimeSessions: ->
|
||||
# Setup monitoring of real-time multiplayer level sessions
|
||||
@realTimeSessionsPlayers = {}
|
||||
# TODO: only request sessions for this level, !team, etc.
|
||||
@realTimeSessions = new RealTimeCollection("multiplayer_level_sessions/#{@levelID}")
|
||||
@realTimeSessions.on 'add', @onRealTimeSessionAdded
|
||||
@realTimeSessions.each (rts) => @watchRealTimeSession rts
|
||||
|
||||
watchRealTimeSession: (rts) ->
|
||||
return if rts.get('state') is 'finished'
|
||||
return if rts.get('levelID') isnt @session.get('levelID')
|
||||
# console.log 'MultiplayerView watchRealTimeSession', rts
|
||||
# Setup monitoring of players for given session
|
||||
# TODO: verify we need this
|
||||
realTimeSession = new RealTimeModel("multiplayer_level_sessions/#{@levelID}/#{rts.id}")
|
||||
realTimeSession.on 'change', @onRealTimeSessionChanged
|
||||
@realTimeSessionsPlayers[rts.id] = new RealTimeCollection("multiplayer_level_sessions/#{@levelID}/#{rts.id}/players")
|
||||
@realTimeSessionsPlayers[rts.id].on 'add', @onRealTimePlayerAdded
|
||||
@findCurrentRealTimeSession rts
|
||||
|
||||
findCurrentRealTimeSession: (rts) ->
|
||||
# Look for our current real-time session (level, level state, member player)
|
||||
return if @currentRealTimeSession or not @realTimeSessionsPlayers?
|
||||
if rts.get('levelID') is @session.get('levelID') and rts.get('state') isnt 'finished'
|
||||
@realTimeSessionsPlayers[rts.id].each (player) =>
|
||||
if player.id is me.id and player.get('state') isnt 'left'
|
||||
# console.log 'MultiplayerView found current real-time session', rts
|
||||
@currentRealTimeSession = new RealTimeModel("multiplayer_level_sessions/#{@levelID}/#{rts.id}")
|
||||
@currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged
|
||||
|
||||
# TODO: Is this necessary? Shouldn't everyone already know we joined a game at this point?
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', realTimeSessionID: @currentRealTimeSession.id
|
||||
|
||||
onRealTimeSessionAdded: (rts) =>
|
||||
@watchRealTimeSession rts
|
||||
@render()
|
||||
|
||||
onRealTimeSessionChanged: (rts) =>
|
||||
# console.log 'MultiplayerView onRealTimeSessionChanged', rts.get('state')
|
||||
# TODO: @realTimeSessions isn't updated before we call render() here
|
||||
# TODO: so this game isn't updated in open games list
|
||||
@render?()
|
||||
|
||||
onCurrentRealTimeSessionChanged: (rts) =>
|
||||
# console.log 'MultiplayerView onCurrentRealTimeSessionChanged', rts
|
||||
if rts.get('state') is 'finished'
|
||||
@currentRealTimeSession.off 'change', @onCurrentRealTimeSessionChanged
|
||||
@currentRealTimeSession = null
|
||||
@render?()
|
||||
|
||||
onRealTimePlayerAdded: (e) =>
|
||||
@render?()
|
||||
|
||||
onCreateRealTimeGame: ->
|
||||
@playSound 'menu-button-click'
|
||||
s = @realTimeSessions.create {
|
||||
creator: @session.get('creator')
|
||||
creatorName: @session.get('creatorName')
|
||||
levelID: @session.get('levelID')
|
||||
created: (new Date()).toISOString()
|
||||
state: 'creating'
|
||||
}
|
||||
@currentRealTimeSession = @realTimeSessions.get(s.id)
|
||||
@currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged
|
||||
# TODO: s.id === @currentRealTimeSession.id ?
|
||||
players = new RealTimeCollection("multiplayer_level_sessions/#{@levelID}/#{@currentRealTimeSession.id}/players")
|
||||
players.create
|
||||
id: me.id
|
||||
state: 'coding'
|
||||
name: @session.get('creatorName')
|
||||
team: @session.get('team')
|
||||
level_session: @session.id
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:created-game', realTimeSessionID: @currentRealTimeSession.id
|
||||
@render()
|
||||
|
||||
onJoinRealTimeGame: (e) ->
|
||||
return if @currentRealTimeSession
|
||||
@playSound 'menu-button-click'
|
||||
item = @$el.find(e.target).data('item')
|
||||
@currentRealTimeSession = @realTimeSessions.get(item.id)
|
||||
@currentRealTimeSession.on 'change', @onCurrentRealTimeSessionChanged
|
||||
if @realTimeSessionsPlayers[item.id]
|
||||
|
||||
# TODO: SpellView updateTeam() should take care of this team swap update in the real-time multiplayer session
|
||||
creatorID = @currentRealTimeSession.get('creator')
|
||||
creator = @realTimeSessionsPlayers[item.id].get(creatorID)
|
||||
creatorTeam = creator.get('team')
|
||||
myTeam = @session.get('team')
|
||||
if myTeam is creatorTeam
|
||||
myTeam = if creatorTeam is 'humans' then 'ogres' else 'humans'
|
||||
|
||||
@realTimeSessionsPlayers[item.id].create
|
||||
id: me.id
|
||||
state: 'coding'
|
||||
name: me.get('name')
|
||||
team: myTeam
|
||||
level_session: @session.id
|
||||
else
|
||||
console.error 'MultiplayerView onJoinRealTimeGame did not have a players collection', @currentRealTimeSession
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:joined-game', realTimeSessionID: @currentRealTimeSession.id
|
||||
@render()
|
||||
|
||||
onLeaveRealTimeGame: (e) ->
|
||||
@playSound 'menu-button-click'
|
||||
if @currentRealTimeSession
|
||||
@currentRealTimeSession.off 'change', @onCurrentRealTimeSessionChanged
|
||||
@currentRealTimeSession = null
|
||||
Backbone.Mediator.publish 'real-time-multiplayer:left-game', userID: me.id
|
||||
else
|
||||
console.error "Tried to leave a game with no currentMultiplayerSession"
|
||||
@render()
|
12
bower.json
12
bower.json
|
@ -32,9 +32,8 @@
|
|||
"firepad": "~0.1.2",
|
||||
"marked": "~0.3.0",
|
||||
"moment": "~2.5.0",
|
||||
"aether": "~0.5.6",
|
||||
"aether": "~0.5.21",
|
||||
"underscore.string": "~2.3.3",
|
||||
"firebase": "~1.0.2",
|
||||
"d3": "~3.4.4",
|
||||
"jsondiffpatch": "0.1.8",
|
||||
"nanoscroller": "~0.8.0",
|
||||
|
@ -44,7 +43,6 @@
|
|||
"validated-backbone-mediator": "~0.1.3",
|
||||
"jquery.browser": "~0.0.6",
|
||||
"modernizr": "~2.8.3",
|
||||
"backfire": "~0.3.0",
|
||||
"fastclick": "~1.0.3",
|
||||
"three.js": "~0.71.0",
|
||||
"lscache": "~1.0.5",
|
||||
|
@ -64,9 +62,6 @@
|
|||
"backbone": {
|
||||
"main": "backbone.js"
|
||||
},
|
||||
"backfire": {
|
||||
"main": "backbone-firebase.min.js"
|
||||
},
|
||||
"lodash": {
|
||||
"main": "dist/lodash.js"
|
||||
},
|
||||
|
@ -112,13 +107,12 @@
|
|||
"aether": {
|
||||
"main": [
|
||||
"build/aether.js",
|
||||
"build/clojure.js",
|
||||
"build/coffeescript.js",
|
||||
"build/io.js",
|
||||
"build/javascript.js",
|
||||
"build/lua.js",
|
||||
"build/python.js",
|
||||
"build/java.js"
|
||||
"build/java.js",
|
||||
"build/html.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -115,7 +115,7 @@ exports.config =
|
|||
'javascripts/app/vendor/aether-lua.js': 'bower_components/aether/build/lua.js'
|
||||
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'
|
||||
'javascripts/app/vendor/aether-python.js': 'bower_components/aether/build/python.js'
|
||||
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'
|
||||
'javascripts/app/vendor/aether-html.js': 'bower_components/aether/build/html.js'
|
||||
|
||||
# Any vendor libraries we don't want the client to load immediately
|
||||
'javascripts/app/vendor/d3.js': regJoin('^bower_components/d3')
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"dependencies": {
|
||||
"JQDeferred": "~2.1.0",
|
||||
"ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz",
|
||||
"aether": "~0.5.6",
|
||||
"aether": "~0.5.21",
|
||||
"async": "0.2.x",
|
||||
"aws-sdk": "~2.0.0",
|
||||
"bayesian-battle": "0.0.7",
|
||||
|
@ -75,7 +75,7 @@
|
|||
"mailchimp-api": "2.0.x",
|
||||
"moment": "~2.5.0",
|
||||
"mongodb": "^2.0.28",
|
||||
"mongoose": "^4.2.9",
|
||||
"mongoose": "4.5.3",
|
||||
"mongoose-cache": "https://github.com/nwinter/mongoose-cache/tarball/master",
|
||||
"node-force-domain": "~0.1.0",
|
||||
"node-gyp": "~0.13.0",
|
||||
|
|
|
@ -31,6 +31,6 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) ->
|
|||
unless config.proxy
|
||||
analyticsMongoose = mongoose.createConnection()
|
||||
analyticsMongoose.open "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}", (error) ->
|
||||
log.error "Couldnt connect to analytics", error if error
|
||||
|
||||
log.error "Couldn't connect to analytics", error if error
|
||||
|
||||
module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection)
|
||||
|
|
|
@ -83,7 +83,7 @@ LevelSessionSchema.pre 'save', (next) ->
|
|||
next()
|
||||
|
||||
LevelSessionSchema.statics.privateProperties = ['code', 'submittedCode', 'unsubscribed']
|
||||
LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state',
|
||||
LevelSessionSchema.statics.editableProperties = ['players', 'code', 'codeLanguage', 'completed', 'state',
|
||||
'levelName', 'creatorName', 'levelID',
|
||||
'chat', 'teamSpells', 'submitted', 'submittedCodeLanguage',
|
||||
'unsubscribed', 'playtime', 'heroConfig', 'team',
|
||||
|
@ -110,7 +110,7 @@ if config.mongo.level_session_replica_string?
|
|||
levelSessionMongo = mongoose.createConnection()
|
||||
levelSessionMongo.open config.mongo.level_session_replica_string, (error) ->
|
||||
if error
|
||||
log.error "Couldnt connect to session mongo!", error
|
||||
log.error "Couldn't connect to session mongo!", error
|
||||
else
|
||||
log.info "Connected to seperate level session server with string", config.mongo.level_session_replica_string
|
||||
else
|
||||
|
@ -122,7 +122,7 @@ if config.mongo.level_session_aux_replica_string?
|
|||
auxLevelSessionMongo = mongoose.createConnection()
|
||||
auxLevelSessionMongo.open config.mongo.level_session_aux_replica_string, (error) ->
|
||||
if error
|
||||
log.error "Couldnt connect to AUX session mongo!", error
|
||||
log.error "Couldn't connect to AUX session mongo!", error
|
||||
else
|
||||
log.info "Connected to seperate level AUX session server with string", config.mongo.level_session_aux_replica_string
|
||||
|
||||
|
|
Loading…
Reference in a new issue